The code should work out of the box so long as your subclasses (as seen in code snippet #2 which calls :newSubclass()
) properly reference their respective superclasses with require()
. I may have forgotten or incorrectly written something somewhere If your finished hierarchy doesn’t seem to function as intended, so I’ll be sure to try and replicate your error if needed.
Once you set up your class tree, modifying it is as easy as creating methods that suit your needs (like my :setOwner(player)
method that handles everything related to item ownership), adding default fields to your classes, overriding whenever applicable, and creating new classes as needed.
Here’s a sword
abstract class that inherits equippable
:
--[[
remember, equippable inherits the item class and subsequently
the "newSubclass" method, so we can call it here
]]
local sword = require(--[[equippable module path]]):newSubclass{
cooldown_rigidity = .85;
swing_speed = 2;
damage = 10;
};
--[[
here's a cool method that ALL items that inherit sword can call.
if a specific sword wants to be fancier, it can define a more complex version.
this method will validate swinging on the backend.
we can propagate its returned value to the client with the sword to tell it to
undo visual effects in the case that the check failed.
]]
function sword:canSlash(): boolean
--for example, let's check swinging cooldown
local t = workspace:GetServerTimeNow();
--[[
remember, "self" refers to whatever sword we called a method on.
here, i make it so that self.last_swing is the last timestamp the
sword was swung.
if self.last_swing is nil, that means it's the first time we're swinging.
this conditional will instantly return false if the cooldown is still
applicable; we calculate cooldown using the inverse of self.swing_speed,
which has a default value of 2. this allows swords to perform :slash()
twice per second (reciprocal of 2 = 1/2).
*in actuality, we need to be more lenient in verifying cooldowns due to
latency between server and client, so we decrease the effective cool-
down by 15% (* .85).
]]
if
self.last_swing and
t - self.last_swing < self.cooldown_rigidity / self.swing_speed
then return false; end -- return false, signifying that the server denied the slash.
-- this boolean can make the remote handler tell the client
-- to undo whatever visual effect they showed
return true; -- tell the caller that the check succeeded
end
function sword:slash()
local t = workspace:GetServerTimeNow();
self.last_swing = t; -- update last_swing because we successfully slashed
-- at this point. the server shouldn't be calling :slash()
-- unless it wants to force one regardless of whether
-- :canSlash() succeeds. you can actually combine the
-- the two methods, but you won't be able to force
-- slashes or check whether it's doable without also
-- slashing.
-- do other fancy things like hitbox calculations and damage
end
return sword; -- since our class embodies a modulescript
Unique items can either be very fancy (unique logic and overrides) or very simple (abide by whatever our abstract sword
already defines). Here’s where we finally make a wooden sword:
local wooden_sword = require(--[[sword module path]]):newSubclass{
model = serverStorage.models.wooden_sword; -- this class is instantiable, so we will meet
-- the requirements for our :new() constructor
}; -- let's not override anything to showcase inheritance
return wooden_sword;
--[[
that's all we'll do for simplicity. you don't even need to
declare wooden_sword here; you can just pass in :newSubclass()
directly to the module's return statement.
]]
Let’s also make an orichalcum rapier to contrast inheritance and overrides:
local ori_rapier = require(--[[sword module path]]):newSubclass{
model = serverStorage.models.orichalcum_rapier;
damage = 75; -- ALOT more damage than the default sword
swing_speed = 5; -- ALOT less cooldown
};
function ori_rapier:new(owner: Player?) -- this will override item.new, but we can still use
-- its code with a little trick akin to java, c#, etc.
-- we would implement extra constructor logic here
return self.super.new(self, owner);
--[[
this calls our superclass's implementation for :new().
"sword" itself does not redefine :new(), but remember
that it inherits "equippable" which also inherits "item".
"item" does have a definition for new which will be used.
]]
end
function ori_rapier:canBackstep(): boolean
return true; -- we should be doing checks to see if we can perform a backstep
end
function ori_rapier:backstep() -- food for thought. our rapier should have a cool ability!
-- backstep logic
end
return ori_rapier;
Let’s test both instantiable classes:
local wooden_sword = require(--[[ wooden sword module path ]]);
local orichalcum_rapier = require(--[[ orichalcum rapier module path ]]);
game.Players.PlayerAdded:Connect(function(plr)
local sword = wooden_sword:new(plr); -- call our constructor and pass in "owner"
print(sword.damage); -- the wooden_sword class itself doesn't specify damage so
-- we reference sword's "damage" field (which is 10).
sword.damage *= 2; -- thanks to metatables, we can just override this
-- specific sword's damage with a single operation.
print(sword.damage); -- should be 20
print(sword:canSlash()); -- should be true
sword:slash();
task.wait(.2);
print(sword:canSlash()); -- should be false
task.wait(.3);
print(sword:canSlash()); -- should be true
local rapier = orichalcum_rapier:new(plr);
print('dmg: ' .. rapier.damage .. ', dex: ' .. rapier.swing_speed); -- should be "dmg: 75, dex: 5"
print(rapier:canSlash()); -- should be true
rapier:slash();
task.wait(.2);
print(rapier:canSlash()); -- should be true again since our swing speed is enhanced
end)
I focused mostly on structure rather than actual functionality because server-client communication and hitbox logic are entirely different rabbit holes, but a streamlined OOP structure seeks to make it a little bit more intuitive down the line despite the difficulty.
Mastering OOP is commendable and has many uses outside Lua, especially in the workforce where C-based languages (Java, C#, etc. are usually object-oriented) dominate most enterprises. The approach I handed you will probably seem much simpler once you take courses that stress inheritance and data structures, as you can then equate several aspects to Lua’s metatables.
Then again, my opinion is advice at best, so don’t hesitate to cast it aside if it’s too inconvenient to adopt. I’ve learned as a hobbyist that obsessing over structure and optimization can be fun at first but eventually very draining and demotivational.
Think of your time dedicated to learning an object-oriented approach as an investment. Try to master it if you believe your time is well-spent, but save it for the future if you plan to learn it formally and for a better reason someday. Programming can be quite the subjective paradigm.