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

Update v2.1.1

3 Likes

Update 2.2.0

  • 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() , validating type(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.

Summary

4 Likes

I have used the new system and it works great like the old one but now the code is more understandable :+1: 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:
image
After:
image

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

2 Likes