Where can OOP be used?

I know this has been asked a million times but I’m really struggling to find a use case for OOP in my current scenario, take for example:

Lets say for example I have a plane spawner which when requested, spawns a plane such as a fighter jet. When that plane is spawned, then this code runs:

local PlaneModule = require(game.ReplicatedStorage.PlaneModule)
PlaneModule:RegisterVehicle(spawned_plane)

Inside of the “PlaneModule” it will look something like this:

-- PlaneModule.lua
local PlaneModule = {}

function PlaneModule:RegisterPlane(model)
    local health = model:WaitForChild("Health")

    health:GetPropertyChangedSignal("Value"):Connect(function()
        if health.Value <= 0 then
            PlaneModule:Explode(model)
        end
    end)
end

function PlaneModule:Explode(model)
    -- blow up
end

return PlaneModule

Now this will manage different types of planes such as Bombers, Fighters, etc. My question is how could I apply OOP into this?

To me this just looks like the best way to go about programming a game using modules.
Any help is appreciated, thanks.

Hi I’m not exactly sure if this is what you mean, but you’d do something like this:

-- Module --

local PlaneModule = {}
PlaneModule.__index = PlaneModule

function PlaneModule.new(Model)
    local self = setmetatable({}, PlaneModule)
    self.Model = Model
    self.Health = Model:WaitForChild("Health")


    return self
end

function PlaneModule:Explode()
    local Model = self.Model

    -- explosion
end

return PlaneModule


-- Server Script --

local PlaneModule = require(game.ReplicatedStorage.PlaneModule)
local NewPlane = PlaneModule.new(...)

NewPlane:Explode()

Also some of my spelling may be wrong I rushed this*

So, the code you posted is an example of object oriented programming.

When people talk about “objects”, they’re really just referring to tables. Effectively every object in roblox is just a table. Module scripts themselves are tables, and the results of their source code are tables aka “objects”.

If you want to get into nuance here, the real advantage of object oriented programming is the ability to create constructor functions, which creates an object of a specific class, which can inherit properties and functions of other higher-level classes (which are just indices in a table)

For example –

Instance.new("Part")

Instance is a class (aka table)

.new is a constructor - which is a way of saying a function that returns a new object. The function is a value, and “new” is the key)

Part is a paramater that specifies which new class of object you are trying to make.

If you go on the wiki and look at the page for Part, on the side column you’ll see something about ‘inherited from PVInstance’. You might be wondering “Well what’s PVInstance?” – its just another class that refers to a specific subset of Instances. In this case “PV” refers to Position & Velocity, i.e. these are physical objects in the workspace. All PVInstances fall under the umbrella of Instance, but not all Instances are a PVInstance. The easier example would be BasePart and Part

Why is it useful? Well remember that thing called inheritance? Classes can inherit properties, functions, etc. from other classes. This is where the role of Metatables comes in and allows you to create new classes without having to copy over every single bit of data from its ancestor class. You can also retroactively add functions to a higher level class that it’s children classes can then use without having to add that same function over and over. You can also add individual functions to individual classes as you find necessary. If you want to edit them, you only have to edit the function in one spot. Sound famiiar? It’s the same way they talk about module scripts, because its the same concept.

If we bring it full circle, OOP is the style of programming that enables you to create a top level class (e.g. Plane) and give it a series of properties, (method) functions, and whatever else you want, that it’s children classes (e.g. Jets, spyplanes, propeller planes, etc.) can all use, without having to rewrite each line of code for each individual class.

You should use it when it makes sense to use it, not because OOP is some magically superior form of coding. It’s why lots of people use it for guns, because a gun is relatively the same ignoring a few subtle changes between each property like damage, fire rate, etc.

OOP can be used whenever two or more objects (in this case, two different plane types) have the same functions plus more. You can use OOP to create a plane class that can fly, explode, etc. and then you can create another class, in this case it might be a bomber, that inherits from the plane class so that it can fly and explode the same way the plane would along with having the other functions of that class.
In addition, you can make the class take in parameters so that your still able to tweak the functionality of the pre-made functions, like flying responsiveness or explosion size.

2 Likes

Thank you for your response,

So I guess the main benefit of OOP compared to the example I posted is Inheritance?

  • Take for example I have a vehicle class which handles basic vehicle logic such as taking damage, exploding, etc.

  • Then I make a plane class which inherits all these functions from the vehicle class and then has its own functions such as stall or fly.

  • Then I make a bomber class which inherits all the planes functions and then has its own functions such as drop bomb.

I’m a bit confused as to where I would actually handle the plane and call those functions. When the plane is spawned then would the script call “.new” on the plane class or the bomber class or the vehicle class?

I’d like the spawner script to only run .new and the rest of the plane is handled inside a module.

Also, do these classes apply across the client - server boundary? so will I have to create the Vehicle class again and whatnot on the client once I already done it on the server?

For the sake of organization, I’d start by defining your classes in a module script and requiring that module in a server script. The server script can then call the constructor function (that you defined in the module script), which will create and return the object within your server script. From there, you can call your objects functions in the same server script. @ethann_n’s post shows a great example of this. Here’s a detailed follow up using that:

Module Script

local PlaneModule = {} -- Our top level table that will be indexed by the server script
PlaneModule.__index = PlaneModule -- The index metamethod allows for recursive searching through tables

function PlaneModule.new(Model) -- Constructor function that creates and returns an object
    local self = setmetatable({}, PlaneModule) -- Self is referring to the object we want to create, and we set its metatable to be the table the modulescript returns so we can access its functions
    self.Model = Model:Clone() -- Give our object a property called Model and assign it to your desired value, same idea as Player.Character. I clone it here so we dont have to call :Clone() each time we create a new plane
    self.Health = Model:WaitForChild("Health") -- Another property


    return self -- Return the object we just created
end

function PlaneModule:TakeDamage(damage:number)
     self.Health -= number -- Lower the health property
     if self.Health <= 0 then -- Check if the plane is out of health
          self:Explode() -- Calls the method function defined below this one
     end
end

function PlaneModule:Explode()
    self.Health = 0
    local ex = Instance.new("Explosion",self.Model)
    -- whatever else you want to happen in the explosion
    self.Model:Destroy() -- Call destroy on the model assuming its an instance
    self = nil -- set our object to nil to clean up memory assuming were done with it
end

return PlaneModule

Server Script

local PlaneService = require(game.ReplicatedStorage.PlaneModule) -- or whatever path you set

local newPlane = PlaneService.new(game.ServerStorage.PlaneModels.Jet) -- Set the parameter as the model we want

-- Now if we wanted to call one of those functions, we can do so using newPlane, for example:
if math.random(1,5) == 5 then
     newPlane:Explode() -- Plane has a random 1/5 chance of exploding (just to demonstrate)
end

Here’s more info on methods and the relevance of using self in OOP



If you use module scripts to define your classes, then the server will have a copy of the module and each client that requires it will have its own copy. The code will be the same, but a change from one will not propagate to the others - just as is the case with any other module script.

2 Likes

In my case I’m also trying to follow single script architecture.

So for example, in the vehicle spawner module it would get the vehicle the player wants to spawn and then call get the module corresponding to the vehicle name and then call .new on it.

Let’s say a player wants to spawn a bomber, I’d preferably like it so the vehicle spawner calls .new and then the bomber handles itself inside its class module. Is this possible?

You’d theoretically be able to handle whatever functionality of the bomber you’re trying to implement within the module, either through calling its class functions, or establishing connections to events when you first spawn your object.

The only real caveat to this would be extensive user input control, as user input has to be detected on the client, but you could set up the remote functions and the code that responds to said remotes being fired within that module.

1 Like

OOP can also be useful just in general for handling things that you are creating multiple times with different properties on initialization.
I use it for an NPC module I made that lets me make as many NPCs as I want and initialize them with different names and outfits when I do it. Then I can just call the NPC object that was created and run other functions on it.

Example:

local NPCObject = require(path.to.module)

--.new(character: Model, name: string, userId: number, defaultEmote: string)
local NPC1 = NPCObject.new(workspace.NPC1, "MrLonely1221", 21467784, "Wave")
local NPC2 = NPCObject.new(workspace.NPC2, "Roblox", 1, "Dance")

NPC2:Emote()

This takes in the character, name to display, userId to pull the outfit from, and the default emote, then you can call all the methods you added to the OOP module on each Object without interfering with the others.

It’s basically just re-usable code that you can inherit for other Objects to use the functions from the other class in that new Object class, or you don’t even have to use it to make new classes.

1 Like

or you could use composition instead of inheritance

1 Like

This is a very good and easy to follow explanation of inheritence, you should consider moving this as a community tutorial!

1 Like

So from my understanding I would be able to do something like:

-- PlaneSpawner.lua

local BomberClass = require(script.BomberClass)
BomberClass.new(spawned_plane)
local Bomber = {}
Bomber.__index = Bomber

function Bomber.new(model)
	local self = setmetatable({}, Bomber)
	self.Model = model
	self.Health = model:WaitForChild("Health")
	
	self:_init()
	return self
end

function Bomber:_init()
	print("loading bombs...")
end

So in the plane class there will be functions such as MovePlane. In the Bomber class would I then call that function since it inherited it? or instead should I do .new on the Plane class?

I guess my view of OOP in this scenario is that I call .new on the Bomber class and all the functions in the PlaneClass and the VehicleClass would run under the hood.

Could you explain the difference between the 2?

In this case shouldn’t the module you are referencing handle the npc instead of the script? what I mean is that shouldn’t :Emote() be called inside the module so that the whole npc is handled by the module and not the script you are calling .new inside of?

Realizing I never gave the full example of inheritance and such, so here is a follow up script that walks through the whole thing.

Module

local PlaneModule = {}
PlaneModule.__index = PlaneModule

function PlaneModule.newPlane() -- Constructor function to create a default plane class, recieves a physical model as its argument to set the plane model
	local plane = setmetatable({}, PlaneModule) -- Our object (aka table), whose metatable is set as the 
	plane.MaxHealth = 100	-- Give it some health
	plane.Health = plane.MaxHealth	-- Always spawn at full health
	plane.Name = "Plane" -- Give it a name to help with clarifying print statements

	return plane -- Return our object
end

function PlaneModule.newBomber(Model:Model) -- Another constructor which makes a bomber class
	local bomber = setmetatable(PlaneModule.newPlane(), PlaneModule) -- Get our newPlane object (i.e. we're going to use an existing class as the template for our bomber  class, thus inheriting its info) and, set its metatable to PlaneModule so we have access to our method function 'Explode'
	print("Bomber HP: "..bomber.Health) -- We never defined "Health" it in this function, but we still have access to the variable since we are inheriting it from our object created with newPlane()
	bomber.Name = "Bomber" -- Name technically already exists, like "Health" does, but lets overwrite it to make it easier to keep track of. 
	-- We could also overwrite "Health" or any other property we wanted 
	
	-- Now lets create a special feature (function) for just the bomber class
	bomber.BombCount = 10 -- Keep track of how many bombs we have
	bomber.DropBombs = function(bombsToDrop:number) -- Create a class-specific function called "DropBombs" that only exists for the bomber class
		if bomber.BombCount > 0 then -- Make sure we have some to drop
			bomber.BombCount -= bombsToDrop -- Subtract the amount we dropped
			print('dropped '..bombsToDrop..' bombs, '..bomber.BombCount..' remaining')
		else
			print('Oops, all berries') -- No more bombs :(
		end
	end
	return bomber -- return our object
end

function PlaneModule:Explode()
	print(self.Name.." Exploded")
end

return PlaneModule

Server

-- So now, lets make our objects
local PlaneModule = require(game.ReplicatedStorage.PlaneModule) -- Just an example path

local plane = PlaneModule.newPlane() -- Our plane object
local bomber = PlaneModule.newBomber() -- Our bomber object

-- Both can use the explode function since they both inherit it from PlaneModule
plane:Explode() --> "Plane exploded"
bomber:Explode() --> "Bomber exploded"

-- But only the bomber class can use DropBombs
bomber.DropBombs(math.random(1,3)) -- Drop a random amount of bombs, will print how many we dropped and how many remain

local success, errorMsg = pcall(plane.DropBombs) -- Use pcall because we know it will error
if not success then 
	print(errorMsg) --> Attempt to call a nil value, (Because we only defined DropBombs for the Bomber) 
else
	print('what the dog doin???') -- Shouldn't be possible under normal circumstances
end

-- Lastly, lets say you only want some bomber classes to be able to use drop bombs
-- Well, we can also delete that function on a case-by-case basis
if math.random(1,3) == 3 then -- Lets say this specific bomber class has a 33% failure rate
	bomber.DropBombs = nil -- Remove the function, because again, just a table and index
end

-- You could also remove an inherited function in the module script by doing the same thing (setting the key that is paired to the function to nil) to get rid of that function for the class as a whole.

You can continue that general setup infinitely to keep creating new classes that inherit whatever you specifically want them to and editing them accordingly.

I have a few questions:

  • Would it be better to separate the Bomber class and the Plane class into different modules?
    (I assume yes, but just wanted to confirm if that’s considered best practice.)
  • Is it proper OOP usage if I handle functions like :DropBomb() directly inside the Bomber class?
    For example:
-- Bomber.lua

local Plane = require(script.Parent.Plane)
local Bomber = setmetatable({}, Plane)
Bomber.__index = Bomber

function Bomber.new(spawnPosition)
    local self = setmetatable(Plane.new(spawnPosition), Bomber)

    self.BombCount = 5
    self:Init() -- Here is what I'm mainly talking about

    return self
end

function Bomber:Init()
    -- Drop bomb instantly
    self:DropBomb()
end

function Bomber:DropBomb()
    self.BombCount -= 1
    print("Bomb dropped! Remaining:", self.BombCount)
end

return Bomber
  • And lastly:
    Let’s say I have 3 different plane types, each with its own module/class, all inheriting from Plane.
    Would I have to manually call :Fly() (which is defined in Plane) inside each one of their :Init() or .new() methods?
    Or is there a clean way to make it automatically run, so that each specific class only has to worry about its own unique behavior and not shared behavior like flying?

Up to you. More scripts = more modules = more memory consumption. But youll also increase memory consumption the longer your script gets as well, however you’d have to compare each style in a server to get a better idea of which is going to be more efficient for you. I will say it tends to be easier to organzie with multiple scripts, but you do also have to require each module you want to inherit, which, depending on your project size can get obnoxious.


Yeah that’s fine (referencing your example)

In your descendant class you are most likely creating it by calling an existing constructor function as this is how you maintain inheritance without repeating yourself. Any functions you call in that original constructor are going to be applied to your new object as well:

local PlaneModule = {}
PlaneModule.__index = {}

function PlaneModule.newPlane()
	local plane = setmetatable({}, PlaneModule}
	plane:Fly() -- This gets called anytime you do PlaneModule.new()

	return plane
end

function PlaneModule.newBomber()
	local bomber = setmetatable(PlaneModule.newPlane(), PlaneModule) -- We call PlaneModule.newPlane here so its going to run Plane:Fly() by default

	return bomber
end

function PlaneModule:Fly()
	-- A man walks into a bar and says ouch.
	-- Get it, cause it was a bar? like a pole? Hah
end
1 Like

I use an module for the NPCObject, then have an NPC handler module I call an init function on to make all of them, then I can call a function to get an NPC by name or by id from any script, then can interact with the NPC.
This was just a down and dirty example for simplicity’s sake.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.