I’m working on a game that has characters with different abilities. Since I really want to organize my code, I’ve been researching paradigms such as OOP. However, I found that creating classes (calling .new() function) and inheritance can be redundant and confusing at times. Is there a certain way to use OOP efficiently as possible?
Usually for abilities I create a module that I can call which handles everything to do with the ability, as it does become a pain and honestly looks messy to call .new() for an ability. I only use Classes (OOP) when creating specific objects like a survival game, plants, builds, etc. This makes it so each plant or build is more dynamic. How you choose to use it is up to you, but this is how I use it.
In my opinion, OOP should only ever be used to reduce repetition. For example, if a controller/service (a unit that implements a specific game mechanic or system) requires some sort of data structure for each player (for instance, a data container perhaps), then using OOP would be a great way to organize that.
However, using it anywhere else is typically not worth it, and it will slow down your program, it will make it harder to debug, and it will make it harder to maintain overall.
-- Skill Functions
return {
SwipeSword = function(self, target)
if self.Weapon.Type ~= "Sword" then return "You need to equip a sword to use this skill" end
target:TakeDamage(self.Damage * self.Weapon.DamageMultiplier)
end
SpinAttack = function(self, target)
-- code
end
AirAttack= function(self, target)
-- code
end
DoubleShot = function(self, target)
-- code
end
}
-- Character Data
local SkillFunctions = require(game.ReplicatedStorage.SkillFunctions)
return {
Swordsman = {
Instance = game.ReplicatedStorage.Characters.Swordsman,
Health = 10,
Damage = 3,
MainSkill = SkillFunctions.SwipeSword,
SecondarySkill = SkillFunctions.SpinAttack,
}
Bowman = {
Instance = game.ReplicatedStorage.Characters.Bowman,
Health = 6,
Damage = 4,
MainSkill = SkillFunctions.AirAttack,
SecondarySkill = SkillFunctions.DoubleShot,
}
}
-- Character Class
local CharacterData = require(game.ReplicatedStorage.CharacterData)
local Notification = require(game.ReplicatedStorage.Notification)
local class = {}
class.__index = class
function class.new(characterType)
local data = CharacterData[characterType]
assert(data, "Character type not found")
local self = setmetatable({}, class)
self.Type = characterType
self.Instance = data.Instance:Clone()
self.Health = data.Health
self.Damage = data.Damage
self.MainSkillFunction = data.MainSkill
self.SecondarySkillFunction = data.SecondarySkill
return self
end
function class:GiveWeapon(weapon)
self:DropWeapon()
self.Weapon = weapon
self.Weapon.Instance.Weld.Part0 = self.Instance.RightHand
end
function class:DropWeapon()
if self.Weapon then
self.Weapon.Instance.Weld.Part0 = nil
self.Weapon = nil
end
end
function class:MainSkill(target)
local notification = self:MainSkillFunction(target)
if notification then Notification:Notify(notification) end
end
function class:SecondarySkill(target)
local notification = self:SecondarySkillFunction(target)
if notification then Notification:Notify(notification) end
end
function class:TakeDamage(amount)
self.Health -= amount
if self.Health <= 0 then
self:DropWeapon()
self:Destroy()
end
end
function class:Destroy()
-- code
end
return class
local CharacterClass = require(game.ReplicatedStorage.CharacterClass)
local WeaponClass = require(game.ReplicatedStorage.WeaponClass)
-- create 2 characters
local character1 = CharacterClass.new("Swordsman")
local character2 = CharacterClass.new("Bowman")
-- give character 1 a sword
local sword = WeaponClass.new("Sword")
character1:GiveWeapon(sword)
-- character 1 uses there main skill on character 2
character1:MainSkill(character2)
-- character 2 uses there secondary skill on character 1
character2:SecondarySkill(character1)
One thing you might notice when doing object oriented design is that you run into problems with type checking because when you pass a object to another script that script wont know that objects type unless you require there module and import the type and then you can easily run into circular dependencies if your always requiring all the classes that’s why I made Global Framework
Global Framework makes OOP and type checking easier
For cashgrab Bad joke nvm
OOP is good when its not abused;
You dont need creating a separate metatable for a damn singleton and more so if you are making a singleton then just go for functional programming.
There is 3 different most used types of OOP as since roblox doesn’t have any “official” OOP technique: Metatable OOP (good when you create A LOT of classes but very bad when you want speed
), C like OOP (full opposite of metatable OOP, its very FAST but could consume a tiny bit more memory, good for stuff that you use very often) C like OOP is when you clone a “generic table” and then mutate it, C like OOP contains dirrect referance to a function aswell works very good in Compiled (native) mode becouse it supports strict typechecking, Closure OOP, this type of OOP is technically even used by roblox’s built in Iterator functions like utf8 | Documentation - Roblox Creator Hub for example, this OOP is like extreme version of C like OOP becouse its the fastest but the most memory consuming out of them all and can’t really and shouldn’t be used everywhere.
It depends on what you wanna use OOP for. Maybe you might not even need it, and can just define some functions in module scripts to handle the data; just keep everything functional first, if u can.
OOP, just like any other techniques, has its own time and place. You shouldn’t use it for everything and it really depends on many factors.
It’s also important not to restrict yourself with some standardized practices but tweak it so it fits the best for your needs. A simple rule of thumb is that the more abstract you want to go the better it is to use OOP and vise versa.
For example, if you’re creating let’s say an RTS game, or a base builder, OOP will absolutely save all your braincells as it made exactly for handling custom datamodels, with common behaviors that can differ in specific cases. However if you’re going for something where one database module with just things like numerical stats and skill functions would do, using OOP might become an unnecessary burden.
It is important to note that you probably cannot make a “bad choice” if you decide to just use OOP for literally everything, as it can fulfill any role, but it also comes with a cost in being objectively more time demanding to script properly when much easier solution is available.
Like other people have said, OOP is mostly only used when repetition or complex product types are needed, and CollectionService can’t solve said repetition.
OOP is not the holy grail, it’s good at representing some things, but not all things. Don’t go down the rabbit hole of purely OOP, else you’ll wind up using odd workarounds for scenarios that wouldn’t exist if you didn’t use OOP!
Plus, pure OOP causes a lot of headaches. No common language is a true purely OOP language—even Java or C#—because nobody wants to write purely OOP code.
I say this in only the context of Roblox, OOP is better off of Roblox, but what I said still (mostly) applies.
In my game I use it to set up any marbles that are spawned, much easier, and it allows for the process to happen quickly. I have a module that has a .new() thing, and when the player spawns, the client requests for a marble to be spawned, which calls the .new() function.
You should not immediately jump into OOP if you wish to create a system.
A programmer should always consider multiple ways of creating a system, a great number of options should be considered before implementing a solution based off one these options.
If you’re still going with OOP however, I would suggest OOP with Composition. If you use it well, you may be able to create a great system for your abilities. I’ve also created a module which should make your experience easier when dealing with OOP on the Luau ecosystem.
Composition over inheritance, use the type checker, and - most importantly - don’t try to force everything into the OOP paradigm. Generally speaking, your code should be structured in the same way you think about it.
OOP is a very useful technique. It helps you from copying and pasting the code over and over. You can write a class for example, guns. Instead of going to each script to change the damage, how it works, fire of rate, etc, i can just write a module script and that will be used on my OOP class. It also makes it cleaner.
When should you use this:
Personally i use this when im making code for more than 1 tool, like the gun example. Or when my code is long and a bit complex.
How to use:
There are many guides on youtube or even on dev forum, but the most important factor is metatables. Think of them like normal lua tables but whenever lua wants to look at a class, it goes there.
Like the developers said, use composition over inheritence. It helps you very much in terms of flexebility. It entirely depends on your code structure and your overall game, do you want your class to be inherinted or do you want your class to be as flexible as possible? Thats up to you.