Showing preview only (993K chars total). Download the full file or copy to clipboard to get everything.
Repository: nidorx/ecs-lua
Branch: master
Commit: c6be4a85b13c
Files: 127
Total size: 947.8 KB
Directory structure:
gitextract_abg99oc9/
├── .editorconfig
├── .gitignore
├── .luacov
├── .travis.yml
├── CONTRIBUTING.md
├── ECS.lua
├── ECS_concat.lua
├── LICENSE
├── README.md
├── build.lua
├── docs/
│ ├── .nojekyll
│ ├── README.md
│ ├── _coverpage.md
│ ├── _navbar.md
│ ├── _sidebar.md
│ ├── api.md
│ ├── architecture.md
│ ├── assets/
│ │ ├── boids.rbxl
│ │ ├── logo-r.psd
│ │ ├── pipeline_ecs.psd
│ │ ├── pipeline_old.psd
│ │ ├── repository-open-graph.psd
│ │ ├── tutorial.rbxl
│ │ └── version.psd
│ ├── faq.md
│ ├── favicon/
│ │ ├── browserconfig.xml
│ │ └── manifest.json
│ ├── getting-started.md
│ ├── index.html
│ ├── pt-br/
│ │ ├── README.md
│ │ ├── _coverpage.md
│ │ ├── _navbar.md
│ │ ├── _sidebar.md
│ │ ├── api.md
│ │ ├── architecture.md
│ │ ├── faq.md
│ │ ├── getting-started.md
│ │ ├── tutorial-boids.md
│ │ ├── tutorial-pacman.md
│ │ ├── tutorial-shoot.md
│ │ └── tutorial.md
│ ├── style.css
│ ├── tutorial-boids.md
│ ├── tutorial-pacman.md
│ ├── tutorial-shoot.md
│ ├── tutorial.md
│ ├── z_old_TECH_DETAILS.md
│ └── z_old_TUTORIAL.md
├── examples/
│ └── pong/
│ ├── .editorconfig
│ ├── .gitignore
│ ├── default.project.json
│ ├── pong.rbxlx
│ └── src/
│ ├── client/
│ │ ├── Constants.lua
│ │ ├── Main.client.lua
│ │ ├── Utility.lua
│ │ ├── components/
│ │ │ ├── AudioSource.lua
│ │ │ ├── Ball.lua
│ │ │ ├── BasePart.lua
│ │ │ ├── Paddle.lua
│ │ │ ├── Player.lua
│ │ │ ├── Position.lua
│ │ │ ├── Score.lua
│ │ │ └── Velocity.lua
│ │ └── systems/
│ │ ├── AudioSystem.lua
│ │ ├── BallSystem.lua
│ │ ├── CameraSystem.lua
│ │ ├── MoveSystem.lua
│ │ ├── PaddleHitSystem.lua
│ │ ├── PaddleSystem.lua
│ │ ├── PlayerAiThinkSystem.lua
│ │ ├── PlayerHumanInputSystem.lua
│ │ ├── RenderSystem.lua
│ │ └── ScoreSystem.lua
│ ├── server/
│ │ └── Main.server.lua
│ └── shared/
│ └── ECS.lua
├── modules/
│ ├── bin/
│ │ └── luacov
│ ├── luacov/
│ │ ├── defaults.lua
│ │ ├── hook.lua
│ │ ├── linescanner.lua
│ │ ├── reporter/
│ │ │ └── default.lua
│ │ ├── reporter.lua
│ │ ├── runner.lua
│ │ ├── stats.lua
│ │ ├── tick.lua
│ │ └── util.lua
│ ├── luacov.lua
│ ├── luaunit.lua
│ └── minify.lua
├── roblox/
│ ├── README.md
│ ├── RobloxUtils.lua
│ └── tutorial/
│ ├── default.project.json
│ └── src/
│ ├── client/
│ │ ├── benchmark/
│ │ │ ├── init.client.lua
│ │ │ └── soa.lua
│ │ └── tutorial/
│ │ └── init.client.lua
│ ├── server/
│ │ └── tutorial/
│ │ └── init.server.lua
│ └── shared/
│ ├── teste.lua
│ └── tutorial/
│ ├── component/
│ │ ├── FiringComponent.lua
│ │ └── WeaponComponent.lua
│ └── system/
│ ├── CleanupFiringSystem.lua
│ ├── FiringSystem.lua
│ └── PlayerShootingSystem.lua
├── src/
│ ├── Archetype.lua
│ ├── Component.lua
│ ├── ComponentFSM.lua
│ ├── ECS.lua
│ ├── Entity.lua
│ ├── EntityRepository.lua
│ ├── Event.lua
│ ├── Query.lua
│ ├── QueryResult.lua
│ ├── RobloxLoopManager.lua
│ ├── System.lua
│ ├── SystemExecutor.lua
│ ├── Timer.lua
│ ├── Utility.lua
│ └── World.lua
├── test/
│ ├── README.md
│ ├── test_Archetype.lua
│ ├── test_Component.lua
│ ├── test_Entity.lua
│ ├── test_EntityRepository.lua
│ ├── test_Event.lua
│ ├── test_Query.lua
│ ├── test_QueryResult.lua
│ ├── test_SystemExecutor.lua
│ └── test_World.lua
└── test.lua
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
[*.{lua,js,json}]
charset = utf-8
indent_style = space
indent_size = 3
================================================
FILE: .gitignore
================================================
# IDE
.idea
.vscode
luacov.report.out
src/*.zip
================================================
FILE: .luacov
================================================
return {
include = {
"^src",
"src%/.+$"
},
exclude = {
"%.test$",
},
runreport = true,
deletestats = true,
--reporter = "html"
}
================================================
FILE: .travis.yml
================================================
language: python
env:
- LUA="lua=5.1"
- LUA="lua=5.2"
- LUA="lua=5.3"
- LUA="lua=5.4"
- LUA="luajit=2.0"
- LUA="luajit=2.1"
before_install:
- pip install hererocks
- hererocks lua_install --$LUA -r latest
- source lua_install/bin/activate
script:
- lua test.lua -v && tail -22 ./luacov.report.out && lua build.lua
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to ECS Lua
Thanks for considering contributing to ECS Lua! This guide has a few tips and guidelines to make contributing to the
project as easy as possible.
## Bug Reports
Any bugs (or things that look like bugs) can be reported on the [GitHub issue tracker](https://github.com/nidorx/ecs-lua/issues).
Make sure you check to see if someone has already reported your bug first! Don't fret about it; if we notice a duplicate
we'll send you a link to the right issue!
## Feature Requests
If there are any features you think are missing from ECS Lua, you can post a request in the
[GitHub issue tracker](https://github.com/nidorx/ecs-lua/issues).
Just like bug reports, take a peak at the issue tracker for duplicates before opening a new feature request.
## Documentation
[ECS Lua documentation](https://nidorx.github.io/ecs-lua/) is built using [Docsify](https://docsify.js.org/#/), a
fairly simple documentation generator.
## Working on ECS Lua
To get started working on ECS Lua, you'll need:
* Git
* Lua 5.1
You can run all of ECS Lua tests with:
```sh
lua test.lua -v
```
The LuaCov coverage report is available in the `luacov.report.out` file.
To build the concatenated and minified versions, run the command
```sh
lua build.lua
```
## Pull Requests
Before starting a pull request, open an issue about the feature or bug. This helps us prevent duplicated and wasted
effort. These issues are a great place to ask for help if you run into problems!
### Code Style
In short:
- **SPACE** for indentation
- Identation size = 3 spaces
- Double quotes
- One statement per line
### Tests
When submitting a bug fix, create a test that verifies the broken behavior and that the bug fix works. This helps us
avoid regressions!
When submitting a new feature, add tests for all functionality.
We use [LuaCov](https://keplerproject.github.io/luacov) for keeping track of code coverage. We'd like it to be as
close to 100% as possible, but it's not always possible. Adding tests just for the purpose of getting coverage isn't
useful; we should strive to make only useful tests!
================================================
FILE: ECS.lua
================================================
--[[
ECS Lua v2.2.0
ECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.
This is a minified version of ECS Lua, to see the full source code visit
https://github.com/nidorx/ecs-lua
Discussions about this script are at https://devforum.roblox.com/t/841175
------------------------------------------------------------------------------
MIT License
Copyright (c) 2021 Alex Rodin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local a,b={},{}local function c(d)if(not a[d])then a[d]={r=b[d]()}end return a[d].r end b["Archetype"]=function()local d={}local e={}local f={}local g=0 local h={}h.__index=h function h.Of(i)local j={}local k={}for m,n in ipairs(i)do if(n.IsCType and not n.isComponent)then if n.IsQualifier then if k[n]==nil then k[n]=true table.insert(j,n.Id)end n=n.SuperClass end if k[n]==nil then k[n]=true table.insert(j,n.Id)end end end table.sort(j)local l="_"..table.concat(j,"_")if d[l]==nil then d[l]=setmetatable({id=l,_components=k},h)g=g+1 end return d[l]end function h.Version()return g end function h:Has(i)return(self._components[i]==true)end function h:With(i)if self._components[i]==true then return self end local j=e[self]if not j then j={}e[self]=j end local k=j[i]if k==nil then local l={i}for m,n in pairs(self._components)do table.insert(l,m)end k=h.Of(l)j[i]=k end return k end function h:WithAll(i)local j={}for k,l in pairs(self._components)do table.insert(j,k)end for k,l in ipairs(i)do if self._components[l]==nil then table.insert(j,l)end end return h.Of(j)end function h:Without(i)if self._components[i]==nil then return self end local j=f[self]if not j then j={}f[self]=j end local k=j[i]if k==nil then local l={}for m,n in pairs(self._components)do if m~=i then table.insert(l,m)end end k=h.Of(l)j[i]=k end return k end function h:WithoutAll(i)local j={}for l,m in ipairs(i)do j[m]=true end local k={}for l,m in pairs(self._components)do if j[l]==nil then table.insert(k,l)end end return h.Of(k)end h.EMPTY=h.Of({})return h end b["Component"]=function()local d=c("Utility")local e=c("ComponentFSM")local f=d.copyDeep local g=d.mergeDeep local h=0 local function i(l,m)h=h+1 local n={Id=h,IsCType=true,SuperClass=m}n.__index=n if m==nil then m=n m._Qualifiers={["Primary"]=n}m._QualifiersArr={n}m._Initializers={}else m.HasQualifier=true n.IsQualifier=true n.HasQualifier=true end local o=m._Qualifiers local p=m._QualifiersArr setmetatable(n,{__call=function(q,r)return n.New(r)end,__index=function(q,r)if(r=="States")then return m.__States end if(r=="Case"or r=="StateInitial")then return rawget(m,r)end end,__newindex=function(q,r,s)if(r=="Case"or r=="States"or r=="StateInitial")then if n==m then if(r=="States")then if not m.IsFSM then e.AddCapability(m,s)for t,u in pairs(o)do if u~=m then e.AddMethods(m,u)end end end else rawset(q,r,s)end end else rawset(q,r,s)end end})if m.IsFSM then e.AddMethods(m,n)end function n.Qualifier(q)if type(q)~="string"then for s,t in ipairs(p)do if t==q then return q end end return nil end local r=o[q]if r==nil then r=i(l,m)o[q]=r table.insert(p,r)end return r end function n.Qualifiers(...)local q={...}if#q==0 then return p else local r={}local s={}for t,u in ipairs({...})do local v=n.Qualifier(u)if v and s[v]==nil then s[v]=true table.insert(r,v)end end return r end end function n.New(q)if(q~=nil and type(q)~="table")then q={value=q}end local r=setmetatable(l(q)or{},n)for s,t in ipairs(m._Initializers)do t(r)end r.isComponent=true r._qualifiers={[n]=r}return r end function n:GetType()return n end function n:Is(q)return q==n or q==m end function n:Primary()return self._qualifiers[m]end function n:Qualified(q)return self._qualifiers[n.Qualifier(q)]end function n:QualifiedAll()local q={}for r,s in pairs(o)do q[r]=self._qualifiers[s]end return q end function n:Merge(q)if m.HasQualifier then if self==q then return end if self._qualifiers==q._qualifiers then return end if not q:Is(m)then return end local r=n local s=q:GetType()local t if r==m then t=self._qualifiers elseif s==m then t=q._qualifiers elseif self._qualifiers[m]~=nil then t=self._qualifiers[m]._qualifiers elseif q._qualifiers[m]~=nil then t=q._qualifiers[m]._qualifiers end if t~=nil then if self._qualifiers~=t then for u,v in pairs(self._qualifiers)do if m~=u then t[u]=v v._qualifiers=t end end end if q._qualifiers~=t then for u,v in pairs(q._qualifiers)do if m~=u then t[u]=v v._qualifiers=t end end end else for u,v in pairs(q._qualifiers)do if r~=u then self._qualifiers[u]=v v._qualifiers=self._qualifiers end end end end end function n:Detach()if not m.HasQualifier then return end self._qualifiers[n]=nil self._qualifiers={[n]=self}end return n end local function j(l)return l or{}end local k={}function k.Create(l)local m=j if l~=nil then local n=type(l)if(n=="function")then m=l else if(n~="table")then l={value=l}end m=function(o)local p=f(l)if(o~=nil)then g(p,o)end return p end end end return i(m,nil)end return k end b["ComponentFSM"]=function()local d=c("Query")local e=d.Filter(function(g,h)local i=h.States local j=h.IsSuperClass local k=h.ComponentClass if j then local l=k.Qualifiers()for m,n in ipairs(l)do local o=g[n]if(o~=nil and i[o:GetState()]==true)then return true end end return false else local l=g[k]if l==nil then return false end return i[l:GetState()]==true end end)local f={}function f.AddCapability(g,h)g.IsFSM=true local i=setmetatable({},{__newindex=function(j,k,l)if(type(l)~="table")then l={l}end if table.find(l,"*")then rawset(j,k,"*")else local m=table.find(l,k)if m~=nil then table.remove(l,m)if#l==0 then l="*"end end rawset(j,k,l)end end})rawset(g,"__States",i)for j,k in pairs(h)do if g.StateInitial==nil then g.StateInitial=j end i[j]=k end f.AddMethods(g,g)table.insert(g._Initializers,function(j)j:SetState(g.StateInitial)end)end function f.AddMethods(g,h)h.IsFSM=true local i=g.States function h.In(...)local j={}local k=0 for l,m in ipairs({...})do if(i[m]~=nil and j[m]==nil)then k=k+1 j[m]=true end end if k==0 then return{}end return e({States=j,IsSuperClass=(h==g),ComponentClass=h,})end function h:SetState(j)if(j==nil or i[j]==nil)then return end local k=self:GetState()if(k==j)then return end if(k~=nil)then local m=i[k]if(m~="*"and table.find(m,j)==nil)then return end end self._state=j self._statePrev=k self._stateTime=os.clock()local l=g.Case and g.Case[j]if l then l(self,k)end end function h:GetState()return self._state or g.StateInitial end function h:GetPrevState()return self._statePrev or nil end function h:GetStateTime()return self._stateTime or 0 end end return f end b["ECS"]=function()local d=c("Query")local e=c("World")local f=c("System")local g=c("Archetype")local h=c("Component")local function i(k)e.LoopManager=k end pcall(function()if(game and game.ClassName=="DataModel")then i(c("RobloxLoopManager")())end end)local j={Query=d,World=e.New,System=f.Create,Archetype=g,Component=h.Create,SetLoopManager=i}if _G.ECS==nil then _G.ECS=j else local k=_G.warn or print k("ECS Lua was not registered in the global variables, there is already another object registered.")end return j end b["Entity"]=function()local d=c("Archetype")local e=0 local function f(m,...)local n={...}local o=m._data if(#n==1)then local q=n[1]if(q.IsCType and not q.isComponent)then return o[q]else return nil end end local p={}for q,r in ipairs(n)do if(r.IsCType and not r.isComponent)then table.insert(p,o[r])end end return table.unpack(p)end local function g(m,n,o)local p=m._data local q for r,s in ipairs(o.Qualifiers())do if s~=o then q=p[s]if q then break end end end if q then q:Merge(n)end end local function h(m,...)local n={...}local o=m._data local p=m.archetype local q=p local r={}local s=n[1]if(s and s.IsCType and not s.isComponent)then local t=n[2]local u if t==nil then local v=o[s]if v then v:Detach()end o[s]=nil q=q:Without(s)elseif(type(t)=="table"and t.isComponent)then local v=o[s]if(v~=t)then if v then v:Detach()end s=t:GetType()o[s]=t q=q:With(s)if(s.HasQualifier or s.IsQualifier)then g(m,t,s)end end else local v=o[s]if v then v:Detach()end local w=s(t)o[s]=w q=q:With(s)if(s.HasQualifier or s.IsQualifier)then g(m,w,s)end end else for t,u in ipairs(n)do if(u.isComponent)then local v=u:GetType()local w=o[v]if(w~=u)then if w then w:Detach()end o[v]=u q=q:With(v)if(v.HasQualifier or v.IsQualifier)then g(m,u,v)end end end end end if(p~=q)then m.archetype=q m._onChange:Fire(m,p)end end local function i(m,...)local n=m._data local o=m.archetype local p=o for q,r in ipairs({...})do if r.isComponent then local s=r:GetType()local t=n[s]if t then t:Detach()end n[s]=nil p=p:Without(s)elseif r.IsCType then local s=n[r]if s then s:Detach()end n[r]=nil p=p:Without(r)end end if m.archetype~=p then m.archetype=p m._onChange:Fire(m,o)end end local function j(m,n)local o=m._data local p={}if(n~=nil and n.IsCType and not n.isComponent)then local q=n.Qualifiers()for r,s in ipairs(q)do local t=o[s]if t then table.insert(p,t)end end else for q,r in pairs(o)do table.insert(p,r)end end return p end local function k(m,n)if(n~=nil and n.IsCType and not n.isComponent)then local o=m._data local p=n.Qualifiers()for q,r in ipairs(p)do local s=o[r]if s then return s end end end end local l={__index=function(m,n)if(type(n)=="table")then return f(m,n)end end,__newindex=function(m,n,o)local p=true if(type(n)=="table"and(n.IsCType and not n.isComponent))then h(m,n,o)else rawset(m,n,o)end end}function l.New(m,n)local o=d.EMPTY local p={}if(n~=nil and#n>0)then local q={}for r,s in ipairs(n)do local t=s:GetType()table.insert(q,t)p[t]=s end o=d.Of(q)end e=e+1 return setmetatable({_data=p,_onChange=m,id=e,isAlive=false,archetype=o,Get=f,Set=h,Unset=i,GetAll=j,GetAny=k,},l)end return l end b["EntityRepository"]=function()local d=c("Event")local e={}e.__index=e function e.New()return setmetatable({_archetypes={},_entitiesArchetype={},},e)end function e:Insert(f)if(self._entitiesArchetype[f]==nil)then local g=f.archetype local h=self._archetypes[g]if(h==nil)then h={count=0,entities={}}self._archetypes[g]=h end h.entities[f]=true h.count=h.count+1 self._entitiesArchetype[f]=g else self:Update(f)end end function e:Remove(f)local g=self._entitiesArchetype[f]if g==nil then return end self._entitiesArchetype[f]=nil local h=self._archetypes[g]if(h~=nil and h.entities[f]==true)then h.entities[f]=nil h.count=h.count-1 if(h.count==0)then self._archetypes[g]=nil end end end function e:Update(f)local g=self._entitiesArchetype[f]if(g==nil or g==f.archetype)then return end self:Remove(f)self:Insert(f)end function e:Query(f)local g={}for h,i in pairs(self._archetypes)do if f:Match(h)then table.insert(g,i.entities)end end return f:Result(g),#g>0 end function e:FastCheck(f)for g,h in pairs(self._archetypes)do if f:Match(g)then return true end end return false end return e end b["Event"]=function()local d={}d.__index=d function d.New(f,g)return setmetatable({_event=f,_handler=g},d)end function d:Disconnect()local f=self._event if(f and not f.destroyed)then local g=table.find(f._handlers,self._handler)if g~=nil then table.remove(f._handlers,g)end end setmetatable(self,nil)end local e={}e.__index=e function e.New()return setmetatable({_handlers={}},e)end function e:Connect(f)if(type(f)=="function")then table.insert(self._handlers,f)return d.New(self,f)end error(("Event:Connect(%s)"):format(typeof(f)),2)end function e:Fire(...)if not self.destroyed then for f,g in ipairs(self._handlers)do g(table.unpack({...}))end end end function e:Destroy()setmetatable(self,nil)self._handlers=nil self.destroyed=true end return e end b["Query"]=function()local d=c("QueryResult")local e={}local f={}f.__index=f setmetatable(f,{__call=function(i,j,k,l)return f.New(j,k,l)end,})local function g(i,j,k)local l={}local m={}local n={}for o,p in ipairs(i)do if(l[p]==nil)then if(p.IsCType and not p.isComponent)then l[p]=true table.insert(m,p)table.insert(n,p.Id)else if p.Filter then l[p]=true p[j]=true table.insert(k,p)end end end end if#m>0 then table.sort(n)local o="_"..table.concat(n,"_")return m,o end end function f.New(i,j,k)local l={}local m,n,o if(j~=nil)then j,m=g(j,"IsAnyFilter",l)end if(i~=nil)then i,n=g(i,"IsAllFilter",l)end if(k~=nil)then k,o=g(k,"IsNoneFilter",l)end return setmetatable({isQuery=true,_any=j,_all=i,_none=k,_anyKey=m,_allKey=n,_noneKey=o,_cache={},_clauses=#l>0 and l or nil},f)end function f:Result(i)return d.New(i,self._clauses)end function f:Match(i)local j=self._cache local k=j[i]if k~=nil then return k else local l=e[i]if(l==nil)then l={Any={},All={},None={}}e[i]=l end local m=self._noneKey if m then local p=l.None[m]if(p==nil)then p=true for q,r in ipairs(self._none)do if i:Has(r)then p=false break end end l.None[m]=p end if(p==false)then j[i]=false return false end end local n=self._anyKey if n then local p=l.Any[n]if(p==nil)then p=false if(l.All[n]==true)then p=true else for q,r in ipairs(self._any)do if i:Has(r)then p=true break end end end l.Any[n]=p end if(p==false)then j[i]=false return false end end local o=self._allKey if o then local p=l.All[o]if(p==nil)then local q=true for r,s in ipairs(self._all)do if(not i:Has(s))then q=false break end end if q then p=true else p=false end l.All[o]=p end j[i]=p return p end j[i]=true return true end end local function h()local i={isQueryBuilder=true}local j function i.All(...)j=nil i._all={...}return i end function i.Any(...)j=nil i._any={...}return i end function i.None(...)j=nil i._none={...}return i end function i.Build()if j==nil then j=f.New(i._all,i._any,i._none)end return j end return i end function f.All(...)return h().All(...)end function f.Any(...)return h().Any(...)end function f.None(...)return h().None(...)end function f.Filter(i)return function(j)return{Filter=i,Config=j}end end return f end b["QueryResult"]=function()local function d(l,m,n)return m,(l(m)==true),true end local function e(l,m,n)return l(m),true,true end local function f(l,m,n)local o=(n<=l)return m,o,o end local function g(l,m,n)local o=true for p,q in ipairs(l)do if(q.Filter(m,q.Config)==true)then o=false break end end return m,o,true end local function h(l,m,n)local o=true for p,q in ipairs(l)do if(q.Filter(m,q.Config)==false)then o=false break end end return m,o,true end local function i(l,m,n)local o=false for p,q in ipairs(l)do if(q.Filter(m,q.Config)==true)then o=true break end end return m,o,true end local j={}local k={}k.__index=k function k.New(l,m)local n=j if(m and#m>0)then local o={}local p={}local q={}n={}for r,s in ipairs(m)do if s.IsNoneFilter then table.insert(q,s)elseif s.IsAnyFilter then table.insert(p,s)else table.insert(o,s)end end if(#q>0)then table.insert(n,{g,q})end if(#o>0)then table.insert(n,{h,o})end if(#p>0)then table.insert(n,{i,p})end end return setmetatable({chunks=l,_pipeline=n,},k)end function k:With(l,m)local n={}for o,p in ipairs(self._pipeline)do table.insert(n,p)end table.insert(n,{l,m})return setmetatable({chunks=self.chunks,_pipeline=n,},k)end function k:Filter(l)return self:With(d,l)end function k:Map(l)return self:With(e,l)end function k:Limit(l)return self:With(f,l)end function k:AnyMatch(l)local m=false self:ForEach(function(n)if l(n)then m=true end return m end)return m end function k:AllMatch(l)local m=true self:ForEach(function(n)if(not l(n))then m=false end return m==false end)return m end function k:FindAny()local l self:ForEach(function(m)l=m return true end)return l end function k:ToArray()local l={}self:ForEach(function(m)table.insert(l,m)end)return l end function k:Iterator()local l=coroutine.create(function()self:ForEach(function(m,n)coroutine.yield(m,n)end)end)return function()local m,n,o=coroutine.resume(l)return o,n end end function k:ForEach(l)local m=1 local n=self._pipeline local o=#n>0 if(not o)then for p,q in ipairs(self.chunks)do for r,s in pairs(q)do if(l(r,m)==true)then return end m=m+1 end end else for p,q in ipairs(self.chunks)do for r,s in pairs(q)do local t=false local u=true local v=r if(u and o)then for w,x in ipairs(n)do local y,z,A=x[1](x[2],v,m)if(not A)then t=true end if z then v=y else u=false break end end end if u then if(l(v,m)==true)then return end m=m+1 end if t then return end end end end end return k end b["RobloxLoopManager"]=function()local function d()local e=game:GetService("RunService")return{Register=function(f)local g=e.Stepped:Connect(function()f:Update("process",os.clock())end)local h=e.Heartbeat:Connect(function()f:Update("transform",os.clock())end)local i if(not e:IsServer())then i=e.RenderStepped:Connect(function()f:Update("render",os.clock())end)end return function()g:Disconnect()h:Disconnect()if i then i:Disconnect()end end end}end return d end b["System"]=function()local d={"task","render","process","transform"}local e={}function e.Create(f,g,h,i)if(f==nil or not table.find(d,f))then error("The step parameter must one of ",table.concat(d,", "))end if(g and type(g)=="function")then i=g g=nil elseif h and type(h)=="function"then i=h h=nil end if(g and type(g)=="table"and(g.isQuery or g.isQueryBuilder))then h=g g=nil end if(g==nil or g<0)then g=50 end if type(h)=="function"then i=h h=nil end if(h and h.isQueryBuilder)then h=h.Build()end local j={Step=f,Order=g,Query=h,}j.__index=j function j.New(k,l)local m=setmetatable({version=0,_world=k,_config=l,},j)if m.Initialize then m:Initialize(l)end return m end function j:GetType()return j end function j:Result(k)return self._world:Exec(k or j.Query)end function j:Destroy()if self.OnDestroy then self.OnDestroy()end setmetatable(self,nil)for k,l in pairs(self)do self[k]=nil end end if i and type(i)=="function"then j.Update=i end return j end return e end b["SystemExecutor"]=function()local function d(i)local j={}local k={}for l,m in ipairs(i)do local n=m:GetType()if(m._TaskState==nil)then m._TaskState="suspended"end if not k[n]then local o={Type=n,System=m,Depends={}}k[n]=o table.insert(j,o)end end for l,m in ipairs(j)do local n=m.Type.Before if(n~=nil and#n>0)then for p,q in ipairs(n)do local r=k[q]if r then r.Depends[m]=true end end end local o=m.Type.After if(o~=nil and#o>0)then for p,q in ipairs(o)do local r=k[q]if r then m.Depends[r]=true end end end end return j end local function e(i,j)return i.Order<j.Order end local f={}f.__index=f function f.New(i)local j=setmetatable({_world=i,_onExit={},_onEnter={},_onRemove={},_task={},_render={},_process={},_transform={},_schedulers={},_lastFrameMatchQueries={},_currentFrameMatchQueries={},},f)i:OnQueryMatch(function(k)j._currentFrameMatchQueries[k]=true end)return j end function f:SetSystems(i)local j={}local k={}local l={}local m={}local n={}local o={}local p={}for q,r in pairs(i)do local s=r.Step if r.Update then if s=="task"then table.insert(m,r)elseif s=="process"then table.insert(o,r)elseif s=="transform"then table.insert(p,r)elseif s=="render"then table.insert(n,r)end end if(r.Query and r.Query.isQuery and s~="task")then if r.OnExit then table.insert(j,r)end if r.OnEnter then table.insert(k,r)end if r.OnRemove then table.insert(l,r)end end end m=d(m)table.sort(j,e)table.sort(k,e)table.sort(l,e)table.sort(n,e)table.sort(o,e)table.sort(p,e)self._onExit=j self._onEnter=k self._onRemove=l self._task=m self._render=n self._process=o self._transform=p end function f:ExecOnExitEnter(i,j)local k=true local l={}for m,n in pairs(j)do local o=l[n]if not o then o={}l[n]=o end local p=m.archetype local q=o[p]if not q then q={}o[p]=q end table.insert(q,m)k=false end if k then return end self:_ExecOnEnter(i,l)self:_ExecOnExit(i,l)end function f:_ExecOnEnter(i,j)local k=self._world for l,m in ipairs(self._onEnter)do local n=m.Query for o,p in pairs(j)do if not n:Match(o)then for q,r in pairs(p)do if n:Match(q)then for s,t in ipairs(r)do k.version=k.version+1 m:OnEnter(i,t)m.version=k.version end end end end end end end function f:_ExecOnExit(i,j)local k=self._world for l,m in ipairs(self._onExit)do local n=m.Query for o,p in pairs(j)do if n:Match(o)then for q,r in pairs(p)do if not n:Match(q)then for s,t in ipairs(r)do k.version=k.version+1 m:OnExit(i,t)m.version=k.version end end end end end end end function f:ExecOnRemove(i,j)local k=true local l={}for n,o in pairs(j)do local p=l[o]if not p then p={}l[o]=p end table.insert(p,n)k=false end if k then return end local m=self._world for n,o in ipairs(self._onRemove)do for p,q in pairs(l)do if o.Query:Match(p)then for r,s in ipairs(q)do m.version=m.version+1 o:OnRemove(i,s)o.version=m.version end end end end end local function g(i,j,k)local l=i._world local m=i._lastFrameMatchQueries local n=i._currentFrameMatchQueries for o,p in ipairs(j)do local q=true if p.Query then local r=p.Query if m[r]==true or n[r]==true then q=true else q=l:FastCheck(r)n[r]=q end end if q then if(p.ShouldUpdate==nil or p.ShouldUpdate(k))then l.version=l.version+1 p:Update(k)p.version=l.version end end end end function f:ExecProcess(i)self._currentFrameMatchQueries={}g(self,self._process,i)end function f:ExecTransform(i)g(self,self._transform,i)end function f:ExecRender(i)g(self,self._render,i)self._lastFrameMatchQueries=self._currentFrameMatchQueries end function f:ExecTasks(i)while i>0 do local j=false local k,l=0,#self._schedulers-1 while k<=l do k=k+1 local m=self._schedulers[k]local n,o=m.Resume(i)if o then j=true end i=i-(n+0.00001)if(i<=0)then break end end if not j then return end end end local function h(i,j,k,l)local m=i.System m._TaskState="running"if(m.ShouldUpdate==nil or m.ShouldUpdate(j))then k.version=k.version+1 m:Update(j)m.version=k.version end m._TaskState="suspended"l(i)end function f:ScheduleTasks(i)local j=self._world local k={}local l={}local m={}local n={}local o={}local p,q=0,#self._task-1 while p<=q do p=p+1 local s=self._task[p]if(s.System._TaskState=="suspended")then s.System._TaskState="scheduled"local t=false for u,v in pairs(s.Depends)do t=true if o[u]==nil then o[u]={}end table.insert(o[u],s)end if(not t)then table.insert(k,s)end m[s]=true end end local function r(s)s.Thread=nil s.LastExecTime=nil n[s]=true if o[s]then local t=o[s]local u,v=0,#t-1 while u<=v do u=u+1 local w=t[u]if m[w]then local x=true for y,z in pairs(w.Depends)do if n[y]~=true then x=false break end end if x then m[w]=nil w.LastExecTime=0 w.Thread=coroutine.create(h)table.insert(l,w)end end end end end if#k>0 then local s,t=0,#k-1 while s<=t do s=s+1 local v=k[s]m[v]=nil v.LastExecTime=0 v.Thread=coroutine.create(h)table.insert(l,v)end local u u={Resume=function(v)table.sort(l,function(A,B)return A.LastExecTime<B.LastExecTime end)local w=0 local x,y=0,#l-1 while x<=y do x=x+1 local A=l[x]if A.Thread~=nil then local B=os.clock()A.LastExecTime=B coroutine.resume(A.Thread,A,i,j,r)w=w+(os.clock()-B)if(w>v)then break end end end for A,B in ipairs(l)do if B.Thread==nil then local C=table.find(l,B)if C~=nil then table.remove(l,C)end end end local z=#l>0 if(not z)then local A=table.find(self._schedulers,u)if A~=nil then table.remove(self._schedulers,A)end end return w,z end}table.insert(self._schedulers,u)end end return f end b["Timer"]=function()local d=4 local function e(g)local h=0.0 local i=0.0 return function(j,k,l,m)local n=g.DeltaFixed local o=j-i if o>0.25 then o=0.25 end i=j g.Now=j h=h+o if k=="process"then if h>=n then g.Interpolation=1 l(g)local p=0 while(h>=n and p<d)do m(g)p=p+1 g.Process=g.Process+n h=h-n end end else g.Interpolation=math.min(math.max(h/n,0),1)l(g)m(g)end end end local f={}f.__index=f function f.New(g)local h={Now=0,Frame=0,Process=0,Delta=0,DeltaFixed=0,Interpolation=0}local i=setmetatable({Time=h,Frequency=0,_update=e(h)},f)i:SetFrequency(g)return i end function f:SetFrequency(g)if g==nil then g=30 end local h=math.floor(math.abs(g)/2)*2 if h<2 then h=2 end if g~=h then g=h end self.Frequency=g self.Time.DeltaFixed=1000/g/1000 end function f:Update(g,h,i,j)self._update(g,h,i,j)end return f end b["Utility"]=function()local d={}if table.unpack==nil then table.unpack=unpack end if table.find==nil then table.find=function(g,h,i)local j=#g for k=i or 1,j,1 do if g[k]==h then return k end end return nil end end local function e(g)local h={}for i,j in pairs(g)do if type(j)=="table"then j=e(j)end h[i]=j end return h end d.copyDeep=e local function f(g,h)for i,j in pairs(h)do if(type(j)=="table")then local k=g[i]if(k==nil or type(k)~="table")then g[i]=e(j)else g[i]=f(k,j)end else g[i]=j end end return g end d.mergeDeep=f return d end b["World"]=function()local d=c("Timer")local e=c("Event")local f=c("Entity")local g=c("Archetype")local h=c("SystemExecutor")local i=c("EntityRepository")local j={}j.__index=j function j.New(k,l,m)local n=setmetatable({version=0,maxTasksExecTime=0.013333333333333334,_dirty=false,_timer=d.New(l),_systems={},_repository=i.New(),_entitiesCreated={},_entitiesRemoved={},_entitiesUpdated={},_onQueryMatch=e.New(),_onChangeArchetypeEvent=e.New(),},j)n._executor=h.New(n)n._onChangeArchetypeEvent:Connect(function(o,p,q)n:_OnChangeArchetype(o,p,q)end)if(k~=nil)then for o,p in ipairs(k)do n:AddSystem(p)end end if(not m and j.LoopManager)then n._loopCancel=j.LoopManager.Register(n)end return n end function j:SetFrequency(k)k=self._timer:SetFrequency(k)end function j:GetFrequency(k)return self._timer.Frequency end function j:AddSystem(k,l)if k then if l==nil then l={}end if self._systems[k]==nil then self._systems[k]=k.New(self,l)self._executor:SetSystems(self._systems)end end end function j:Entity(...)local k=f.New(self._onChangeArchetypeEvent,{...})self._dirty=true self._entitiesCreated[k]=true k.version=self.version k.isAlive=false return k end function j:Remove(k)if self._entitiesRemoved[k]==true then return end if self._entitiesCreated[k]==true then self._entitiesCreated[k]=nil else self._repository:Remove(k)self._entitiesRemoved[k]=true if self._entitiesUpdated[k]==nil then self._entitiesUpdated[k]=k.archetype end end self._dirty=true k.isAlive=false end function j:Exec(k)if(k.isQueryBuilder)then k=k.Build()end local l,m=self._repository:Query(k)if m then self._onQueryMatch:Fire(k)end return l end function j:FastCheck(k)if(k.isQueryBuilder)then k=k.Build()end return self._repository:FastCheck(k)end function j:OnQueryMatch(k)return self._onQueryMatch:Connect(k)end function j:Update(k,l)self._timer:Update(l,k,function(m)if k=="process"then self._executor:ScheduleTasks(m)end self._executor:ExecTasks(self.maxTasksExecTime)end,function(m)if k=="process"then self._executor:ExecProcess(m)elseif k=="transform"then self._executor:ExecTransform(m)else self._executor:ExecRender(m)end while self._dirty do self._dirty=false local n={}for q,r in pairs(self._entitiesRemoved)do n[q]=self._entitiesUpdated[q]self._entitiesUpdated[q]=nil end self._entitiesRemoved={}self._executor:ExecOnRemove(m,n)n=nil local o={}local p=false for q,r in pairs(self._entitiesUpdated)do if(r~=q.archetype)then p=true o[q]=r end end self._entitiesUpdated={}for q,r in pairs(self._entitiesCreated)do p=true o[q]=g.EMPTY q.isAlive=true self._repository:Insert(q)end self._entitiesCreated={}if p then self._executor:ExecOnExitEnter(m,o)o=nil end end end)end function j:Destroy()if self._loopCancel then self._loopCancel()self._loopCancel=nil end if self._onChangeArchetypeEvent then self._onChangeArchetypeEvent:Destroy()self._onChangeArchetypeEvent=nil end self._repository=nil if self._systems then for k,l in pairs(self._systems)do l:Destroy()end self._systems=nil end self._timer=nil self._ExecPlan=nil self._entitiesCreated=nil self._entitiesUpdated=nil self._entitiesRemoved=nil setmetatable(self,nil)end function j:_OnChangeArchetype(k,l,m)if k.isAlive then if self._entitiesUpdated[k]==nil then self._dirty=true self._entitiesUpdated[k]=l end self._repository:Update(k)k.version=self.version end end return j end return c("ECS")
================================================
FILE: ECS_concat.lua
================================================
--[[
ECS Lua v2.2.0
ECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.
This is a minified version of ECS Lua, to see the full source code visit
https://github.com/nidorx/ecs-lua
Discussions about this script are at https://devforum.roblox.com/t/841175
------------------------------------------------------------------------------
MIT License
Copyright (c) 2021 Alex Rodin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local __M__, __F__ = {}, {}
local function __REQUIRE__(m)
if (not __M__[m]) then
__M__[m] = { r = __F__[m]() }
end
return __M__[m].r
end
__F__["Archetype"] = function()
-- src/Archetype.lua
local archetypes = {}
local CACHE_WITH = {}
local CACHE_WITHOUT = {}
-- Version of the last registered archetype. Used to cache the systems execution plan
local Version = 0
--[[
An Archetype is a unique combination of component types. The EntityRepository uses the archetype to group all
entities that have the same sets of components.
An entity can change archetype fluidly over its lifespan. For example, when you add or remove components,
the archetype of the affected entity changes.
An archetype object is not a container; rather it is an identifier to each unique combination of component
types that an application has created at run time, either directly or implicitly.
You can create archetypes directly using ECS.Archetype.Of(Components[]). You also implicitly create archetypes
whenever you add or remove a component from an entity. An Archetype object is an immutable singleton;
creating an archetype with the same set of components, either directly or implicitly, results in the same
archetype.
The ECS framework uses archetypes to group entities that have the same structure together. The ECS framework stores
component data in blocks of memory called chunks. A given chunk stores only entities having the same archetype.
You can get the Archetype object for a chunk from its Archetype property.
Use ECS.Archetype.Of(Components[]) to get a Archetype reference.
]]
local Archetype = {}
Archetype.__index = Archetype
--[[
Gets the reference to an archetype from the informed components
@param componentClasses {ComponentClass[]} Component that define this archetype
@return Archetype
]]
function Archetype.Of(componentClasses)
local ids = {}
local cTypes = {}
for _, cType in ipairs(componentClasses) do
if (cType.IsCType and not cType.isComponent) then
if cType.IsQualifier then
if cTypes[cType] == nil then
cTypes[cType] = true
table.insert(ids, cType.Id)
end
cType = cType.SuperClass
end
if cTypes[cType] == nil then
cTypes[cType] = true
table.insert(ids, cType.Id)
end
end
end
table.sort(ids)
local id = "_" .. table.concat(ids, "_")
if archetypes[id] == nil then
archetypes[id] = setmetatable({
id = id,
_components = cTypes
}, Archetype)
Version = Version + 1
end
return archetypes[id]
end
--[[
Get the version of archetype definitions
@return number
]]
function Archetype.Version()
return Version
end
--[[
Checks whether this archetype has the informed component
@param componentClass {ComponentClass}
@return bool
]]
function Archetype:Has(componentClass)
return (self._components[componentClass] == true)
end
--[[
Gets the reference to an archetype that has the current components + the informed component
@param componentClass {ComponentClass}
@return Archetype
]]
function Archetype:With(componentClass)
if self._components[componentClass] == true then
-- component exists in that list, returns the archetype itself
return self
end
local cache = CACHE_WITH[self]
if not cache then
cache = {}
CACHE_WITH[self] = cache
end
local other = cache[componentClass]
if other == nil then
local componentTs = {componentClass}
for component,_ in pairs(self._components) do
table.insert(componentTs, component)
end
other = Archetype.Of(componentTs)
cache[componentClass] = other
end
return other
end
--[[
Gets the reference to an archetype that has the current components + the informed components
@param componentClasses {ComponentClass[]}
@return Archetype
]]
function Archetype:WithAll(componentClasses)
local cTypes = {}
for component,_ in pairs(self._components) do
table.insert(cTypes, component)
end
for _,component in ipairs(componentClasses) do
if self._components[component] == nil then
table.insert(cTypes, component)
end
end
return Archetype.Of(cTypes)
end
--[[
Gets the reference to an archetype that has the current components - the informed component
@param componentClass {ComponentClass}
@return Archetype
]]
function Archetype:Without(componentClass)
if self._components[componentClass] == nil then
-- component does not exist in this list, returns the archetype itself
return self
end
local cache = CACHE_WITHOUT[self]
if not cache then
cache = {}
CACHE_WITHOUT[self] = cache
end
local other = cache[componentClass]
if other == nil then
local componentTs = {}
for component,_ in pairs(self._components) do
if component ~= componentClass then
table.insert(componentTs, component)
end
end
other = Archetype.Of(componentTs)
cache[componentClass] = other
end
return other
end
--[[
Gets the reference to an archetype that has the current components - the informed components
@param componentClasses {ComponentClass[]}
@return Archetype
]]
function Archetype:WithoutAll(componentClasses)
local toIgnoreIdx = {}
for _,component in ipairs(componentClasses) do
toIgnoreIdx[component] = true
end
local cTypes = {}
for component,_ in pairs(self._components) do
if toIgnoreIdx[component] == nil then
table.insert(cTypes, component)
end
end
return Archetype.Of(cTypes)
end
-- Generic archetype, for entities that do not have components
Archetype.EMPTY = Archetype.Of({})
return Archetype
end
__F__["Component"] = function()
-- src/Component.lua
local Utility = __REQUIRE__("Utility")
local ComponentFSM = __REQUIRE__("ComponentFSM")
local copyDeep = Utility.copyDeep
local mergeDeep = Utility.mergeDeep
local CLASS_SEQ = 0
--[[
@param initializer {function(table) => table}
@param superClass {ComponentClass}
@return ComponentClass
]]
local function createComponentClass(initializer, superClass)
CLASS_SEQ = CLASS_SEQ + 1
local ComponentClass = {
Id = CLASS_SEQ,
IsCType = true,
-- Primary component
SuperClass = superClass
}
ComponentClass.__index = ComponentClass
if superClass == nil then
superClass = ComponentClass
superClass._Qualifiers = { ["Primary"] = ComponentClass }
superClass._QualifiersArr = { ComponentClass }
superClass._Initializers = {}
else
superClass.HasQualifier = true
ComponentClass.IsQualifier = true
ComponentClass.HasQualifier = true
end
local Qualifiers = superClass._Qualifiers
local QualifiersArr = superClass._QualifiersArr
setmetatable(ComponentClass, {
__call = function(t, value)
return ComponentClass.New(value)
end,
__index = function(t, key)
if (key == "States") then
return superClass.__States
end
if (key == "Case" or key == "StateInitial") then
return rawget(superClass, key)
end
end,
__newindex = function(t, key, value)
if (key == "Case" or key == "States" or key == "StateInitial") then
-- (FMS) Finite State Machine
if ComponentClass == superClass then
if (key == "States") then
if not superClass.IsFSM then
ComponentFSM.AddCapability(superClass, value)
for _, qualifiedClass in pairs(Qualifiers) do
if qualifiedClass ~= superClass then
ComponentFSM.AddMethods(superClass, qualifiedClass)
end
end
end
else
rawset(t, key, value)
end
end
else
rawset(t, key, value)
end
end
})
if superClass.IsFSM then
ComponentFSM.AddMethods(superClass, ComponentClass)
end
--[[
Gets a qualifier for this type of component. If the qualifier does not exist, a new class will be created,
otherwise it brings the already registered class qualifier reference with the same name.
@param qualifier {string|ComponentClass}
@return ComponentClass
]]
function ComponentClass.Qualifier(qualifier)
if type(qualifier) ~= "string" then
for _, qualifiedClass in ipairs(QualifiersArr) do
if qualifiedClass == qualifier then
return qualifier
end
end
return nil
end
local qualifiedClass = Qualifiers[qualifier]
if qualifiedClass == nil then
qualifiedClass = createComponentClass(initializer, superClass)
Qualifiers[qualifier] = qualifiedClass
table.insert(QualifiersArr, qualifiedClass)
end
return qualifiedClass
end
--[[
Get all qualified class
@param ... {string|ComponentClass} (Optional) Allows to filter the specific qualifiers
@return ComponentClass[]
]]
function ComponentClass.Qualifiers(...)
local filter = {...}
if #filter == 0 then
return QualifiersArr
else
local qualifiers = {}
local cTypes = {}
for _,qualifier in ipairs({...}) do
local qualifiedClass = ComponentClass.Qualifier(qualifier)
if qualifiedClass and cTypes[qualifiedClass] == nil then
cTypes[qualifiedClass] = true
table.insert(qualifiers, qualifiedClass)
end
end
return qualifiers
end
end
--[[
Constructor
@param value {any} If the value is not a table, it will be converted to the format "{ value = value}"
@return Component
]]
function ComponentClass.New(value)
if (value ~= nil and type(value) ~= "table") then
-- local MyComponent = Component({ value = Vector3.new(0, 0, 0) })
-- local component = MyComponent(Vector3.new(10, 10, 10))
value = { value = value }
end
local component = setmetatable(initializer(value) or {}, ComponentClass)
for _, fn in ipairs(superClass._Initializers) do
fn(component)
end
component.isComponent = true
component._qualifiers = { [ComponentClass] = component }
return component
end
--[[
Get this component's class
@return ComponentClass
]]
function ComponentClass:GetType()
return ComponentClass
end
--[[
Check if this component is of the type informed
@param componentClass {ComponentClass}
@return bool
]]
function ComponentClass:Is(componentClass)
return componentClass == ComponentClass or componentClass == superClass
end
--[[
Get the instance for the primary qualifier of this class
@return Component|nil
]]
function ComponentClass:Primary()
return self._qualifiers[superClass]
end
--[[
Get the instance for the given qualifier of this class
@param name {string|ComponentClass}
@return Component|nil
]]
function ComponentClass:Qualified(qualifier)
return self._qualifiers[ComponentClass.Qualifier(qualifier)]
end
--[[
Get all instances for all qualifiers of that class
@return Component[]
]]
function ComponentClass:QualifiedAll()
local qualifiedAll = {}
for name, qualifiedClass in pairs(Qualifiers) do
qualifiedAll[name] = self._qualifiers[qualifiedClass]
end
return qualifiedAll
end
--[[
Merges data from the other component into the current component. This method should not be invoked, it is used
by the entity to ensure correct retrieval of a component's qualifiers.
@param other {Component}
]]
function ComponentClass:Merge(other)
if superClass.HasQualifier then
if self == other then
return
end
if self._qualifiers == other._qualifiers then
return
end
if not other:Is(superClass) then
return
end
local selfClass = ComponentClass
local otherClass = other:GetType()
-- does anyone know the reference to the primary entity?
local primaryQualifiers
if selfClass == superClass then
primaryQualifiers = self._qualifiers
elseif otherClass == superClass then
primaryQualifiers = other._qualifiers
elseif self._qualifiers[superClass] ~= nil then
primaryQualifiers = self._qualifiers[superClass]._qualifiers
elseif other._qualifiers[superClass] ~= nil then
primaryQualifiers = other._qualifiers[superClass]._qualifiers
end
if primaryQualifiers ~= nil then
if self._qualifiers ~= primaryQualifiers then
for qualifiedClass, component in pairs(self._qualifiers) do
if superClass ~= qualifiedClass then
primaryQualifiers[qualifiedClass] = component
component._qualifiers = primaryQualifiers
end
end
end
if other._qualifiers ~= primaryQualifiers then
for qualifiedClass, component in pairs(other._qualifiers) do
if superClass ~= qualifiedClass then
primaryQualifiers[qualifiedClass] = component
component._qualifiers = primaryQualifiers
end
end
end
else
-- none of the instances know the Primary, use the current object reference
for qualifiedClass, component in pairs(other._qualifiers) do
if selfClass ~= qualifiedClass then
self._qualifiers[qualifiedClass] = component
component._qualifiers = self._qualifiers
end
end
end
end
end
--[[
Unlink this component with the other qualifiers
]]
function ComponentClass:Detach()
if not superClass.HasQualifier then
return
end
-- remove old unlink
self._qualifiers[ComponentClass] = nil
-- new link
self._qualifiers = { [ComponentClass] = self }
end
return ComponentClass
end
local function defaultInitializer(value)
return value or {}
end
--[[
A Component is an object that can store data but should have not behaviour (As that should be handled by systems).
]]
local Component = {}
--[[
Register a new ComponentClass
@param template {table|function(table?) -> table}
When `table`, this template will be used for creating component instances
When it's a `function`, it will be invoked when a new component is instantiated. The creation parameter of the
component is passed to template function
If the template type is different from `table` and `function`, **ECS Lua** will generate a template in the format
`{ value = template }`.
@return ComponentClass
]]
function Component.Create(template)
local initializer = defaultInitializer
if template ~= nil then
local ttype = type(template)
if (ttype == "function") then
initializer = template
else
if (ttype ~= "table") then
template = { value = template }
end
initializer = function(value)
local data = copyDeep(template)
if (value ~= nil) then
mergeDeep(data, value)
end
return data
end
end
end
return createComponentClass(initializer, nil)
end
return Component
end
__F__["ComponentFSM"] = function()
-- src/ComponentFSM.lua
--[[
Facilitate the construction and use of a Finite State Machine (FSM) using ECS
Example:
local Movement = ECS.Component({ Speed = 0 })
Movement.States = {
Standing = "*",
Walking = {"Standing", "Running"},
Running = {"Walking"}
}
Movement.StateInitial = "Standing"
Movement.Case = {
Standing = function(self, previous)
self.Speed = 0
end,
Walking = function(self, previous)
self.Speed = 5
end,
Running = function(self, previous)
self.Speed = 10
end
}
local movement = entity[Movement]
print(movement:GetState()) -- "Standing"
movement:SetState("Walking")
print(movement:GetPrevState()) -- "Standing"
movement:GetStateTime()
if (movement:GetState() == "Standing") then
movement.Speed = 0
end
]]
local Query = __REQUIRE__("Query")
--[[
Filter used in Query and QueryResult
@see QueryResult.lua
Ex. ECS.Query.All(Movement.In("Standing", "Walking"))
]]
local queryFilterCTypeStateIn = Query.Filter(function(entity, config)
local states = config.States
local isSuperClass = config.IsSuperClass
local componentClass = config.ComponentClass
if isSuperClass then
local qualifiers = componentClass.Qualifiers()
for _, qualifier in ipairs(qualifiers) do
local component = entity[qualifier]
if (component ~= nil and states[component:GetState()] == true) then
return true
end
end
return false
else
local component = entity[componentClass]
if component == nil then
return false
end
return states[component:GetState()] == true
end
end)
local ComponentFSM = {}
--[[
Adds FSM capability to a ComponentClass
@param superClass {ComonentClass}
@param states { {[key=string] => string|string[]}}
@see Component.lua - createComponentClass() - ComponentClass__newindex
]]
function ComponentFSM.AddCapability(superClass, states)
superClass.IsFSM = true
local cTypeStates = setmetatable({}, {
__newindex = function(states, newState, value)
if (type(value) ~= "table") then
value = {value}
end
if table.find(value, "*") then
rawset(states, newState, "*")
else
local idxSelf = table.find(value, newState)
if idxSelf ~= nil then
table.remove(value, idxSelf)
if #value == 0 then
value = "*"
end
end
rawset(states, newState, value)
end
end
})
rawset(superClass, "__States", cTypeStates)
for state,value in pairs(states) do
if superClass.StateInitial == nil then
superClass.StateInitial = state
end
cTypeStates[state] = value
end
ComponentFSM.AddMethods(superClass, superClass)
table.insert(superClass._Initializers, function(component)
component:SetState(superClass.StateInitial)
end)
end
--[[
Adds FSM state change methods to a ComponentClass
@param superClass {ComponentClass}
@param componentClass {ComponentClass}
]]
function ComponentFSM.AddMethods(superClass, componentClass)
componentClass.IsFSM = true
local cTypeStates = superClass.States
--[[
Creates a clause used to filter repository entities in a Query or QueryResult
@param ... {string[]}
@return Clause
Ex. ECS.Query.All(Movement.In("Walking", "Running"))
]]
function componentClass.In(...)
local states = {}
local count = 0
for _,state in ipairs({...}) do
if (cTypeStates[state] ~= nil and states[state] == nil) then
count = count + 1
states[state] = true
end
end
if count == 0 then
-- In any state
return {}
end
return queryFilterCTypeStateIn({
States = states,
IsSuperClass = (componentClass == superClass),
ComponentClass = componentClass,
})
end
--[[
Defines the current state of the FSM
@param newState {string}
]]
function componentClass:SetState(newState)
if (newState == nil or cTypeStates[newState] == nil) then
return
end
local actual = self:GetState()
if (actual == newState) then
return
end
if (actual ~= nil ) then
local transtions = cTypeStates[actual]
if (transtions ~= "*" and table.find(transtions, newState) == nil) then
-- not allowed
return
end
end
self._state = newState
self._statePrev = actual
self._stateTime = os.clock()
local action = superClass.Case and superClass.Case[newState]
if action then
action(self, actual)
end
end
--[[
Get the current state of the FSM
@return string
]]
function componentClass:GetState()
return self._state or superClass.StateInitial
end
--[[
Get the previous state of the FSM
@return string|nil
]]
function componentClass:GetPrevState()
return self._statePrev or nil
end
--[[
Gets the time it changed to the current state
]]
function componentClass:GetStateTime()
return self._stateTime or 0
end
end
return ComponentFSM
end
__F__["ECS"] = function()
-- src/ECS.lua
--[[
ECS Lua v2.2.0
ECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.
https://github.com/nidorx/ecs-lua
Discussions about this script are at https://devforum.roblox.com/t/841175
------------------------------------------------------------------------------
MIT License
Copyright (c) 2020 Alex Rodin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local Query = __REQUIRE__("Query")
local World = __REQUIRE__("World")
local System = __REQUIRE__("System")
local Archetype = __REQUIRE__("Archetype")
local Component = __REQUIRE__("Component")
local function setLoopManager(manager)
World.LoopManager = manager
end
pcall(function()
if (game and game.ClassName == "DataModel") then
-- is roblox
setLoopManager(__REQUIRE__("RobloxLoopManager")())
end
end)
--[[
@TODO
- Server entities
- Client - Server sincronization (snapshot, delta, spatial index, grid manhatham distance)
- Table pool (avoid GC)
- System readonly? Paralel execution
- Debugging?
- Benchmark (Local Script vs ECS implementation)
- Basic physics (managed)
- SharedComponent?
- Serializaton
- world:Serialize()
- world:Serialize(entity)
- entity:Serialize()
- component:Serialize()
]]
local ECS = {
Query = Query,
World = World.New,
System = System.Create,
Archetype = Archetype,
Component = Component.Create,
SetLoopManager = setLoopManager
}
if _G.ECS == nil then
_G.ECS = ECS
else
local warn = _G.warn or print
warn("ECS Lua was not registered in the global variables, there is already another object registered.")
end
return ECS
end
__F__["Entity"] = function()
-- src/Entity.lua
--[[
The entity is a fundamental part of the Entity Component System. Everything in your game that has data or an
identity of its own is an entity. However, an entity does not contain either data or behavior itself. Instead,
the data is stored in the components and the behavior is provided by the systems that process those components.
]]
local Archetype = __REQUIRE__("Archetype")
local SEQ = 0
--[[
[GET]
01) comp1 = entity[CompType1]
02) comp1 = entity:Get(CompType1)
03) comp1, comp2, comp3 = entity:Get(CompType1, CompType2, CompType3)
]]
local function getComponent(entity, ...)
local values = {...}
local data = entity._data
if (#values == 1) then
local cType = values[1]
if (cType.IsCType and not cType.isComponent) then
-- 01) comp1 = entity[CompType1]
-- 02) comp1 = entity:Get(CompType1)
return data[cType]
else
return nil
end
end
-- 03) comp1, comp2, comp3 = entity:Get(CompType1, CompType2, CompType3)
local components = {}
for i,cType in ipairs(values) do
if (cType.IsCType and not cType.isComponent) then
table.insert(components, data[cType])
end
end
return table.unpack(components)
end
--[[
Merges the qualifiers of a new added component.
@param entity {Entity}
@param newComponent {Component}
@param newComponentClass {ComponentClass}
]]
local function mergeComponents(entity, newComponent, newComponentClass)
local data = entity._data
local otherComponent
-- get first instance
for _,oCType in ipairs(newComponentClass.Qualifiers()) do
if oCType ~= newComponentClass then
otherComponent = data[oCType]
if otherComponent then
break
end
end
end
if otherComponent then
otherComponent:Merge(newComponent)
end
end
--[[
[SET]
01) entity[CompType1] = nil
02) entity[CompType1] = value
03) entity:Set(CompType1, nil)
04) entity:Set(CompType1, value)
05) entity:Set(comp1)
06) entity:Set(comp1, comp2, ...)
]]
local function setComponent(entity, ...)
local values = {...}
local data = entity._data
local archetypeOld = entity.archetype
local archetypeNew = archetypeOld
local toMerge = {}
local cType = values[1]
if (cType and cType.IsCType and not cType.isComponent) then
local value = values[2]
local component
-- 01) entity[CompType1] = nil
-- 02) entity[CompType1] = value
-- 03) entity:Set(CompType1, nil)
-- 04) entity:Set(CompType1, value)
if value == nil then
local old = data[cType]
if old then
old:Detach()
end
data[cType] = nil
archetypeNew = archetypeNew:Without(cType)
elseif (type(value) == "table" and value.isComponent) then
local old = data[cType]
if (old ~= value) then
if old then
old:Detach()
end
cType = value:GetType()
data[cType] = value
archetypeNew = archetypeNew:With(cType)
-- merge components
if (cType.HasQualifier or cType.IsQualifier) then
mergeComponents(entity, value, cType)
end
end
else
local old = data[cType]
if old then
old:Detach()
end
local component = cType(value)
data[cType] = component
archetypeNew = archetypeNew:With(cType)
-- merge components
if (cType.HasQualifier or cType.IsQualifier) then
mergeComponents(entity, component, cType)
end
end
else
-- 05) entity:Set(comp1)
-- 06) entity:Set(comp1, comp2, ...)
for i,component in ipairs(values) do
if (component.isComponent) then
local cType = component:GetType()
local old = data[cType]
if (old ~= component) then
if old then
old:Detach()
end
data[cType] = component
archetypeNew = archetypeNew:With(cType)
-- merge components
if (cType.HasQualifier or cType.IsQualifier) then
mergeComponents(entity, component, cType)
end
end
end
end
end
if (archetypeOld ~= archetypeNew) then
entity.archetype = archetypeNew
entity._onChange:Fire(entity, archetypeOld)
end
end
--[[
[UNSET]
01) enity:Unset(comp1)
02) entity[CompType1] = nil
03) enity:Unset(CompType1)
04) enity:Unset(comp1, comp1, ...)
05) enity:Unset(CompType1, CompType2, ...)
]]
local function unsetComponent(entity, ...)
local data = entity._data
local archetypeOld = entity.archetype
local archetypeNew = archetypeOld
for _,value in ipairs({...}) do
if value.isComponent then
-- 01) enity:Unset(comp1)
-- 04) enity:Unset(comp1, comp1, ...)
local cType = value:GetType()
local old = data[cType]
if old then
old:Detach()
end
data[cType] = nil
archetypeNew = archetypeNew:Without(cType)
elseif value.IsCType then
-- 02) entity[CompType1] = nil
-- 03) enity:Unset(CompType1)
-- 05) enity:Unset(CompType1, CompType2, ...)
local old = data[value]
if old then
old:Detach()
end
data[value] = nil
archetypeNew = archetypeNew:Without(value)
end
end
if entity.archetype ~= archetypeNew then
entity.archetype = archetypeNew
entity._onChange:Fire(entity, archetypeOld)
end
end
--[[
01) comps = entity:GetAll()
01) qualifiers = entity:GetAll(PrimaryClass)
]]
local function getAll(entity, qualifier)
local data = entity._data
local components = {}
if (qualifier ~= nil and qualifier.IsCType and not qualifier.isComponent) then
local ctypes = qualifier.Qualifiers()
for _,cType in ipairs(ctypes) do
local component = data[cType]
if component then
table.insert(components, component)
end
end
else
for _, component in pairs(data) do
table.insert(components, component)
end
end
return components
end
--[[
01) comp = entity:GetAny(PrimaryClass)
]]
local function getAny(entity, qualifier)
if (qualifier ~= nil and qualifier.IsCType and not qualifier.isComponent) then
local data = entity._data
local ctypes = qualifier.Qualifiers()
for _,cType in ipairs(ctypes) do
local component = data[cType]
if component then
return component
end
end
end
end
local Entity = {
__index = function(e, key)
if (type(key) == "table") then
-- 01) local comp1 = entity[CompType1]
-- 01) local comps = entity[{CompType1, CompType2, ...}]
return getComponent(e, key)
end
end,
__newindex = function(e, key, value)
local isComponentSet = true
if (type(key) == "table" and (key.IsCType and not key.isComponent)) then
-- 01) entity[CompType1] = nil
-- 02) entity[CompType1] = value
setComponent(e, key, value)
else
rawset(e, key, value)
end
end
}
--[[
Creates an entity having components of the specified types.
@param onChange {Event}
@param components {Component[]} (Optional)
]]
function Entity.New(onChange, components)
local archetype = Archetype.EMPTY
local data = {}
if (components ~= nil and #components > 0) then
local cTypes = {}
for _, component in ipairs(components) do
local cType = component:GetType()
table.insert(cTypes, cType)
data[cType] = component
end
archetype = Archetype.Of(cTypes)
end
SEQ = SEQ + 1
return setmetatable({
_data = data,
_onChange = onChange,
id = SEQ,
isAlive = false,
archetype = archetype,
Get = getComponent,
Set = setComponent,
Unset = unsetComponent,
GetAll = getAll,
GetAny = getAny,
}, Entity)
end
return Entity
end
__F__["EntityRepository"] = function()
-- src/EntityRepository.lua
local Event = __REQUIRE__("Event")
--[[
The repository (database) of entities in a world.
The repository indexes entities by archetype. Whenever the entity's archetype is changed, the entity is
transported to the correct storage.
]]
local EntityRepository = {}
EntityRepository.__index = EntityRepository
--[[
Create a new repository
@return EntityRepository
]]
function EntityRepository.New()
return setmetatable({
_archetypes = {},
_entitiesArchetype = {},
}, EntityRepository)
end
--[[
Insert an entity into this repository
@param entity {Entity}
]]
function EntityRepository:Insert(entity)
if (self._entitiesArchetype[entity] == nil) then
local archetype = entity.archetype
local storage = self._archetypes[archetype]
if (storage == nil) then
storage = { count = 0, entities = {} }
self._archetypes[archetype] = storage
end
storage.entities[entity] = true
storage.count = storage.count + 1
self._entitiesArchetype[entity] = archetype
else
self:Update(entity)
end
end
--[[
Remove an entity from this repository
@param entity {Entity}
]]
function EntityRepository:Remove(entity)
local archetypeOld = self._entitiesArchetype[entity]
if archetypeOld == nil then
return
end
self._entitiesArchetype[entity] = nil
local storage = self._archetypes[archetypeOld]
if (storage ~= nil and storage.entities[entity] == true) then
storage.entities[entity] = nil
storage.count = storage.count - 1
if (storage.count == 0) then
self._archetypes[archetypeOld] = nil
end
end
end
--[[
Updates the entity in the repository, if necessary, moves the entity from one storage to another
@param entity {Entity}
]]
function EntityRepository:Update(entity)
local archetypeOld = self._entitiesArchetype[entity]
if (archetypeOld == nil or archetypeOld == entity.archetype) then
return
end
self:Remove(entity)
self:Insert(entity)
end
--[[
Execute the query entered in this repository
@param query {Query}
@return QueryResult
]]
function EntityRepository:Query(query)
local chunks = {}
for archetype, storage in pairs(self._archetypes) do
if query:Match(archetype) then
table.insert(chunks, storage.entities)
end
end
return query:Result(chunks), #chunks > 0
end
--[[
Quick check to find out if a query is applicable.
@param query {Query}
@return bool
]]
function EntityRepository:FastCheck(query)
for archetype, storage in pairs(self._archetypes) do
if query:Match(archetype) then
return true
end
end
return false
end
return EntityRepository
end
__F__["Event"] = function()
-- src/Event.lua
--[[
Subscription
]]
local Connection = {}
Connection.__index = Connection
function Connection.New(event, handler)
return setmetatable({ _event = event, _handler = handler }, Connection)
end
-- Unsubscribe
function Connection:Disconnect()
local event = self._event
if (event and not event.destroyed) then
local idx = table.find(event._handlers, self._handler)
if idx ~= nil then
table.remove(event._handlers, idx)
end
end
setmetatable(self, nil)
end
--[[
Observer Pattern
Allows the application to fire events of a particular type.
]]
local Event = {}
Event.__index = Event
function Event.New()
return setmetatable({ _handlers = {} }, Event)
end
function Event:Connect(handler)
if (type(handler) == "function") then
table.insert(self._handlers, handler)
return Connection.New(self, handler)
end
error(("Event:Connect(%s)"):format(typeof(handler)), 2)
end
function Event:Fire(...)
if not self.destroyed then
for i,handler in ipairs(self._handlers) do
handler(table.unpack({...}))
end
end
end
function Event:Destroy()
setmetatable(self, nil)
self._handlers = nil
self.destroyed = true
end
return Event
end
__F__["Query"] = function()
-- src/Query.lua
local QueryResult = __REQUIRE__("QueryResult")
--[[
Global cache result.
The validated components are always the same (reference in memory, except within the archetypes),
in this way, you can save the result of a query in an archetype, reducing the overall execution
time (since we don't need to iterate all the time)
@type KEY string = concat(Array<ComponentClass.Id>, "_")
@Type {
[Archetype] = {
Any = { [KEY] = bool },
All = { [KEY] = bool },
None = { [KEY] = bool },
}
}
]]
local CACHE = {}
--[[
Interface for creating filters for existing entities in the ECS world
]]
local Query = {}
Query.__index = Query
setmetatable(Query, {
__call = function(t, all, any, none)
return Query.New(all, any, none)
end,
})
local function parseFilters(list, clauseGroup, clauses)
local indexed = {}
local cTypes = {}
local cTypeIds = {}
for i,item in ipairs(list) do
if (indexed[item] == nil) then
if (item.IsCType and not item.isComponent) then
indexed[item] = true
table.insert(cTypes, item)
table.insert(cTypeIds, item.Id)
else
-- clauses
if item.Filter then
indexed[item] = true
item[clauseGroup] = true
table.insert(clauses, item)
end
end
end
end
if #cTypes > 0 then
table.sort(cTypeIds)
local cTypesKey = "_" .. table.concat(cTypeIds, "_")
return cTypes, cTypesKey
end
end
--[[
Create a new Query used to filter entities in the world. It makes use of local and global cache in order to
decrease the validation time (avoids looping in runtime of systems)
@param all {Array<ComponentClass|Clause>} Optional All component types in this array must exist in the archetype
@param any {Array<ComponentClass|Clause>} Optional At least one of the component types in this array must exist in the archetype
@param none {Array<ComponentClass|Clause>} Optional None of the component types in this array can exist in the archetype
]]
function Query.New(all, any, none)
-- used by QueryResult
local clauses = {}
local anyKey, allKey, noneKey
if (any ~= nil) then
any, anyKey = parseFilters(any, "IsAnyFilter", clauses)
end
if (all ~= nil) then
all, allKey = parseFilters(all, "IsAllFilter", clauses)
end
if (none ~= nil) then
none, noneKey = parseFilters(none, "IsNoneFilter", clauses)
end
return setmetatable({
isQuery = true,
_any = any,
_all = all,
_none = none,
_anyKey = anyKey,
_allKey = allKey,
_noneKey = noneKey,
_cache = {}, -- local cache (L1)
_clauses = #clauses > 0 and clauses or nil
}, Query)
end
--[[
Generate a QueryResult with the chunks entered and the clauses of the current query
@param chunks {Chunk}
@return QueryResult
]]
function Query:Result(chunks)
return QueryResult.New(chunks, self._clauses)
end
--[[
Checks if the entered archetype is valid by the query definition
@param archetype {Archetype}
@return bool
]]
function Query:Match(archetype)
-- cache L1
local localCache = self._cache
-- check local cache (L1)
local cacheResult = localCache[archetype]
if cacheResult ~= nil then
return cacheResult
else
-- check global cache (executed by other filter instance)
local globalCache = CACHE[archetype]
if (globalCache == nil) then
globalCache = { Any = {}, All = {}, None = {} }
CACHE[archetype] = globalCache
end
-- check if these combinations exist in this component array
local noneKey = self._noneKey
if noneKey then
local isNoneValid = globalCache.None[noneKey]
if (isNoneValid == nil) then
isNoneValid = true
for _, cType in ipairs(self._none) do
if archetype:Has(cType) then
isNoneValid = false
break
end
end
globalCache.None[noneKey] = isNoneValid
end
if (isNoneValid == false) then
localCache[archetype] = false
return false
end
end
local anyKey = self._anyKey
if anyKey then
local isAnyValid = globalCache.Any[anyKey]
if (isAnyValid == nil) then
isAnyValid = false
if (globalCache.All[anyKey] == true) then
isAnyValid = true
else
for _, cType in ipairs(self._any) do
if archetype:Has(cType) then
isAnyValid = true
break
end
end
end
globalCache.Any[anyKey] = isAnyValid
end
if (isAnyValid == false) then
localCache[archetype] = false
return false
end
end
local allKey = self._allKey
if allKey then
local isAllValid = globalCache.All[allKey]
if (isAllValid == nil) then
local haveAll = true
for _, cType in ipairs(self._all) do
if (not archetype:Has(cType)) then
haveAll = false
break
end
end
if haveAll then
isAllValid = true
else
isAllValid = false
end
globalCache.All[allKey] = isAllValid
end
localCache[archetype] = isAllValid
return isAllValid
end
-- empty query = SELECT * FROM
localCache[archetype] = true
return true
end
end
local function builder()
local builder = {
isQueryBuilder = true
}
local query
function builder.All(...)
query = nil
builder._all = {...}
return builder
end
function builder.Any(...)
query = nil
builder._any = {...}
return builder
end
function builder.None(...)
query = nil
builder._none = {...}
return builder
end
function builder.Build()
if query == nil then
query = Query.New(builder._all, builder._any, builder._none)
end
return query
end
return builder
end
function Query.All(...)
return builder().All(...)
end
function Query.Any(...)
return builder().Any(...)
end
function Query.None(...)
return builder().None(...)
end
--[[
Create custom filters that can be used in Queries. Its execution is delayed, invoked only in QueryResult methods
The result of executing the clause depends on how it was used in the query.
Ex. If used in Query.All() the result is the inverse of using the same clause in Query.None()
local Player = ECS.Component({ health = 100 })
local HealthPlayerFilter = ECS.Query.Filter(function(entity, config)
local player = entity[Player]
return player.health >= config.minHealth and player.health <= config.maxHealth
end)
local healthyClause = HealthPlayerFilter({
minHealth = 80,
maxHealth = 100,
})
local healthyQuery = ECS.Query.All(Player, healthyClause)
world:Exec(healthyQuery):ForEach(function(entity)
-- this player is very healthy
end)
local notHealthyQuery = ECS.Query.All(Player).None(healthyClause)
world:Exec(healthyQuery):ForEach(function(entity)
-- this player is NOT very healthy
end)
local dyingClause = HealthPlayerClause({
minHealth = 1,
maxHealth = 20,
})
local dyingQuery = ECS.Query.All(Player, dyingClause)
world:Exec(dyingQuery):ForEach(function(entity)
-- this player is about to die
end)
local notDyingQuery = ECS.Query.All(Player).None(dyingClause)
world:Exec(notDyingQuery):ForEach(function(entity)
-- this player is NOT about to die
end)
@param filter {function(entity, config):bool}
@return function(config):Clause
]]
function Query.Filter(filter)
return function (config)
return {
Filter = filter,
Config = config
}
end
end
return Query
end
__F__["QueryResult"] = function()
-- src/QueryResult.lua
--[[
OperatorFunction = function(param, value, count) => newValue, acceptItem, mustContinue
]]
local function operatorFilter(predicate, value, count)
return value, (predicate(value) == true), true
end
local function operatorMap(mapper, value, count)
return mapper(value), true, true
end
local function operatorLimit(limit, value, count)
local accept = (count <= limit)
return value, accept, accept
end
local function operatorClauseNone(clauses, value, count)
local acceptItem = true
for _,clause in ipairs(clauses) do
if (clause.Filter(value, clause.Config) == true) then
acceptItem = false
break
end
end
return value, acceptItem, true
end
local function operatorClauseAll(clauses, value, count)
local acceptItem = true
for _,clause in ipairs(clauses) do
if (clause.Filter(value, clause.Config) == false) then
acceptItem = false
break
end
end
return value, acceptItem, true
end
local function operatorClauseAny(clauses, value, count)
local acceptItem = false
for _,clause in ipairs(clauses) do
if (clause.Filter(value, clause.Config) == true) then
acceptItem = true
break
end
end
return value, acceptItem, true
end
local EMPTY_OBJECT = {}
--[[
The result of a Query that was executed on an EntityStorage.
QueryResult provides several methods to facilitate the filtering of entities resulting from the execution of the
query.
]]
local QueryResult = {}
QueryResult.__index = QueryResult
--[[
Build a new QueryResult
@param chunks { Array<{ [Entity] = true }> }
@clauses {Clause[]}
@see Query.lua
@see EntityRepository:Query(query)
@return QueryResult
]]
function QueryResult.New(chunks, clauses)
local pipeline = EMPTY_OBJECT
if (clauses and #clauses > 0) then
local all = {}
local any = {}
local none = {}
pipeline = {}
for i,clause in ipairs(clauses) do
if clause.IsNoneFilter then
table.insert(none, clause)
elseif clause.IsAnyFilter then
table.insert(any, clause)
else
table.insert(all, clause)
end
end
if (#none > 0) then
table.insert(pipeline, {operatorClauseNone, none})
end
if (#all > 0) then
table.insert(pipeline, {operatorClauseAll, all})
end
if (#any > 0) then
table.insert(pipeline, {operatorClauseAny, any})
end
end
return setmetatable({
chunks = chunks,
_pipeline = pipeline,
}, QueryResult)
end
--[[ -------------------------------------------------------------------------------------------------------------------
Intermediate Operations
Intermediate operations return a new QueryResult. They are always lazy; executing an intermediate operation such as
QueryResult:Filter() does not actually perform any filtering, but instead creates a new QueryResult that, when traversed,
contains the elements of the initial QueryResult that match the given predicate. Traversal of the pipeline source
does not begin until the terminal operation of the pipeline is executed.
]] ---------------------------------------------------------------------------------------------------------------------
--[[
Returns a QueryResult consisting of the elements of this QueryResult with a new pipeline operation
@param operation {function(param, value, count) -> newValue, accept, continues}
@param param {any}
@return the new QueryResult
]]
function QueryResult:With(operation, param)
local pipeline = {}
for _,operator in ipairs(self._pipeline) do
table.insert(pipeline, operator)
end
table.insert(pipeline, { operation, param })
return setmetatable({
chunks = self.chunks,
_pipeline = pipeline,
}, QueryResult)
end
--[[
Returns a QueryResult consisting of the elements of this QueryResult that match the given predicate.
@param predicate {function(value) -> bool} a predicate to apply to each element to determine if it should be included
@return the new QueryResult
]]
function QueryResult:Filter(predicate)
return self:With(operatorFilter, predicate)
end
--[[
Returns a QueryResult consisting of the results of applying the given function to the elements of this QueryResult.
@param mapper {function(value) -> newValue} a function to apply to each element
@return the new QueryResult
]]
function QueryResult:Map(mapper)
return self:With(operatorMap, mapper)
end
--[[
Returns a QueryResult consisting of the elements of this QueryResult, truncated to be no longer than maxSize in length.
This is a short-circuiting stateful intermediate operation.
@param maxSize {number}
@return the new QueryResult
]]
function QueryResult:Limit(maxSize)
return self:With(operatorLimit, maxSize)
end
--[[ -------------------------------------------------------------------------------------------------------------------
Terminal Operations
Terminal operations, such as QueryResult:ForEach or QueryResult.AllMatch, may traverse the QueryResult to produce a
result or a side-effect. After the terminal operation is performed, the pipeline is considered consumed, and can no
longer be used; if you need to traverse the same data source again, you must return to the data source to get a new
QueryResult.
]] ---------------------------------------------------------------------------------------------------------------------
--[[
Returns whether any elements of this result match the provided predicate.
@param predicate { function(value) -> bool} a predicate to apply to elements of this result
@returns true if any elements of the result match the provided predicate, otherwise false
]]
function QueryResult:AnyMatch(predicate)
local anyMatch = false
self:ForEach(function(value)
if predicate(value) then
anyMatch = true
end
-- break if true
return anyMatch
end)
return anyMatch
end
--[[
Returns whether all elements of this result match the provided predicate.
@param predicate { function(value) -> bool} a predicate to apply to elements of this result
@returns true if either all elements of the result match the provided predicate or the result is empty, otherwise false
]]
function QueryResult:AllMatch(predicate)
local allMatch = true
self:ForEach(function(value)
if (not predicate(value)) then
allMatch = false
end
-- break if false
return allMatch == false
end)
return allMatch
end
--[[
Returns some element of the result, or nil if the result is empty.
This is a short-circuiting terminal operation.
The behavior of this operation is explicitly nondeterministic; it is free to select any element in the result.
Multiple invocations on the same result may not return the same value.
@return {any}
]]
function QueryResult:FindAny()
local out
self:ForEach(function(value)
out = value
-- break
return true
end)
return out
end
--[[
Returns an array containing the elements of this QueryResult.
This is a terminal operation.
]]
function QueryResult:ToArray()
local array = {}
self:ForEach(function(value)
table.insert(array, value)
end)
return array
end
--[[
Returns an Iterator, to use in for loop
for count, entity in result:Iterator() do
print(entity.id)
end
]]
function QueryResult:Iterator()
local thread = coroutine.create(function()
self:ForEach(function(value, count)
-- These will be passed back again next iteration
coroutine.yield(value, count)
end)
end)
return function()
local success, item, index = coroutine.resume(thread)
return index, item
end
end
--[[
Performs an action for each element of this QueryResult.
This is a terminal operation.
The behavior of this operation is explicitly nondeterministic. This operation does not guarantee to respect the
encounter order of the QueryResult.
@param action {function(value, count) -> bool} A action to perform on the elements, breaks execution case returns true
]]
function QueryResult:ForEach(action)
local count = 1
local pipeline = self._pipeline
local hasPipeline = #pipeline > 0
if (not hasPipeline) then
-- faster
for _, entities in ipairs(self.chunks) do
for entity, _ in pairs(entities) do
if (action(entity, count) == true) then
return
end
count = count + 1
end
end
else
-- Pipeline this QueryResult, applying callback to each value
for i, entities in ipairs(self.chunks) do
for entity,_ in pairs(entities) do
local mustStop = false
local itemAccepted = true
local value = entity
if (itemAccepted and hasPipeline) then
for _, operator in ipairs(pipeline) do
local newValue, acceptItem, canContinue = operator[1](operator[2], value, count)
if (not canContinue) then
mustStop = true
end
if acceptItem then
value = newValue
else
itemAccepted = false
break
end
end
end
if itemAccepted then
if (action(value, count) == true) then
return
end
count = count + 1
end
if mustStop then
return
end
end
end
end
end
return QueryResult
end
__F__["RobloxLoopManager"] = function()
-- src/RobloxLoopManager.lua
local function InitManager()
local RunService = game:GetService("RunService")
return {
Register = function(world)
-- if not RunService:IsRunning() then
-- return
-- end
local beforePhysics = RunService.Stepped:Connect(function()
world:Update("process", os.clock())
end)
local afterPhysics = RunService.Heartbeat:Connect(function()
world:Update("transform", os.clock())
end)
local beforeRender
if (not RunService:IsServer()) then
beforeRender = RunService.RenderStepped:Connect(function()
world:Update("render", os.clock())
end)
end
return function()
beforePhysics:Disconnect()
afterPhysics:Disconnect()
if beforeRender then
beforeRender:Disconnect()
end
end
end
}
end
return InitManager
end
__F__["System"] = function()
-- src/System.lua
local STEPS = { "task", "render", "process", "transform" }
local System = {}
--[[
Create new System Class
@param step {process|transform|render|task}
@param order {number} (Optional) Allows you to set an execution order (for systems that are not `task`). Default 50
@param query {Query|QueryBuilder} (Optional) Filters the entities that will be processed by this system
@param updateFn {function(self, Time)} (Optional) A shortcut for creating systems that only have the Update method
@return SystemClass
]]
function System.Create(step, order, query, updateFn)
if (step == nil or not table.find(STEPS, step)) then
error("The step parameter must one of ", table.concat(STEPS, ", "))
end
if (order and type(order) == "function") then
updateFn = order
order = nil
elseif query and type(query) == "function" then
updateFn = query
query = nil
end
if (order and type(order) == "table" and (order.isQuery or order.isQueryBuilder)) then
query = order
order = nil
end
if (order == nil or order < 0) then
order = 50
end
if type(query) == "function" then
updateFn = query
query = nil
end
if (query and query.isQueryBuilder) then
query = query.Build()
end
local SystemClass = {
Step = step,
-- Allows you to define the execution priority level for this system
Order = order,
Query = query,
-- After = {SystemC, SystemD}, When the system is a task, it allows you to define that this system should run AFTER other specific systems.
-- Before = {SystemA, SystemB}, When the system is a task, it allows you to define that this system should run BEFORE other specific systems.
--[[
ShouldUpdate(Time) -> bool - Invoked before 'Update', allows you to control the execution of the update
Update(Time)
[QuerySystem]
OnRemove(Time, enity)
OnExit(Time, entity)
OnEnter(Time, entity)
]]
}
SystemClass.__index = SystemClass
--[[
Create an instance of this system
@param world {World}
@param config {table}
]]
function SystemClass.New(world, config)
local system = setmetatable({
version = 0,
_world = world,
_config = config,
}, SystemClass)
if system.Initialize then
system:Initialize(config)
end
return system
end
--[[
Get this system class
@return SystemClass
]]
function SystemClass:GetType()
return SystemClass
end
--[[
Run a query in the world. A shortcut to `self._world:Exec(query)`
@query {Query|QueryBuilder} Optional If nil, use default query
@return QueryResult
]]
function SystemClass:Result(query)
return self._world:Exec(query or SystemClass.Query)
end
--[[
destroy this instance
]]
function SystemClass:Destroy()
if self.OnDestroy then
self.OnDestroy()
end
setmetatable(self, nil)
for k,v in pairs(self) do
self[k] = nil
end
end
if updateFn and type(updateFn) == "function" then
SystemClass.Update = updateFn
end
return SystemClass
end
return System
end
__F__["SystemExecutor"] = function()
-- src/SystemExecutor.lua
--[[
After = {SystemC, SystemD}, An update order that requests ECS update this system after it updates another specified system.
Before = {SystemA, SystemB}, An update order that requests ECS update this system before it updates another specified system.
]]
local function mapTaskDependencies(systems)
local nodes = {}
local nodesByType = {}
for i,system in ipairs(systems) do
local sType = system:GetType()
if (system._TaskState == nil) then
-- suspended, scheduled, running
system._TaskState = "suspended"
end
if not nodesByType[sType] then
local node = {
Type = sType,
System = system,
-- @type {[Node]=true}
Depends = {}
}
nodesByType[sType] = node
table.insert(nodes, node)
end
end
for _, node in ipairs(nodes) do
-- this system will update Before another specified system
local before = node.Type.Before
if (before ~= nil and #before > 0) then
for _,sTypeOther in ipairs(before) do
local otherNode = nodesByType[sTypeOther]
if otherNode then
otherNode.Depends[node] = true
end
end
end
-- this system will update After another specified system
local after = node.Type.After
if (after ~= nil and #after > 0) then
for _,sTypeOther in ipairs(after) do
local otherNode = nodesByType[sTypeOther]
if otherNode then
node.Depends[otherNode] = true
end
end
end
end
return nodes
end
local function orderSystems(a, b)
return a.Order < b.Order
end
--[[
Responsible for coordinating and executing the systems methods
]]
local SystemExecutor = {}
SystemExecutor.__index = SystemExecutor
function SystemExecutor.New(world)
local executor = setmetatable({
_world = world,
_onExit = {},
_onEnter = {},
_onRemove = {},
_task = {},
_render = {},
_process = {},
_transform = {},
_schedulers = {},
_lastFrameMatchQueries = {},
_currentFrameMatchQueries = {},
}, SystemExecutor)
world:OnQueryMatch(function(query)
executor._currentFrameMatchQueries[query] = true
end)
return executor
end
function SystemExecutor:SetSystems(systems)
local onExit = {}
local onEnter = {}
local onRemove = {}
-- system:Update()
local updateTask = {}
local updateRender = {}
local updateProcess = {}
local updateTransform = {}
for _, system in pairs(systems) do
local step = system.Step
if system.Update then
if step == "task" then
table.insert(updateTask, system)
elseif step == "process" then
table.insert(updateProcess, system)
elseif step == "transform" then
table.insert(updateTransform, system)
elseif step == "render" then
table.insert(updateRender, system)
end
end
if (system.Query and system.Query.isQuery and step ~= "task") then
if system.OnExit then
table.insert(onExit, system)
end
if system.OnEnter then
table.insert(onEnter, system)
end
if system.OnRemove then
table.insert(onRemove, system)
end
end
end
updateTask = mapTaskDependencies(updateTask)
table.sort(onExit, orderSystems)
table.sort(onEnter, orderSystems)
table.sort(onRemove, orderSystems)
table.sort(updateRender, orderSystems)
table.sort(updateProcess, orderSystems)
table.sort(updateTransform, orderSystems)
self._onExit = onExit
self._onEnter = onEnter
self._onRemove = onRemove
self._task = updateTask
self._render = updateRender
self._process = updateProcess
self._transform = updateTransform
end
--[[
@param Time
@param changedEntities { { [Entity] = Old<Archetype> } }
]]
function SystemExecutor:ExecOnExitEnter(Time, changedEntities)
local isEmpty = true
-- { [Old<Archetype>] = { [New<Archetype>] = {Entity, Entity, ...} } }
local oldIndexed = {}
for entity, archetypeOld in pairs(changedEntities) do
local newIndexed = oldIndexed[archetypeOld]
if not newIndexed then
newIndexed = {}
oldIndexed[archetypeOld] = newIndexed
end
local archetypeNew = entity.archetype
local entities = newIndexed[archetypeNew]
if not entities then
entities = {}
newIndexed[archetypeNew] = entities
end
table.insert(entities, entity)
isEmpty = false
end
if isEmpty then
return
end
self:_ExecOnEnter(Time, oldIndexed)
self:_ExecOnExit(Time, oldIndexed)
end
--[[
Executes the systems' OnEnter method
@param Time {Time}
@param entities {{[Key=Entity] => Archetype}}
]]
function SystemExecutor:_ExecOnEnter(Time, oldIndexed)
local world = self._world
for _, system in ipairs(self._onEnter) do
local query = system.Query
for archetypeOld, newIndexed in pairs(oldIndexed) do
if not query:Match(archetypeOld) then
for archetypeNew, entities in pairs(newIndexed) do
if query:Match(archetypeNew) then
for i,entity in ipairs(entities) do
world.version = world.version + 1 -- increment Global System Version (GSV)
system:OnEnter(Time, entity) -- local dirty = entity.version > system.version
system.version = world.version -- update last system version with GSV
end
end
end
end
end
end
end
--[[
Executes the systems' OnExit method
@param Time {Time}
@param entities {{[Key=Entity] => Archetype}}
]]
function SystemExecutor:_ExecOnExit(Time, oldIndexed)
local world = self._world
for _, system in ipairs(self._onExit) do
local query = system.Query
for archetypeOld, newIndexed in pairs(oldIndexed) do
if query:Match(archetypeOld) then
for archetypeNew, entities in pairs(newIndexed) do
if not query:Match(archetypeNew) then
for i,entity in ipairs(entities) do
world.version = world.version + 1 -- increment Global System Version (GSV)
system:OnExit(Time, entity) -- local dirty = entity.version > system.version
system.version = world.version -- update last system version with GSV
end
end
end
end
end
end
end
--[[
Executes the systems' OnRemove method
@param Time {Time}
@param entities {{[Key=Entity] => Archetype}}
]]
function SystemExecutor:ExecOnRemove(Time, removedEntities)
local isEmpty = true
local oldIndexed = {}
for entity, archetypeOld in pairs(removedEntities) do
local entities = oldIndexed[archetypeOld]
if not entities then
entities = {}
oldIndexed[archetypeOld] = entities
end
table.insert(entities, entity)
isEmpty = false
end
if isEmpty then
return
end
local world = self._world
for _, system in ipairs(self._onRemove) do
for archetypeOld, entities in pairs(oldIndexed) do
if system.Query:Match(archetypeOld) then
for i,entity in ipairs(entities) do
world.version = world.version + 1 -- increment Global System Version (GSV)
system:OnRemove(Time, entity) -- local dirty = entity.version > system.version
system.version = world.version -- update last system version with GSV
end
end
end
end
end
local function execUpdate(executor, systems, Time)
local world = executor._world
local lastFrameMatchQueries = executor._lastFrameMatchQueries
local currentFrameMatchQueries = executor._currentFrameMatchQueries
for j, system in ipairs(systems) do
local canExec = true
if system.Query then
local query = system.Query
if lastFrameMatchQueries[query] == true or currentFrameMatchQueries[query] == true then
-- If the query ran in the last frame, it is likely to run successfully on this
canExec = true
else
-- Always revalidates, the repository undergoes constant change
canExec = world:FastCheck(query)
currentFrameMatchQueries[query] = canExec
end
end
if canExec then
if (system.ShouldUpdate == nil or system.ShouldUpdate(Time)) then
world.version = world.version + 1 -- increment Global System Version (GSV)
system:Update(Time) -- local dirty = entity.version > system.version
system.version = world.version -- update last system version with GSV
end
end
end
end
function SystemExecutor:ExecProcess(Time)
self._currentFrameMatchQueries = {}
execUpdate(self, self._process, Time)
end
function SystemExecutor:ExecTransform(Time)
execUpdate(self, self._transform, Time)
end
function SystemExecutor:ExecRender(Time)
execUpdate(self, self._render, Time)
self._lastFrameMatchQueries = self._currentFrameMatchQueries
end
--[[
Starts the execution of Jobs.
Each Job is performed in an individual coroutine
@param maxExecTime {number} limits the amount of time jobs can run
]]
function SystemExecutor:ExecTasks(maxExecTime)
while maxExecTime > 0 do
local hasMore = false
-- https://github.com/wahern/cqueues/issues/231#issuecomment-562838785
local i, len = 0, #self._schedulers-1
while i <= len do
i = i + 1
local scheduler = self._schedulers[i]
local tasksTime, hasMoreTask = scheduler.Resume(maxExecTime)
if hasMoreTask then
hasMore = true
end
maxExecTime = maxExecTime - (tasksTime + 0.00001)
if (maxExecTime <= 0) then
break
end
end
if not hasMore then
return
end
end
end
local function execTask(node, Time, world, onComplete)
local system = node.System
system._TaskState = "running"
if (system.ShouldUpdate == nil or system.ShouldUpdate(Time)) then
world.version = world.version + 1 -- increment Global System Version (GSV)
system:Update(Time) -- local dirty = entity.version > system.version
system.version = world.version -- update last system version with GSV
end
system._TaskState = "suspended"
onComplete(node)
end
--[[
Invoked at the beginning of each frame, it schedules the execution of the next tasks
]]
function SystemExecutor:ScheduleTasks(Time)
local world = self._world
local rootNodes = {} -- Node[]
local runningNodes = {} -- Node[]
local scheduled = {} -- { [Node] = true }
local completed = {} -- { [Node] = true }
local dependents = {} -- { [Node] = Node[] }
local i, len = 0, #self._task-1
while i <= len do
i = i + 1
local node = self._task[i]
if (node.System._TaskState == "suspended") then
-- will be executed
node.System._TaskState = "scheduled"
local hasDependencies = false
for other,_ in pairs(node.Depends) do
hasDependencies = true
if dependents[other] == nil then
dependents[other] = {}
end
table.insert(dependents[other], node)
end
if (not hasDependencies) then
table.insert(rootNodes, node)
end
scheduled[node] = true
end
end
-- suspended, scheduled, running
local function onComplete(node)
node.Thread = nil
node.LastExecTime = nil
completed[node] = true
-- alguma outra tarefa depende da execucao deste no para executar?
if dependents[node] then
local dependentsFromNode = dependents[node]
local i, len = 0, #dependentsFromNode-1
while i <= len do
i = i + 1
local dependent = dependentsFromNode[i]
if scheduled[dependent] then
local allDependenciesCompleted = true
for otherNode,_ in pairs(dependent.Depends) do
if completed[otherNode] ~= true then
allDependenciesCompleted = false
break
end
end
if allDependenciesCompleted then
scheduled[dependent] = nil
dependent.LastExecTime = 0
dependent.Thread = coroutine.create(execTask)
table.insert(runningNodes, dependent)
end
end
end
end
end
if #rootNodes > 0 then
local i, len = 0, #rootNodes-1
while i <= len do
i = i + 1
local node = rootNodes[i]
scheduled[node] = nil
node.LastExecTime = 0
node.Thread = coroutine.create(execTask)
table.insert(runningNodes, node)
end
local scheduler
scheduler = {
Resume = function(maxExecTime)
-- orders the threads, executing the ones with the least execution time first this prevents long tasks
-- from taking up all the processing time
table.sort(runningNodes, function(nodeA, nodeB)
return nodeA.LastExecTime < nodeB.LastExecTime
end)
local totalTime = 0
-- https://github.com/wahern/cqueues/issues/231#issuecomment-562838785
local i, len = 0, #runningNodes-1
while i <= len do
i = i + 1
local node = runningNodes[i]
if node.Thread ~= nil then
local execTime = os.clock()
node.LastExecTime = execTime
coroutine.resume(node.Thread, node, Time, world, onComplete)
totalTime = totalTime + (os.clock() - execTime)
if (totalTime > maxExecTime) then
break
end
end
end
-- remove completed
for i,node in ipairs(runningNodes) do
if node.Thread == nil then
local idx = table.find(runningNodes, node)
if idx ~= nil then
table.remove(runningNodes, idx)
end
end
end
local hasMore = #runningNodes > 0
if (not hasMore) then
local idx = table.find(self._schedulers, scheduler)
if idx ~= nil then
table.remove(self._schedulers, idx)
end
end
return totalTime, hasMore
end
}
table.insert(self._schedulers, scheduler)
end
end
return SystemExecutor
end
__F__["Timer"] = function()
-- src/Timer.lua
-- if execution is slow, perform a maximum of 4 simultaneous updates in order to keep the fixrate
local MAX_SKIP_FRAMES = 4
local function loop(Time)
local accumulator = 0.0
local lastStepTime = 0.0
return function (newTime, stepName, beforeUpdateFn, updateFn)
local dtFixed = Time.DeltaFixed
local stepTime = newTime - lastStepTime
if stepTime > 0.25 then
stepTime = 0.25
end
lastStepTime = newTime
Time.Now = newTime
-- 1000/30/1000 = 0.03333333333333333
accumulator = accumulator + stepTime
--[[
Adjusting the framerate, the world must run on the same frequency,
this ensures determinism in the execution of the scripts
Each system in "transform" step is executed at a predetermined frequency (in Hz).
Ex. If the game is running on the client at 30FPS but a system needs to be run at
120Hz or 240Hz, this logic will ensure that this frequency is reached
@see https://gafferongames.com/post/fix_your_timestep/
@see https://gameprogrammingpatterns.com/game-loop.html
@see https://bell0bytes.eu/the-game-loop/
]]
if stepName == "process" then
if accumulator >= dtFixed then
Time.Interpolation = 1
beforeUpdateFn(Time)
local nLoops = 0
while (accumulator >= dtFixed and nLoops < MAX_SKIP_FRAMES) do
updateFn(Time)
nLoops = nLoops + 1
Time.Process = Time.Process + dtFixed
accumulator = accumulator - dtFixed
end
end
else
Time.Interpolation = math.min(math.max(accumulator/dtFixed, 0), 1)
beforeUpdateFn(Time)
updateFn(Time)
end
end
end
local Timer = {}
Timer.__index = Timer
function Timer.New(frequency)
local Time = {
Now = 0,
-- The time at the beginning of this frame. The world receives the current time at the beginning
-- of each frame, with the value increasing per frame.
Frame = 0,
Process = 0, -- The time the latest process step has started.
Delta = 0, -- The completion time in seconds since the last frame.
DeltaFixed = 0,
-- INTERPOLATION: The proportion of time since the previous transform relative to processDeltaTime
Interpolation = 0
}
local timer = setmetatable({
-- Public, visible by systems
Time = Time,
Frequency = 0,
_update = loop(Time)
}, Timer)
timer:SetFrequency(frequency)
return timer
end
--[[
Changes the frequency of execution of the "process" step
@param frequency {number}
]]
function Timer:SetFrequency(frequency)
-- frequency: number,
-- The maximum times per second this system should be updated. Defaults 30
if frequency == nil then
frequency = 30
end
local safeFrequency = math.floor(math.abs(frequency)/2)*2
if safeFrequency < 2 then
safeFrequency = 2
end
if frequency ~= safeFrequency then
frequency = safeFrequency
end
self.Frequency = frequency
self.Time.DeltaFixed = 1000/frequency/1000
end
function Timer:Update(now, step, beforeUpdate, update)
self._update(now, step, beforeUpdate, update)
end
return Timer
end
__F__["Utility"] = function()
-- src/Utility.lua
--[[
Utility library.
]]
local Utility = {}
if table.unpack == nil then
table.unpack = unpack
end
if table.find == nil then
--[[
Within the given array-like table haystack, find the first occurrence of value needle, starting from index init
or the beginning if not provided. If the value is not found, nil is returned.
A linear search algorithm is performed.
]]
table.find = function(haystack, needle, init)
local len = #haystack
for i = init or 1, len, 1 do
if haystack[i] == needle then
return i
end
end
return nil
end
end
local function copyDeep(src)
local copy = {}
for k, v in pairs(src) do
if type(v) == "table" then
v = copyDeep(v)
end
copy[k] = v
end
return copy
end
Utility.copyDeep = copyDeep
--[[
Faz o merge dos atributos src com o dest
Quando o um atributo do segundo é um "table", faz uma copia do valor
]]
local function mergeDeep(dest, src)
for k,valueSrc in pairs(src) do
if (type(valueSrc) == "table") then
local valueDest = dest[k]
if (valueDest == nil or type(valueDest) ~= "table") then
dest[k] = copyDeep(valueSrc)
else
dest[k] = mergeDeep(valueDest, valueSrc)
end
else
dest[k] = valueSrc
end
end
return dest
end
Utility.mergeDeep = mergeDeep
return Utility
end
__F__["World"] = function()
-- src/World.lua
local Timer = __REQUIRE__("Timer")
local Event = __REQUIRE__("Event")
local Entity = __REQUIRE__("Entity")
local Archetype = __REQUIRE__("Archetype")
local SystemExecutor = __REQUIRE__("SystemExecutor")
local EntityRepository = __REQUIRE__("EntityRepository")
local World = {}
World.__index = World
--[[
Create a new world instance
@param systemClasses {SystemClass[]} (Optional) Array of system classes
@param frequency {number} (Optional) define the frequency that the `process` step will be executed. Default 30
@param disableAutoUpdate {bool} (Optional) when `~= false`, the world automatically registers in the `LoopManager`,
receiving the `World:Update()` method from it. Default false
]]
function World.New(systemClasses, frequency, disableAutoUpdate)
local world = setmetatable({
--[[
Global System Version (GSV).
Before executing the Update method of each system, the world version is incremented, so at this point, the
world version will always be higher than the running system version.
Whenever an entity archetype is changed (received or lost component) the entity's version is updated to the
current version of the world.
After executing the System Update method, the version of this system is updated to the current world version.
This mechanism allows a system to know if an entity has been modified after the last execution of this same
system, as the entity's version is superior to the version of the last system execution. Thus, a system can
contain logic if it only operates on "dirty" entities, which have undergone changes. The code for this
validation on a system is `local isDirty = entity.version > self.version`
]]
version = 0,
--[[
Allows you to define the maximum time that the JobSystem can operate in each frame.
The default value is 0.011666666666666665 = ((1000/60/1000)*0.7)
A game that runs at 30fps has 0.0333 seconds to do all the processing for each frame, including rendering
- 30FPS = ((1000/30/1000)*0.7)/3 = 0.007777777777777777
A game that runs at 60fps has 0.0166 seconds to do all the processing for each frame, including rendering
- 60FPS = ((1000/60/1000)*0.7)/3 = 0.0038888888888888883
]]
maxTasksExecTime = 0.013333333333333334,
_dirty = false, -- True when create/remove entity, add/remove entity component (change archetype)
_timer = Timer.New(frequency),
_systems = {}, -- systems in this world
_repository = EntityRepository.New(),
_entitiesCreated = {}, -- created during the execution of the Update
_entitiesRemoved = {}, -- removed during execution (only removed after the last execution step)
_entitiesUpdated = {}, -- changed during execution (received or lost components, therefore, changed the archetype)
_onQueryMatch = Event.New(),
_onChangeArchetypeEvent = Event.New(),
}, World)
-- System execution plan
world._executor = SystemExecutor.New(world)
world._onChangeArchetypeEvent:Connect(function(entity, archetypeOld, archetypeNew)
world:_OnChangeArchetype(entity, archetypeOld, archetypeNew)
end)
-- add systems
if (systemClasses ~= nil) then
for _, systemClass in ipairs(systemClasses) do
world:AddSystem(systemClass)
end
end
if (not disableAutoUpdate and World.LoopManager) then
world._loopCancel = World.LoopManager.Register(world)
end
return world
end
--[[
Changes the frequency of execution of the "process" step
@param frequency {number}
]]
function World:SetFrequency(frequency)
frequency = self._timer:SetFrequency(frequency)
end
--[[
Get the frequency of execution of the "process" step
@return number
]]
function World:GetFrequency(frequency)
return self._timer.Frequency
end
--[[
Add a new system to the world.
Only one instance per type is accepted. If there is already another instance of this system in the world, any new
invocation of this method will be ignored.
@param systemClass {SystemClass} The system to be added in the world
@param config {table} (Optional) System instance configuration
]]
function World:AddSystem(systemClass, config)
if systemClass then
if config == nil then
config = {}
end
if self._systems[systemClass] == nil then
self._systems[systemClass] = systemClass.New(self, config)
self._executor:SetSystems(self._systems)
end
end
end
--[[
Create a new entity.
The entity is created in DEAD state (entity.isAlive == false) and will only be visible for queries after the
cleaning step (OnRemove, OnEnter, OnExit) of the current step
@param args {Component[]}
@return Entity
]]
function World:Entity(...)
local entity = Entity.New(self._onChangeArchetypeEvent, {...})
self._dirty = true
self._entitiesCreated[entity] = true
entity.version = self.version -- update entity version using current Global System Version (GSV)
entity.isAlive = false
return entity
end
--[[
Performs immediate removal of an entity.
If the entity was created in this step and the cleanup process has not happened yet (therefore the entity is
inactive, entity.isAlive == false), the `OnRemove` event will never be fired.
If the entity is alive (entity.isAlive == true), even though it is removed immediately, the `OnRemove` event will be
fired at the end of the current step.
@param entity {Entity}
]]
function World:Remove(entity)
if self._entitiesRemoved[entity] == true then
return
end
if self._entitiesCreated[entity] == true then
self._entitiesCreated[entity] = nil
else
self._repository:Remove(entity)
self._entitiesRemoved[entity] = true
if self._entitiesUpdated[entity] == nil then
self._entitiesUpdated[entity] = entity.archetype
end
end
self._dirty = true
entity.isAlive = false
end
--[[
Run a query in this world
@param query {Query|QueryBuilder}
@return QueryResult
]]
function World:Exec(query)
if (query.isQueryBuilder) then
query = query.Build()
end
local result, match = self._repository:Query(query)
if match then
self._onQueryMatch:Fire(query)
end
return result
end
--[[
Quick check to find out if a query is applicable.
@param query {Query|QueryBuilder}
@return QueryResult
]]
function World:FastCheck(query)
if (query.isQueryBuilder) then
query = query.Build()
end
return self._repository:FastCheck(query)
end
--[[
Add a callback that is reported whenever a query has been successfully executed. Used internally
to quickly find out if a QuerySystem will run.
]]
function World:OnQueryMatch(callback)
return self._onQueryMatch:Connect(callback)
end
--[[
Perform world update.
When registered, LoopManager will invoke World Update for each step in the sequence.
- process At the beginning of each frame
- transform After the game engine's physics engine runs
- render Before rendering the current frame
@param step {"process"|"transform"|"render"}
@param now {number} Usually os.clock()
]]
function World:Update(step, now)
self._timer:Update(
now, step,
function(Time)
--[[
JobSystem
.------------------.
| pipeline |
|------------------|
| s:ShouldUpdate() |
| s:Update() |
'------------------'
]]
if step == "process" then
self._executor:ScheduleTasks(Time)
end
-- run suspended Tasks
self._executor:ExecTasks(self.maxTasksExecTime)
end,
function(Time)
--[[
.------------------.
| pipeline |
|------------------|
| s:ShouldUpdate() |
| s:Update() |
| |
|-- CLEAR ---------|
| s:OnRemove() |
| s:OnExit() |
| s:OnEnter() |
'------------------'
]]
if step == "process" then
self._executor:ExecProcess(Time)
elseif step == "transform" then
self._executor:ExecTransform(Time)
else
self._executor:ExecRender(Time)
end
-- cleans up after running scripts
while self._dirty do
self._dirty = false
-- 1: remove entities
local entitiesRemoved = {}
for entity,_ in pairs(self._entitiesRemoved) do
entitiesRemoved[entity] = self._entitiesUpdated[entity]
self._entitiesUpdated[entity] = nil
end
self._entitiesRemoved = {}
self._executor:ExecOnRemove(Time, entitiesRemoved)
entitiesRemoved = nil
local changed = {}
local hasChange = false
-- 2: Update entities in memory
for entity, archetypeOld in pairs(self._entitiesUpdated) do
if (archetypeOld ~= entity.archetype) then
hasChange = true
changed[entity] = archetypeOld
end
end
self._entitiesUpdated = {}
-- 3: Add new entities
for entity, _ in pairs(self._entitiesCreated) do
hasChange = true
changed[entity] = Archetype.EMPTY
entity.isAlive = true
self._repository:Insert(entity)
end
self._entitiesCreated = {}
if hasChange then
self._executor:ExecOnExitEnter(Time, changed)
changed = nil
end
end
end
)
end
--[[
Destroy this instance, removing all entities, systems and events
]]
function World:Destroy()
if self._loopCancel then
self._loopCancel()
self._loopCancel = nil
end
if self._onChangeArchetypeEvent then
self._onChangeArchetypeEvent:Destroy()
self._onChangeArchetypeEvent = nil
end
self._repository = nil
if self._systems then
for _,system in pairs(self._systems) do
system:Destroy()
end
self._systems = nil
end
self._timer = nil
self._ExecPlan = nil
self._entitiesCreated = nil
self._entitiesUpdated = nil
self._entitiesRemoved = nil
setmetatable(self, nil)
end
function World:_OnChangeArchetype(entity, archetypeOld, archetypeNew)
if entity.isAlive then
if self._entitiesUpdated[entity] == nil then
self._dirty = true
self._entitiesUpdated[entity] = archetypeOld
end
self._repository:Update(entity)
-- update entity version using current Global System Version (GSV)
entity.version = self.version
end
end
return World
end
return __REQUIRE__("ECS")
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2020 Alex Rodin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
<p align="center">
<a href="https://nidorx.github.io/ecs-lua">
<img
src="docs/assets/logo.svg"
alt="https://nidorx.github.io/ecs-lua"
/>
</a>
</p>
<p align="center">
<a href="https://app.travis-ci.com/nidorx/ecs-lua">
<img src="https://app.travis-ci.com/nidorx/ecs-lua.svg?branch=master" alt="Build Status" />
</a>
</p>
<p align="center">
<strong><a href="https://nidorx.github.io/ecs-lua#/">Read the Documentation</a></strong>
</p>
# What is it?
<strong>ECS Lua</strong> is a fast and easy to use ECS (Entity Component System) engine for game development.
<div align="center">

</div>
The basic idea of this pattern is to stop defining entities using a
[hierarchy](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)) of classes and start doing use of
[composition](https://en.wikipedia.org/wiki/Object_composition) in a Data Oriented Programming paradigm.
([More information on Wikipedia](https://en.wikipedia.org/wiki/Entity_component_system)).
Programming with an ECS can result in code that is more efficient and easier to extend over time.
# How does it work?
<div align="center">

</div>
# Talk is cheap. Show me the code!
```lua
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](https://luau-lang.org/)
- **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.
# Goal
To be a lightweight, simple, ergonomic and high-performance ECS library that can be easily extended. The **ECS Lua**
does not strictly follow _"pure ECS design"_.
# Usage
Read our [Full Documentation][docs] to learn how to use **ECS Lua**.
# Get involved
All kinds of contributions are welcome!
🐛 **Found a bug?**
Let me know by [creating an issue][new-issue].
❓ **Have a question?**
[Roblox DevForum][discussions] is a good place to start.
⚙️ **Interested in fixing a [bug][bugs] or adding a [feature][features]?**
Check out the [contributing guidelines](CONTRIBUTING.md).
📖 **Can we improve [our documentation][docs]?**
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.
[docs]: https://nidorx.github.io/ecs-lua
[bugs]: https://github.com/nidorx/ecs-lua/issues?q=is%3Aissue+is%3Aopen+label%3Abug
[features]: https://github.com/nidorx/ecs-lua/issues?q=is%3Aissue+is%3Aopen+label%3Afeature
[new-issue]: https://github.com/nidorx/ecs-lua/issues/new/choose
[discussions]: https://devforum.roblox.com/t/841175
[Lua 5.1]:https://app.travis-ci.com/github/nidorx/ecs-lua
[Lua 5.2]:https://app.travis-ci.com/github/nidorx/ecs-lua
[Lua 5.3]:https://app.travis-ci.com/github/nidorx/ecs-lua
[Lua 5.4]:https://app.travis-ci.com/github/nidorx/ecs-lua
[LuaJit]:https://app.travis-ci.com/github/nidorx/ecs-lua
[coroutines]:http://www.lua.org/pil/9.1.html
# License
This code is distributed under the terms and conditions of the [MIT license](LICENSE).
================================================
FILE: build.lua
================================================
local OUTPUT_CONCAT = "ECS_concat"
local OUTPUT_MINIFIED = "ECS"
local SRC_FILES = {
"Archetype",
"Component",
"ComponentFSM",
"ECS",
"Entity",
"EntityRepository",
"Event",
"Query",
"QueryResult",
"RobloxLoopManager",
"System",
"SystemExecutor",
"Timer",
"Utility",
"World"
}
local HEADER = [[
ECS Lua v2.2.0
ECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.
This is a minified version of ECS Lua, to see the full source code visit
https://github.com/nidorx/ecs-lua
Discussions about this script are at https://devforum.roblox.com/t/841175
------------------------------------------------------------------------------
MIT License
Copyright (c) 2021 Alex Rodin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.]]
HEADER = "--[[\n"..HEADER.."\n]]\n"
package.path = package.path .. ";modules/?.lua"
local function concat()
local concatContent = {
HEADER,
"local __M__, __F__ = {}, {}",
"local function __REQUIRE__(m)",
" if (not __M__[m]) then",
" __M__[m] = { r = __F__[m]() }",
" end",
" return __M__[m].r",
"end",
"",
}
for i,name in ipairs(SRC_FILES) do
local sourceFile = io.open("./src/"..name..".lua", "r")
if not sourceFile then
error("Could not open the input file `" .. OUTPUT_MINIFIED .. "`", 0)
end
local content = sourceFile:read( "*a" )
for _,oname in ipairs(SRC_FILES) do
content = content:gsub('require[(]["\']'..oname..'["\'][)]', '__REQUIRE__("'..oname..'")')
end
table.insert(concatContent, table.concat({
'__F__["'..name..'"] = function()',
(" -- src/"..name..".lua\n"..content):gsub("\n", "\n "),
"end",
"",
}, "\n"))
sourceFile:close()
end
table.insert(concatContent, 'return __REQUIRE__("ECS")')
-- write ECS_concat.lua
local fileConcat = io.open(OUTPUT_CONCAT..".lua", "w" )
fileConcat:write( table.concat(concatContent, "\n"))
fileConcat:close()
-- teste import
local ecsConcat = require(OUTPUT_CONCAT)
_G.ECS = nil
end
local function minify()
local min = require('minify')
local sourceFile = io.open(OUTPUT_CONCAT..".lua", 'r')
if not sourceFile then
error("Could not open the input file `" .. OUTPUT_CONCAT..".lua" .. "`", 0)
end
local data = sourceFile:read('*all')
local ast = min.CreateLuaParser(data)
local global_scope, root_scope = min.AddVariableInfo(ast)
min.MinifyVariables(global_scope, root_scope)
min.StripAst(ast)
local minifiedContent = min.AstToString(ast)
-- write ECS.lua
local fileMinified = io.open(OUTPUT_MINIFIED..".lua", "w" )
fileMinified:write(HEADER .. minifiedContent)
fileMinified:close()
-- teste import
local ecsMinified = require(OUTPUT_MINIFIED)
_G.ECS = nil
end
concat()
minify()
================================================
FILE: docs/.nojekyll
================================================
================================================
FILE: docs/README.md
================================================
# 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](https://en.wikipedia.org/wiki/Inheritance_(object-oriented_programming)) of classes and start doing use of
[composition](https://en.wikipedia.org/wiki/Object_composition) in a Data Oriented Programming paradigm.
([More information on Wikipedia](https://en.wikipedia.org/wiki/Entity_component_system)).
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!
```lua
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](https://luau-lang.org/)
- **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.
# Goal
To be a lightweight, simple, ergonomic and high-performance ECS library that can be easily extended. The **ECS Lua**
does not strictly follow _"pure ECS design"_.
# Next steps
You can browse or search for specific subjects in the side menu. Here are some relevant links:
<br>
<br>
<div class="home-row clearfix" style="text-align:center">
<div class="home-col"><div class="panel home-panel"><div class="panel-body">
[](/getting-started?id=installation)
</div><div class="panel-heading">
[Installation](/getting-started?id=installation)
</div></div></div>
<div class="home-col"><div class="panel home-panel"><div class="panel-body">
[](/getting-started?id=general-concepts)
</div><div class="panel-heading">
[General Concepts](/getting-started?id=general-concepts)
</div></div></div>
<div class="home-col"><div class="panel home-panel"><div class="panel-body">
[](/architecture)
</div><div class="panel-heading">
[Architecture](/architecture)
</div></div></div>
<div class="home-col"><div class="panel home-panel"><div class="panel-body">
[](/tutorial)
</div><div class="panel-heading">
[Tutorials](/tutorial)
</div></div></div>
</div>
[Lua 5.1]:https://app.travis-ci.com/github/nidorx/ecs-lua
[Lua 5.2]:https://app.travis-ci.com/github/nidorx/ecs-lua
[Lua 5.3]:https://app.travis-ci.com/github/nidorx/ecs-lua
[Lua 5.4]:https://app.travis-ci.com/github/nidorx/ecs-lua
[LuaJit]:https://app.travis-ci.com/github/nidorx/ecs-lua
================================================
FILE: docs/_coverpage.md
================================================
<div class="logo-container">
<div class="heats">
<div class="h r1 c1"></div>
<div class="h r1 c2"></div>
<div class="h r1 c3"></div>
<div class="h r1 c4"></div>
<div class="h r1 c5"></div>
<div class="h r2 c1"></div>
<div class="h r2 c2"></div>
<div class="h r2 c3"></div>
<div class="h r2 c4"></div>
<div class="h r2 c5"></div>
<div class="h r3 c1"></div>
<div class="h r3 c2"></div>
<div class="h r3 c3"></div>
<div class="h r3 c4"></div>
<div class="h r3 c5"></div>
<div class="h r4 c1"></div>
<div class="h r4 c2"></div>
<div class="h r4 c3"></div>
<div class="h r4 c4"></div>
<div class="h r4 c5"></div>
<div class="h r5 c1"></div>
<div class="h r5 c2"></div>
<div class="h r5 c3"></div>
<div class="h r5 c4"></div>
<div class="h r5 c5"></div>
<svg
class="ghost"
width="50mm"
height="50mm"
version="1.1"
viewBox="0 0 50 50">
<path d="m4.8916 45.894-2e-6 -2.9104h-2.9104l-1e-6 -20.902h2.9104v-8.9958h3.175v-3.1992h3.4396v-2.8862l6.8792-9.1e-6v-2.9104h13.229v2.9104h6.4824v2.8862h3.7041v3.1992h-6.7469v3.199l-3.4396 1e-3 -2e-6 8.4414h3.4396l2e-6 2.9104 6.7469-1.7e-4v-5.5559l6.2178 2.3e-4v20.902h-3.175v2.9265l-6.7469-0.016v-2.9104h-3.0428v-3.1749h-3.4396v3.1749l-3.4396 0.0256v2.8848h-6.6146v-2.9104h-3.175v-3.1749h-3.175v3.1749h-3.7042c0.0062 0.97335 0 1.9371 0 2.9104-2.2049-2e-4 -4.4097 0-6.6146 0zm16.669-18.256-2e-6 -2.9104h3.4397v-8.4426h-3.4397v-3.199h-6.35v3.199h-3.7042l2e-6 8.4425 3.7042 1.5e-4v2.9103z" fill="#fb860c" class="body"></path>
<g class="eye eye--left">
<g class="pupil">
<g class="inner">
<path class="pupil-color" d="m18.385 24.728v-5.5563h6.6147v5.5563z" fill="#020202"></path>
<path class="eyelid" d="m18.385 24.728v-5.5563h6.6147v5.5563z" fill="#edf4f7"></path>
</g>
</g>
</g>
<g class="eye eye--right">
<g class="pupil">
<g class="inner">
<path class="pupil-color" d="m38.494 24.728v-5.5563h6.6147v5.5563z" fill="#020202"></path>
<path class="eyelid" d="m38.494 24.728v-5.5563h6.6147v5.5563z" fill="#edf4f7"></path>
</g>
</g>
</g>
</svg>
</div>
</div>
# ECS Lua
[](https://app.travis-ci.com/nidorx/ecs-lua)
[GitHub](https://github.com/nidorx/ecs-lua)
[Get Started](/?id=what-is-it)
<!-- background color -->

================================================
FILE: docs/_navbar.md
================================================
- [Home](/)
- Language <span class="arrow">▾</span>
- [English](/)
- [Português do Brasil](/pt-br/)
================================================
FILE: docs/_sidebar.md
================================================
- [Home](/)
- [Installation](/getting-started?id=installation)
- [General Concepts](/getting-started?id=general-concepts)
- [Component](/getting-started?id=component)
- [Systems and Queries](/getting-started?id=systems-and-queries)
- [World](/getting-started?id=world)
- [Entity](/getting-started?id=entity)
- [Example](/getting-started?id=putting-everything-together)
- [Architecture](/architecture)
- [Component](/architecture?id=component)
- [Qualifiers](/architecture?id=qualifiers)
- [FSM - Finite State Machines](/architecture?id=fsm-finite-state-machines)
- [Entity](/architecture?id=entity)
- [Query](/architecture?id=query)
- [Sistema](/architecture?id=system)
- [Tasks](/architecture?id=task)
- [World](/architecture?id=world)
- [Tutorials](/tutorial)
- [Shoot](/tutorial-shoot)
- [Pacman](/tutorial-pacman)
- [Boids](/tutorial-boids)
- [API](/api)
- [ECS](/api?id=ecs)
- [Archetype](/api?id=archetype)
- [Component](/api?id=component)
- [Entity](/api?id=entity)
- [LoopManager](/api?id=loopmanager)
- [Query](/api?id=query)
- [QueryBuilder](/api?id=querybuilder)
- [QueryResult](/api?id=queryresult)
- [System](/api?id=system)
- [Time](/api?id=time)
- [World](/api?id=world)
================================================
FILE: docs/api.md
================================================
# API
<div class="api-docs">
# ECS
- `ECS.Query`
- _@type_ `QueryClass`
- `ECS.Archetype`
- _@type_ `ArchetypeClass`
- `ECS.World(systemClasses, frequency, disableAutoUpdate)`
- Create a new world instance
- _@param_ `systemClasses` `SystemClass[]` _optional_ Array of system classes
- _@param_ `frequency` `number` _optional_ Define the frequency that the `process` step will be executed. Default 30
- _@param_ `disableAutoUpdate` `bool` _optional_ When `~= false`, the world automatically registers in the `LoopManager`,
receiving the `World:Update()` method from it. Default false
- _@return_ `World`
- `ECS.System(step, order, query, updateFn)`
- Create new System Class
- _@param_ `step` `process|transform|render|task`
- _@param_ `order` `number` _optional_ Allows you to set an execution order (for systems that are not `task`). Default 50
- _@param_ `query` `Query|QueryBuilder` _optional_ Filters the entities that will be processed by this system
- _@param_ `updateFn` `Function(self, Time)` _optional_ A shortcut for creating systems that only have the Update method
- _@return_ `SystemClass`
- `ECS.Component(template)`
- Register a new ComponentClass
- _@param_ `template` `table|function(table):table|any`
- When `table`, this template will be used for creating component instances
- When it's a `function`, it will be invoked when a new component is instantiated. The creation parameter of the
component is passed to template function
- If the template type is different from `table` and `function`, **ECS Lua** will generate a template in the format
`{ value = template }`.
- _@return_ `ComponentClass`
- `ECS.SetLoopManager(manager)`
- Defines the LoopManager that will be used by the worlds to receive the automatic update
- _@param_ `manager` `LoopManager`
# Archetype
An Archetype is a unique combination of component types. The EntityRepository uses the archetype to group all
entities that have the same sets of components.
An entity can change archetype fluidly over its lifespan. For example, when you add or remove components, the archetype
of the affected entity changes.
An archetype object is not a container; rather it is an identifier to each unique combination of component types that
an application has created at run time, either directly or implicitly.
You can create archetypes directly using `ECS.Archetype.Of(Components[])`. You also implicitly create archetypes
whenever you add or remove a component from an entity. An Archetype object is an immutable singleton; creating an
archetype with the same set of components, either directly or implicitly, results in the same archetype.
The ECS framework uses archetypes to group entities that have the same structure together. The ECS framework stores
component data in blocks of memory called chunks. A given chunk stores only entities having the same archetype. You can
get the Archetype object for a chunk from its Archetype property.
Use `ECS.Archetype.Of(Components[])` to get a Archetype reference.
- `Archetype.EMPTY`
- Generic archetype, for entities that do not have components
- _@type_ `Archetype`
- `Archetype.Of(componentClasses)`
- Gets the reference to an archetype from the informed components
- _@param_ `componentClasses` `ComponentClass[]` Component that define this archetype
- _@return_ `Archetype`
- `Archetype.Version()`
- Get the version of archetype definitions
- _@return_ `number`
- `Archetype:Has(componentClass)`
- Checks whether this archetype has the informed component
- _@param_ `componentClass` `ComponentClass`
- _@return_ `bool`
- `Archetype:With(componentClass)`
- Gets the reference to an archetype that has the current components `+` the informed component
- _@param_ `componentClass` `ComponentClass`
- _@return_ `Archetype`
- `Archetype:WithAll(componentClasses)`
- Gets the reference to an archetype that has the current components `+` the informed components
- _@param_ `componentClass` `ComponentClass[]`
- _@return_ `Archetype`
- `Archetype:Without(componentClass)`
- Gets the reference to an archetype that has the current components `-` the informed component
- _@param_ `componentClass` `ComponentClass`
- _@return_ `Archetype`
- `Archetype:WithoutAll(componentClasses)`
- Gets the reference to an archetype that has the current components `-` the informed components
- _@param_ `componentClass` `ComponentClass[]`
- _@return_ `Archetype`
# Component
- `ComponentClass.Id`
- Identifier of this component
- _@type_ `number`
- `ComponentClass.IsCType`
- Indicates that this class is a Component
- _@type_ `true`
- `ComponentClass.SuperClass`
- Used internally for Qualifiers, it indicates the base class of this Component (or primary component).
- _@type_ `ComponentClass`
- `ComponentClass.HasQualifier`
- Indicates that this Component has qualifiers
- _@type_ `bool`
- `ComponentClass.IsQualifier`
- Indicates that this specific class is a Component qualifier.
- _@type_ `bool`
- `ComponentClass.IsFSM`
- Indicates that this Component is a [FSM - Finite State Machine][fsm]
- _@type_ `bool`
- `ComponentClass.States`
- When set, this Component becomes a [FSM - Finite State Machine][fsm]
```lua
local Movement = ECS.Component({ speed = 0 })
Movement.States = {
Standing = {"Walking"},
Walking = "*",
Running = {"Walking"}
}
```
- _@type_ `table` _optional_
- `ComponentClass.StateInitial`
- When the component is [FSM][fsm], it allows defining the initial state for new instances.
- _@type_ `string` _optional_
- `ComponentClass.Case`
- When the component is [FSM][fsm], it allows the component to handle transactions between states.
```lua
Movement.Case = {
Standing = function(self, previous)
self.speed = 0
end,
Walking = function(self, previous)
self.speed = 5
end,
Running = function(self, previous)
self.speed = 10
end
}
```
- _@type_ `table` _optional_
- `ComponentClass.Qualifier(qualifier)`
- Gets a qualifier for this type of component. If the qualifier does not exist, a new class will be created,
otherwise it brings the already registered class qualifier reference with the same name.
- _@param_ `qualifier` `string|ComponentClass`
- _@return_ `ComponentClass`
- `ComponentClass.Qualifiers(...)`
- Get all qualified class
- _@param_ `...` `string|ComponentClass` _optional_ Allows to filter the specific qualifiers
- _@return_ `ComponentClass[]`
- `ComponentClass(value)` | `ComponentClass.New(value)`
- Builder, instantiate a new component of this type
- _@param_ `value` `any` _optional_ If the value is not a table, it will be converted to the format `{ value = value}`
- _@return_ `Component`
- `ComponentClass:GetType()`
- Get this component's class
- _@return_ `ComponentClass`
- `ComponentClass:Is(componentClass)`
- Check if this component is of the type informed
- _@param_ `componentClass` `ComponentClass|ComponentSuperClass`
- _@return_ `bool`
- `ComponentClass:Primary()`
- Get the instance for the primary qualifier of this class
- _@return_ `Component|nil`
- `ComponentClass:Qualified(qualifier)`
- Get the instance for the given qualifier of this class
- _@param_ `qualifier` `string|ComponentClass`
- _@return_ `Component|nil`
- `ComponentClass:QualifiedAll()`
- Get all instances for all qualifiers of that class
- _@return_ `Component[]`
- `ComponentClass:Merge(other)`
- Merges data from the other component into the current component. **IMPORTANT!** This method should not be invoked,
it is used by the entity to ensure correct retrieval of a component's qualifiers.
- _@param_ `other` `Component`
- `ComponentClass:Detach()`
- Unlink this component with the other qualifiers. **IMPORTANT!** This method should not be invoked, it is used by
the entity to ensure correct retrieval of a component's qualifiers.
- `ComponentClass.In(...)`
- When the component is [FSM][fsm], creates a clause used to filter repository entities in a Query or QueryResult.
```lua
ECS.Query.All(Movement.In("Walking", "Running"))
```
- _@param_ `...` `string[]`
- _@return_ `Clause`
- `ComponentClass:SetState(newState)`
- When the component is [FSM][fsm], defines the current state
- _@param_ `newState` `string`
- `ComponentClass:GetState()`
- When the component is [FSM][fsm], get the current state
- _@return_ `string`
- `ComponentClass:GetPrevState()`
- When the component is [FSM][fsm], get the previous state
- _@return_ `string|nil`
- `ComponentClass:GetStateTime()`
- When the component is [FSM][fsm], gets the time it changed to the current state. Whenever the state is changed, the
instant is persisted internally using `os.clock()`
- _@return_ `number`
# Entity
- `Entity.id`
- Identifier of this entity
- _@type_ `number`
- `Entity.isAlive`
- The entity is created in _DEAD_ state (`entity.isAlive == false`) and will only be visible for queries after the
cleaning step _(`OnRemove`,`OnEnter`,`OnExit`)_ by the world
- _@type_ `bool`
- `Entity.archetype`
- The entity archetype
- _@type_ `Archetype`
- `Entity.New(onChange, components)`
- Creates an entity having components of the specified types.
- _@param_ `onChange` `Event`
- _@param_ `components` `Component[]` _optional_
- _@return_ `Entity`
- `Entity:Get(componentClass)` | `entity[componentClass]`
- Gets an entity component
- _@param_ `componentClass` `ComponentClass`
- _@return_ `Component`
- `Entity:Get(...)`
- Get multiple entity components at once
```lua
local comp1, comp2, comp3 = entity:Get(CompType1, CompType2, CompType3)
```
- _@param_ `..` `ComponentClass[]`
- _@return_ `Component ...`
- `Entity:Set(componentClass, value)` | `entity[componentClass] = value`
- Sets the value of a component
- _@param_ `componentClass` `ComponentClass`
- _@param_ `value` `any|nil` When nil, unset the component.
- `Entity:Set(...)`
- Arrow one or more instances of a component
- _@param_ `...` `Component`
- `Entity:Unset(componentClass|Component, ...)` | `entity[componentClass] = nil`
- Remove one or more components from the entity
- _@param_ `...` `componentClass|Component`
# LoopManager
In order for the world's systems to receive an update, the `World:Update(step, now)` method must be invoked on each
frame. To automate this process, **ECS Lua** provides a functionality so that, at the time of instantiation of a
new world, it registers to receive the update automatically.
```lua
local MyLoopManager = {
Register = function(world)
local beforePhysics = MyGameEngine.BeforePhysics(function()
world:Update("process", os.clock())
end)
local afterPhysics = MyGameEngine.AfterPhysics(function()
world:Update("transform", os.clock())
end)
local beforeRender
if (not MyGameEngine.IsServer()) then
beforeRender = MyGameEngine.BeforeRender(function()
world:Update("render", os.clock())
end)
end
return function()
beforePhysics:Disconnect()
afterPhysics:Disconnect()
if beforeRender then
beforeRender:Disconnect()
end
end
end
}
ECS.SetLoopManager(MyLoopManager)
```
- `LoopManager.Register(world)`
- Allows the world to register to be updated.
- _@param_ `world` `World`
- _@return_ `function` The world will invoke when destroyed
# Query
- `Query(all, any, none)` | `Query.New(all, any, none)`
- Create a new Query used to filter entities in the world. It makes use of local and global cache in order to
decrease the validation time (avoids looping in runtime of systems)
- _@param_ `all` `Array<ComponentClass|Clause>` _optional_
- _@param_ `any` `Array<ComponentClass|Clause>` _optional_
- _@param_ `none` `Array<ComponentClass|Clause>` _optional_
- _@return_ `Query`
- `Query.All(...)`
- _@param_ `...` `Array<ComponentClass|Clause>`
- _@return_ `QueryBuilder`
- `Query.Any(...)`
- _@param_ `...` `Array<ComponentClass|Clause>`
- _@return_ `QueryBuilder`
- `Query.None(...)`
- _@param_ `...` `Array<ComponentClass|Clause>`
- _@return_ `QueryBuilder`
- `Query.Filter(filter)`
- Create custom filters that can be used in Queries. Its execution is delayed, invoked only in `QueryResult` methods.
The result of executing the clause depends on how it was used in the query.
Ex. If used in `Query.All()` the result is the inverse of using the same clause in `Query.None()`
```lua
local Player = ECS.Component({ health = 100 })
local HealthPlayerFilter = ECS.Query.Filter(function(entity, config)
local player = entity[Player]
return player.health >= config.minHealth and player.health <= config.maxHealth
end)
local healthyClause = HealthPlayerFilter({
minHealth = 80,
maxHealth = 100,
})
local healthyQuery = ECS.Query.All(Player, healthyClause)
world:Exec(healthyQuery):ForEach(function(entity)
-- this player is very healthy
end)
local notHealthyQuery = ECS.Query.All(Player).None(healthyClause)
world:Exec(healthyQuery):ForEach(function(entity)
-- this player is NOT very healthy
end)
local dyingClause = HealthPlayerClause({
minHealth = 1,
maxHealth = 20,
})
local dyingQuery = ECS.Query.All(Player, dyingClause)
world:Exec(dyingQuery):ForEach(function(entity)
-- this player is about to die
end)
local notDyingQuery = ECS.Query.All(Player).None(dyingClause)
world:Exec(notDyingQuery):ForEach(function(entity)
-- this player is NOT about to die
end)
```
- _@param_ `filter` `function(entity, config) -> bool`
- _@return_ `function(config) -> Clause`
- `Query:Result(chunks)`
- Generate a `QueryResult` with the chunks entered and the clauses of the current query
- _@param_ `chunks` `Array<{ [Entity] = true }>`
- _@return_ `QueryResult`
- `Query:Match(archetype)`
- Checks if the entered archetype is valid by the query definition
- _@param_ `archetype` `Archetype`
- _@return_ `bool`
# QueryBuilder
- `QueryBuilder.isQueryBuilder`
- Indicates that this is an instance of a QueryBuilder
- _@type_ `true`
- `QueryBuilder.All(...)`
- _@param_ `...` `Array<ComponentClass|Clause>`
- _@return_ `QueryBuilder`
- `QueryBuilder.Any(...)`
- _@param_ `...` `Array<ComponentClass|Clause>`
- _@return_ `QueryBuilder`
- `QueryBuilder.None(...)`
- _@param_ `...` `Array<ComponentClass|Clause>`
- _@return_ `QueryBuilder`
- `QueryBuilder.Build()`
- _@return_ `Query`
# QueryResult
The result of a Query that was executed on an EntityStorage.
QueryResult provides several methods to facilitate the filtering of entities resulting from the execution of the query.
- **Intermediate Operations**
- Intermediate operations return a new QueryResult. They are always lazy; executing an intermediate operation such as
`QueryResult:Filter()` does not actually perform any filtering, but instead creates a new QueryResult that, when traversed,
contains the elements of the initial QueryResult that match the given predicate. Traversal of the pipeline source
does not begin until the terminal operation of the pipeline is executed.
- **Terminal Operations**
- Terminal operations, such as `QueryResult:ForEach` or `QueryResult.AllMatch`, may traverse the QueryResult to produce a
result or a side-effect.
- `QueryResult.New(chunks, clauses)`
- Build a new QueryResult
- _@param_ `chunks` `Array<{ [Entity] = true }>`
- _@param_ `clauses` `Clause[]` _optional_
- _@return_ `QueryResult`
- `QueryResult:With(operation, param)`
- Returns a QueryResult consisting of the elements of this QueryResult with a new pipeline operation
- _@param_ `operation` `function(param, value, count) -> newValue, acceptItem, continuesLoop`
- _@param_ `param` `any`
- _@return_ `QueryResult` the new QueryResult
- `QueryResult:Filter(predicate)`
- Returns a QueryResult consisting of the elements of this QueryResult that match the given predicate.
- _@param_ `predicate` `function(value) -> bool` a predicate to apply to each element to determine if it should be included
- _@return_ `QueryResult` the new QueryResult
- `QueryResult:Map(mapper)`
- Returns a QueryResult consisting of the results of applying the given function to the elements of this QueryResult.
- _@param_ `mapper` `function(value) -> newValue` a function to apply to each element
- _@return_ `QueryResult` the new QueryResult
- `QueryResult:Limit(maxSize)`
- Returns a QueryResult consisting of the elements of this QueryResult, truncated to be no longer than maxSize in length.
- _@param_ `maxSize` `number`
- _@return_ `QueryResult` the new QueryResult
- `QueryResult:AnyMatch(predicate)`
- Returns whether any elements of this result match the provided predicate.
- _@param_ `predicate` `function(value) -> bool` a predicate to apply to elements of this result
- _@return_ `true` if any elements of the result match the provided predicate, otherwise `false`
- `QueryResult:AllMatch(predicate)`
- Returns whether all elements of this result match the provided predicate.
- _@param_ `predicate` `function(value) -> bool` a predicate to apply to elements of this result
- _@return_ `true` if either all elements of the result match the provided predicate or the result is empty, otherwise `false`
- `QueryResult:FindAny()`
- Returns some element of the result, or nil if the result is empty.
This is a short-circuiting terminal operation.
The behavior of this operation is explicitly nondeterministic; it is free to select any element in the result.
Multiple invocations on the same result may not return the same value.
- _@return_ `any`
- `QueryResult:ForEach(action)`
- Performs an action for each element of this QueryResult.
This is a terminal operation.
The behavior of this operation is explicitly nondeterministic. This operation does not guarantee to respect the
encounter order of the QueryResult.
- _@param_ `action` `function(value, count) -> bool` A action to perform on the elements, breaks execution case returns true
- `QueryResult:ToArray()`
- Returns an array containing the elements of this QueryResult.
- _@return_ `Array<any>`
- `QueryResult:Iterator()`
- Returns an Iterator, to use in for loop
```lua
for count, entity in result:Iterator() do
print(entity.id)
break
end
```
- _@return_ `Iterator`
# System
- `System.Step`
- Step that this system will run
- _@type_ `string` `process|transform|render|task`
- `System.Order`
- For systems that are not `task`, execution order
- _@type_ `number`
- `System.Query`
- Filters the entities that will be processed by this system
- _@type_ `number`
- `System.After`
- When the system is a task, it allows you to define that this system should run AFTER other specific systems.
```lua
local log = {}
local Task_A = System.Create('task', function()
-- In this example, TASK_A takes time to execute, delaying its execution
local i = 0
while i <= 4000 do
i = i + 1
if i%1000 == 0 then
coroutine.yield()
end
end
table.insert(log, 'A')
end)
local Task_B = System.Create('task', function()
table.insert(log, 'B')
end)
local Task_C = System.Create('task', function()
table.insert(log, 'C')
end)
local Task_D = System.Create('task', function()
table.insert(log, 'D')
end)
local Task_E = System.Create('task', function()
table.insert(log, 'E')
end)
local Task_F = System.Create('task', function()
table.insert(log, 'F')
end)
local Task_G = System.Create('task', function()
table.insert(log, 'G')
end)
local Task_H = System.Create('task', function(self)
table.insert(log, 'H')
end)
--[[
A<-------C<---+-----F<----+
| | | |
+----+ E<----+ H
| | |
B<--+----D<---+------G<---+
A - has no dependency
B - has no dependency
C - Depends on A,B
D - Depends on B
E - Depends on A,B,C,D
F - Depends on A,B,C,D,E
G - Depends on B,D
H - Depends on A,B,C,D,E,F,G
Completion order will be B,D,G,A,C,E,F,H
> In this example, TASK_A takes time to execute, delaying its execution
]]
Task_A.Before = {Task_C}
Task_B.Before = {Task_D}
Task_C.After = {Task_B}
Task_D.Before = {Task_G}
Task_F.After = {Task_E}
Task_E.After = {Task_D, Task_C}
Task_C.Before = {Task_F}
Task_H.After = {Task_F, Task_G}
```
- _@type_ `SystemClass[]`
- `System.Before`
- When the system is a task, it allows you to define that this system should run BEFORE other specific systems.
- _@see_ `System.After`
- _@type_ `SystemClass[]`
- `System.version`
- System Version (GSV).
- _@see_ `World.version`
- _@type_ `Number`
- `System._world`
- _@type_ `World`
- `System._config`
- _@type_ `table`
- `System.New(world, config)`
- Create an instance of this system
- _@param_ `world` `World`
- _@param_ `config` `table`
- _@return_ `System`
- `System:GetType()`
- Get this system class
- _@return_ `SystemClass`
- `System:Result(query)`
- Run a query in the world. A shortcut to `self._world:Exec(query)`
- _@param_ `query` `Query|QueryBuilder` _optional_ If nil, use default query
- _@return_ `QueryResult`
- `System:Destroy()`
- Destroy this instance
- `System:OnDestroy()`
- Allows you to perform some processing or cleaning when the instance is being destroyed
- `System:ShouldUpdate(Time)`
- Invoked before 'Update', allows you to control the execution of the update
- _@param_ `Time` `Time`
- _@return_ `bool` If true, the Update method will be invoked.
- `System:Update(Time)`
- Run the system's main method
- _@param_ `Time` `Time`
- `System:OnRemove(Time, entity)`
- When it is a `QuerySystem`, it allows to be informed when an entity with the characteristics of the query is
removed from the world. This method is performed in the step cleanup process.
- _@param_ `Time` `Time`
- _@param_ `entity` `Entity`
- `System:OnExit(Time, entity)`
- When it is a `QuerySystem`, it allows to be informed when an entity has lost the characteristics of that query
(has suffered an archetype change and the current query no longer applies). This method is performed in the step
cleanup process.
- _@param_ `Time` `Time`
- _@param_ `entity` `Entity`
- `System:OnEnter(Time, entity)`
- When it is a QuerySystem, it allows to be informed when an entity received the characteristics expected by this
query (it suffered an archetype change and the current query now applies). This method is performed in the step
cleanup process.
- _@param_ `Time` `Time`
- _@param_ `entity` `Entity`
# Time
Singleton, reference to the world's global processing time.
- `Time.Now`
- World Runtime
- _@type_ `number`
- `Time.NowReal`
- Real time, received in `World:Update(step, now)` method
- _@type_ `number`
- `Time.Frame`
- The time at the beginning of this frame (process). The world receives the current time at the beginning of each
frame, with the value increasing per frame.
- _@type_ `number`
- `Time.FrameReal`
- The REAL time at the beginning of this frame (`World:Update(step, now)`).
- _@type_ `number`
- `Time.Process`
- The time the latest process step has started.
- _@type_ `number`
- `Time.Delta`
- The completion time in seconds since the last frame.
- _@type_ `number`
- `Time.DeltaFixed`
- Based on world update frequency (`process` step).
```lua
DeltaFixed = 1000/frequency/1000
```
- _@see_ `World:SetFrequency()`
- _@type_ `number`
- `Time.Interpolation`
- The proportion of time since the previous transform relative to processDeltaTime. Used to do interpolation during
the rendering step. Allows the `process` step to run at low frequency _(Ex. 30hz)_ and `render` at the maximum rate
of the player's device _(Ex. 60hz)_
- _@type_ `number`
# World
- `World.version`
- Global System Version (GSV).
Before executing the `Update()` method of each system, the world version is incremented, so at this point, the
world version will always be higher than the running system version.
Whenever an entity archetype is changed (received or lost component) the entity's version is updated to the current
version of the world.
After executing the System Update method, the version of this system is updated to the current world version.
This mechanism allows a system to know if an entity has been modified after the last execution of this same system,
as the entity's version is superior to the version of the last system execution. Thus, a system can contain logic if
it only operates on "dirty" entities, which have undergone changes. The code for this validation on a system is:
- `local isDirty = entity.version > self.version`
- _@type_ `number`
- `World.maxTasksExecTime`
- Allows you to define the maximum time that the `JobSystem` can operate in each frame. The default value is
`0.011666666666666665` = `((1000/60/1000)*0.7)`
- A game that runs at 30fps has 0.0333 seconds to do all the processing for each frame, including rendering
- 30FPS = `(1000/30/1000)` = `0.03333333333333333`
- A game that runs at 60fps has 0.0166 seconds to do all the processing for each frame, including rendering
- 60FPS = `(1000/60/1000)` = `0.016666666666666666`
- _@type_ `number` Default `0.011666666666666665`
- `World:SetFrequency(frequency)`
- Define the frequency that the `process` step will be executed
- _@param_ `frequency` `number` _optional_ Default 30
- `World:GetFrequency()`
- Get the frequency of execution of the `process` step
- _@return_ `number`
- `World:AddSystem(systemClass, config)`
- Add a new system to the world. Only one instance per type is accepted. If there is already another instance of this
system in the world, any new invocation of this method will be ignored.
- _@param_ `systemClass` `SystemClass` The system to be added in the world
- _@param_ `config` `table` _optional_ System instance configuration
- `World:Entity(...)`
- Create a new entity. The entity is created in _DEAD_ state (`entity.isAlive == false`) and will only be visible for
queries after the cleaning step _(`OnRemove`,`OnEnter`,`OnExit`)_ of the current step
- _@param_ `...` `Component[]` _optional_ Instance of the components that this entity will have
- _@return_ `Entity`
- `World:Remove(entity)`
- Performs immediate removal of an entity.
If the entity was created in this step and the cleanup process has not happened yet (therefore the entity is inactive,
`entity.isAlive == false`), the `OnRemove` event will never be fired.
If the entity is alive (`entity.isAlive == true`), even though it is removed immediately, the `OnRemove` event will
be fired at the end of the current step.
- _@param_ `entity` `Entity`
- `World:Exec(query)`
- Run a query in this world
- _@param_ `query` `Query|QueryBuilder`
- _@return_ `QueryResult`
- `World:Update(step, now)`
- Perform world update. When registered, the `LoopManager` will invoke World Update for each step in the sequence.
- `process` At the beginning of each frame
- `transform` After the game engine's physics engine runs
- `render` Before rendering the current frame
- _@param_ `step` `"process"|"transform"|"render"`
- _@param_ `now` `number` Usually os.clock()
- `World:Destroy()`
- Destroy this instance, removing all entities, systems and events
</div>
[fsm]:https://en.wikipedia.org/wiki/Finite-state_machine
================================================
FILE: docs/architecture.md
================================================
# Architecture
In Software Engineering, ECS is the acronym for Entity Component System, is a software architecture pattern used
primarily in video game development. An ECS follows the principle of "composition rather than inheritance" that allows
greater flexibility in defining entities, where each object in a game scene is an entity (eg enemies, projectiles,
vehicles, etc.). Each entity consists of of one or more components that add behavior or functionality. Therefore,
the behavior of an entity can be changed at runtime by simply adding or removing components. This eliminates problems of
The ambiguity with which deep and vast inheritance hierarchies, which are difficult to understand, maintain and extend.
For more details:
- [Frequently Asked Questions about ECS](https://github.com/SanderMertens/ecs-faq)
- [Entity Systems Wiki](http://entity-systems.wikidot.com/)
- [Evolve your hierarchy](http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/)
- [ECS on Wikipedia](https://en.wikipedia.org/wiki/Entity_component_system)
- [ECS with Elixir](https://yos.io/2016/09/17/entity-component-systems/)
- [2017 GDC - Overwatch Gameplay Architecture e Netcode](https://www.youtube.com/watch?v=W3aieHjyNvw&ab_channel=GDC)
## Component
They represent the different characteristics of an entity, such as position, speed, geometry, physics, and hit points.
Components only store raw data for an aspect of the object and how it interacts with the world. In others words, the
component labels the entity as having this particular aspect.
In **ECS Lua**, the creation of a component is done through the `ECS.Component(template)` method.
The `template` parameter can be of any type, where:
- When `table`, this template will be used for creating component instances
```lua
local Component = ECS.Component({
x = 0, y = 0, z = 0
})
local comp = Component({ x = 33, z = 80 })
print(comp.x, comp.y, comp.z) -- > 33, 0, 80
-- it is the same as
local comp = Component.New({ x = 33, z = 80 })
print(comp.x, comp.y, comp.z) -- > 33, 0, 80
```
- When it's a `function`, it will be invoked when a new component is instantiated. The creation parameter of the
component is passed to template function
```lua
local Component = ECS.Component(function(param)
return {
x = param.x or 1,
y = param.y or 1,
z = param.z or 1
}
end)
local comp = Component({ x = 33, z = 80 })
print(comp.x, comp.y, comp.z) -- > 33, 1, 80
```
- If the template type is different from `table` and `function`, **ECS Lua** will generate a template in the format
`{ value = template }`.
```lua
local Component = ECS.Component(55)
local comp1 = Component()
print(comp1.value) -- > 55
local comp2 = Component({ value = 80 })
print(comp2.value) -- > 80
local comp3 = Component("XPTO")
print(comp3.value) -- > "XPTO"
```
### Methods
In **ECS Lua**, components are classes and can therefore have auxiliary methods.
> IMPORTANT! Avoid creating methods that modify component instance data directly, the ideal is that these logics
stay within the systems, which are, by definition, responsible for changing the data of the entities and their
components.
```lua
local Person = ECS.Component({
name = "",
surname = "",
birth = 0
})
function Person:FullName()
return self.name.." "..self.surname
end
function Person:Age()
return tonumber(os.date("%Y", os.time())) - self.birth
end
local person = Person({ name = "John", surname = "Doe", birth = 2000 })
print(person:FullName()) -- John Doe
print(person:Age()) -- 21
```
### Qualifiers
In "pure ECS" implementations there is a premise that a component can only be added once to an entity. In the vast
majority of scenarios, this is true. For example, you don't want your entity to have two positions, it makes no sense!
Therefore, your entity will only have one component of type Position.
But sometimes you will build some functionality that needs your entity to have this behavior, to have more than one
component of the same **TYPE**. When the framework doesn't support this kind of implementation, you end up with code
full of hacks to [work around](https://en.wikipedia.org/wiki/Workaround) the problem.
**ECS Lua** implements the **Qualifiers** mechanism so that you can represent a category of components.
To illustrate the usage, let's think about the following scenario: I want to add a
[Buff](https://en.wikipedia.org/wiki/Game_balance#Buff) system to my game so that my character can receive extra life
points in specific situations. We want to have the freedom to increase or decrease the amount of buff according to the
region of the map where the player is.
Looking at the scenario above, we could create, at first, the solution below. A `HealthBuff` component to record the
amount of extra life and two systems. The first `MapRegionSystem` decides how many additional health points the player
will have for each region, while the second `HealthSystem` represents some system functionality that need to get the
player's life total at a certain time.
```lua
-- components
local Player = ECS.Component({ health = 100, region = "easy", healthTotal = 100 })
local HealthBuff = ECS.Component({ value = 10 })
-- systems
local MapRegionSystem = System("process", 1, Query.All(Player))
function MapRegionSystem:Update(Time)
for i, entity in self:Result():Iterator() do
local player = entity[Player]
if player.region == "easy" then
entity[HealthBuff] = nil -- remove buff
else
local buff = entity[HealthBuff]
if buff == nil then
buff = HealthBuff(0)
entity:Set(buff)
end
if player.region == "hard" then
buff.value = 15
elseif player.region == "hell" then
buff.value = 40
end
end
end
end
local HealthSystem = System("process", 2, Query.All(Player).Any(HealthBuff))
function HealthSystem:Update(Time)
for i, entity in self:Result():Iterator() do
local player = entity[Player]
local buff = entity[HealthBuff]
if buff then
player.healthTotal = player.health + buff.value
else
player.healthTotal = player.health
end
end
end
```
So far quiet. However, imagine now that my player can receive **SEVERAL** buffers.
- He can receive a buffer for the character he is using;
- another buff when unlocking an item and;
- you can also buy buffs from the in-game shop.
In this new scenario our solution does not meet, because the `MapRegionSystem` system code does not have the information
about the other factors, and to attend to it, it will have to know or manage several possible states to decide which is
the amount of health the player will receive for being in a specific region. The other systems in the game too needed
to know the region to decide how much buffer to add. In a "pure ECS" solution, we're going to start:
1. share state between systems
1. create "Component TAGs" to facilitate the management of this distributed state,
1. inflate components with an attribute for each system type.
At first this doesn't seem to be a problem, but over time, multiple systems will be called unnecessarily (just to do an
if and not process that entity). These systems now have extra responsibilities, increasing the complexity of the code,
making maintenance difficult and facilitating the appearance of bugs.
In **ECS Lua** we solve this kind of problem by creating qualifiers, through the static method
`ComponentClass.Qualifier(qualifier)`. It accepts a string as a parameter and returns a reference to a specialized class
of our component. This generated class maintains a strong link with the base class, allowing more complex queries.
Let's change our example using qualifiers.
```lua
-- components
local Player = ECS.Component({ health = 100, region = "easy", healthTotal = 100 })
local HealthBuff = ECS.Component({ value = 10 })
local HealthBuffItem = HealthBuff.Qualifier("Item")
local HealthBuffMapRegion = HealthBuff.Qualifier("Region")
local Item = ECS.Component({ rarity = 0 })
-- systems
local PlayerItemSystem = System("process", 1, Query.All(Player, Item))
function PlayerItemSystem:Update(Time)
for i, entity in self:Result():Iterator() do
local item = entity[Item]
local player = entity[Player]
if item.rarity == "legendary" then
entity[HealthBuffItem] = 15 -- same as entity:Set(HealthBuffItem.New(15))
else
entity[HealthBuffItem] = nil
end
end
end
local MapRegionSystem = System("process", 1, Query.All(Player))
function MapRegionSystem:Update(Time)
for i, entity in self:Result():Iterator() do
local player = entity[Player]
if player.region == "easy" then
entity[HealthBuffMapRegion] = nil
else
local buff = entity[HealthBuffMapRegion]
if buff == nil then
buff = HealthBuffMapRegion(0)
entity:Set(buff)
end
if player.region == "hard" then
buff.value = 15
elseif player.region == "hell" then
buff.value = 40
end
end
end
end
local HealthSystem = System("process", 2, Query.All(Player).Any(HealthBuff))
function HealthSystem:Update(Time)
for i, entity in self:Result():Iterator() do
local player = entity[Player]
local healthTotal = player.health
local buffers = entity:GetAll(HealthBuff)
for i,buff in ipairs(buffers) do
healthTotal = healthTotal + buff.value
end
player.healthTotal = player.health
end
end
```
Okay, in this new implementation, the `MapRegionSystem` system only cares about the `HealthBuffMapRegion` qualifier,
while the `PlayerItemSystem` system only manages the `HealthBuffItem` qualifier. We can now create systems that
specialize in qualifiers and manage only this attribute of the entity. The `HealthSystem` gets and processes all
entities that have any qualifier from the `HealthBuff` component.
[Check the API](/api?id=component) other methods that can be useful when working with qualifiers.
### FSM - Finite State Machines
__UNDER_CONSTRUCTION__
```lua
local Movement = Component.Create({ Speed = 0 })
-- [Standing] <--> [Walking] <--> [Running]
Movement.States = {
Standing = {"Walking"},
Walking = "*",
Running = {"Walking"}
}
Movement.StateInitial = "Standing"
Movement.Case = {
Standing = function(self, previous)
print("Transition from "..previous.." to Standing")
end,
Walking = function(self, previous)
print("Transition from "..previous.." to Walking")
end,
Running = function(self, previous)
print("Transition from "..previous.." to Running")
end
}
local movement = Movement()
movement:SetState("Walking")
movement:SetState("Running")
print(movement:GetState()) -- Running
print(movement:GetPrevState()) -- Walking
movement:SetState("Standing") -- invalid, Running -> Walking|Running
print(movement:GetState()) -- Running
print(movement:GetPrevState()) -- Walking
movement:SetState(nil)
print(movement:GetState()) -- Running
print(movement:GetPrevState()) -- Walking
movement:SetState("INVALID_STATE")
print(movement:GetState()) -- Running
print(movement:GetPrevState()) -- Walking
-- query
local queryStanding = Query.All(Movement.In("Standing"))
local queryInMovement = Query.Any(Movement.In("Walking", "Running"))
-- qualifier
local MovementB = Movement.Qualifier("Sub")
-- ignored, "States", "StateInitial" and "Case" only work in primary class
MovementB.States = { Standing = {"Walking"} }
```
## Entity
__UNDER_CONSTRUCTION__
```lua
--[[
[GET]
01) comp1 = entity[CompType1]
02) comp1 = entity:Get(CompType1)
03) comp1, comp2, comp3 = entity:Get(CompType1, CompType2, CompType3)
]]
--[[
[SET]
01) entity[CompType1] = nil
02) entity[CompType1] = value
03) entity:Set(CompType1, nil)
04) entity:Set(CompType1, value)
05) entity:Set(comp1)
06) entity:Set(comp1, comp2, ...)
]]
--[[
[UNSET]
01) enity:Unset(comp1)
02) entity[CompType1] = nil
03) enity:Unset(CompType1)
04) enity:Unset(comp1, comp1, ...)
05) enity:Unset(CompType1, CompType2, ...)
]]
--[[
[Utils]
01) comps = entity:GetAll()
01) qualifiers = entity:GetAll(PrimaryClass)
]]
```
## Query
__UNDER_CONSTRUCTION__
## System
__UNDER_CONSTRUCTION__
## Task
__UNDER_CONSTRUCTION__
```lua
local log = {}
local Task_A = System.Create('task', function()
-- In this example, TASK_A takes time to execute, delaying its execution
local i = 0
while i <= 4000 do
i = i + 1
if i%1000 == 0 then
-- Processing is parallel, any time-consuming task must invoke coroutine.yield() after a period of time to
-- not block processing
coroutine.yield()
end
end
table.insert(log, 'A')
end)
local Task_B = System.Create('task', function()
table.insert(log, 'B')
end)
local Task_C = System.Create('task', function()
table.insert(log, 'C')
end)
local Task_D = System.Create('task', function()
table.insert(log, 'D')
end)
local Task_E = System.Create('task', function()
table.insert(log, 'E')
end)
local Task_F = System.Create('task', function()
table.insert(log, 'F')
end)
local Task_G = System.Create('task', function()
table.insert(log, 'G')
end)
local Task_H = System.Create('task', function(self)
table.insert(log, 'H')
end)
--[[
A<-------C<---+-----F<----+
| | | |
+----+ E<----+ H
| | |
B<--+----D<---+------G<---+
A - has no dependency
B - has no dependency
C - Depends on A,B
D - Depends on B
E - Depends on A,B,C,D
F - Depends on A,B,C,D,E
G - Depends on B,D
H - Depends on A,B,C,D,E,F,G
Completion order will be B,D,G,A,C,E,F,H
> In this example, TASK_A takes time to execute, delaying its execution
]]
Task_A.Before = {Task_C}
Task_B.Before = {Task_D}
Task_C.After = {Task_B}
Task_D.Before = {Task_G}
Task_F.After = {Task_E}
Task_E.After = {Task_D, Task_C}
Task_C.Before = {Task_F}
Task_H.After = {Task_F, Task_G}
```
## World
__UNDER_CONSTRUCTION__
================================================
FILE: docs/faq.md
================================================
# FAQ
__UNDER_CONSTRUCTION__
================================================
FILE: docs/favicon/browserconfig.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/ms-icon-70x70.png"/><square150x150logo src="/ms-icon-150x150.png"/><square310x310logo src="/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
================================================
FILE: docs/favicon/manifest.json
================================================
{
"name": "App",
"icons": [
{
"src": "\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}
================================================
FILE: docs/getting-started.md
================================================
# Installation
**ECS Lua** has no external dependencies, so just download the latest version available on
[releases page](https://github.com/nidorx/ecs-lua/releases) of the project.
There are 3 options to use **ECS Lua**
1. **ECS.lua** Minified version in a single file
1. **ECS_concat.lua** Version concatenated with the original comments, which can be used for debugging during the
development
1. **ECS.zip** Version with files from the `src` directory.
> Important! All files do the `require` for dependencies that are in the same directory, if it is
using in a Lua project, register in `package.path`.
> These `require` do not work in Roblox Luau, due to the import format that Roblox uses.
After importing **ECS Lua**, it is ready to be used. **ECS Lua** registers the global variable `_G.ECS` to
make it easy to use, so you can use the engine in both ways `local ECS = require("ECS")`
(in Roblox `local ECS = require(game.ReplicatedStorage:WaitForChild("ECS"))`) or simply `_G.ECS`.
## LoopManager
In order for the world's systems to receive an update, the `World:Update(step, now)` method must be invoked on each
frame. To automate this process, **ECS Lua** provides a functionality so that, at the time of instantiation of a
new world, it registers to receive the update automatically.
The implementation of this method is very simple and more details can be seen in the section
[Architecture - World](/architecture?id=world).
> If you use Roblox you don't need to worry, **ECS Lua** already has a default implementation when it runs on
Roblox, more details below.
## Roblox
You can install directly from Roblox Studio by searching the toolbox for `ECS-lua`, this is the
[minified engine version](https://www.roblox.com/library/5887881675). When using **ECS Lua** in Roblox, the engine
already automatically identifies and registers a `LoopManager`, so no additional steps are needed.
# General Concepts
Some common terms in ECS engines are:
- [Entities](/architecture?id=entity): An object with a unique ID that can have multiple components attached to it.
- [Components](/architecture?id=component): Different characteristics of an entity. eg geometry, physics, hit points. Data is only stored in components.
- [Systems](/architecture?id=system): It does the real work, applying the rules of the game, processing entities and modifying their components.
- [Queries](/architecture?id=query): Used by systems to determine which entities they are interested in, based on the components the entities have.
- [World](/architecture?id=world): A container for entities, components, systems and queries.

The normal workflow when building an ECS-based program:
- Create the `Components` that shape the data you need to use in your game/application.
- Create the `Systems` that will use these `Components` to read and transform the entity data.
- Create `Entities` and attach `Components` to them.
- Run all systems at each frame, perform `Query` in `World` to decide which entities will be modified.
## Component
Components are objects that contain data. In **ECS Lua**, just call the `ECS.Component(template)` method to define a
`Class` of a component.
The `template` parameter can be of any type, where:
- When `table`, this template will be used for creating component instances;
- When it's a `function`, it will be invoked on instantiation.
- If the template type is different, **ECS Lua** will generate a template in the format `{ value = template }`, this is
the format used in the `Acceleration` component below.
```lua
local Position = ECS.Component({
x = 0, y = 0, z = 0
})
-- the same as:
-- ECS.Component({ value = 0.1 })
l
gitextract_abg99oc9/ ├── .editorconfig ├── .gitignore ├── .luacov ├── .travis.yml ├── CONTRIBUTING.md ├── ECS.lua ├── ECS_concat.lua ├── LICENSE ├── README.md ├── build.lua ├── docs/ │ ├── .nojekyll │ ├── README.md │ ├── _coverpage.md │ ├── _navbar.md │ ├── _sidebar.md │ ├── api.md │ ├── architecture.md │ ├── assets/ │ │ ├── boids.rbxl │ │ ├── logo-r.psd │ │ ├── pipeline_ecs.psd │ │ ├── pipeline_old.psd │ │ ├── repository-open-graph.psd │ │ ├── tutorial.rbxl │ │ └── version.psd │ ├── faq.md │ ├── favicon/ │ │ ├── browserconfig.xml │ │ └── manifest.json │ ├── getting-started.md │ ├── index.html │ ├── pt-br/ │ │ ├── README.md │ │ ├── _coverpage.md │ │ ├── _navbar.md │ │ ├── _sidebar.md │ │ ├── api.md │ │ ├── architecture.md │ │ ├── faq.md │ │ ├── getting-started.md │ │ ├── tutorial-boids.md │ │ ├── tutorial-pacman.md │ │ ├── tutorial-shoot.md │ │ └── tutorial.md │ ├── style.css │ ├── tutorial-boids.md │ ├── tutorial-pacman.md │ ├── tutorial-shoot.md │ ├── tutorial.md │ ├── z_old_TECH_DETAILS.md │ └── z_old_TUTORIAL.md ├── examples/ │ └── pong/ │ ├── .editorconfig │ ├── .gitignore │ ├── default.project.json │ ├── pong.rbxlx │ └── src/ │ ├── client/ │ │ ├── Constants.lua │ │ ├── Main.client.lua │ │ ├── Utility.lua │ │ ├── components/ │ │ │ ├── AudioSource.lua │ │ │ ├── Ball.lua │ │ │ ├── BasePart.lua │ │ │ ├── Paddle.lua │ │ │ ├── Player.lua │ │ │ ├── Position.lua │ │ │ ├── Score.lua │ │ │ └── Velocity.lua │ │ └── systems/ │ │ ├── AudioSystem.lua │ │ ├── BallSystem.lua │ │ ├── CameraSystem.lua │ │ ├── MoveSystem.lua │ │ ├── PaddleHitSystem.lua │ │ ├── PaddleSystem.lua │ │ ├── PlayerAiThinkSystem.lua │ │ ├── PlayerHumanInputSystem.lua │ │ ├── RenderSystem.lua │ │ └── ScoreSystem.lua │ ├── server/ │ │ └── Main.server.lua │ └── shared/ │ └── ECS.lua ├── modules/ │ ├── bin/ │ │ └── luacov │ ├── luacov/ │ │ ├── defaults.lua │ │ ├── hook.lua │ │ ├── linescanner.lua │ │ ├── reporter/ │ │ │ └── default.lua │ │ ├── reporter.lua │ │ ├── runner.lua │ │ ├── stats.lua │ │ ├── tick.lua │ │ └── util.lua │ ├── luacov.lua │ ├── luaunit.lua │ └── minify.lua ├── roblox/ │ ├── README.md │ ├── RobloxUtils.lua │ └── tutorial/ │ ├── default.project.json │ └── src/ │ ├── client/ │ │ ├── benchmark/ │ │ │ ├── init.client.lua │ │ │ └── soa.lua │ │ └── tutorial/ │ │ └── init.client.lua │ ├── server/ │ │ └── tutorial/ │ │ └── init.server.lua │ └── shared/ │ ├── teste.lua │ └── tutorial/ │ ├── component/ │ │ ├── FiringComponent.lua │ │ └── WeaponComponent.lua │ └── system/ │ ├── CleanupFiringSystem.lua │ ├── FiringSystem.lua │ └── PlayerShootingSystem.lua ├── src/ │ ├── Archetype.lua │ ├── Component.lua │ ├── ComponentFSM.lua │ ├── ECS.lua │ ├── Entity.lua │ ├── EntityRepository.lua │ ├── Event.lua │ ├── Query.lua │ ├── QueryResult.lua │ ├── RobloxLoopManager.lua │ ├── System.lua │ ├── SystemExecutor.lua │ ├── Timer.lua │ ├── Utility.lua │ └── World.lua ├── test/ │ ├── README.md │ ├── test_Archetype.lua │ ├── test_Component.lua │ ├── test_Entity.lua │ ├── test_EntityRepository.lua │ ├── test_Event.lua │ ├── test_Query.lua │ ├── test_QueryResult.lua │ ├── test_SystemExecutor.lua │ └── test_World.lua └── test.lua
Condensed preview — 127 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,031K chars).
[
{
"path": ".editorconfig",
"chars": 189,
"preview": "root = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n[*"
},
{
"path": ".gitignore",
"chars": 50,
"preview": "# IDE\n.idea\n.vscode\n\nluacov.report.out\n\nsrc/*.zip\n"
},
{
"path": ".luacov",
"chars": 152,
"preview": "return {\n\tinclude = {\n\t\t\"^src\",\n \"src%/.+$\"\n\t},\n\texclude = {\n\t\t\"%.test$\",\n\t},\n runreport = true,\n deletestats ="
},
{
"path": ".travis.yml",
"chars": 337,
"preview": "language: python\n\nenv:\n - LUA=\"lua=5.1\"\n - LUA=\"lua=5.2\"\n - LUA=\"lua=5.3\"\n - LUA=\"lua=5.4\"\n - LUA=\"luajit=2.0\"\n - "
},
{
"path": "CONTRIBUTING.md",
"chars": 2106,
"preview": "# Contributing to ECS Lua\nThanks for considering contributing to ECS Lua! This guide has a few tips and guidelines to ma"
},
{
"path": "ECS.lua",
"chars": 28553,
"preview": "--[[\n\tECS Lua v2.2.0\n\n\tECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.\n\n\tTh"
},
{
"path": "ECS_concat.lua",
"chars": 101378,
"preview": "--[[\n\tECS Lua v2.2.0\n\n\tECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.\n\n\tTh"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2020 Alex Rodin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 4234,
"preview": "<p align=\"center\">\n <a href=\"https://nidorx.github.io/ecs-lua\">\n <img \n src=\"docs/assets/logo.svg\" \n "
},
{
"path": "build.lua",
"chars": 3951,
"preview": "\n\nlocal OUTPUT_CONCAT = \"ECS_concat\"\n\nlocal OUTPUT_MINIFIED = \"ECS\"\n\nlocal SRC_FILES = {\n \"Archetype\",\n \"Component\","
},
{
"path": "docs/.nojekyll",
"chars": 0,
"preview": ""
},
{
"path": "docs/README.md",
"chars": 3818,
"preview": "# What is it?\n\n**ECS Lua** is a fast and easy to use ECS (Entity Component System) engine for game development.\n\n \n- Language <span class=\"arrow\">▾</span>\n - [English](/)\n - [Português do Brasil](/pt-"
},
{
"path": "docs/_sidebar.md",
"chars": 1251,
"preview": "- [Home](/)\n- [Installation](/getting-started?id=installation)\n- [General Concepts](/getting-started?id=general-concepts"
},
{
"path": "docs/api.md",
"chars": 28622,
"preview": "# API\n<div class=\"api-docs\">\n\n\n# ECS\n- `ECS.Query`\n - _@type_ `QueryClass`\n- `ECS.Archetype`\n - _@type_ `ArchetypeCl"
},
{
"path": "docs/architecture.md",
"chars": 14312,
"preview": "# Architecture\n\nIn Software Engineering, ECS is the acronym for Entity Component System, is a software architecture patt"
},
{
"path": "docs/faq.md",
"chars": 30,
"preview": "# FAQ\n\n__UNDER_CONSTRUCTION__\n"
},
{
"path": "docs/favicon/browserconfig.xml",
"chars": 281,
"preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<browserconfig><msapplication><tile><square70x70logo src=\"/ms-icon-70x70.png\"/><s"
},
{
"path": "docs/favicon/manifest.json",
"chars": 720,
"preview": "{\n \"name\": \"App\",\n \"icons\": [\n {\n \"src\": \"\\/android-icon-36x36.png\",\n \"sizes\": \"36x36\",\n \"type\": \"image\\/png\",\n "
},
{
"path": "docs/getting-started.md",
"chars": 11860,
"preview": "# Installation\n\n**ECS Lua** has no external dependencies, so just download the latest version available on \n[releases pa"
},
{
"path": "docs/index.html",
"chars": 6352,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\" />\n <title>ECS-lua - Entity Component System in Lua<"
},
{
"path": "docs/pt-br/README.md",
"chars": 4073,
"preview": "# O que é?\n\n**ECS Lua** é um motor ECS (Entity Component System) rápido e fácil de usar para o desenvolvimento de jogos."
},
{
"path": "docs/pt-br/_coverpage.md",
"chars": 2559,
"preview": "<div class=\"logo-container\">\n <div class=\"heats\">\n <div class=\"h r1 c1\"></div>\n <div class=\"h r1 c2\"></div>\n <"
},
{
"path": "docs/pt-br/_navbar.md",
"chars": 132,
"preview": "- [Início](/pt-br/) \n- Idiomas <span class=\"arrow\">▾</span>\n - [English](/)\n - [Português do Brasi"
},
{
"path": "docs/pt-br/_sidebar.md",
"chars": 1460,
"preview": "- [Início](/pt-br/)\n- [Instalação](/pt-br/getting-started?id=instalação)\n- [Conceitos Gerais](/pt-br/getting-started?id="
},
{
"path": "docs/pt-br/api.md",
"chars": 28622,
"preview": "# API\n<div class=\"api-docs\">\n\n\n# ECS\n- `ECS.Query`\n - _@type_ `QueryClass`\n- `ECS.Archetype`\n - _@type_ `ArchetypeCl"
},
{
"path": "docs/pt-br/architecture.md",
"chars": 13138,
"preview": "# Arquitetura\n\nEm Engenharia de Software, ECS é o acrônimo de Entity Component System (em português: Sistema de Componen"
},
{
"path": "docs/pt-br/faq.md",
"chars": 47,
"preview": "# Perguntas Frequentes\n\n__UNDER_CONSTRUCTION__\n"
},
{
"path": "docs/pt-br/getting-started.md",
"chars": 12628,
"preview": "# Instalação\n\nO **ECS Lua** nao possui dependencias externas, portanto, basta fazer o download da ultima versao disponív"
},
{
"path": "docs/pt-br/tutorial-boids.md",
"chars": 178,
"preview": "# Tutorial - Boids\n\n__UNDER_CONSTRUCTION__\n\n> OBJETIVO: Aplicar o uso do Job System para atualizar algumas centenas (ou "
},
{
"path": "docs/pt-br/tutorial-pacman.md",
"chars": 138,
"preview": "# Tutorial - Pacman\n\n__UNDER_CONSTRUCTION__\n\n> OBJETIVO: Demonstrar o uso de maquinas de estado para criar a \"inteligenc"
},
{
"path": "docs/pt-br/tutorial-shoot.md",
"chars": 153,
"preview": "# Tutorial - Jogo de Tiro\n\n__UNDER_CONSTRUCTION__\n\n> OBJETIVO: Demonstrar o uso das principais funcionalidades do ECS Lu"
},
{
"path": "docs/pt-br/tutorial.md",
"chars": 36,
"preview": "# Tutoriais\n\n__UNDER_CONSTRUCTION__\n"
},
{
"path": "docs/style.css",
"chars": 8639,
"preview": ".meta-container {\n text-align: right;\n}\n\n.meta-container>.edit-button {\n text-decoration: none;\n font-size : "
},
{
"path": "docs/tutorial-boids.md",
"chars": 171,
"preview": "# Tutorial - Boids\n\n__UNDER_CONSTRUCTION__\n\n> OBJECTIVE: To apply the use of the Job System to update a few hundred (or "
},
{
"path": "docs/tutorial-pacman.md",
"chars": 135,
"preview": "# Tutorial - Pacman\n\n__UNDER_CONSTRUCTION__\n\n> OBJECTIVE: Demonstrate the use of state machines to create the \"intellige"
},
{
"path": "docs/tutorial-shoot.md",
"chars": 148,
"preview": "# Tutorial - Shooting Game\n\n__UNDER_CONSTRUCTION__\n\n> OBJECTIVE: Demonstrate the use of the main features of ECS Lua and"
},
{
"path": "docs/tutorial.md",
"chars": 36,
"preview": "# Tutorials\n\n__UNDER_CONSTRUCTION__\n"
},
{
"path": "docs/z_old_TECH_DETAILS.md",
"chars": 9232,
"preview": "# Roblox-ECS - Technical implementation details\n\n## Roblox Pipeline\n\nBefore going into the details, let's review some im"
},
{
"path": "docs/z_old_TUTORIAL.md",
"chars": 21263,
"preview": "## Roblox-ECS Tutorial - Shooting Game\n\nIn this topic, we will see how to implement a simple shooting game, inspired by "
},
{
"path": "examples/pong/.editorconfig",
"chars": 197,
"preview": "root = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\ninsert_final_newline = true\n\n[*"
},
{
"path": "examples/pong/.gitignore",
"chars": 117,
"preview": "# IDE\n.idea\n*.iml\n.vscode\n\n# Roblox Studio\n# place-dev.rbxl\n**/*.rbxl.lock\n**/*.rbxlx.lock\n**/*.blend1\n\n# misc\nbuild\n"
},
{
"path": "examples/pong/default.project.json",
"chars": 638,
"preview": "{\n\t\"name\": \"ecs-lua-pong\",\n\t\"tree\": {\n \"$className\": \"DataModel\",\n \"ReplicatedStorage\": {\n \"$className"
},
{
"path": "examples/pong/pong.rbxlx",
"chars": 125033,
"preview": "<roblox xmlns:xmime=\"http://www.w3.org/2005/05/xmlmime\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noName"
},
{
"path": "examples/pong/src/client/Constants.lua",
"chars": 345,
"preview": "\nlocal BALL_SPEED = 50\n\nlocal constants = {\n BALL_BOOST = 0.3,\n BALL_RADIUS = 1,\n BALL_SPEED = BALL_SPEED,\n BALL"
},
{
"path": "examples/pong/src/client/Main.client.lua",
"chars": 1437,
"preview": "\nlocal ECS = require(game.ReplicatedStorage:WaitForChild(\"ECS\"))\n\nlocal Constants = require(script.Parent:WaitForChild(\""
},
{
"path": "examples/pong/src/client/Utility.lua",
"chars": 224,
"preview": "\nlocal Utility = {}\n\nfunction Utility.map(x, inMin, inMax, outMin, outMax)\n return (x - inMin)*(outMax - outMin)/(inMa"
},
{
"path": "examples/pong/src/client/components/AudioSource.lua",
"chars": 597,
"preview": "\nlocal AudioSource = _G.ECS.Component({\n clip = \"\", -- sound asset\n volume = 10, -- playback volume between ["
},
{
"path": "examples/pong/src/client/components/Ball.lua",
"chars": 98,
"preview": "\nlocal Ball = _G.ECS.Component({\n secondary = false,\n initialDirection = nil,\n})\n\nreturn Ball\n"
},
{
"path": "examples/pong/src/client/components/BasePart.lua",
"chars": 54,
"preview": "\nlocal BasePart = _G.ECS.Component()\n\nreturn BasePart\n"
},
{
"path": "examples/pong/src/client/components/Paddle.lua",
"chars": 191,
"preview": "local Paddle = _G.ECS.Component({\n side = \"left\",\n hits = 0,\n target = 0, -- -1 = bottom, 0 = middle, 1 = top\n "
},
{
"path": "examples/pong/src/client/components/Player.lua",
"chars": 136,
"preview": "\nlocal Player = _G.ECS.Component()\nlocal PlayerAI = Player.Qualifier(\"AI\")\nlocal PlayerHuman = Player.Qualifier(\"Human\")"
},
{
"path": "examples/pong/src/client/components/Position.lua",
"chars": 74,
"preview": "\nlocal Position = _G.ECS.Component(Vector3.new(0, 0, 0))\n\nreturn Position\n"
},
{
"path": "examples/pong/src/client/components/Score.lua",
"chars": 49,
"preview": "\nlocal Score = _G.ECS.Component(0)\n\nreturn Score\n"
},
{
"path": "examples/pong/src/client/components/Velocity.lua",
"chars": 74,
"preview": "\nlocal Velocity = _G.ECS.Component(Vector3.new(0, 0, 0))\n\nreturn Velocity\n"
},
{
"path": "examples/pong/src/client/systems/AudioSystem.lua",
"chars": 1604,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Components = Client.components\nlocal BasePart = require(Co"
},
{
"path": "examples/pong/src/client/systems/BallSystem.lua",
"chars": 2720,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Constants = require(Client.Constants)\n\nlocal Components = "
},
{
"path": "examples/pong/src/client/systems/CameraSystem.lua",
"chars": 327,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Constants = require(Client.Constants)\n\nlocal CFRAME = CFra"
},
{
"path": "examples/pong/src/client/systems/MoveSystem.lua",
"chars": 612,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Components = Client.components\nlocal Velocity = require(Co"
},
{
"path": "examples/pong/src/client/systems/PaddleHitSystem.lua",
"chars": 4728,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Utility = require(Client.Utility)\nlocal Constants = requir"
},
{
"path": "examples/pong/src/client/systems/PaddleSystem.lua",
"chars": 1843,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Utility = require(Client.Utility)\nlocal Constants = requir"
},
{
"path": "examples/pong/src/client/systems/PlayerAiThinkSystem.lua",
"chars": 2049,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\n\nlocal Utility = require(Client.Utility)\nlocal Constants = requi"
},
{
"path": "examples/pong/src/client/systems/PlayerHumanInputSystem.lua",
"chars": 1012,
"preview": "local ECS = _G.ECS\n\nlocal UserInputService = game:GetService(\"UserInputService\")\nlocal CurrentCamera = game.workspace.Cu"
},
{
"path": "examples/pong/src/client/systems/RenderSystem.lua",
"chars": 640,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Components = Client.components\nlocal Position = require(Co"
},
{
"path": "examples/pong/src/client/systems/ScoreSystem.lua",
"chars": 2442,
"preview": "local ECS = _G.ECS\n\nlocal Client = script.Parent.Parent\nlocal Constants = require(Client.Constants)\n\nlocal Components = "
},
{
"path": "examples/pong/src/server/Main.server.lua",
"chars": 40,
"preview": "game.Players.CharacterAutoLoads = false\n"
},
{
"path": "examples/pong/src/shared/ECS.lua",
"chars": 28606,
"preview": "--[[\n\tECS Lua v2.2.0\n\n\tECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.\n\n\tTh"
},
{
"path": "modules/bin/luacov",
"chars": 2526,
"preview": "#!/usr/bin/env lua\nlocal runner = require(\"luacov.runner\")\n\nlocal patterns = {}\nlocal configfile\nlocal reporter\n\nlocal h"
},
{
"path": "modules/luacov/defaults.lua",
"chars": 2889,
"preview": "--- Default values for configuration options.\n-- For project specific configuration create '.luacov' file in your projec"
},
{
"path": "modules/luacov/hook.lua",
"chars": 2186,
"preview": "------------------------\n-- Hook module, creates debug hook used by LuaCov.\n-- @class module\n-- @name luacov.hook\nlocal "
},
{
"path": "modules/luacov/linescanner.lua",
"chars": 11136,
"preview": "local LineScanner = {}\nLineScanner.__index = LineScanner\n\nfunction LineScanner:new()\n return setmetatable({\n firs"
},
{
"path": "modules/luacov/reporter/default.lua",
"chars": 32,
"preview": "return require \"luacov.reporter\""
},
{
"path": "modules/luacov/reporter.lua",
"chars": 13890,
"preview": "------------------------\n-- Report module, will transform statistics file into a report.\n-- @class module\n-- @name luaco"
},
{
"path": "modules/luacov/runner.lua",
"chars": 21936,
"preview": "---------------------------------------------------\n-- Statistics collecting module.\n-- Calling the module table is a sh"
},
{
"path": "modules/luacov/stats.lua",
"chars": 2078,
"preview": "-----------------------------------------------------\n-- Manages the file with statistics (being) collected.\n-- @class m"
},
{
"path": "modules/luacov/tick.lua",
"chars": 323,
"preview": "\n--- Load luacov using this if you want it to periodically\n-- save the stats file. This is useful if your script is\n-- a"
},
{
"path": "modules/luacov/util.lua",
"chars": 2371,
"preview": "---------------------------------------------------\n-- Utility module.\n-- @class module\n-- @name luacov.util\nlocal util "
},
{
"path": "modules/luacov.lua",
"chars": 281,
"preview": "--- Loads `luacov.runner` and immediately starts it.\n-- Useful for launching scripts from the command-line. Returns the "
},
{
"path": "modules/luaunit.lua",
"chars": 126555,
"preview": "--[[\n luaunit.lua\n\nDescription: A unit testing framework\nHomepage: https://github.com/bluebird75/luaunit\nDevelopm"
},
{
"path": "modules/minify.lua",
"chars": 87031,
"preview": "--[[\n MIT License\n\n Copyright (c) 2017 Mark Langen\n\n Permission is hereby granted, free of charge, to any person o"
},
{
"path": "roblox/README.md",
"chars": 4543,
"preview": "# Roblox-ECS\n\nLINK: https://github.com/nidorx/ecs-lua/blob/master/src/shared/ECSUtil.lua\n\n## Utility Systems and Compone"
},
{
"path": "roblox/RobloxUtils.lua",
"chars": 16828,
"preview": "local ECS = require(game.ReplicatedStorage:WaitForChild(\"ECS\"))\n\n-- precision\nlocal EPSILON = 0.000000001\n\nlocal functio"
},
{
"path": "roblox/tutorial/default.project.json",
"chars": 515,
"preview": "{\n \"name\": \"dat.GUI\",\n \"tree\": {\n \"$className\": \"DataModel\",\n \"ReplicatedStorage\": {\n \"$className\""
},
{
"path": "roblox/tutorial/src/client/benchmark/init.client.lua",
"chars": 1699,
"preview": "repeat wait() until game:GetService('Players').LocalPlayer.Character\n\nlocal DISABLED = true\n\n--[[\n Benchmark, data ori"
},
{
"path": "roblox/tutorial/src/client/benchmark/soa.lua",
"chars": 2906,
"preview": "\nlocal case = {\n name = 'Struct of Arrays vs. Array of Structs'\n}\n\n-- produce equal sequences of numbers for both test"
},
{
"path": "roblox/tutorial/src/client/tutorial/init.client.lua",
"chars": 2198,
"preview": "repeat wait() until game.Players.LocalPlayer.Character\n\nlocal Players \t = game:GetService(\"Players\")\nlocal Player \t "
},
{
"path": "roblox/tutorial/src/server/tutorial/init.server.lua",
"chars": 0,
"preview": ""
},
{
"path": "roblox/tutorial/src/shared/teste.lua",
"chars": 2465,
"preview": "local ECS = {}\n\nlocal Query, System, Component, = ECS.Query, ECS.System, ECS.Component,\n\n\nlocal Transform = Component({"
},
{
"path": "roblox/tutorial/src/shared/tutorial/component/FiringComponent.lua",
"chars": 206,
"preview": "local ECS = require(game.ReplicatedStorage:WaitForChild(\"ECS\"))\n\nreturn ECS.Component('Firing', function(firedAt)\n if "
},
{
"path": "roblox/tutorial/src/shared/tutorial/component/WeaponComponent.lua",
"chars": 96,
"preview": "local ECS = require(game.ReplicatedStorage:WaitForChild(\"ECS\"))\n\nreturn ECS.Component('Weapon')\n"
},
{
"path": "roblox/tutorial/src/shared/tutorial/system/CleanupFiringSystem.lua",
"chars": 698,
"preview": "\nlocal ECS = require(game.ReplicatedStorage:WaitForChild(\"ECS\"))\n\n-- Components\nlocal Components = game.ReplicatedStorag"
},
{
"path": "roblox/tutorial/src/shared/tutorial/system/FiringSystem.lua",
"chars": 1557,
"preview": "\nlocal ECS = require(game.ReplicatedStorage:WaitForChild(\"ECS\"))\nlocal ECSUtil = require(game.ReplicatedStorage:Wa"
},
{
"path": "roblox/tutorial/src/shared/tutorial/system/PlayerShootingSystem.lua",
"chars": 872,
"preview": "\nlocal UserInputService = game:GetService(\"UserInputService\")\nlocal ECS = require(game.ReplicatedStorage:WaitForChild(\"E"
},
{
"path": "src/Archetype.lua",
"chars": 5688,
"preview": "\nlocal archetypes = {}\n\nlocal CACHE_WITH = {}\nlocal CACHE_WITHOUT = {}\n\n-- Version of the last registered archetype. Use"
},
{
"path": "src/Component.lua",
"chars": 10047,
"preview": "local Utility = require(\"Utility\")\nlocal ComponentFSM = require(\"ComponentFSM\")\n\nlocal copyDeep = Utility.copyDeep\nlocal"
},
{
"path": "src/ComponentFSM.lua",
"chars": 5435,
"preview": "--[[\n Facilitate the construction and use of a Finite State Machine (FSM) using ECS\n\n Example:\n local Movement "
},
{
"path": "src/ECS.lua",
"chars": 2672,
"preview": "--[[\n ECS Lua v2.2.0\n\n ECS Lua is a fast and easy to use ECS (Entity Component System) engine for game development.\n"
},
{
"path": "src/Entity.lua",
"chars": 8252,
"preview": "--[[\n The entity is a fundamental part of the Entity Component System. Everything in your game that has data or an \n "
},
{
"path": "src/EntityRepository.lua",
"chars": 2733,
"preview": "local Event = require(\"Event\")\n\n--[[\n The repository (database) of entities in a world.\n\n The repository indexes ent"
},
{
"path": "src/Event.lua",
"chars": 1211,
"preview": "\n--[[\n Subscription\n]]\nlocal Connection = {}\nConnection.__index = Connection\n\nfunction Connection.New(event, handler)\n"
},
{
"path": "src/Query.lua",
"chars": 8224,
"preview": "\nlocal QueryResult = require(\"QueryResult\")\n\n--[[\n Global cache result.\n\n The validated components are always the sa"
},
{
"path": "src/QueryResult.lua",
"chars": 9796,
"preview": "\n--[[\n OperatorFunction = function(param, value, count) => newValue, acceptItem, mustContinue\n]]\n\nlocal function opera"
},
{
"path": "src/RobloxLoopManager.lua",
"chars": 971,
"preview": "local function InitManager()\n local RunService = game:GetService(\"RunService\")\n \n return {\n Register = functi"
},
{
"path": "src/System.lua",
"chars": 3255,
"preview": "\nlocal STEPS = { \"task\", \"render\", \"process\", \"transform\" }\n\nlocal System = {}\n\n--[[\n Create new System Class\n\n @par"
},
{
"path": "src/SystemExecutor.lua",
"chars": 15144,
"preview": "\n--[[\n After = {SystemC, SystemD}, An update order that requests ECS update this system after it updates another speci"
},
{
"path": "src/Timer.lua",
"chars": 3293,
"preview": "\n-- if execution is slow, perform a maximum of 4 simultaneous updates in order to keep the fixrate\nlocal MAX_SKIP_FRAMES"
},
{
"path": "src/Utility.lua",
"chars": 1413,
"preview": "--[[\n Utility library.\n]]\nlocal Utility = {}\n\nif table.unpack == nil then\n\ttable.unpack = unpack\nend\n\nif table.find =="
},
{
"path": "src/World.lua",
"chars": 11000,
"preview": "local Timer = require(\"Timer\")\nlocal Event = require(\"Event\")\nlocal Entity = require(\"Entity\")\nlocal Archetype = require"
},
{
"path": "test/README.md",
"chars": 40,
"preview": "see [CONTRIBUTING.md](/CONTRIBUTING.md)\n"
},
{
"path": "test/test_Archetype.lua",
"chars": 5071,
"preview": "local lu = require('luaunit')\n\nlocal Archetype = require('Archetype')\n\nTestArchetype = {}\n\nlocal SEQ = 1\nlocal function "
},
{
"path": "test/test_Component.lua",
"chars": 15242,
"preview": "local lu = require('luaunit')\n\nfunction sleep(a)\n local sec = tonumber(os.clock() + a); \n while (os.clock() < sec) d"
},
{
"path": "test/test_Entity.lua",
"chars": 20972,
"preview": "local lu = require('luaunit')\n\nlocal Event = require('Event')\nlocal Entity = require('Entity')\nlocal Component = require"
},
{
"path": "test/test_EntityRepository.lua",
"chars": 4069,
"preview": "local lu = require('luaunit')\n\nlocal EntityRepository = require('EntityRepository')\n\nlocal function query(...)\n local "
},
{
"path": "test/test_Event.lua",
"chars": 944,
"preview": "local lu = require('luaunit')\n\nlocal Event = require('Event')\n\nTestEvent = {}\n\nfunction TestEvent:test_ConnectFireDiscon"
},
{
"path": "test/test_Query.lua",
"chars": 2416,
"preview": "local lu = require('luaunit')\n\nlocal Query = require('Query')\nlocal Archetype = require('Archetype')\nlocal Component = r"
},
{
"path": "test/test_QueryResult.lua",
"chars": 10318,
"preview": "local lu = require('luaunit')\n\nlocal Entity = require('Entity')\nlocal Archetype = require('Archetype')\nlocal Component ="
},
{
"path": "test/test_SystemExecutor.lua",
"chars": 13182,
"preview": "local lu = require('luaunit')\n\nlocal World = require('World')\nlocal Query = require('Query')\nlocal System = require('Sys"
},
{
"path": "test/test_World.lua",
"chars": 10101,
"preview": "local lu = require('luaunit')\n\nlocal World = require('World')\nlocal Query = require('Query')\nlocal System = require('Sys"
},
{
"path": "test.lua",
"chars": 634,
"preview": "\npackage.path = package.path .. \";modules/?.lua\"\npackage.path = package.path .. \";src/?.lua\"\nlocal lu = require(\"luaunit"
}
]
// ... and 7 more files (download for full content)
About this extraction
This page contains the full source code of the nidorx/ecs-lua GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 127 files (947.8 KB), approximately 258.9k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.