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.
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())
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.
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
Friendship ended with OOP, now ECS is my best friend
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.
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?
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
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
Here’s a really minor pull request to fix the error readability for newer users who don’t like to read documentation
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.
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?
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
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.
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…
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)
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
})
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
--- 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
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👍.