ECS-Lua - A tiny and easy to use ECS (Entity Component System) engine

logo

Build Status

Read the Documentation

What is it?

ECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.

The basic idea of this pattern is to stop defining entities using a hierarchy of classes and start doing use of composition in a Data Oriented Programming paradigm.
(More information on Wikipedia).
Programming with an ECS can result in code that is more efficient and easier to extend over time.

How does it work?

Talk is cheap. Show me the code!

local World, System, Query, Component = ECS.World, ECS.System, ECS.Query, ECS.Component

local Health = Component(100)
local Position = Component({ x = 0, y = 0})

local isInAcid = Query.Filter(function()
   return true  -- it's wet season
end)

local InAcidSystem = System("process", Query.All( Health, Position, isInAcid() ))

function InAcidSystem:Update()
   for i, entity in self:Result():Iterator() do
      local health = entity[Health]
      health.value = health.value - 0.01
   end
end

local world = World({ InAcidSystem })

world:Entity(Position({ x = 5.0 }), Health())

Features

ECS Lua has no external dependencies and is compatible and tested with Lua 5.1, Lua 5.2, Lua 5.3, Lua 5.4, LuaJit and Roblox Luau

  • Game engine agnostic: It can be used in any engine that has the Lua scripting language.
  • Ergonomic: Focused on providing a simple yet efficient API
  • FSM: Finite State Machines in an easy and intuitive way
  • JobSystem: To running systems in parallel (through coroutines)
  • Reactive: Systems can be informed when an entity changes
  • Predictable:
    • The systems will work in the order they were registered or based on the priority set when registering them.
    • Reactive events do not generate a random callback when issued, they are executed at a predefined step.

Usage

Read our Full Documentation to learn how to use ECS Lua.

docs-card-1 docs-card-2

docs-card-3 docs-card-4

Get involved

All kinds of contributions are welcome!

:bug: Found a bug?
Let me know by creating an issue.

:question: Have a question?
Roblox DevForum is a good place to start.

:gear: Interested in fixing a bug or adding a feature?
Check out the contributing guidelines.

:open_book: Can we improve our documentation?
Pull requests even for small changes can be helpful. Each page in the docs can be edited by clicking the “Edit on GitHub” link at the bottom right.

License

This code is distributed under the terms and conditions of the MIT license.

102 Likes

I really like this, if you have tried minecraft modding then you will love this too. Its really what I think we are missing. I will use this for my game

4 Likes

This system is pretty awesome, I’ll def use it

2 Likes

Interesting content related to ECS and netcode

3 Likes

Original https://habr.com/ru/company/pixonic/blog/415959/

2 Likes

Friendship ended with OOP, now ECS is my best friend :+1:

Yeah, I’m really enjoying this system of organizing my code and the fact you can just add and drop components compared to inheritance issues of OOP especially with vague and broad classes like my current project which is to make multiple types of vehicles.

My problem with OOP

Yeah with OOP you will have to worry a lot about how you are going to handle inheritance. I was thinking of one technique I saw on the scripting support forms of separating it into ObjectShared, ObjectClient, and ObjectServer but then yeah eventually it’s just not versatile enough. Like what if I want a CarClient and PlaneClient class both inherited from general VehicleClient to share the same methods for example? Yeah thinking about how to avoid this inheritance issue is a pain so I switched to ECS which yeah solves the inheritance issue entirely since it’s all components that can be split apart and recombined.

Yeah that’s my big upside from using this resource so far the organizational benefits.

However, I’ll list down some issues I found while testing the system.

  1. Outdated documentation.

Yeah for anyone planning to use this I recommend going to GitHub as the forum documentation is outdated with outdated syntax like:

When it has been simplified to just:

   ClientSystems.testSystem = ECS.RegisterSystem()

Perhaps remove the tutorial entirely from the forum and just point towards the GitHub?

  1. The different work flow

Yeah this system requires a totally different work flow which takes some adjusting to especially the error messages. Here is one major one I found when I was experimenting:

Error message
--pointed to this part of the code:
      if table.getn(config.RequireAny) == 0 then
         error('You must enter at least one component id in the "RequireAny" field')
      end
--error message:
 ReplicatedStorage.Aero.Shared.ECS:129: You must enter at least one component id in the "RequireAny" field  -  Client  -  ECS:129
  23:53:42.893  Stack Begin  -  Studio
  23:53:42.893  Script 'ReplicatedStorage.Aero.Shared.ECS', Line 129 - function Filter  -  Studio  -  ECS:129
  23:53:42.893  Script 'ReplicatedStorage.Aero.Shared.ECS', Line 1879  -  Studio  -  ECS:1879
  23:53:42.893  Script 'ReplicatedStorage.Aero.Shared.ECS', Line 2528 - function CleanupEnvironment  -  Studio  -  ECS:2528
  23:53:42.893  Script 'ReplicatedStorage.Aero.Shared.ECS', Line 2623 - function Update  -  Studio  -  ECS:2623
  23:53:42.893  Script 'ReplicatedStorage.Aero.Shared.ECS', Line 2726  -  Studio  -  ECS:2726
  23:53:42.894  Stack End  -  Studio

Can you guess what I did wrong from that error message if you were new to using the system?

The answer is I created an entity without any components.

    local vehicleEntity = world.Create()

Yeah, I spent a frustrating amount of time wondering why this line caused an error, as I couldn’t understand at all the relationship between a component ID and why it caused an error during the update step of the world.

I guess the lesson is follow the tutorial? Anyways, please add this as an additional error message into the engine and a disclaimer in the documentation. Yeah I missed that “and” that was noted in the documentation :sob:
documentation quote:

It consists only of a unique ID and the list of components that make up this entity

The rest is mostly just syntax errors because of all the components you will need to access and require, for this, I recommend storing them all in one big table like how ECSUtil does it instead of the suggested method in the tutorial of separating each component into individual module scripts as component data is mostly just a bunch of one-liners.

And that’s it, otherwise, the documentation on GitHub was definitely well made and neat for how the API worked. For a potential improvement, I would swap out weapons for the System improvements:

   OnRemove = function(time, world, entity, index, weapons)
      -- on new entity
      print('Entity removed ', entity)
      return false
   end,

as weapons should have represented the component_N_data instead, so I guess it should have been left as a variadic function to represent … the general case of what the additional parameters are returning.

Also the outdated API here for system config caused an error once

   --  [Optional] Invoked before executing the update method
   BeforeUpdate = function(time, interpolation, world, system)
      -- called before update
      print(system.config.customConfig)
   end,

Yeah overall, a really great resource that works. Just could be a little more user friendly with the documentation :+1:

Here’s a really minor pull request to fix the error readability for newer users who don’t like to read documentation :stuck_out_tongue:

Am I doing it correctly?

2 Likes

I already said this is really awesome, and now that I am creating a game I am thinking to actually implement this into the game.

Sorry to ask, but I can‘t understand this. What is the problem with the inheritance? If you want that a CarClient and PlaneClient class share both inherited methods from a VehicleClient class, you just can make both CarClient and PlaneClient inherit from VehicleClient. This works, I self do it, so I can‘t understand what‘s the problem.

1 Like

It’s fine to ask, I should have explained clearer, but for your scenario it’s fine if you leave it like that as PlaneClient and CarClient.

…But what if you want to combine CarClient and PlaneClient because well it sounds cool right definitely a cool gameplay element should be easy with OOP right?

nope

I learned it from this article I found in this post after researching how to structure an OOP game. It’s a very famous problem since 1991 if we were to cite Wikipedia :stuck_out_tongue:

Even on wikipedia it lists no solutions to the problem only mitigations so yeah I wanted to avoid this problem entirely in order to make my code clean to edit in the future so yeah I was thinking of splitting it up into components…yep ECS should provide more flexibility in this area I’ll see how this goes tho.

2 Likes

Oh, I understand it better now. In java for example, one way to solve this would be to use interfaces. It‘s not the best, but it‘s better as nothing. I think you can inherit of multiple classes in C# (I think), but in Lua… there‘s no way, we don‘t have something like interfaces so this is unsolvable. Thanks for clarifying what you mean…

1 Like

I have been using the system and found an issue concerning detecting when a component is removed from an entity. Currently, the OnRemove event only fires when the entity is removed via:

world.Remove(gunEntity)--OnRemove Fires
world.Remove(gunEntity,replicationComponent)--OnRemove doesn't fire

So this will potentially cause a memory leaks if you have made an event during the OnEnter event. (For my scenario it’s when I have a weapon entity, with a fast cast component and I want to add and remove a replication on hit component everytime the ray hits an enemy).

To solve this I just stuffed a signal into the OnRemove function and called it ComponentRemoved and it fires every time the world.Remove(entity,component) function is called. Not sure if this is the best solution, but here is my fork of it, +another copy of ECS with the init.lua rojo format for putting modules as children of the script:

local signal
signal = world.ComponentRemoved:Connect(function(entityRemoved,component) 
   if entity == entityRemoved and component == someRandomComponent then
      entityEventThatIsUnconnected:Disconnect() -- something like this
signal:Disconnect()
   end
end)
1 Like

The problem was solved in the v1.2.2, with the addition of the OnExit method. The OnRemove method remains invoked when an entity is removed from the world

This new method works in conjunction with OnEnter , allowing the system to be informed when the entity has undergone changes that no longer satisfy the system filter.

In the example below, I have a TAG component called ControlsDisabled , whenever my local entity receives such a component ( world.Set(entity, ControlsDisabled) ), the system below blocks the player’s control.

After removing this component from the entity ( world.Remove(entity, ControlsDisabled) ) the system re-enables local player control

local ControlsDisabled = ECS.RegisterComponent('ControlsDisabled', nil, true) -- tag component

local Controls

return ECS.RegisterSystem({
   Name  = 'ControlsDisabled',
   Step  = 'process',
   Order = 10,
   RequireAll = {
      Player,
      LocalPlayer,
      ControlsDisabled
   },
   OnEnter = function(time, world, entity, index, players, localPlayers, controls)
      local player = players[index]
      
      Controls = require(player.Player.PlayerScripts:WaitForChild("PlayerModule")):GetControls()
      
      Controls:Disable() 

      return false
   end,
   OnExit = function(time, world, entity, index, players, localPlayers, controls)

      Controls:Enable() 

      return false
   end
})
1 Like

Thanks for this! I’m currently rewriting a project using Roblox-ECS and it’s going really nicely so far!

2 Likes

Any plans to introduce singleton components in the future?

Y’ello, inspired by @Dog2puppy recent Code templates plugin I have learnt how to make VsCode snippets because if you are going to use this engine/framework you will need em.

253 lines of components +100 client sided ones so far

VsCode Json Snippets
{
	// RobloxECS Engine component creation snippets
	"Create constructor component": {
		"scope": "Luau,lua,Lua",
		"prefix":  [".ConstructorComponent"],
		"body": [
			".${1:ComponentName}Component = ECS.RegisterComponent('${1:ComponentName}Component', function(${2:VariableName})",
			"\treturn ${2:VariableName}",
			"end)",
		],
		"description": "RobloxECS create a constructor component and index it in a table"
	},
	"Create local constructor component": {
		"scope": "Luau,lua,Lua",
		"prefix":  ["ConstructorComponent"],
		"body": [
			"${1:ComponentName}Component = ECS.RegisterComponent('${1:ComponentName}Component', function(${2:VariableName})",
			"\treturn ${2:VariableName}",
			"end)",
		],
		"description": "RobloxECS create a constructor component and put in a local variable"
	},
	"Create tag component": {
		"scope": "Luau,lua,Lua",
		"prefix": ".TagComponent",
		"body": [
			".${1:ComponentName}Component = ECS.RegisterComponent('${1:ComponentName}Component', nil, true)",
		],
		"description": "RobloxECS create a tag component"
	},
	"Create local tag component": {
		"scope": "Luau,lua,Lua",
		"prefix": "TagComponent",
		"body": [
			"${1:ComponentName}Component = ECS.RegisterComponent('${1:ComponentName}Component', nil, true)",
		],
		"description": "RobloxECS create a tag component and put in a local variable"
	},
}

Otherwise yeah really enjoying it, the versatility is like playing with blocks/legos but you have to manufacture each unique lego blocks by hand though it should be easier if you use the snippet now.

So far I have made a part damage system where each part is a armor entity with a root part and health using this engine really cool :+1:

2 Likes
--- Instances ---

local folder = workspace.Enemies

--- Services ---

local ECS = require(game.ReplicatedStorage:WaitForChild("ECS"))
local ECSUtil = require(game.ReplicatedStorage:WaitForChild("ECSUtil"))

--local ChasingComponent = require(script.ChasingComponent)

local StateComponent = require(script.StateComponent)

local ScannerSystem = ECS.RegisterSystem({
	Name = 'Scanner',
	--Step = 'transform',
	RequireAny = {
		ECSUtil.PositionComponent,
		StateComponent,
	},

	onEnter = function(time, world, entity, index, weapons)

		print('New entity added ', entity)
		return false
	end,

	update = function (time, world, dirty, entity, index, positions, State)

		print(entity)
		return false
	end
})

local world = ECS.CreateWorld(nil, { Frequency = 10 })
world.AddSystem(ScannerSystem)
ECSUtil.AddDefaultSystems(world)

local enemies = folder:GetChildren()

for e = 1, #enemies do

	for _, part in ipairs(enemies[e]:GetDescendants()) do

		if part:IsA("BasePart") then
			
			part:SetNetworkOwner(nil)
			if part.Name == "HumanoidRootPart" then
				
				local enemy = ECSUtil.NewBasePartEntity(world, part, true, false)
				world.Set(enemy, StateComponent)
			end
		end
	end
end

I can’t understand why it doesn’t work, I’ve tried everything but the update function is not being called. I also checked if the enemies were being set

Classic syntax error, I also received the same issue.

The syntax on the dev forum post is outdated gotta capitalize Update now and the rest of the system keys like OnEnter, Update, OnExit.

Otherwise the rest of the code looks ok with the components being properly set.

BIG UPDATE 2.0.0

New architecture

IMPORTANT! Various improvements and complete breaking of compatibility with previous versions

Documentation available at: ECS-lua - Entity Component System in Lua

Get involved
All kinds of contributions are welcome!

2 Likes

Update v2.1.0

Complete API documentation ECS-lua - Entity Component System in Lua

3 Likes

Wow amazing, loving the new qualifiers, I had the problem of being able to distinguish damage types like player 1 damage, player 2 damage, vs player 3 damage.

Also the new documentation and API is awesome and neater. I’ll have to refactor my code but it’ll be worth it as it was getting messy anyways👍.

2 Likes

The old architecture was very stuck, I applied some things that complicated the maintenance, the code didn’t smell good :sweat_smile:.

I confess that I liked the change I made, the code was clean and easy to maintain. I’m also migrating some code here to the new API :grimacing:.

2 Likes