--- ubiquitousse.ecs -- Optional dependency: ubiquitousse.scene, to allow quick creation of ECS-based scenes. local loaded, scene = pcall(require, (...):match("^(.-)ecs").."scene") if not loaded then scene = nil end --- Entity Component System library, inspired by the excellent tiny-ecs. Main differences include: -- * ability to nest systems; -- * instanciation of systems for each world (no shared state); -- * adding and removing entities is done instantaneously -- * ability to add and remove components from entities after they were added to the world. let ecs -- TODO: Implement a skip list for faster search. -- better control over system order: process, draw, methods? (for lag reasons and dependencies) -- more generic events? -- populate component? --- Recursively remove subsystems from a system. let recDestroySystems = (system) for i=#system.systems, 1, -1 do let s = system.systems[i] recDestroySystems(s) s:onDestroy() system.systems[i] = nil if s.name then system.world.s[s.name] = nil end end end --- Recursively call :clear and :onRemoveFromWorld to a list of systems in a world. let recCallOnRemoveFromWorld = (world, systems) for _, s in ipairs(systems) do s:clear() recCallOnRemoveFromWorld(world, s.systems) s:onRemoveFromWorld(world) end end --- Iterate through the next entity, based on state s: { previousLinkedListItem } let nextEntity = (s) if s[1] then let var = s[1][1] s[1] = s[1][2] return var else return nil end end --- Recursively copy content of a into b if it isn't already present. No cycle detection. let copy = (a, b) for k, v in pairs(a) do if type(v) == "table" then if b[k] == nil then b[k] = {} copy(v, b[k]) elseif b[k] == "table" then copy(v, b[k]) end elseif b[k] == nil then b[k] = v end end end --- System fields and methods. -- When they are added to a world, a new, per-world self table is created and used for every method call (which we call "instancied system"). -- Instancied systems can be retrieved in system.s or system.systems. -- Oh, the "world" is just the top-level system, behaving in exactly the same way as other systems. -- Every field defined below is optional. let system_mt = { --- Read-only after creation system options --- -- I mean, you can try to change them afterwards. But, heh. --- Name of the system (optional). -- Used to create a field with the system's name in world.s and into each entity (the "entity's system table"). name = nil, --- List of subsystems. -- On a instancied system, this is a list of the same subsystems, but instancied for this world. systems = nil, --- Modifiable system options --- --- Returns true if the entity should be added to this system (and therefore its subsystems). -- If this is a string or a table, it will be converted to a filter function on instanciation using ecs.any. -- If this true, will accept every entity; if false, reject every entity. -- Will only test entities when they are added; changing this after system creation will not affect entities already in the system. -- By default, rejects everything. filter = :(e) return false end, --- Returns true if e1 <= e2. -- Used to place the entity in the sorted entity list when it is added; changing this after system creation -- will not change the order of entities already in the system. compare = :(e1, e2) return true end, --- Called when adding an entity to the system. onAdd = :(s) end, --- Called when removing an entity from the system. onRemove = :(s) end, --- Called when the system is instancied, before any call to :onnAddToWorld (including other systems in the world). onInstance = :() end, --- Called when the system is added to a world. onAddToWorld = :(world) end, --- Called when the system is removed from a world (i.e., the world is destroyed). onRemoveFromWorld = :(world) end, --- Called when the world is destroyed, after every call to :onRemoveFromWorld (including other systems in the world). onDestroy = :() end, --- Called when updating the system. onUpdate = :(dt) end, --- Called when drawing the system. onDraw = :() end, --- Called when updating the system, for every entity the system contains. Called after :onUpdate was called on the system. process = :(s, dt) end, --- Called when drawing the system, for every entity the system contains. Called after :onDraw was called on the system. render = :(s) end, --- If not false, the system will only update every interval seconds. interval = false, --- The system and its susbsystems will only update if this is true. active = true, --- The system and its subsystems will only draw if this is true. visible = true, --- Defaults value to put into the entities's system table when they are added. Will recursively fill missing values. -- When an entity is added to a system, a .entity field is created in the system table, referring to the full entity table. -- Changing this will not affect entities already in the system. default = nil, --- Defaults methods to assign to the entities's system table when they are added. -- When calling the methods with entity.systemName:method(...), the method will actually receive the -- arguments method(system, entity system table, ...). Methamethods are accepted. New methods can be -- created anytime. methods = nil, --- Read-only system options --- --- The world the system belongs to. world = nil, --- Number of entities in the system. entityCount = 0, --- Map of named systems in the world (not only subsystems). Same for every system from the same world. s = nil, --- Private fields --- --- First element of the linked list of entities: { entity, next_element }. _first = nil, --- Associative map of entities in the system and their previous linked list element (or true if first element). -- This make the list effectively a doubly linked list, but with easy access to the previous element using this map (and therefore O(1) deletion). _previous = nil, --- Amount of time waited since last update (if interval is set). _waited = 0, --- Metatable of entities' system table. -- Contains the methods defined in the methods field, wrapped to be called with the correct arguments, as well as a -- __index field (if not redefined in methods). _methods_mt = nil, --- Methods --- --- Add entities to the system and its subsystems. -- Will skip entities that are already in the system. -- Entities are added to subsystems after they were succesfully added to their parent system. -- If this is called on a subsystem instead of the world, be warned that this will bypass all the parent's systems filters. -- Since :remove will not search for entities in systems where they should have been filtered out, the added entities will not be removed -- when calling :remove on a parent system or the world. The entity can be removed by calling :remove on the system :add was called on. -- Complexity: O(1) per unordered system, O(entityCount) per ordered system. add = :(e, ...) if e ~= nil and not @_previous[e] and @filter(e) then -- setup entity if @name then if not e[@name] e[@name] = {} if @default copy(@default, e[@name]) if @methods setmetatable(e[@name], @_methods_mt) e[@name].entity = e end -- add to linked list if @_first == nil then @_first = { e, nil } @_previous[e] = true elseif @compare(e, @_first[1]) then let nxt = @_first @_first = { e, nxt } @_previous[e] = true @_previous[nxt[1]] = @_first else let entity = @_first while entity[2] ~= nil do if @compare(e, entity[2][1]) then let nxt = entity[2] entity[2] = { e, nxt } @_previous[e] = entity @_previous[nxt[1]] = entity[2] break end entity = entity[2] end if entity[2] == nil then entity[2] = { e, nil } @_previous[e] = entity end end -- notify addition @entityCount += 1 @onAdd(e[@name]) -- add to subsystems for _, s in ipairs(@systems) do s:add(e) end end if ... then return e, @add(...) else return e end end, --- Refresh an entity's systems. -- Behave similarly to :add, but if the entity is already in the system, instead of skipping it, it -- will check for new and removed components and add and remove from (sub)systems accordingly. -- Complexity: O(1) per system + add/remove complexity. refresh = :(e, ...) if e ~= nil then if not @_previous[e] then @add(e) elseif @_previous[e] then if not @filter(e) then @remove(e) else for _, s in ipairs(@systems) do s:refresh(e) end end end end if ... then return e, @refresh(...) else return e end end, --- Remove entities to the system and its subsystems. -- Will skip entities that are not in the system. -- Entities are removed from subsystems before they are removed from their parent system. -- If you intend to call this on a subsystem instead of the world, please read the warning in :add. -- Returns all removed entities. -- Complexity: O(1) per system. remove = :(e, ...) if e ~= nil then if @_previous[e] then -- remove from subsystems for _, s in ipairs(@systems) do s:remove(e) end end if @_previous[e] then -- recheck in case it was removed already from a subsystem onRemove callback -- remove from linked list let prev = @_previous[e] if prev == true then @_first = @_first[2] if @_first then @_previous[@_first[1]] = true end else prev[2] = prev[2][2] if prev[2] then @_previous[prev[2][1]] = prev end end -- notify removal @_previous[e] = nil @entityCount -= 1 @onRemove(e[@name]) end end if ... then return e, @remove(...) else return e end end, --- Returns true if every entity is in the system. -- Complexity: O(1). has = :(e, ...) let has = e == nil or not not @_previous[e] if ... then return has and @has(...) else return has end end, --- Returns an iterator that iterate through the entties in this system. iter = :() return nextEntity, { @_first } end, --- Remove every entity from the system and its subsystems. clear = :() for e in @iter() do @remove(e) end for _, s in ipairs(@systems) do s:clear() end end, --- Try to update the system and its subsystems. Should be called on every game update. -- Subsystems are updated after their parent system. update = :(dt) if @active then if @interval then @_waited += dt if @_waited < @interval then return end end @onUpdate(dt) if @process ~= system_mt.process then for e in @iter() do @process(e[@name], dt) end end for _, s in ipairs(@systems) do s:update(dt) end if @interval then @_waited -= @interval end end end, --- Try to draw the system and its subsystems. Should be called on every game draw. -- Subsystems are drawn after their parent system. draw = :() if @visible then @onDraw() if @render ~= system_mt.render then for e in @iter() do @render(e[@name]) end end for _, s in ipairs(@systems) do s:draw() end end end, --- Remove all the entities and subsystems in this system. destroy = :() recCallOnRemoveFromWorld(@world, { @ }) recDestroySystems({ systems = { @ } }) end } --- Self descriptive let alwaysTrue = () return true end let alwaysFalse = () return true end --- Recursively instanciate a list of systems for a world: -- * create their self table with instance fields set -- * create a field with their name in world.s (if name defined) let recInstanciateSystems = (world, systems) let t = {} for _, s in ipairs(systems) do let system -- setup method table let methods_mt = {} if s.methods then methods_mt.__index = methods_mt for k, v in pairs(s.methods) do methods_mt[k] = :(...) return v(system, @, ...) end end setmetatable(s.methods, { __newindex = :(k, v) rawset(@, k, v) methods_mt[k] = :(...) return v(system, @, ...) end end }) end -- instanciate system system = setmetatable({ systems = recInstanciateSystems(world, s.systems or {}), world = world, s = world.s, _previous = {}, _methods_mt = methods_mt }, { __index = :(k) if s[k] ~= nil then return s[k] else return system_mt[k] end end }) if type(s.filter) == "string" then system.filter = (_, e) return e[s.filter] ~= nil end elseif type(s.filter) == "table" then system.filter = ecs.any(unpack(s.filter)) elseif type(s.filter) == "boolean" then if s.filter then system.filter = alwaysTrue else system.filter = alwaysFalse end end -- add system table.insert(t, system) if s.name then world.s[s.name] = system end system:onInstance() end return t end --- Recursively call :onAddToWorld to a list of systems in a world. let recCallOnAddToWorld = (world, systems) for _, s in ipairs(systems) do recCallOnAddToWorld(world, s.systems) s:onAddToWorld(world) end end --- ECS module. ecs = { --- Create and returns a world system based on a list of systems. -- The systems will be instancied for this world. world = (...) let world = setmetatable({ filter = ecs.all(), s = {}, _previous = {} }, { __index = system_mt }) world.world = world world.systems = recInstanciateSystems(world, {...}) recCallOnAddToWorld(world, world.systems) return world end, --- Returns a filter that returns true if, for every argument, a field with the same name exists in the entity. all = (...) if ... then let l = {...} return function(s, e) for _, k in ipairs(l) do if e[k] == nil then return false end end return true end else return alwaysTrue end end, --- Returns a filter that returns true if one of the arguments if the name of a field in the entity. any = (...) if ... then let l = {...} return function(s, e) for _, k in ipairs(l) do if e[k] ~= nil then return true end end return false end else return alwaysFalse end end, --- If uqt.scene is available, returns a new scene that will consist of a ECS world with the specified systems and entities. scene = (name, systems={}, entities={}) assert(scene, "ubiquitousse.scene unavailable") let s = scene.new(name) let w function s:enter() w = ecs.world(unpack(systems)) w:add(unpack(entities)) end function s:exit() w:destroy() end function s:update(dt) w:update(dt) end function s:draw() w:draw() end return s end } return ecs