What is the best way to work with OOP?

Hi,

I learned about object-oriented programming a while ago, but I never really used it in my projects. I know how it works in Lua and how to create objects from classes. However, I’m uncertain about the way in which OOP should be used.

For instance, if I would create a class called Enemy so that multiple enemies can share similar behavior, how would I actually access the Enemy objects that are created?

local Enemy = {}
Enemy.__index = Enemy

function Enemy.new(character)
    local self = setmetatable({}, Enemy)
    self.Instance = character
    self.MaxHealth = 100
    self.Health = 100

    return self
end

function Enemy:Damage(damage)
    self.Health = self.Health - damage
end

So, if I want to create an Enemy object and access its properties and methods, there are two ways that I can think of:

  • Store it in a table inside a ModuleScript called CharacterManager that would contain all the character objects and require it everytime I want to access them
  • Create a ModuleScript inside of the character model itself (to sort of act as a component like in Unity) which has the following:
local Enemy = require(path.to.module)

return Enemy.new(script.Parent)

And do something like

local Enemy = character:FindFirstChild("Enemy")
if Enemy then
    require(Enemy):Damage(10)
end

Would it be inefficient?

5 Likes

The second method is not useful or even remotely efficient unless you are creating a custom humanoid.

The first method would be alright but would need to be in serverscriptservice.

If you want them all to have the same behavior you don’t need them to be stored in a table of a ModuleScript. Instead just do the necessary behavior functions that they will all need and then make them require it. If you need to access them, place them all in a workspace folder and just determine a way to differentiate between them (ID system or name them differently idk).

If you want to damage them just have a Humanoid, it’ll save you a lot of time. And if they die then have a module script function for what happens on death.

Generally you can avoid OOP in roblox because the workspace can serve as a work-around. Plus in my experience, unless you are working on something that needs to virtually simulated (ie a neural network, or hive mind) in roblox it is highly inefficient and makes the code much too unreadable.

Of course if you are working on a UI game or something that can’t use the workspace or anything else to work around OOP then yes your table system would be alright.

Yes, sort of. Normally what you’d have inside the CharacterManager would be a table of the Enemy objects (tables) that were returned each time you called Enemy.new(enemyCharacter). You only require the Enemy module once, and store its class table in a variable you then call new on for each enemy you make.

No, you probably don’t want to do this because it makes independent components of the enemies that don’t communicate with anything else, and chances are pretty good, at least if these are NPCs, that you want something on your server akin to your idea of a CharacterManager that is tracking them. That’s easiest and cleanest to do if the manager creates them, rather than having to use something like a BindableEvent to register the enemies with the server code. They could also get a reference to a manager that is injected via a class variable, but I still would not prefer this solution. It seems more cumbersome to maintain, especially if you have more than one stored enemy Model, and you need them all to have this stub ModuleScript. That can be avoided.

How this sort of thing generally looks in one of my games is that there is an NPC manager module in ServerScriptService (SSS), and it requires an NPC module also in SSS, e.g.:

local NPC = require(SSS:FindFirstChild("NPCModule"))

When the manager spawns a new NPC, it clones the NPC’s model from ServerStorage or ReplicatedStorage, and then does something along the lines of:

local npcModel = ServerStorage:FindFirstChild("SomeNPC"):Clone()
local newNPC = NPC.new(npcModel)
table.insert(self.spawnedNPCS, newNPC)

This all being inside the manager class.

6 Likes

The Enemy class was more of an example because if it was just to handle health then I would have just used the character’s humanoid like you said. One of my goals with it is to possibly have multiple different stats for each enemy such as an Armor rating and I have read in other posts on the forum that Value Objects should be avoided in favor of having values stored in scripts, or in ModuleScripts since only their variables can be accessed if they return it.

Also, another goal with my approach with OOP is related to tools as well. I want to minimize copy pasting as even though the scripts inside the tools can access functions in a ModuleScript, I would have to modify all of them if let’s say I decided to add a new functionality because I would still need to call the ModuleScript’s new functions from within the tools.

1 Like

Blasphemy! Who would say such a thing!? :rofl:

Just to be clear, so that what you may have read in this other thread is not taken as too-wide a condemnation of ValueObjects, using ValueObjects inside the prototype Model (that you clone) for each NPC is totally fine for setting their initialization values. This is exactly how Value Objects are intended to be used and is a completely acceptable practice. Your Enemy class would read the values off the objects one time (ideally one time per server, not per NPC creation if there are a lot of them), to initialize the in-memory properties of the instance of Enemy. Of course you can also give each NPC type a table of values in a module instead.

The specific usage of ValueObjects that is frowned upon is creating and destroying tons of them at runtime for values you’re going to read and write to a lot, such that the time it takes to get the reference to them, and the property change events they all fire, and having to clean up after them, all becomes unwanted overhead. In other words, you should not use a value object where you can use a variable or table.

What about stats that can be changed with abilities that can buff them or are used to track a certain resource like mana? Does this usage fall under the same category as the ones that are frowned upon?

The rule of thumb I would use is that any value known at publish time is fair game to go into a value object that is stored with your place. Something that changes a lot at runtime is better left as a member of your class. Things like min, max, defaults, etc… can all be value objects if you want that ease of editing them. I do this sort of thing for when I’m coding a system like loot drops or NPCs, but it’s a non-programmer game designer who is actually setting the values. This avoid them having to have scripts open in team create at the same time as the coders, or having to worry about breaking the game with a bad Lua table format.

3 Likes