- Improved documentation, pipeline correction.
- Added diagram showing steps
- Improvement in
Timer.lua
, removal of unnecessary properties and simplification of the loop - Added
Entity:GetAny(CompType)
method to get any qualifier of a type - Fixed
Entity:Set()
, validatingtype(value) == "table"
before accessing component properties - Creating the Pong Game Example
Pong game
Created this game to be used in documentation tutorials.
The game’s source code is available at ecs-lua/examples/pong at master · nidorx/ecs-lua · GitHub
The tutorial is not ready yet.
I have used the new system and it works great like the old one but now the code is more understandable especially with understanding how the loop manager ties with RunService.
However, I would like to know if there is a better way of accessing an entities world? Currently I have to create a WorldComponent for an entity to access it which works but feels weird in a way, let me know if it’s ok.
My use case is for something like an entity factory for translating collection service instances to ECS entities or an gun entity creating a bullet entity.
I think this approach works well and is similar to Knit Component which has worked well in the past and can be applied to the ECS engine. I will also share some things I found while working with this resource.
Example entity factory module
--Entity factory module
local CollectionService = game:GetService("CollectionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ECS = _G.ECS
local Components = require(script.Parent.Parent.Components)
local CollectionServiceTagComponent = Components.CollectionServiceTagComponent
local WorldComponent = Components.WorldComponent
local World, System, Query, Component = ECS.World, ECS.System, ECS.Query,
ECS.Component
local CollectionClassSystem = System("transform", 10, Query.All(
CollectionServiceTagComponent,
WorldComponent))
local function createAndSetupEntity(world, setupEntityFunction, instance)
local entity = world:Entity()
setupEntityFunction(entity, instance)
return entity
end
local function setupCollectionClassInstance(tag, setupEntityFunction, world)
local entities = {}
CollectionService:GetInstanceAddedSignal(tag):Connect(function(instance)
entities[instance] = createAndSetupEntity(world, setupEntityFunction, instance)
end)
local instances = CollectionService:GetTagged(tag)
for _, instance in pairs(instances) do
entities[instance] = createAndSetupEntity(world, setupEntityFunction, instance)
end
CollectionService:GetInstanceRemovedSignal(tag):Connect(function(instance)
local entity = entities[instance]
world:Remove(entity)
end)
end
function CollectionClassSystem:OnEnter(Time, entity)
local csData = entity[CollectionServiceTagComponent]
local world = entity[WorldComponent].value
local tag = csData.Tag
local components = csData.Components
setupCollectionClassInstance(tag, components, world)
return true
end
return CollectionClassSystem
Within the main script
Usage within the primary server script
local vehicleEntityFactory = world:Entity()
local function setupVehicleEntity(vehicleEntity, modelInstance)
vehicleEntity:Set(ModelComponent(modelInstance))
vehicleEntity:Set(VehicleSeatComponent())
vehicleEntity:Set(WorldComponent(world))
-- vehicleEntity:Set(HealthComponent())
end
vehicleEntityFactory:Set(CollectionServiceTagComponent({"Vehicle", setupVehicleEntity}))
vehicleEntityFactory:Set(WorldComponent(world))
Also some more snippets I made
Summary
{
"Class": {
"prefix": ["class"],
"body": [
"local ${0:$TM_FILENAME_BASE} = {}",
"${0:$TM_FILENAME_BASE}.__index = ${0:$TM_FILENAME_BASE}",
"",
"",
"function ${0:$TM_FILENAME_BASE}.new()",
"\tlocal self = setmetatable({}, ${0:$TM_FILENAME_BASE})",
"\treturn self",
"end",
"",
"",
"function ${0:$TM_FILENAME_BASE}:Destroy()",
"\t",
"end",
"",
"",
"return ${0:$TM_FILENAME_BASE}",
""
],
"description": "Lua Class"
},
"ECS System": {
"prefix": "ECSSystem",
"body": [
"local ReplicatedStorage = game:GetService(\"ReplicatedStorage\")",
"",
"local Components = require(script.Parent.Parent.Components)",
"",
"local ECS = require(ReplicatedStorage.Shared.ECSFolder.ECS)",
"",
"local World, System, Query, Component = ECS.World, ECS.System, ECS.Query, ECS.Component",
"",
"local ${TM_FILENAME_BASE} = System(\"transform\", 1, Query.All())",
"",
"",
"",
"return ${TM_FILENAME_BASE}"
],
"description": "ECS System"
},
"ECS OnArchetype": {
"prefix": "ECSOnMethod",
"body": [
"function ${TM_FILENAME_BASE}:${1|OnEnter,OnExit,OnRemove|}(Time, entity)",
"",
"end"
],
"description": "ECS OnEnter"
},
"ECS Update": {
"prefix": "ECSUpdate",
"body": [
"function ${TM_FILENAME_BASE}:Update(Time)",
" for i, entity in self:Result():Iterator() do",
" end",
"end"
],
"description": "ECS Update"
},
"Component Template": {
"prefix": "ECSComponent",
"body": [
"local ${TM_FILENAME_BASE} = _G.ECS.Component()",
"",
"return ${TM_FILENAME_BASE}"
],
"description": "Component Template"
},
}
Even though I put the components in _G for “easy access” similar to your example most of the time I use a module to just fetch and run the components module from a folder like so which works great.
Also some meta table thing to catch that weird error when I haven’t created a component yet.
Component getter
local ReplicatedStorage = game:GetService("ReplicatedStorage")
--[[
Collects all the descendant components and stores it,
also puts it in _G cuz why not
]]
local Components = {}
local descendants = script:GetDescendants()
for _, componentModule in pairs(descendants) do
if componentModule:IsA("ModuleScript") then
local component = require(componentModule)
Components[componentModule.Name] = component
end
end
--Also add in shared components, between server and client
local SharedComponents = require(ReplicatedStorage.Shared.Components)
for key, value in pairs(SharedComponents) do
Components[key] = value
end
local mt = {}
mt.__index = function(table, keyComponentName)
warn(keyComponentName, ", hasn't been created yet!")
end
setmetatable(Components,mt)
return Components
Also I have found out that you don’t need Knit when using ECS, you can replicate the functionalities of Knit yourself and it saves the time of having to look at Knit documentation as well you understand it better if you do it yourself.
Also saves a lot of looking through folders:
Before:
After:
Yeah it was pretty messy trying to combine both systems, an additional 2 folders full of stuff to look at. Now it’s more like the Pong example.
can you make it uncopylocked so we can see how your module applies to a game in roblox?
Yeah! This was where I was at too, I had to do the same thing last night… only problem was ECS.Component kept returning an empty table… I checked the source code and I figured out the initializer wasnt being called and having its data added to the class, ill make an issue on the repo later today but heres what I did incase anybody encounters a similar issue
Components and Systems are folders containing modules that return respective components and systems for easy expandability and organization etc:
https://gyazo.com/8505edb997ce25dc264455dbdcc34693
https://gyazo.com/f4188039c9a6b95db58a605355a0d033
Heres my fix for the Component constructor:
https://gyazo.com/b6f765256d00493c90ec89deebe16470
Basically, create a reference to the data you pass your component and have the __index metamethod check that table and return the value.
No clue if anyone else had this issue but its a quickfix