I’m rewriting an entity system for my game using composition. Though, as I’m watching a video on how to add composition, he uses return setmetatable({}, metatable). I’m wondering how that works and how I can use that to make OOP classes or whatnot.
metatables can add metamethods to tables
roblox docs explain it better here
for OOP the most common approach is doing something like this:
local Class = {}
Class.__index = Class -- the index metamethod makes it so if you try to index a member of a table that isn't inside the table, it either searches a table (what this does) or does a function
function Class.new(x, y)
local self = {}
self.x = x
self.y = y
return setmetatable(self, Class) -- sets metatable to Class so that it has the __index metamethod
end
function Class:method() -- this syntax makes it so it automatically passes `self` as the first arg, nothing else
print(self.x, self.y)
end
local Entity = {}
Entity.__index = Entity
function Entity.new(stats)
local self = {}
self.speed = stats.speed
self.damage = stats.damage
self.hp = stats.hp
self.state = "idle"
return setmetatable(self, Entity)
end
function Entity:takeDamage(damage)
self.hp -= damage
end
-- you can add more methods here like update cycles if that's what you want your entity to have
return Entity
of course this is a very oversimplified and generalized example
then you could do
local Entity = require(path.to.entity.module)
local zombie = Entity.new({speed = 1, damage = 1, hp = 100})
zombie:takeDamage(1)
print(zombie.hp) --> 99 (hopefully)
also i just realized you specifically meant composition in the original post so here's how composition works i guess
composition is when you have different reusable “components” for different parts of code
for example: a Car could have an Engine (separate component/object) and could have Wheels (another separate object). the car object itself wouldn’t be the one directly handling engine rpm or wheel rotation. the respective components would be doing that work
here’s a small example i guess:
-- Wheels
local Wheels = {}
Wheels.__index = Wheels
function Wheels.new()
local self = {}
-- some properties here
return setmetatable(self, Wheels)
end
-- Engine
local Engine = {}
Engine.__index = Engine
function Engine.new()
local self = {}
-- some other properties here
return setmetatable(self, Engine)
end
-- Car
local Wheels = require(path.to.wheels.component)
local Engine = require(path.to.engine.component)
local Car = {}
Car.__index = Car
function Car.new()
local self = {}
self.wheels = Wheels.new() -- new wheels component
self.engine = Engine.new() -- new engine component
return setmetatable(self, Car)
end
this is useful because it makes it really easy to reuse components for different things, like you could probably reuse wheels component for a Bike class, etc.
also the difference between composition and inheritance (the other example with entities i provided) is that composition is a “has-a” (a Car has an Engine) relationship whereas inheritance is a “is-a” relationship (a Truck is a Car)
if anything here is wrong then feel free to correct me
return setmetatable({}, metatable) is the most efficient memory-wise approach one can take, ~0.046KB (newproxy(true) is at least ~0.062KB, unless newproxy(meta) gets supported, then it would be the cheapest at ~0.015KB). By the referenced table being empty, it forces the __index and __newindex methods to be called upon. Typically the methods, or tables, point to another table that stores the necessary values to be accessed or modified.
This approach setmetatable({}, ...), or using newproxy(true) is necessary when you want to support access modifiers, or other neat features.