├── API.md ├── README.md ├── ecs.lua └── test.lua /API.md: -------------------------------------------------------------------------------- 1 | ## Filters 2 | 3 | A Filter is a function that selects which Entities apply to a System. Filters take two parameters, the System and the Entity, and return a boolean value indicating if the Entity should be processed by the System. A truthy value includes the entity, while a falsey (nil or false) value excludes the entity. 4 | 5 | Filters must be added to Systems by setting the `filter` field of the System 6 | 7 | ```lua 8 | function someSystem:filter(e) 9 | return e.name ~= nil and e.mass ~= nil and e.phrase ~= nil 10 | end 11 | ``` 12 | 13 | ## Systems 14 | 15 | A System is a wrapper around function callbacks for manipulating Entities. Systems are implemented as tables that contain at least one method: an update function that takes parameters like so: 16 | 17 | `function system:process(dt)` 18 | 19 | The entities currently available for any system to process are in the table `self.entities`. 20 | 21 | There are also a few other optional callbacks: 22 | 23 | * `function system:filter(entity)` - Returns true if this System should include this Entity, otherwise should return false. If this isn't specified, no Entities are included in the System. 24 | * `function system:onAdd(entity)` - Called when an Entity is added to the System. 25 | * `function system:onRemove(entity)` - Called when an Entity is removed from the System. 26 | * `function system:onAddToWorld(world)` - Called when the System is added to the World, before any entities are added to the system. 27 | * `function system:onRemoveFromWorld(world)` - Called when the System is 28 | removed from the world, after all Entities are removed from the System. 29 | * `function system:preUpdate(dt)` - Called on each system before `process` is called on any system 30 | * `function system:postUpdate(dt)` - Called on each system after `process` is called on each system 31 | 32 | The idea behind `preUpdate` and `postUpdate` is to allow for systems that modify the behavior of other systems. Say there is a DrawingSystem, which draws sprites to the screen, and a PostProcessingSystem, that adds some blur and bloom effects. In the `preUpdate` method of the PostProcessingSystem, the System could set the drawing target for the DrawingSystem to a special buffer instead the screen. In the `postUpdate` method, the PostProcessingSystem could then modify the buffer and render it to the screen. In this setup, the PostProcessingSystem would be added to the World after the DrawingSystem (a similar but less flexible behavior could be accomplished with a single custom update function in the DrawingSystem). 33 | 34 | All Systems also have a few important fields that are initialized when the system is added to the World. A few are important, and few should be less commonly used. 35 | 36 | * The `world` field points to the World that the System belongs to. Useful for adding and removing Entities from the world dynamically via the System. 37 | * The `active` flag is whether or not the System is updated automatically. Defaults to true. 38 | * The `entities` field is an ordered list of Entities in the System. This list can be used to quickly iterate through all Entities in a System. 39 | * The `interval` field is an optional field that makes Systems update at certain intervals using buffered time, regardless of World update frequency. For example, to make a System update once a second, set the System's interval 40 | to 1. 41 | * The `index` field is the System's index in the World. Lower indexed Systems are processed before higher indices. The `index` is a read only 42 | field; to set the `index`, use `world:setSystemIndex(system)`. 43 | * The `indices` field is a table of Entity keys to their indices in the `entities` list. Most Systems can ignore this. 44 | * The `modified` flag is an indicator if the System has been modified in the last update 45 | 46 | There is another option to (hopefully) increase performance in systems that have items added to or removed from them often, and have lots of entities in them. Setting the `nocache` field of the system might improve performance. It is still experimental. There are some restriction to systems without caching, however: 47 | 48 | * There is no `entities` table. 49 | * Callbacks such onAdd and onRemove will never be called 50 | * Noncached systems cannot be sorted (There is no entities list to sort). 51 | 52 | 53 | #### Creating Systems 54 | 55 | `function ecs.newSystem(table)` 56 | - Creates a new System or System class. Systems have a process method called each tick. 57 | 58 | * `function system:process(dt)` Process self.entities in your own loop 59 | * `function system:process(entity, dt)` when system.nocache is set, entities are processed individually 60 | 61 | `function ecs.sortedSystem(table)` 62 | - Creates a new Sorted System. Sorted Systems sort their Entities according to a user-defined method, `system:compare(e1, e2)`, which should return true if `e1` should come before `e2` and false otherwise. 63 | 64 | 65 | ### World 66 | 67 | A World is a container that manages Entities and Systems. Typically, a program uses one World at a time. 68 | 69 | For all World functions except `ecs.newWorld(...)`, object-oriented syntax is used. For example `world:addEntity(e)` 70 | 71 | `function ecs.newWorld(...)` 72 | - Creates a new World. Can optionally add default Systems. Returns the new World. 73 | 74 | `function World:addEntity(entity)` 75 | - Adds an Entity to the world. Also call this on Entities that have changed Components such that they match different Filters. Returns the Entity. 76 | 77 | `function World:addSystem(system)` 78 | - Adds a System to the world. Returns the System. 79 | 80 | `function World:removeEntity(entity)` 81 | - Removes an Entity from the World. Returns the Entity. 82 | 83 | `function World:removeSystem(system)` 84 | - Removes a System from the world. Returns the System. 85 | 86 | `function World:update(dt, filter)` 87 | - Updates the World by dt (delta time). Takes an optional parameter, `filter`, which is a Filter that selects Systems from the World, and updates only those Systems. If `filter` is not supplied, all Systems are updated. Put this function in your main loop. 88 | 89 | `function World:clearEntities()` 90 | - Removes all Entities from the World 91 | 92 | `function World:clearSystems()` 93 | - Removes all Systems from the World 94 | 95 | `function World:setSystemIndex(system, index)` 96 | - Sets the index of a System in the World, and returns the old index. Changes the order in which they Systems processed, because lower indexed Systems are processed first. Returns the old system.index. 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ecs 2 | 3 | `ecs` is a _luafied_ Entity Component System. Lua isn't like other languages so the design of an ECS should support the features of lua. While `ecs` has the main paradigms of standard Entity Component Systems, it differs quite a bit in the details. A standard ECS is often created for raw speed, cache locality and other performance reasons. With lua, we dont have structs so there is no point in matching the way standard Entity Component Systems are designed since we just don't have that kind of control over the memory layout. 4 | 5 | `ecs` is more flexible and would be better described as a way to centralize, organize, sort and filter the objects in your game. `ecs` lets you have lightweight entities (a plain old table is an entity) with components (any key/value pair in the table is a component) that can all be batch processed by a system. You can also have "heavy entities" with the more traditional `update` and `draw` methods. 6 | 7 | For example, you may have tens to hundreds of bullets if you were making a shooter so these make sense as lightweight entity tables all processed in a batch by a system. The player is a much more complex beast and there is only one of them so making player be a "heavy entity" with an `update` and `draw` method makes more sense. The flexibility to choose is yours. 8 | 9 | ## Overview 10 | `ecs` has four important types: Worlds, Filters, Systems, and Entities. 11 | 12 | ### Worlds 13 | Worlds are the outermost containers in `ecs` that contain both Systems and Entities. In typical use, only one World is used at a time. 14 | 15 | ### Entities 16 | Entities are simply Lua tables of data that gets processed by Systems. Entities should contain only data rather than code, as it is the System's job to do logic on data. Henceforth, a key-value pair in an Entity will be referred to as a Component. 17 | 18 | ### Systems 19 | Systems in `ecs` describe how to update Entities. Systems select certain Entities using a Filter, and then only update those select Entities. Much like Entities, Systems are just a plain old Lua table with two required methods: `process` and `filter`. There are several optional methods that can be implemented to get various state change information as well, such as `onAdd`, `onRemove`, `onAddToWorld` and `onRemoveFromWorld`. 20 | 21 | ### Filters 22 | Filters are used to select Entities. Filters are just a simple Lua function that takes an Entity as a parameter and returns true/false. 23 | 24 | ### API 25 | An API overview is available [here](API.md) 26 | 27 | ## Example 28 | 29 | ```lua 30 | local ecs = require 'ecs' 31 | 32 | -- create a system 33 | local talkingSystem = ecs.newSystem() 34 | 35 | -- this system will only process enttiies that have a name and mass 36 | function talkingSystem:filter(e) 37 | return e.name ~= nil and e.mass ~= nil 38 | end 39 | 40 | -- called once per frame, this is where we will process the entities. self.entities contains the 41 | -- filtered, sorted list of entities. 42 | function talkingSystem:process(e, dt) 43 | for _, e in ipairs(self.entities) do 44 | e.mass = e.mass + dt * 3 45 | print(("%s who weighs %d pounds"):format(e.name, e.mass) 46 | end 47 | end 48 | 49 | local joe = { 50 | name = "Joe", 51 | mass = 150, 52 | hairColor = "brown" 53 | } 54 | 55 | local world = ecs.newWorld(talkingSystem, joe) 56 | 57 | for i = 1, 20 do 58 | -- normally, this would be called in your update method with a delta time 59 | world:update(1) 60 | end 61 | ``` 62 | 63 | ## Credits 64 | ecs is a modified fork of [Tiny-ecs](https://github.com/bakpakin/tiny-ecs) 65 | -------------------------------------------------------------------------------- /ecs.lua: -------------------------------------------------------------------------------- 1 | local ecs = {} 2 | 3 | -- Local versions of standard lua functions 4 | local tinsert = table.insert 5 | local tremove = table.remove 6 | 7 | 8 | -- Systems 9 | 10 | -- Update function for all Systems 11 | local function processingSystemUpdate(system, dt) 12 | local process = system.process 13 | 14 | if process then 15 | if system.nocache then 16 | local entities = system.world.entities 17 | local filter = system.filter 18 | if filter then 19 | for i = 1, #entities do 20 | local entity = entities[i] 21 | if filter(system, entity) then 22 | process(system, entity, dt) 23 | end 24 | end 25 | end 26 | else 27 | process(system, dt) 28 | end 29 | end 30 | end 31 | 32 | -- Sorts Systems by a function system.sortDelegate(entity1, entity2) on modify. 33 | local function sortedSystemOnModify(system) 34 | local entities = system.entities 35 | local indices = system.indices 36 | local sortDelegate = system.sortDelegate 37 | if not sortDelegate then 38 | local compare = system.compare 39 | sortDelegate = function(e1, e2) 40 | return compare(system, e1, e2) 41 | end 42 | system.sortDelegate = sortDelegate 43 | end 44 | table.sort(entities, sortDelegate) 45 | for i = 1, #entities do 46 | indices[entities[i]] = i 47 | end 48 | end 49 | 50 | --- Creates a new System or System class. Systems have a process method called each tick. Systems have three extra callbacks: 51 | -- 52 | -- function system:process(dt) -- Process self.entities 53 | -- function system:process(entity, dt) -- when system.nocache is set, entities are processed individually 54 | -- 55 | -- Systems have their own `update` method, so don't implement a a custom `update` callback for Systems. 56 | function ecs.newSystem(table) 57 | table = table or {} 58 | table.update = processingSystemUpdate 59 | return table 60 | end 61 | 62 | --- Creates a new Sorted System. Sorted Systems sort their Entities according to a user-defined method, `system:compare(e1, e2)`, which should return true if `e1` should come before `e2` and false otherwise. 63 | function ecs.sortedSystem(table) 64 | table = table or {} 65 | table.update = processingSystemUpdate 66 | table.onModify = sortedSystemOnModify 67 | return table 68 | end 69 | 70 | 71 | --- World functions 72 | 73 | -- Adds and removes Systems that have been marked from the World. 74 | local function _manageSystems(world) 75 | local s2a, s2r = world.systemsToAdd, world.systemsToRemove 76 | 77 | -- Early exit 78 | if #s2a == 0 and #s2r == 0 then 79 | return 80 | end 81 | 82 | local worldEntityList = world.entities 83 | local systems = world.systems 84 | 85 | -- Remove Systems 86 | for i = 1, #s2r do 87 | local system = s2r[i] 88 | local index = system.index 89 | local onRemove = system.onRemove 90 | if onRemove and not system.nocache then 91 | local entityList = system.entities 92 | for j = 1, #entityList do 93 | onRemove(system, entityList[j]) 94 | end 95 | end 96 | tremove(systems, index) 97 | for j = index, #systems do 98 | systems[j].index = j 99 | end 100 | local onRemoveFromWorld = system.onRemoveFromWorld 101 | if onRemoveFromWorld then 102 | onRemoveFromWorld(system, world) 103 | end 104 | s2r[i] = nil 105 | 106 | -- Clean up System 107 | system.world = nil 108 | system.entities = nil 109 | system.indices = nil 110 | system.index = nil 111 | end 112 | 113 | -- Add Systems 114 | for i = 1, #s2a do 115 | local system = s2a[i] 116 | if systems[system.index or 0] ~= system then 117 | if not system.nocache then 118 | system.entities = {} 119 | system.indices = {} 120 | system.modified = true 121 | end 122 | 123 | if system.active == nil then 124 | system.active = true 125 | end 126 | 127 | system.world = world 128 | local index = #systems + 1 129 | system.index = index 130 | systems[index] = system 131 | local onAddToWorld = system.onAddToWorld 132 | if onAddToWorld then 133 | onAddToWorld(system, world) 134 | end 135 | 136 | -- Try to add Entities 137 | if not system.nocache then 138 | local entityList = system.entities 139 | local entityIndices = system.indices 140 | local onAdd = system.onAdd 141 | local filter = system.filter 142 | if filter then 143 | for j = 1, #worldEntityList do 144 | local entity = worldEntityList[j] 145 | if filter(system, entity) then 146 | local entityIndex = #entityList + 1 147 | entityList[entityIndex] = entity 148 | entityIndices[entity] = entityIndex 149 | if onAdd then 150 | onAdd(system, entity) 151 | end 152 | end 153 | end 154 | end 155 | end 156 | end 157 | s2a[i] = nil 158 | end 159 | 160 | local count = #world.systemsToAdd 161 | for i = 0, count do world.systemsToAdd[i] = nil end 162 | 163 | count = #world.systemsToRemove 164 | for i = 0, count do world.systemsToRemove[i] = nil end 165 | end 166 | 167 | -- Adds, removes, and changes Entities that have been marked. 168 | local function _manageEntities(world) 169 | local e2r = world.entitiesToRemove 170 | local e2c = world.entitiesToChange 171 | 172 | -- Early exit 173 | if #e2r == 0 and #e2c == 0 then 174 | return 175 | end 176 | 177 | local entities = world.entities 178 | local systems = world.systems 179 | 180 | -- Change Entities 181 | for i = 1, #e2c do 182 | local entity = e2c[i] 183 | -- Add if needed 184 | if not entities[entity] then 185 | local index = #entities + 1 186 | entities[entity] = index 187 | entities[index] = entity 188 | end 189 | for j = 1, #systems do 190 | local system = systems[j] 191 | if not system.nocache then 192 | local ses = system.entities 193 | local seis = system.indices 194 | local index = seis[entity] 195 | local filter = system.filter 196 | if filter and filter(system, entity) then 197 | if not index then 198 | system.modified = true 199 | index = #ses + 1 200 | ses[index] = entity 201 | seis[entity] = index 202 | local onAdd = system.onAdd 203 | if onAdd then 204 | onAdd(system, entity) 205 | end 206 | end 207 | elseif index then 208 | system.modified = true 209 | local tmpEntity = ses[#ses] 210 | ses[index] = tmpEntity 211 | seis[tmpEntity] = index 212 | seis[entity] = nil 213 | ses[#ses] = nil 214 | local onRemove = system.onRemove 215 | if onRemove then 216 | onRemove(system, entity) 217 | end 218 | end 219 | end 220 | end 221 | e2c[i] = nil 222 | end 223 | 224 | -- Remove Entities 225 | for i = 1, #e2r do 226 | local entity = e2r[i] 227 | e2r[i] = nil 228 | local listIndex = entities[entity] 229 | if listIndex then 230 | -- Remove Entity from world state 231 | local lastEntity = entities[#entities] 232 | entities[lastEntity] = listIndex 233 | entities[entity] = nil 234 | entities[listIndex] = lastEntity 235 | entities[#entities] = nil 236 | -- Remove from cached systems 237 | for j = 1, #systems do 238 | local system = systems[j] 239 | if not system.nocache then 240 | local ses = system.entities 241 | local seis = system.indices 242 | local index = seis[entity] 243 | if index then 244 | system.modified = true 245 | local tmpEntity = ses[#ses] 246 | ses[index] = tmpEntity 247 | seis[tmpEntity] = index 248 | seis[entity] = nil 249 | ses[#ses] = nil 250 | local onRemove = system.onRemove 251 | if onRemove then 252 | onRemove(system, entity) 253 | end 254 | end 255 | end 256 | end 257 | end 258 | end 259 | 260 | local count = #world.entitiesToChange 261 | for i = 0, count do world.entitiesToChange[i] = nil end 262 | 263 | count = #world.entitiesToRemove 264 | for i = 0, count do world.entitiesToRemove[i] = nil end 265 | end 266 | 267 | 268 | local World = {} 269 | World.__index = World 270 | 271 | --- Creates a new World. 272 | -- Can optionally add default Systems. Returns the new World 273 | function ecs.newWorld(...) 274 | local world = setmetatable({ 275 | -- List of Entities to remove 276 | entitiesToRemove = {}, 277 | 278 | -- List of Entities to change 279 | entitiesToChange = {}, 280 | 281 | -- List of Entities to add 282 | systemsToAdd = {}, 283 | 284 | -- List of Entities to remove 285 | systemsToRemove = {}, 286 | 287 | -- Set of Entities 288 | entities = {}, 289 | 290 | -- List of Systems 291 | systems = {} 292 | }, World) 293 | 294 | for i = 1, select('#', ...) do 295 | local sys = select(i, ...) 296 | if sys then world:addSystem(sys) end 297 | end 298 | 299 | _manageSystems(world) 300 | 301 | return world 302 | end 303 | 304 | --- Adds an Entity to the world. 305 | -- Also call this on Entities that have changed Components such that they match different Filters. Returns the Entity. 306 | function World:addEntity(entity) 307 | local e2c = self.entitiesToChange 308 | e2c[#e2c + 1] = entity 309 | return entity 310 | end 311 | 312 | --- Adds a System to the world. Returns the System. 313 | function World:addSystem(system) 314 | assert(system.world == nil, "System already belongs to a World.") 315 | local s2a = self.systemsToAdd 316 | s2a[#s2a + 1] = system 317 | system.world = self 318 | return system 319 | end 320 | 321 | --- Removes an Entity from the World. Returns the Entity. 322 | function World:removeEntity(entity) 323 | local e2r = self.entitiesToRemove 324 | e2r[#e2r + 1] = entity 325 | return entity 326 | end 327 | 328 | --- Removes a System from the world. Returns the System. 329 | function World:removeSystem(system) 330 | local s2r = self.systemsToRemove 331 | s2r[#s2r + 1] = system 332 | return system 333 | end 334 | 335 | --- Updates the World by dt (delta time). Takes an optional parameter, `filter`, which is a Filter that selects Systems from the World, and updates only those Systems. If `filter` is not supplied, all Systems are updated. Put this function in your main loop. 336 | function World:update(dt, filter) 337 | _manageSystems(self) 338 | _manageEntities(self) 339 | 340 | local systems = self.systems 341 | 342 | -- Iterate through Systems IN ORDER 343 | for i = 1, #systems do 344 | local system = systems[i] 345 | if system.active then 346 | -- Call the modify callback on Systems that have been modified so they can be sorted 347 | local onModify = system.onModify 348 | if onModify and system.modified then 349 | onModify(system, dt) 350 | end 351 | 352 | local preUpdate = system.preUpdate 353 | if preUpdate and 354 | ((not filter) or filter(world, system)) then 355 | preUpdate(system, dt) 356 | end 357 | end 358 | end 359 | 360 | -- Iterate through Systems IN ORDER 361 | for i = 1, #systems do 362 | local system = systems[i] 363 | if system.active and ((not filter) or filter(world, system)) then 364 | -- Update Systems that have an update method (most Systems) 365 | local update = system.update 366 | if update then 367 | local interval = system.interval 368 | if interval then 369 | local bufferedTime = (system.bufferedTime or 0) + dt 370 | while bufferedTime >= interval do 371 | bufferedTime = bufferedTime - interval 372 | update(system, interval) 373 | end 374 | system.bufferedTime = bufferedTime 375 | else 376 | update(system, dt) 377 | end 378 | end 379 | 380 | system.modified = false 381 | end 382 | end 383 | 384 | -- Iterate through Systems IN ORDER AGAIN 385 | for i = 1, #systems do 386 | local system = systems[i] 387 | local postUpdate = system.postUpdate 388 | if postUpdate and system.active and 389 | ((not filter) or filter(world, system)) then 390 | postUpdate(system, dt) 391 | end 392 | end 393 | 394 | end 395 | 396 | --- Removes all Entities from the World 397 | function World:clearEntities() 398 | local el = self.entities 399 | for i = 1, #el do 400 | self:removeEntity(el[i]) 401 | end 402 | end 403 | 404 | --- Removes all Systems from the World 405 | function World:clearSystems() 406 | local systems = self.systems 407 | for i = #systems, 1, -1 do 408 | self:removeSystem(systems[i]) 409 | end 410 | end 411 | 412 | --- Sets the index of a System in the World, and returns the old index. Changes the order in which they Systems processed, because lower indexed Systems are processed first. Returns the old system.index. 413 | function World:setSystemIndex(system, index) 414 | local oldIndex = system.index 415 | local systems = self.systems 416 | 417 | if index < 0 then 418 | index = #self.systems + 1 + index 419 | end 420 | 421 | tremove(systems, oldIndex) 422 | tinsert(systems, index, system) 423 | 424 | for i = oldIndex, index, index >= oldIndex and 1 or -1 do 425 | systems[i].index = i 426 | end 427 | 428 | return oldIndex 429 | end 430 | 431 | return ecs 432 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | local ecs = require 'ecs' 2 | 3 | 4 | local talkingSystem = ecs.newSystem() 5 | 6 | function talkingSystem:filter(e) 7 | return e.name ~= nil and e.mass ~= nil 8 | end 9 | 10 | function talkingSystem:preUpdate(dt) 11 | print('talkingSystem', 'preUpdate') 12 | end 13 | 14 | function talkingSystem:process(dt) 15 | print('talkingSystem', 'process', #self.entities, 'modified', self.modified) 16 | for _, e in ipairs(self.entities) do 17 | e.mass = e.mass + dt * 3 18 | end 19 | end 20 | 21 | function talkingSystem:postUpdate(dt) 22 | print('talkingSystem', 'postUpdate') 23 | end 24 | 25 | 26 | local noCacheSystem = ecs.newSystem() 27 | noCacheSystem.nocache = true 28 | 29 | function noCacheSystem:filter(e) 30 | return e.name ~= nil and e.mass ~= nil 31 | end 32 | 33 | function noCacheSystem:onModify(dt) 34 | print('noCacheSystem onModify', dt) 35 | end 36 | 37 | function noCacheSystem:process(e, dt) 38 | print('noCacheSystem', 'process', e, dt) 39 | end 40 | 41 | 42 | 43 | function makeJoe() 44 | return { 45 | name = "Joe", 46 | mass = 150, 47 | hairColor = "brown" 48 | } 49 | end 50 | 51 | 52 | local world = ecs.newWorld(talkingSystem, noCacheSystem) 53 | 54 | world:addEntity(makeJoe()) 55 | world:addEntity(makeJoe()) 56 | local e = world:addEntity(makeJoe()) 57 | 58 | world:update(0.016) 59 | 60 | world:setSystemIndex(noCacheSystem, 1) 61 | 62 | world:addEntity(makeJoe()) 63 | world:addEntity(makeJoe()) 64 | world:removeEntity(e) 65 | 66 | print('---------------') 67 | world:update(0.016) 68 | 69 | print('---------------') 70 | world:update(0.016) 71 | 72 | --------------------------------------------------------------------------------