I’m working on a combat game with different heroes and I’m trying to make use of OOP since a lot of the heroes will have similarities the issue though is trying to find an organized approach mine is kind of a mess. The idea was to have a shared base class for data, but the way I have my code makes it weird since things like getCharacter() and getRoot() have to be separated and it just didn’t turn out as nice as I thought.
function ServerFighter.new(player,character)
local self = Fighter.new()
local model = models.Fighter:Clone()
local AI = false
function self.getCharacter()
return model
end
function self.getRoot()
return model.HumanoidRootPart
end
function self.getHumanoid()
return model.Humanoid
end
if player and character then
player.Character = model
character:Destroy()
elseif not player and not character then
AI = true
end
model.Parent = workspace
CollectionService:AddTag(self.getRoot(),"CameraFocus")
model:SetAttribute("MoveSpeed",self.getMoveSpeed())
model:SetAttribute("JumpForce",self.getJumpForce())
model:SetAttribute("Health",self.getHealth())
return self
end
function ClientFighter.new(model,Client)
local self = Fighter.new()
local model = model
local rotationSmoothness = 0
local renderName = "Update"
local renderPriority = Enum.RenderPriority.First.Value
local function update()
faceOpponent()
end
function self.getOtherPlayer()
end
function self.getCharacter()
return model
end
function self.getRoot()
return model.HumanoidRootPart
end
function self.getHumanoid()
return model.Humanoid
end
function self.getMoveSpeed()
return model:GetAttribute("MoveSpeed")
end
function self.getJumpForce()
return model:GetAttribute("JumpForce")
end
function self.getHealth()
return model:GetAttribute("Health")
end
RunService:BindToRenderStep("Movement",Enum.RenderPriority.First.Value,function(deltaTime)
-- Movement Stuff
end)
RunService:BindToRenderStep(renderName,renderPriority,update)
return self
end
function Fighter.new()
local self = BaseCharacter.new()
local health = 1000
local moveSpeed = 17
local jumpForce = 50
function self.getMoveSpeed()
return moveSpeed
end
function self.getJumpForce()
return jumpForce
end
function self.getHealth()
return health
end
return self
end
I’m super inexperienced at object-orientated programming, I rarely use them so correct me if I am wrong, but couldn’t you do this in the Fighter.new() function?
function Fighter.new()
local self = BaseCharacter.new()
self.health = 1000
self.moveSpeed = 17
self.jumpForce = 50
return self
end
Same with the other scripts that use a function to return one singular value, not saying it’s impractical, but it’s honestly kind of unnecessary to call a function so you can get a constant variable. Maybe it’s your style of coding so it’s easier to make forward compatibility, which honestly I can’t blame.
It just feels a little more elegant and more practical basically.
This is my own opinion so ignore it if you want to. But it won’t hurt to just do self.getCharacter().HumanoidRootPart or self.getCharacter().Humanoid, honestly.
The biggest thing I see right away is that you’re creating several functions every time you make a new object, if you make self's metatable Fighter (and make fighter.__index fighter), you can have all of your objects share a single library of functions. Put it this way, say you want to create 5 methods for your class. If you create functions on a per-object method, 100 objects means you’re now creating 500 functions. If you use metatables, you can create 100 objects, and for 100 objects, you still only have 5 functions.
Second thing is that you can probably have a function that just takes a property and returns it. For example:
function fighter:getProperty(propertyName: string)
return self[propertyName] or self._model:GetAttribute(propertyName) or self._model:FindFirstChild(propertyName)
end
I usually like to use the note feature to create Sections in my code, for example, I can use them like
--\\Values//--
local RunService = game:GetService("RunService")
local Tween = game:GetService("TweenService")
local Value = "Hello World"
local IntegerValue = 12345
local BoolValue = true
--\\Functions//--
local function HelloWorld()
print("Hello!")
end
--\\Methods//--
RunService.Heartbeat:Connect() --Whatever lol
It helps navigate through your scripts a way that allows you to find important sections just by thinking of their use case!
You can also seperate Values based on their types, which is what i did here too.
As for the OOP side of the question, you can probably start with this principal: Have as few functions as possible
For instance, you can probably have a single function that returns an array of ALL character parts
function fighter:GetAllCharacterParts()
return self._model:GetChildren()
end
^There you could use the find Method from the table class to just find the character parts in the haystack, there’s obviously much better examples, but I only need simple ways to explain this.
*Try and cram as many abilities into a single function as you can, but try to keep each function serving its own general purpose, dont cram two character and two data saving functions into a single function, for instance.
Yeah the method I use for creating classes in Lua lets me have private variables which is usually what I do and then make getter and setter functions so I can’t accidentally make the jumpForce a string or something idk.
You have the same functions repeated over and over with 0 difference, all you need is OOP, you can also override the functions if you want. If you don’t know OOP u can ask me to make you a code.
You basically need to write the repeated functions in a main class and then use setmetatable.
local Object = {}
Object.__index = Object
function Object.new()
local self = setmetatable({}, Object)
return self
end
function Object:Destroy()
end
return Object
@Konjointed Made it use actual OOP stuff. download the rbxm (1.6 KB) file and view it, if you faced any problems, you can ask me
NOTE: I only edited the code you provided, any extra services/modules/classes required will not be there as idk anything abt them, you can easily add them manually.
I found an alternative method to writing classes that I prefer over the metatable method. My way gives the ability to actually make public and private variables or functions which I think is useful since when I do inheritance or even just create a new object of the class I can’t access things and accidentally change something. I do believe the metatable is faster for performance, but at the end of the day they both have the same result.
Edit: my method is referred to as the closure approach which is actually faster than metatables, but not as memory efficient
There’s no actual standard for scripting in Roblox, each programmer / studio have their own “set of rules”, when it comes to OOP in Luau it’s not a built-in feature, majorly due to the focus being metatables. However, there is a module which imitates classes (usually present in most of object-oriented programming languages).
I know that’s not exactly what you asked for, however you can implement inheritance which might solve your problem with the getCharacter() and getRoot() methods.
Edit: This class module is simply a wrapper to make the programmers life easier, under the hood it actually uses metatables.
Kind of random but I don’t feel like making a new thread. What are thoughts on using attributes with OOP? I tried experimenting with it and it seems useful since it replicates not sure if it’s practical though.
It’s really personal opinion. Both have their pros and cons.
Attributes were primarily created for team workflows and allow someone other than a programmer to tweak a custom object’s properties without accessing the code.
Personally, I would not use Attributes as the go-to method to storing object’s data because it’s limited to what data can be stored in Attributes. I would simply use them to expose some of the custom properties an object has for other developers.
However the primary benefit to Attributes that a table based data structure won’t have is being easily accessed by other scripts. As the Attributes are part of the object instead of the object being part of the data structure.
Well I’m probably over complicating things more then I need to, but I wanna try and do my own custom things instead of having to rely on the humanoid properties which is why I’ve made my own movement code which uses a move speed variable instead of the humanoid speed property issue with this is the server can’t adjust it without using remotes which is why having it as an attribute seems like a better option since it replicates.
In either case you’re going to have to cross the client/server boundary, be it manually via remotes or automatically through Roblox’s replication system. If you’re creating a custom character controller you may want to instead look into setting network ownership of the character to the client, and optionally on the server do movement validation to ensure they aren’t moving illegally. I agree with xZylter, I personally only use attributes if I need to expose something to someone who doesn’t program.
Even so, not all properties are replicated across client/server. If you look at the humanoid docs for example, you will see that many of the humanoid properties aren’t replicated. I’m not for certain but I would believe they aren’t replicated since the client basically has full control of their character wrt movement. This seems to be a common practice in games, but most of the time the server anticheat will pick up and deal with the player accordingly.
Alright sorry again another unrelated question, but I had some time to try out some different ways to write my code and I was trying to make more use of events and I think I found a way that could actually work fine only issue is I’m not sure if using FastSignal and remote events like this would end up confusing things.
-- ReplicatedStorage
-- Global so that any script can access this and listen to this event
BaseCharacter.onTakeDamage = FastSignal.new()
function BaseCharacter.new()
local self = {}
self.takeDamageRE = game:GetService("ReplicatedStorage").Remotes.RemoteEvent
function self.takeDamage(amount)
print("Taking ",amount,"of damage!")
BaseCharacter.onTakeDamage:Fire(amount,self)
end
return self
end
-- ServerScriptService
local Combat = {}
local BaseCharacter = require(game:GetService("ReplicatedStorage").Modules.BaseCharacter)
BaseCharacter.onTakeDamage:Connect(function(amount,obj)
print("Ok, I gotta do",amount,"of damage")
obj.takeDamageRE:FireAllClients(amount)
end)
-- ServerScriptService
Players.PlayerAdded:Connect(function(player)
local obj = ServerTestCharacter.new()
obj.takeDamage(25)
end)
I don’t think this confuses anything. Overall your code is certainly readable and it uses syntax like Roblox’s so I think it’s fine and it isn’t confusing from a readability perspective.
If you want to clean it up and not manually fire remotes from a script, you could probably return different signals/an entirely different interface depending on whether or not the environment is on the client. The only thing that sticks out and would confuse people, in my opinion, is that I think you’re creating events/assigning properties backwards which is an inefficient use of memory.
In essence, you should only create properties in the constructor that need to be made on a per object basis. For reference, events are properties in Roblox’s implementation.
For example,
local isClient = game:GetService('RunService'):IsClient()
if isClient then
-- In the case where this script is required on the client, we want to return just a dictionary of events
return {
onTakeDamage = script.OnTakeDamage.OnClientEvent;
-- etc
}
end
-- else, this is required on the server so we return the appropriate methods
local baseCharacter = {}
baseCharacter.__index = baseCharacter
function baseCharacter:takeDamage(amount)
self.health -= amount
script:FindFirstChild('onTakeDamage'):FireAllClients(amount)
self.onTakeDamage:Fire(amount)
end
function baseCharacter.new()
local self = setmetatable({
onTakeDamage = signal.new();
health = 100
}, baseCharacter)
-- optionally you can hook into real instance events as well as those would be created only on an as-needed basis
return self
end
return baseCharacter
On the server,
local baseCharacter = require(path.to.module)
local char = baseCharacter.new()
char.onTakeDamage:Connect(print)
char:TakeDamage(50) -- should print 50 because of the connection above^
On the client now,
local baseCharacter = require(path.to.module)
baseCharacter.onTakeDamage:Connect(print)
Using the metatable method will take up less memory as I mentioned in my post above.