Better Object Oriented Programming Support

As Roblox developers, we often attach special meaning to Object Instances which we customize to fit our meaning by adding a script to the object itself and then using script.Parent to reference that object. However, this tends to cause a disconnect especially when we want to interact with that object from a separate script (Yes, I know about BindableFunctions but bear with me here). For true OOP, the object and code should be (or appear to be) one. What I think would be great is if there was some way to Extend the Object Instances themselves to fit them to our needs.

I am proposing that somehow joining a table to an existing object (like you would use a metatable) we can define additional methods and properties and use those methods directly on the object.

For example, lets say you want to spawn a bomb and have it explode after so many bounces. The bomb object will very likely be a Part, so we want to extend that Part to do what we want. There are a few ways to go about doing this, but I think one of the best would be able to create a CustomObject that you specify which object type it Inherits from.

-- Implementation idea:
-- A modulescript named "Bomb" would go into a special folder under game called CustomObjects.

local bombClass = {}
inheritFromClass(bombClass,"Part") -- Essentially works like setmetatable.  Anything missing from bombClass will pull from Part.

function bombClass:new() -- Constructor, could be called automatically after Instance.new("Bomb")
	self.Touched:connect(function() checkExplode(self) end)	
end

bombClass.NumBounces = 0

function bombClass:Explode() -- self would be the Part object
	local ex = Instance.new("Explosion")
	ex.Position = self.Position
	ex.Parent = self
    delay(1,function() self:Destroy() end)
end

function checkExplode(self)
	self.NumBounces = self.NumBounces + 1
	if self.NumBounces > 10 then
		self:Explode()
	end
end

return bombClass

Then somewhere in a regular script…

local bomb = Instance.new("Bomb")
bomb.Position = Vector3.new(0,100,0)
delay(10,function() if bomb.Parent then bomb:Explode() end)
6 Likes

This is not a good idea. Here are a few of the problems with it:

  • How do you serialize this with the place file?
  • How do you replicate this to clients?
  • What would the replication behavior even be?
  • What happens if you add two classes with the same name?
  • Do your methods shadow the built-in methods?

Here is the approach I personally (not as an official representative of Roblox) recommend. Have a single server script, and a single client script, and write all your code in module scripts. Use the idiomatic Lua OOP paradigm:

local Bomb = {}
Bomb.__index = Bomb

function Bomb.new(instance)
    local self = setmetatable({
        instance = instance,
        numBounces = 0,
    }, Bomb)

    self.touchConn = self.instance.Touched:Connect(function(part)
        self:CheckExplode(part)
    end)

    delay(10, function()
        if self.Instance.Parent then
            self:Explode()
        end
    end)

    return self
end

function Bomb:CheckExplode(part)
	self.NumBounces = self.NumBounces + 1
	if self.NumBounces > 10 then
		self:Explode()
	end
end

function Bomb:Explode()
    local ex = Instance.new("Explosion")
    ex.Position = self.Position
    ex.Parent = self
    delay(1,function() self:Destroy() end)
end

function Bomb:Destroy()
    self.touchConn:Disconnect()
    self.instance:Destroy()
end

return Bomb

Then hook your Bomb class up to the data model somehow. There are a few ways to do this, but the one I recommend is to use CollectionService. You can call the setup() method from your server script when the server starts.

local CollectionService = game:GetService("CollectionService")

Bomb.bombList = {}

local BOMB_TAG = "Bomb"

function Bomb.getFromInstance(instance)
    return Bomb.bombList[instance]
end

function Bomb.setup()
    local function add(instance)
        local bomb = Bomb.new(instance)
        Bomb.bombList[instance] = bomb
    end

    local function remove(instance)
        local bomb = Bomb.bombList[instance]
        if bomb then
            bomb:Destroy()
            Bomb.bombList[instance] = nil
        end
    end

    for _,instance in pairs(CollectionService:GetTagged(BOMB_TAG)) do
        add(instance)
    end
    CollectionService:GetInstanceAddedSignal(BOMB_TAG):Connect(add)
    CollectionService:GetInstanceRemovedSignal(BOMB_TAG):Connect(remove)
end

There are a number of advantages of doing it this way.

  • You do not ever need to use BindableFunction/BindableEvent, or any Value objects in order to pass data around.
  • It is easy to perform operations that act on all of the bomb objects in existence, like a special ability that clears all the bombs in a radius.
  • It is much easier to handle things that are ordering sensitive, because Roblox does not guarantee any order for when scripts run.
  • You can reuse code more easily.
  • You are not copying and pasting script objects into every single bomb, making it hard to change the code.
  • Even compared to LinkedSource, you still avoid a number of downsides, like being unable to add a new ModuleScript or BindableFunction to every instance because the structure is fixed in place from the initial version you make.

As far as I know, many developers have switched to using approaches like this now that we offer first class support for it. The old way of having a thousand copies of the same kill brick script that you now can never change are moving behind us.

22 Likes

In my opinion, OOP and Lua themselves don’t actually mix well; OOP tends to be slow on the Lua end so unless you have something with a full C side implementation you’d likely end up writing laggy code when say trying to call a method every other frame or any sort of performance-heavy algorithms relating to a specific instance of a specific object type.

1 Like

Having a C side OOP implementation would be way slower, because now you have to cross the Lua/C boundary, which is very expensive. The implementation I just described will not be the bottleneck in your code unless your code consists of calling “Number:IncrementByOne()” ten thousand times per second. In order to get better performance, you have to contort your code in ways that don’t scale:

  • Declaring Lua functions in scope and calling them directly. This avoids any expensive table lookups. This is a pretty fast way to hit the limit of 200 local variables.
  • Not using functions at all, store everything as variables. Not only do you avoid table lookups, you also avoid expensive function calls. You will quickly hit the limit of 200 local variables and be unable to continue writing code.
8 Likes

I did think about some of those implementation issues beforehand, but have limited exposure to how roblox works behind the scenes so I may be off base here.

How do you serialize this with the place file? Only the ModuleScripts would need to be saved. I kind of envisioned the classes only existing during run-time… but for someone trying to copy/paste the objects could either be forced to have Archivable set to false, or the object could just be saved as its BaseType (“Part” in this instance) with no expectation of saving additional state.

How do you replicate this to clients? Since its a ModuleScript, could easily replicate that to the client and have the client construct the Custom Class from the ModuleScript

What would the replication behavior even be? Could additional data fields for a given object come across the network? If the client was able to construct the class from the ModuleScript, it will understand what those extra data fields are for and set them appropriately.

What happens if you add two classes with the same name? Run-time error or ignore with warning.

Do your methods shadow the built-in methods? If by Shadow you mean Override then yes, I would think as a general rule of OOP inherited classes should override anything of the same name in the BaseClass. But given that this might violate some Lua/C barrier I could understand if that wasn’t allowed.

There are probably better ways to implement this but I’ll have to leave that to a Roblox Engineer if they choose to experiment and see if this is feasible.

1 Like

Keep it simple. Here’s what I usually do:

local Class = {}
local Methods = {}
local Metatable = {__index = Methods}
function Methods:SomeMethod()
	print(self.Something)
end
function Class.new()
	return setmetatable({
		Something = 1;
	}, Metatable)
end
return Class

Using the same table for Class, Methods, and Metatable works, but separating them is more flexible.

I find dependency injection to be much more powerful than inheritance. My game is over 100,000 lines and I basically use the above snippet everywhere (hundreds of times).

2 Likes

I am not sure if this feature exist, but I am wondering if one day Roblox will allow us to create our own objects that appears in explorer. I know about OOP Programing, I am able to create my own properties, but I would like to have an option of creating my own object. For example, let’s use the MarketplaceService as a new object. As you can see you can access it directly from the game, it has it’s own functions and properties. This type of stuff I would like to achieve. This is why, I would like to have my own option of creating my own objects and select who has access to it, if the Client or Server to it’s functions and properties. :slight_smile: I don’t know if you understand my point. Thank you, focasds.