ECS in Roblox Studio: Why is it better than OOP and how to "cook" it

As mentioned in the previous post , Luau does not have a classic class syntax; objects and their behavior are defined through tables and metatables.


Instead of rigid inheritance hierarchies, it is convenient to use the Entity-Component-System (ECS) approach, which is based on composition. Personally, I came to this architecture about four months ago without realizing it, as it seemed simpler and more natural to me. Later, I specifically rewrote the code for a cleaner ECS implementation.
Below, we will discuss what ECS is and why it is often chosen over OOP, as well as how to properly organize the code structure with a core and modules.

What is ECS?

  • Entity is a maximally abstract container object, usually represented simply by a unique ID. The entity does not have its own data, it only connects the components describing its state.
  • Component is a module or a table with pure data describing one characteristic of an object (for example, health, speed, position). Components in ECS do not contain logic – only data fields
  • System is a set of logic for processing component data. At each frame, the system iterates over all entities that have the required set of components and updates them. Systems do not store entity data themselves, acting as a processing “pipeline”.

In other words, in ECS, everything is built around component composition rather than inheritance. The idea is to stop defining entities through a class hierarchy and start using composition. In ECS, the principle of “composition over inheritance” is widely accepted: Any object can be described as a set of small properties (components), and complex behavior arises from a set of corresponding systems.

Why ECS instead of OOP?

ECS is becoming increasingly popular in games and game development. For example, many modern games (Overwatch, Minecraft, Rust, etc.) use this architecture, I will immediately highlight the key advantages

  • Weak interconnection: Logic is divided into systems and interacts through data in entities, rather than through rigid inheritance chains. This simplifies refactoring: new functionality can be added “on the side” without modifying existing code. In the ECS pipeline, everything is expressed in terms of data. The entity itself is essentially similar to an Object with no guaranteed fields, and systems interact through shared data fields.
  • Modularity and reusability: Since components contain only data and systems contain only logic, they can be easily transferred between projects or run on any data. This means that the same system can be applied to different games or situations: it is enough to provide the expected format of the component data. At the same time, testing the systems is easier: you can run them on a dummy set of components.
  • Combinatorics of properties: Any object can receive any combination of components on the fly. For example, you can add a Health component to a camera, and the damage system will automatically start processing it; then you can attach OnFire, and the camera will receive damage from fire. This is especially useful for dynamic game worlds, where game designers can add new features to entities on the fly. There are no restrictions on the combination of components, so you can create hybrids from arbitrary data without worrying about inheritance issues.
  • Simplicity of SRP (Single Responsibility): Each system in ECS is responsible for one specific task (such as movement, rendering, or physics). The logic is purely functional and separated from the data, making it easy to control and divide as needed. The system code is concise and clear (usually a for loop over a set of components), making it easier to profile and debug

In the end, ECS is a really awesome framework for your game, because it gives you something that any other project doesn’t have - flexibility. By the way, classic functional programming is much closer than you might think, but more on that another day

How to Structure ECS in Luau

Now that we’ve established the why, let’s look at the how. Since Luau doesn’t provide class syntax, ECS fits very naturally with its table philosophy. The goal is to keep data (components) and logic (systems) decoupled while letting entities act as simple identifiers that glue everything together.

  • Entity Manager: Responsible for creating and destroying entities. Typically, an entity is just an integer ID. The manager may also maintain a mapping of entities to their assigned components.
  • Component Storage: Each component type (like Health, Transform, Velocity) has its own table keyed by entity IDs. This way, systems can quickly fetch the data they need without searching through all entities.
local Health = {}
Health[entityId] = { value = 100, max = 100 }
  • systems: Each system is just a function or module that iterates over entities with specific components. For example, a MovementSystem might look for entities that have both Transform and Velocity.
local function MovementSystem(entities, Transform, Velocity, dt) 
	for id in pairs(entities) do 
		local pos = Transform[id] 
		local vel = Velocity[id] 
		if pos and vel then 
			pos.x += vel.x * dt pos.y += vel.y * dt 
		end 
	end 
end

But let’s be honest, it’s inconvenient to write such functions forever… I agree. That’s why we make engines

ECS in a Roblox engine composition

Roblox itself offers an object-oriented paradigm: everything in the game is a hierarchy of Instance-objects that have methods, events, and properties. At first glance, it may seem that ECS and Roblox-style conflict, but in reality, they perfectly complement each other. ECS can be layered on top of Roblox’s classic object model and utilize the engine’s built-in features, such as CollectionService.
Components via tags?
In Roblox, each Instance can be tagged using CollectionService. The tag effectively acts as a “component”

local CollectionService = game:GetService("CollectionService")
CollectionService:AddTag(npcModel, "Enemy") 
CollectionService:AddTag(npcModel, "HasHealth")

systems with listeners
Systems can be implemented as modules that listen to CollectionService:GetInstanceAddedSignal(tag) and GetInstanceRemovedSignal(tag) events, allowing the system to “come to life” when an object with the desired set of tags is added.

local CollectionService = game:GetService("CollectionService") 
local function DamageSystem() 
	CollectionService:GetInstanceAddedSignal("HasHealth"):Connect(function(obj)
		obj:SetAttribute("Health", 100) 
	end) 
	CollectionService:GetInstanceRemovedSignal("HasHealth"):Connect(function(obj) 
		print(obj)
	end) 
end 

return DamageSystem

Similarly, you can implement movement, animation, interaction, and other systems that simply follow objects with the appropriate tags.

A combination of ECS and OOP

Instead of choosing between “ECS only” and “OOP only” in Roblox, you can use both approaches:

  • ECS manages “high-level game logic”: a set of tags determines which systems are applied to an object. This is flexible and allows you to quickly combine properties.
  • Roblox’s regular OOP is reserved for low-level details: Humanoid, BasePart, RemoteEvent, and other classes provide ready-made mechanics on which components are built.

An NPC can be a model with a Humanoid (the OOP part), but in ECS, it gets the Enemy, HasHealth, and CanAttack tags. The attack and damage logic is handled by the systems, while the Humanoid is responsible for physics etc.
In the end, we come to the conclusion that the best option is the ECS abstraction layer, without completely reshaping the paradigm. You can still use OOP within ECS. In fact, every system you create, unless it's a function or a table (which is recommended), is an object that was created using OOP. Therefore, it's challenging to consider ECS as a complete replacement for OOP, unless you create your own engine within the existing one, but even then, you'll need to use OOP for interaction. Essentially, they operate on different layers.
why? (The advantages of this approachuses):

  • We use Roblox’s native features, without unnecessary layers of abstraction.
  • Systems automatically respond to the creation and deletion of objects.
  • Components (tags and attributes) can be easily edited directly in the Studio, making it convenient for designers.
  • ECS remains an “add-on” rather than a replacement for the standard engine.

ECS in composition with Roblox turns into a lightweight tool that uses the engine’s tags and events to organize flexible logic. We don’t rewrite Roblox-OOP, but simply add a composition layer to enjoy the convenience of ECS: modularity, reusability, and loose coupling of systems.

Download structure made by me: (its not only about ECS)

Struct.rbxm (8,7 КБ)

Tutorial

This structure is built around simplicity and clarity: there is Core (the core), Controllers (input and client logic controllers), and Systems (systems that respond to tags and attributes). Together, they create a convenient ECS layer on top of Roblox.

  1. Creating controller
    A controller is a module that listens for any actions. For example, InputController. For the sake of simplicity, I will say “anything that is not a system is a controller,” which is a bit more complicated, but ultimately correct in relation to this structure.
  2. CreatingSystem
    Write your tag in analogy with the example, after creating methods InitializeEntity, DeInitializeEntity and OnUpdate. If your system is reactive then get rid of OnUpdate which is triggered every frame, after all optimization is an important thing. Classic ECS like every frame is processed for each system, but it seemed like nonsense to me, except for the fact that it is useful for laying out
    Then, simply add a tag to the object, and everything will work.

You can see an example of their implementation in the modules I have already prepared.

How do you rate it from 1 to 5?
  • 1
  • 2
  • 3
  • 4
  • 5
0 voters

Also will be useful:

Pseudo-OOP in luau: Click here
Deterministic and non-deterministic state machines: Click here

21 Likes

You can instead use npcModel:AddTag("Enemy") :slightly_smiling_face: Added around mid-2023

5 Likes

That not really a ECS
If it were ECS it would’ve been either hardcodded or optional lookup

local Health = {
	[Plr] = 100;
}
local MaxHealth = {
	[Plr] = 125;
}

print("Max Health is:",MaxHealth[Plr] or 100)
1 Like

In other words, in ECS, everything is built around component composition rather than inheritance.

Cant u use composition with OOP?

  • ECS manages “high-level game logic”: a set of tags determines which systems are applied to an object. This is flexible and allows you to quickly combine properties.

Could u give an example of some “high-level game logic”? im currently trying to find some reasons as to y i should implement ECS into my game as it seems very convenient to have but i currently dont see how this is any better than OOP with composition. is it just a preference or am i just missing the point lol. if u could help clear things up a bit it would be greatly appreciated!

The entire point of ECS is separating data from code. Both, the things presented in the topic as well as what you have presented, can be ECS if you do so.

OOP is traditionally about using inheritance over composition etc, however that doesn’t mean you need to be doing something “traditionally”. Look at your game code, and think about what works the best for you. If you think that OOP-style code and composition-style code combined together works well for you, then that’s your choice.

6 Likes

high-level game logic is about game rules and behavior in the world which apply to entire groups of entities at once and are not tied to the details of a specific object.

for example:
You have a Zone, and all entities with the Player tag inside it get the Poisoned tag.
PoisonSystem automatically applies damage to all players with this tag.
You don’t need to manually list the players — ECS will process them itself.

ECS or OOP with composition?
In Luau, everything is based on tables → they are already objects
The system in my code (and 90% tiny-ECS) is an object (a table with its own state and methods).
Systems are collected in the manager → this is another object (table) that “contains” the others.
Behavior in ECS is not derived from inheritance, but from the fact that these component objects work together (composition).

And from here you can sum up:
Essentially, ECS in Luau implementation is an abstraction over compositional OOP. The difference is that in composition, the logic lies within the object, while in ECS, it is controlled by the “engine”.

An entity is just an ID or a reference to an Instance. Components and systems are objects (tables) organized compositively within managers. That is, we simply raise the level of abstraction: instead of arranging fields and methods in a single object (as in classical OOP), we compose the objects themselves (systems, components, managers) at the architectural level.

in my code its likely:
System: {Tag: string,otherAttributes: string, Methods(upd,init,deinit): function(id,enttyInstance)} - its object that have methods and attributes
SystemManager: {System, Methods(Register,Update): function(system | dt) → () } – its object that contains systems (objects) and have methods.

2 Likes