All about Entity Component System


All about ECS!


Foreword

I am basically going to copy the exact format of magnalite’s post All about Object Oriented Programming because it is the most known post of a similar topic. At times, I will even be using the same exact wording to make a reference, so I expect you to have read the aforementioned post.
For the tutorial, I will be using Matter because I believe it is the best available ECS, you can follow this page to get instructions on how to install it. I want to give acknowledgement to other great alternatives:

Table of Contents

What is ECS?
How does it help me?
How do I make this work in Roblox?
Sources

What is ECS?

The Entity Component System (also known as ECS) provides infrastructre for representing distinct objects with loosely coupled state and behaviour. Many modern games developed outside of Roblox in the outer game industry uses ECS in some shape or form, namely Overwatch, Minecraft, Hearthstone, Rust and many more! An ECS world consists of any number of entities (unique ids) associated with components which are pure data that contain no behaviour. The world is then manipulated by systems that implement self-contained behaviour, each of which access a set of a specific component type.

How does it help me?

Most who are new to programming find it difficult to express what they want to achieve with their code because of their lack of proper and fundamental approach to logic. Most believe that it is hard to break things down to step-by-step.
OOP tells to think in terms of objects with layers of abstractions that handles underlying processes. For example, let’s say you’re making a racing game. It would be useful to be able to do car = Instance.new(“Car”, Workspace) and then be able to find useful information such as car.Position or use methods such as car:Respawn(). This is a problem because you are tying behaviour to a specific construct, which means you cannot use that behaviour without using that construct.
In simpler terms, you cannot use ::Respawn() without car unless you inherit those traits and behaviours. So, to make objects, you do inheritance. The following snippet shows what has been described.

Car = require(game.ReplicatedStorage.Car)
Truck = {}
Truck.__index = Truck
function Truck.new(position, driver, model, powerup)
    local newtruck = Car.new(position, driver, model)
    setmetatable(newtruck, Truck)
    newtruck.Powerup = powerup
    return newtruck
end
local truck = Truck.new()
truck:Respawn()

To demonstrate the issue with this pattern, look at this example where both electric and gasoline cars inherits properties of Car.


Once you add a new type of class Hybrid that inherits from both the Electric and Gasoline classes. Notice how in the diagram, this forms this kind of diamond shape

The hybrid car is a subset of both the electric and gasoline car, which results in a duplication of paths which is impossible to express with a prototypal-like language, i.e Luau.
OOP claims that inheritance can be seen everywhere in the world, e.g. a child inherits traits such as hair-, eye- colour, skin tone, etc from their parents. But after acquiring traits from their parents, they also develop their own personalities on top of those traits. Think of it as a top-to-bottom hierarchy.
ECS does this the opposite way, it makes us think of what specific data a thing contains instead of what that thing is.

Pretend you have a game which uses ECS, and this game contains planes and ships, they may share a few traits such as position (where the game object is in the world), velocity (how fast it is), and etc. One day you want to update the game and add floatplanes, which can both fly in the air with its wings and yet float in the water with its hull. This is not a problem with an ECS, if you want your entity to fly, you just add a Fly component or maybe a Gravity component with negative values. If you want it to float, you just add a Float component

image

Due to the nature of the ECS paradigm that allows for new components and systems to be added without conflicting with existing logic makes it suitable for games where many layers of overlapping behaviour will be defined on the same set of game objects, i.e. new behaviour defined in the future.

How does it work in Roblox?

Let’s begin with creating our world first!

local world = Matter.World.new()

In Matter, we construct components using the component function of the Matter namespace.

local Velocity = Matter.component("Velocity")
local Renderable = Matter.component("Renderable")

But these components don’t contain any data yet! When we spawn our entity, we need to call these components again with some data.

local arrowModel = game:GetService("ReplicatedStorage").Assets.ArrowModel
arrowModel.Parent = workspace
local arrow = world:spawn(
    Velocity({ speed = 50}),
    Renderable({ model = arrowModel })
)

Now we want to give some behaviour to our arrows, because right now they would be stationary. So we have to query the world for entities that both contain the data we are looking for, namely velocity and a renderable. We are going to use the component tables to query the world with. Behind the scenes, it is going to look for matching archetypes or a similar set of components and return every entity matching that description.

local function arrowsFly(world)
    for id, vel, render in world:query(Velocity, Renderable) do
        local currentPosition = render.model:GetPrimaryPartCFrame()
        render.model:PivotTo(CFrame.new(currentPosition + Vector3.new(vel.speed, 0 ,0)))
    end
end

The problem right now is that it is going to move 50 studs every single frame, you can imagine it is going to be incredibly fast. Luckily there exists a very useful hook function named useDeltaTime which gives us the time between frames.

render.model:PivotTo(CFrame.new(currentPosition + Vector3.new(vel.speed * Matter.useDeltaTime(), 0 ,0)))

Now let’s make it hurt some people on contact! We want to use the .Touched signal to evaluate whenever the arrow has gotten any contact. Here we will use another useful hook useEvent.

local function arrowsHurt(world)
    for id, vel, render in world:query(Velocity, Renderable) do
        for _, hit in Matter.useEvent(render.Model.PrimaryPart, "Touched") do
            if Players:GetPlayerFromCharacter(hit.Parent) then
                hit.Parent.Humanoid:TakeDamage(5)
            end
        end
    end
end

Lastly we need to run these systems! We will be using the loop export from the Matter namespace. The Loop class accepts multiple arguments which will be passed into every single system (look at each of our functions, they have a world parameter).

local loop = Matter.Loop.new(world)
loop:scheduleSystems({
    arrowsFly,
    arrowsMove,
    arrowsHurt
})

loop:begin({
    default: RunService.Heartbeat -- you can set it to whatever signal you want but commonly you will use Heartbeat
})

Sources

55 Likes

the funny thing is, i was planning on making my own OOP plane(not my first time using OOP but with this type of use case). So this is really useful!

1 Like

Wish matter was better documented. If you’re coming in from OOP with no prior experience with ECS, the documentation reads like hieroglyphics. From what little I can make out of it, it doesn’t feel as intuitive to use as objects, at least on Roblox.

5 Likes

I pretty much agree, I don’t think this is really necessary for roblox development, but I haven’t had enough experience with it to draw a strong conclusion. I guess it depends on the scope of the project.

1 Like

Awesome explaination on ECS & Matter, personally Matter has been super beneficial to my development from it’s ease-of-use, decoupled design & in-built debugger.

Matter is a great library, and ECS is a great way of structuring your game. I do wish that more people would use ECS over OOP though.

1 Like

Does Matter have automatic support for parallel luau so we can take advantage of multi threading?
Is this ECS beneficial to performance or is it just serving as another way of structuring games?

Does Matter have automatic support for parallel luau

No, multithreaded Luau can’t work in how you’re thinking it should work in other languages. Luau is a dynamic language and the actor model was made with that in mind to forbid unsynchronized access to garbage collectable tables to prevcnt race conditions.

Is this ECS beneficial to performance or is it just serving as another way of structuring games?

Matter is the most performant option for an ECS library right now, with benchmarks to prove it. The archetypal storage layout makes iteration blazingly fast. However, the real sell of Matter is for organisational benefits and the ergonomics of its usage. You can read more here. ECS in general will extrapolate problematic performance issues making them easier to find and debug, and Matter also provides a debugger that shows you the performance of each system.

4 Likes