From cda04ce5a99af606133c8c18013d264d92985b6a Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 3 May 2024 12:14:45 -0400 Subject: [PATCH 01/46] Update newMatter.lua (#19) * Update newMatter.lua * Update newMatter.lua * Update newMatter.lua --- newMatter.lua | 98 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 88 insertions(+), 10 deletions(-) diff --git a/newMatter.lua b/newMatter.lua index 150bbb8..4a50791 100644 --- a/newMatter.lua +++ b/newMatter.lua @@ -434,11 +434,6 @@ local function transitionArchetype( from: Archetype, sourceRow: i24 ) - -- local columns = sourceArchetype.columns - -- local sourceEntities = sourceArchetype.entities - -- local destinationEntities = destinationArchetype.entities - -- local destinationColumns = destinationArchetype.columns - local columns = from.columns local sourceEntities = from.entities local destinationEntities = to.entities @@ -550,6 +545,7 @@ function World.new() local self = setmetatable({ entityIndex = {}, componentIndex = {}, + componentIdToComponent = {}, archetypes = {}, archetypeIndex = {}, nextId = 0, @@ -623,7 +619,12 @@ local function archetypeTraverseAdd(world: World, componentId: i53, archetype: A end local function componentAdd(world: World, entityId: i53, componentInstance) - local componentId = #getmetatable(componentInstance) + local component = getmetatable(componentInstance) + local componentId = #component + + -- TODO: + -- This never gets cleaned up + world.componentIdToComponent[componentId] = component local record = world:ensureRecord(entityId) local sourceArchetype = record.archetype @@ -792,7 +793,29 @@ function World.entity(world: World) end function World:__iter() - return error("NOT IMPLEMENTED YET") + local previous = nil + return function() + local entityId, data = next(self.entityIndex, previous) + previous = entityId + + if entityId == nil then + return nil + end + + local archetype = data.archetype + if not archetype then + return entityId, {} + end + + local columns = archetype.columns + local components = {} + for i, map in columns do + local componentId = archetype.types[i] + components[self.componentIdToComponent[componentId]] = map[data.row] + end + + return entityId, components + end end function World._trackChanged(world: World, metatable, id, old, new) @@ -921,7 +944,7 @@ local emptyQueryResult = setmetatable({ }) local function queryResult(compatibleArchetypes, components: { number }, queryLength, ...): any - local a: any, b: any, c: any, d: any, e: any = ... + local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ... local lastArchetype, archetype = next(compatibleArchetypes) if not lastArchetype then return emptyQueryResult @@ -967,6 +990,31 @@ local function queryResult(compatibleArchetypes, components: { number }, queryLe columns[archetypeRecords[c]][row], columns[archetypeRecords[d]][row], columns[archetypeRecords[e]][row] + elseif queryLength == 6 then + return entityId, + columns[archetypeRecords[a]][row], + columns[archetypeRecords[b]][row], + columns[archetypeRecords[c]][row], + columns[archetypeRecords[d]][row], + columns[archetypeRecords[e]][row], + columns[archetypeRecords[f]][row] + elseif queryLength == 7 then + return columns[archetypeRecords[a]][row], + columns[archetypeRecords[b]][row], + columns[archetypeRecords[c]][row], + columns[archetypeRecords[d]][row], + columns[archetypeRecords[e]][row], + columns[archetypeRecords[f]][row], + columns[archetypeRecords[g]][row] + elseif queryLength == 8 then + return columns[archetypeRecords[a]][row], + columns[archetypeRecords[b]][row], + columns[archetypeRecords[c]][row], + columns[archetypeRecords[d]][row], + columns[archetypeRecords[e]][row], + columns[archetypeRecords[f]][row], + columns[archetypeRecords[g]][row], + columns[archetypeRecords[h]][row] end for i, componentId in components do @@ -1229,7 +1277,7 @@ function World.query(world: World, ...: Component): any local components = { ... } local archetypes = world.archetypes local queryLength = select("#", ...) - local a: any, b: any, c: any, d: any, e: any = ... + local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ... if queryLength == 0 then return emptyQueryResult @@ -1319,6 +1367,36 @@ function World.query(world: World, ...: Component): any e = #e components = { a, b, c, d, e } + elseif queryLength == 6 then + a = #a + b = #b + c = #c + d = #d + e = #e + f = #f + + components = { a, b, c, d, e, f } + elseif queryLength == 7 then + a = #a + b = #b + c = #c + d = #d + e = #e + f = #f + g = #g + + components = { a, b, c, d, e, f, g } + elseif queryLength == 8 then + a = #a + b = #b + c = #c + d = #d + e = #e + f = #f + g = #g + h = #h + + components = { a, b, c, d, e, f, g, h } else for i, component in components do components[i] = (#component) :: any @@ -1354,7 +1432,7 @@ function World.query(world: World, ...: Component): any end end - return queryResult(compatibleArchetypes, components :: any, queryLength, a, b, c, d, e) + return queryResult(compatibleArchetypes, components :: any, queryLength, a, b, c, d, e, f, g, h) end local function cleanupQueryChanged(hookState) From 283243350f9d8b906bdde7346763d99c933a39f6 Mon Sep 17 00:00:00 2001 From: howmanysmall <26746527+howmanysmall@users.noreply.github.com> Date: Sat, 4 May 2024 17:52:01 -0600 Subject: [PATCH 02/46] Fix style and add some micro optimizations (#27) --- .gitignore | 3 + aftman.toml | 2 +- benches/query.lua | 468 ++++++++++++++++----------------- benches/visual/spawn.bench.lua | 44 ++-- lib/init.lua | 433 +++++++++++++++--------------- selene.toml | 4 + stylua.toml | 5 + testez-companion.toml | 3 + wally.toml | 3 - 9 files changed, 496 insertions(+), 469 deletions(-) create mode 100644 selene.toml create mode 100644 stylua.toml create mode 100644 testez-companion.toml diff --git a/.gitignore b/.gitignore index a43fa5f..9143a00 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ WallyPatches roblox.toml sourcemap.json drafts/*.lua + +*.code-workspace +roblox.yml diff --git a/aftman.toml b/aftman.toml index 73e1123..ace6bc1 100644 --- a/aftman.toml +++ b/aftman.toml @@ -3,4 +3,4 @@ wally = "upliftgames/wally@0.3.1" rojo = "rojo-rbx/rojo@7.4.1" stylua = "johnnymorganz/stylua@0.19.1" selene = "kampfkarren/selene@0.26.1" -wally-patch-package="Barocena/wally-patch-package@1.2.1" \ No newline at end of file +wally-patch-package = "Barocena/wally-patch-package@1.2.1" diff --git a/benches/query.lua b/benches/query.lua index de60944..2c4cd55 100644 --- a/benches/query.lua +++ b/benches/query.lua @@ -1,11 +1,11 @@ --!optimize 2 --!native -local testkit = require('../testkit') +local testkit = require("../testkit") local BENCH, START = testkit.benchmark() local function TITLE(title: string) - print() - print(testkit.color.white(title)) + print() + print(testkit.color.white(title)) end local jecs = require("../mirror/init") @@ -15,285 +15,285 @@ local oldMatter = require("../oldMatter") local newMatter = require("../newMatter") type i53 = number -do TITLE (testkit.color.white_underline("Jecs query")) - local ecs = jecs.World.new() - do TITLE "one component in common" +do + TITLE(testkit.color.white_underline("Jecs query")) + local ecs = jecs.World.new() + do + TITLE("one component in common") - local function view_bench( - world: jecs.World, - A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53 - ) + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("1 component", function() + for _ in world:query(A) do + end + end) - BENCH("1 component", function() - for _ in world:query(A) do end - end) + BENCH("2 component", function() + for _ in world:query(A, B) do + end + end) - BENCH("2 component", function() - for _ in world:query(A, B) do end - end) + BENCH("4 component", function() + for _ in world:query(A, B, C, D) do + end + end) - BENCH("4 component", function() - for _ in world:query(A, B, C, D) do - end - end) + BENCH("8 component", function() + for _ in world:query(A, B, C, D, E, F, G, H) do + end + end) + end - BENCH("8 component", function() - for _ in world:query(A, B, C, D, E, F, G, H) do end - end) - end + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() - local D1 = ecs:component() - local D2 = ecs:component() - local D3 = ecs:component() - local D4 = ecs:component() - local D5 = ecs:component() - local D6 = ecs:component() - local D7 = ecs:component() - local D8 = ecs:component() + local function flip() + return math.random() >= 0.15 + end - local function flip() - return math.random() >= 0.15 - end - - local added = 0 + local added = 0 local archetypes = {} - for i = 1, 2^16-2 do - local entity = ecs:entity() + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() - local combination = "" + local combination = "" - if flip() then - combination ..= "B" - ecs:set(entity, D2, {value = true}) - end - if flip() then - combination ..= "C" - ecs:set(entity, D3, { value = true }) - end - if flip() then - combination ..= "D" - ecs:set(entity, D4, { value = true}) - end - if flip() then - combination ..= "E" - ecs:set(entity, D5, { value = true}) - end - if flip() then - combination ..= "F" - ecs:set(entity, D6, {value = true}) - end - if flip() then - combination ..= "G" - ecs:set(entity, D7, { value = true}) - end - if flip() then - combination ..= "H" - ecs:set(entity, D8, {value = true}) + if flip() then + combination ..= "B" + ecs:set(entity, D2, {value = true}) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, {value = true}) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, {value = true}) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, {value = true}) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, {value = true}) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, {value = true}) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, {value = true}) + end - end - - if #combination == 7 then - added += 1 - ecs:set(entity, D1, { value = true}) - end + if #combination == 7 then + added += 1 + ecs:set(entity, D1, {value = true}) + end archetypes[combination] = true - end + end local a = 0 - for _ in archetypes do a+= 1 end + for _ in archetypes do + a += 1 + end - view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) - end + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end end -do TITLE(testkit.color.white_underline("OldMatter query")) +do + TITLE(testkit.color.white_underline("OldMatter query")) - local ecs = oldMatter.World.new() - local component = oldMatter.component + local ecs = oldMatter.World.new() + local component = oldMatter.component - do TITLE "one component in common" - local function view_bench( - world: jecs.World, - A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53 - ) + do + TITLE("one component in common") + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("1 component", function() + for _ in world:query(A) do + end + end) - BENCH("1 component", function() - for _ in world:query(A) do end - end) + BENCH("2 component", function() + for _ in world:query(A, B) do + end + end) - BENCH("2 component", function() - for _ in world:query(A, B) do end - end) + BENCH("4 component", function() + for _ in world:query(A, B, C, D) do + end + end) - BENCH("4 component", function() - for _ in world:query(A, B, C, D) do - end - end) + BENCH("8 component", function() + for _ in world:query(A, B, C, D, E, F, G, H) do + end + end) + end - BENCH("8 component", function() - for _ in world:query(A, B, C, D, E, F, G, H) do end - end) - end + local D1 = component() + local D2 = component() + local D3 = component() + local D4 = component() + local D5 = component() + local D6 = component() + local D7 = component() + local D8 = component() - local D1 = component() - local D2 = component() - local D3 = component() - local D4 = component() - local D5 = component() - local D6 = component() - local D7 = component() - local D8 = component() + local function flip() + return math.random() >= 0.15 + end - local function flip() - return math.random() >= 0.15 - end - - local added = 0 + local added = 0 local archetypes = {} - for i = 1, 2^16-2 do - local entity = ecs:spawn() + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:spawn() - local combination = "" + local combination = "" - if flip() then - combination ..= "B" - ecs:insert(entity, D2({value = true})) - end - if flip() then - combination ..= "C" - ecs:insert(entity, D3({value = true})) - end - if flip() then - combination ..= "D" - ecs:insert(entity, D4({value = true})) - end - if flip() then - combination ..= "E" - ecs:insert(entity, D5({value = true})) - end - if flip() then - combination ..= "F" - ecs:insert(entity, D6({value = true})) + if flip() then + combination ..= "B" + ecs:insert(entity, D2({value = true})) + end + if flip() then + combination ..= "C" + ecs:insert(entity, D3({value = true})) + end + if flip() then + combination ..= "D" + ecs:insert(entity, D4({value = true})) + end + if flip() then + combination ..= "E" + ecs:insert(entity, D5({value = true})) + end + if flip() then + combination ..= "F" + ecs:insert(entity, D6({value = true})) + end + if flip() then + combination ..= "G" + ecs:insert(entity, D7({value = true})) + end + if flip() then + combination ..= "H" + ecs:insert(entity, D8({value = true})) + end - end - if flip() then - combination ..= "G" - ecs:insert(entity, D7({value = true})) - - end - if flip() then - combination ..= "H" - ecs:insert(entity, D8({value = true})) - end - - if #combination == 7 then - added += 1 - ecs:insert(entity, D1({value = true})) - - end + if #combination == 7 then + added += 1 + ecs:insert(entity, D1({value = true})) + end archetypes[combination] = true - end + end local a = 0 - for _ in archetypes do a+= 1 end - - view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) - end + for _ in archetypes do + a += 1 + end + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end end -do TITLE(testkit.color.white_underline("NewMatter query")) +do + TITLE(testkit.color.white_underline("NewMatter query")) - local ecs = newMatter.World.new() - local component = newMatter.component + local ecs = newMatter.World.new() + local component = newMatter.component - do TITLE "one component in common" - local function view_bench( - world: jecs.World, - A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53 - ) + do + TITLE("one component in common") + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("1 component", function() + for _ in world:query(A) do + end + end) - BENCH("1 component", function() - for _ in world:query(A) do end - end) + BENCH("2 component", function() + for _ in world:query(A, B) do + end + end) - BENCH("2 component", function() - for _ in world:query(A, B) do end - end) + BENCH("4 component", function() + for _ in world:query(A, B, C, D) do + end + end) - BENCH("4 component", function() - for _ in world:query(A, B, C, D) do - end - end) + BENCH("8 component", function() + for _ in world:query(A, B, C, D, E, F, G, H) do + end + end) + end - BENCH("8 component", function() - for _ in world:query(A, B, C, D, E, F, G, H) do end - end) - end + local D1 = component() + local D2 = component() + local D3 = component() + local D4 = component() + local D5 = component() + local D6 = component() + local D7 = component() + local D8 = component() - local D1 = component() - local D2 = component() - local D3 = component() - local D4 = component() - local D5 = component() - local D6 = component() - local D7 = component() - local D8 = component() + local function flip() + return math.random() >= 0.15 + end - local function flip() - return math.random() >= 0.15 - end - - local added = 0 + local added = 0 local archetypes = {} - for i = 1, 2^16-2 do - local entity = ecs:spawn() + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:spawn() - local combination = "" + local combination = "" - if flip() then - combination ..= "B" - ecs:insert(entity, D2({value = true})) - end - if flip() then - combination ..= "C" - ecs:insert(entity, D3({value = true})) - end - if flip() then - combination ..= "D" - ecs:insert(entity, D4({value = true})) - end - if flip() then - combination ..= "E" - ecs:insert(entity, D5({value = true})) - end - if flip() then - combination ..= "F" - ecs:insert(entity, D6({value = true})) + if flip() then + combination ..= "B" + ecs:insert(entity, D2({value = true})) + end + if flip() then + combination ..= "C" + ecs:insert(entity, D3({value = true})) + end + if flip() then + combination ..= "D" + ecs:insert(entity, D4({value = true})) + end + if flip() then + combination ..= "E" + ecs:insert(entity, D5({value = true})) + end + if flip() then + combination ..= "F" + ecs:insert(entity, D6({value = true})) + end + if flip() then + combination ..= "G" + ecs:insert(entity, D7({value = true})) + end + if flip() then + combination ..= "H" + ecs:insert(entity, D8({value = true})) + end - end - if flip() then - combination ..= "G" - ecs:insert(entity, D7({value = true})) - - end - if flip() then - combination ..= "H" - ecs:insert(entity, D8({value = true})) - end - - if #combination == 7 then - added += 1 - ecs:insert(entity, D1({value = true})) - - end + if #combination == 7 then + added += 1 + ecs:insert(entity, D1({value = true})) + end archetypes[combination] = true - end + end local a = 0 - for _ in archetypes do a+= 1 end + for _ in archetypes do + a += 1 + end - view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) - end - -end \ No newline at end of file + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end diff --git a/benches/visual/spawn.bench.lua b/benches/visual/spawn.bench.lua index 962064e..c5e6aef 100644 --- a/benches/visual/spawn.bench.lua +++ b/benches/visual/spawn.bench.lua @@ -2,41 +2,37 @@ --!native local ReplicatedStorage = game:GetService("ReplicatedStorage") -local rgb = require(ReplicatedStorage.rgb) local Matter = require(ReplicatedStorage.DevPackages.Matter) -local jecs = require(ReplicatedStorage.Lib) local ecr = require(ReplicatedStorage.DevPackages.ecr) +local jecs = require(ReplicatedStorage.Lib) +local rgb = require(ReplicatedStorage.rgb) local newWorld = Matter.World.new() local ecs = jecs.World.new() - return { ParameterGenerator = function() - local registry2 = ecr.registry() + local registry2 = ecr.registry() return registry2 - end, + end; Functions = { - Matter = function() - for i = 1, 1000 do - newWorld:spawn() - end - end, + Matter = function() + for i = 1, 1000 do + newWorld:spawn() + end + end; + ECR = function(_, registry2) + for i = 1, 1000 do + registry2.create() + end + end; - ECR = function(_, registry2) - for i = 1, 1000 do - registry2.create() - end - end, - - - Jecs = function() - for i = 1, 1000 do - ecs:entity() - end - end - - }, + Jecs = function() + for i = 1, 1000 do + ecs:entity() + end + end; + }; } diff --git a/lib/init.lua b/lib/init.lua index 35a9b9c..b3e31bc 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -6,10 +6,10 @@ type i53 = number type i24 = number -type Ty = { i53 } +type Ty = {i53} type ArchetypeId = number -type Column = { any } +type Column = {any} type Archetype = { id: number, @@ -20,9 +20,9 @@ type Archetype = { }, }, types: Ty, - type: string | number, - entities: { number }, - columns: { Column }, + type: string | number, + entities: {number}, + columns: {Column}, records: {}, } @@ -31,13 +31,13 @@ type Record = { row: number, } -type EntityIndex = { [i24]: Record } -type ComponentIndex = { [i24]: ArchetypeMap} +type EntityIndex = {[i24]: Record} +type ComponentIndex = {[i24]: ArchetypeMap} type ArchetypeRecord = number -type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number } -type Archetypes = { [ArchetypeId]: Archetype } - +type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number} +type Archetypes = {[ArchetypeId]: Archetype} + type ArchetypeDiff = { added: Ty, removed: Ty, @@ -64,17 +64,17 @@ local function transitionArchetype( local types = from.types for i, column in columns do - -- Retrieves the new column index from the source archetype's record from each component + -- Retrieves the new column index from the source archetype's record from each component -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. local targetColumn = destinationColumns[tr[types[i]]] -- Sometimes target column may not exist, e.g. when you remove a component. - if targetColumn then + if targetColumn then targetColumn[destinationRow] = column[sourceRow] end -- If the entity is the last row in the archetype then swapping it would be meaningless. local last = #column - if sourceRow ~= last then + if sourceRow ~= last then -- Swap rempves columns to ensure there are no holes in the archetype. column[sourceRow] = column[last] end @@ -82,24 +82,27 @@ local function transitionArchetype( end -- Move the entity from the source to the destination archetype. - destinationEntities[destinationRow] = sourceEntities[sourceRow] - entityIndex[sourceEntities[sourceRow]].row = destinationRow + local atSourceRow = sourceEntities[sourceRow] + destinationEntities[destinationRow] = atSourceRow + entityIndex[atSourceRow].row = destinationRow -- Because we have swapped columns we now have to update the records -- corresponding to the entities' rows that were swapped. local movedAway = #sourceEntities - if sourceRow ~= movedAway then - sourceEntities[sourceRow] = sourceEntities[movedAway] - entityIndex[sourceEntities[movedAway]].row = sourceRow + if sourceRow ~= movedAway then + local atMovedAway = sourceEntities[movedAway] + sourceEntities[sourceRow] = atMovedAway + entityIndex[atMovedAway].row = sourceRow end - + sourceEntities[movedAway] = nil end -local function archetypeAppend(entity: i53, archetype: Archetype): i24 +local function archetypeAppend(entity: number, archetype: Archetype): number local entities = archetype.entities - table.insert(entities, entity) - return #entities + local length = #entities + 1 + entities[length] = entity + return length end local function newEntity(entityId: i53, record: Record, archetype: Archetype) @@ -122,47 +125,49 @@ local function hash(arr): string | number return table.concat(arr, "_") end -local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?) - local destinationCount = #to.types +local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?) local destinationIds = to.types + local records = to.records + local id = to.id - for i = 1, destinationCount do - local destinationId = destinationIds[i] + for i, destinationId in destinationIds do + local archetypesMap = componentIndex[destinationId] - if not componentIndex[destinationId] then - componentIndex[destinationId] = { size = 0, sparse = {} } + if not archetypesMap then + archetypesMap = {size = 0, sparse = {}} + componentIndex[destinationId] = archetypesMap end - local archetypesMap = componentIndex[destinationId] - archetypesMap.sparse[to.id] = i - to.records[destinationId] = i + archetypesMap.sparse[id] = i + records[destinationId] = i end end -local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype +local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype local ty = hash(types) - world.nextArchetypeId = (world.nextArchetypeId::number)+ 1 - local id = world.nextArchetypeId + local id = world.nextArchetypeId + 1 + world.nextArchetypeId = id - local columns = {} :: { any } + local length = #types + local columns = table.create(length) :: {any} - for _ in types do - table.insert(columns, {}) + for index in types do + columns[index] = {} end local archetype = { - id = id, - types = types, - type = ty, - columns = columns, - entities = {}, - edges = {}, - records = {}, + columns = columns; + edges = {}; + entities = {}; + id = id; + records = {}; + type = ty; + types = types; } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype - if #types > 0 then + if length > 0 then createArchetypeRecords(world.componentIndex, archetype, prev) end @@ -171,42 +176,42 @@ end local World = {} World.__index = World -function World.new() +function World.new() local self = setmetatable({ - entityIndex = {}, - componentIndex = {}, - archetypes = {}, - archetypeIndex = {}, - ROOT_ARCHETYPE = (nil :: any) :: Archetype, - nextEntityId = 0, - nextComponentId = 0, - nextArchetypeId = 0, + archetypeIndex = {}; + archetypes = {}; + componentIndex = {}; + entityIndex = {}; hooks = { - [ON_ADD] = {} - } + [ON_ADD] = {}; + }; + nextArchetypeId = 0; + nextComponentId = 0; + nextEntityId = 0; + ROOT_ARCHETYPE = (nil :: any) :: Archetype; }, World) - return self + return self end -local function emit(world, eventDescription) +local function emit(world, eventDescription) local event = eventDescription.event table.insert(world.hooks[event], { - ids = eventDescription.ids, - archetype = eventDescription.archetype, - otherArchetype = eventDescription.otherArchetype, - offset = eventDescription.offset + archetype = eventDescription.archetype; + ids = eventDescription.ids; + offset = eventDescription.offset; + otherArchetype = eventDescription.otherArchetype; }) end -local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) - if #added > 0 then +local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) + if #added > 0 then emit(world, { - event = ON_ADD, - ids = added, - archetype = archetype, - otherArchetype = otherArchetype, - offset = row, + archetype = archetype; + event = ON_ADD; + ids = added; + offset = row; + otherArchetype = otherArchetype; }) end end @@ -217,7 +222,7 @@ local function ensureArchetype(world: World, types, prev) if #types < 1 then return world.ROOT_ARCHETYPE end - + local ty = hash(types) local archetype = world.archetypeIndex[ty] if archetype then @@ -227,10 +232,8 @@ local function ensureArchetype(world: World, types, prev) return archetypeOf(world, types, prev) end -local function findInsert(types: { i53 }, toAdd: i53) - local count = #types - for i = 1, count do - local id = types[i] +local function findInsert(types: {i53}, toAdd: i53) + for i, id in types do if id == toAdd then return -1 end @@ -238,13 +241,13 @@ local function findInsert(types: { i53 }, toAdd: i53) return i end end - return count + 1 + return #types + 1 end local function findArchetypeWith(world: World, node: Archetype, componentId: i53) local types = node.types -- Component IDs are added incrementally, so inserting and sorting - -- them each time would be expensive. Instead this insertion sort can find the insertion + -- them each time would be expensive. Instead this insertion sort can find the insertion -- point in the types array. local at = findInsert(types, componentId) if at == -1 then @@ -259,48 +262,57 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53 end local function ensureEdge(archetype: Archetype, componentId: i53) - if not archetype.edges[componentId] then - archetype.edges[componentId] = {} :: any + local edges = archetype.edges + local edge = edges[componentId] + if not edge then + edge = {} :: any + edges[componentId] = edge end - return archetype.edges[componentId] + return edge end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype - if not from then + if not from then -- If there was no source archetype then it should return the ROOT_ARCHETYPE - if not world.ROOT_ARCHETYPE then - local ROOT_ARCHETYPE = archetypeOf(world, {}, nil) - world.ROOT_ARCHETYPE = ROOT_ARCHETYPE - end - from = world.ROOT_ARCHETYPE + local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE + if not ROOT_ARCHETYPE then + ROOT_ARCHETYPE = archetypeOf(world, {}, nil) + world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never + end + from = ROOT_ARCHETYPE end + local edge = ensureEdge(from, componentId) - - if not edge.add then - -- Save an edge using the component ID to the archetype to allow + local add = edge.add + if not add then + -- Save an edge using the component ID to the archetype to allow -- faster traversals to adjacent archetypes. - edge.add = findArchetypeWith(world, from, componentId) + add = findArchetypeWith(world, from, componentId) + edge.add = add :: never end - return edge.add + return add end local function ensureRecord(entityIndex, entityId: i53): Record - local id = entityId - if not entityIndex[id] then - entityIndex[id] = {} + local record = entityIndex[entityId] + + if not record then + record = {} + entityIndex[entityId] = record end - return entityIndex[id] :: Record + + return record :: Record end -function World.set(world: World, entityId: i53, componentId: i53, data: unknown) +function World.set(world: World, entityId: i53, componentId: i53, data: unknown) local record = ensureRecord(world.entityIndex, entityId) local from = record.archetype local to = archetypeTraverseAdd(world, componentId, from) - if from == to then - -- If the archetypes are the same it can avoid moving the entity - -- and just set the data directly. + if from == to then + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. local archetypeRecord = to.records[componentId] from.columns[archetypeRecord][record.row] = data -- Should fire an OnSet event here. @@ -308,13 +320,13 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) end if from then - -- If there was a previous archetype, then the entity needs to move the archetype + -- If there was a previous archetype, then the entity needs to move the archetype moveEntity(world.entityIndex, entityId, record, to) else if #to.types > 0 then -- When there is no previous archetype it should create the archetype newEntity(entityId, record, to) - onNotifyAdd(world, to, from, record.row, { componentId }) + onNotifyAdd(world, to, from, record.row, {componentId}) end end @@ -326,28 +338,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype local edge = ensureEdge(from, componentId) - - if not edge.remove then - local to = table.clone(from.types) + local remove = edge.remove + if not remove then + local to = table.clone(from.types) table.remove(to, table.find(to, componentId)) - edge.remove = ensureArchetype(world, to, from) + remove = ensureArchetype(world, to, from) + edge.remove = remove :: never end - return edge.remove + return remove end -function World.remove(world: World, entityId: i53, componentId: i53) - local record = ensureRecord(world.entityIndex, entityId) +function World.remove(world: World, entityId: i53, componentId: i53) + local entityIndex = world.entityIndex + local record = ensureRecord(entityIndex, entityId) local sourceArchetype = record.archetype local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) - if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) + if sourceArchetype and not (sourceArchetype == destinationArchetype) then + moveEntity(entityIndex, entityId, record, destinationArchetype) end end -- Keeping the function as small as possible to enable inlining -local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24) +local function get(record: Record, componentId: i24) local archetype = record.archetype local archetypeRecord = archetype.records[componentId] @@ -360,35 +374,35 @@ end function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) local id = entityId - local componentIndex = world.componentIndex local record = world.entityIndex[id] if not record then return nil end - local va = get(componentIndex, record, a) + local va = get(record, a) if b == nil then return va elseif c == nil then - return va, get(componentIndex, record, b) + return va, get(record, b) elseif d == nil then - return va, get(componentIndex, record, b), get(componentIndex, record, c) + return va, get(record, b), get(record, c) elseif e == nil then - return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d) + return va, get(record, b), get(record, c), get(record, d) else error("args exceeded") end end -local function noop(self: Query, ...: i53): () -> (number, ...any) - return function() - end :: any +-- the less creation the better +local function actualNoOperation() end +local function noop(_self: Query, ...: i53): () -> (number, ...any) + return actualNoOperation :: any end local EmptyQuery = { - __iter = noop, - without = noop + __iter = noop; + without = noop; } EmptyQuery.__index = EmptyQuery setmetatable(EmptyQuery, EmptyQuery) @@ -396,25 +410,28 @@ setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) function World.query(world: World, ...: i53): Query - local compatibleArchetypes = {} - local components = { ... } - local archetypes = world.archetypes - local queryLength = #components - - if queryLength == 0 then + -- breaking? + if (...) == nil then error("Missing components") end + local compatibleArchetypes = {} + local length = 0 + + local components = {...} + local archetypes = world.archetypes + local queryLength = #components + local firstArchetypeMap local componentIndex = world.componentIndex - for i, componentId in components do + for _, componentId in components do local map = componentIndex[componentId] if not map then return EmptyQuery end - if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then + if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then firstArchetypeMap = map end end @@ -422,110 +439,107 @@ function World.query(world: World, ...: i53): Query for id in firstArchetypeMap.sparse do local archetype = archetypes[id] local archetypeRecords = archetype.records - local indices = {} + local indices = {} local skip = false - - for i, componentId in components do + + for i, componentId in components do local index = archetypeRecords[componentId] - if not index then + if not index then skip = true break end - indices[i] = archetypeRecords[componentId] + indices[i] = index end - if skip then + if skip then continue end - table.insert(compatibleArchetypes, { archetype, indices }) + + length += 1 + compatibleArchetypes[length] = {archetype, indices} end local lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then + if not lastArchetype then return EmptyQuery end - + local preparedQuery = {} preparedQuery.__index = preparedQuery - function preparedQuery:without(...) - local components = { ... } - for i = #compatibleArchetypes, 1, -1 do + function preparedQuery:without(...) + local withoutComponents = {...} + for i = #compatibleArchetypes, 1, -1 do local archetype = compatibleArchetypes[i][1] + local records = archetype.records local shouldRemove = false - for _, componentId in components do - if archetype.records[componentId] then + + for _, componentId in withoutComponents do + if records[componentId] then shouldRemove = true break end end - if shouldRemove then + + if shouldRemove then table.remove(compatibleArchetypes, i) end - end + end lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then + if not lastArchetype then return EmptyQuery end - + return self end local lastRow local queryOutput = {} - - function preparedQuery:__iter() - return function() + function preparedQuery:__iter() + return function() local archetype = compatibleArchetype[1] local row = next(archetype.entities, lastRow) - while row == nil do + while row == nil do lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) - if lastArchetype == nil then - return + if lastArchetype == nil then + return end archetype = compatibleArchetype[1] row = next(archetype.entities, row) end lastRow = row - + local entityId = archetype.entities[row :: number] local columns = archetype.columns local tr = compatibleArchetype[2] - - if queryLength == 1 then + + if queryLength == 1 then return entityId, columns[tr[1]][row] - elseif queryLength == 2 then + elseif queryLength == 2 then return entityId, columns[tr[1]][row], columns[tr[2]][row] - elseif queryLength == 3 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row] - elseif queryLength == 4 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row] - elseif queryLength == 5 then - return entityId, + elseif queryLength == 3 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] + elseif queryLength == 4 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] + elseif queryLength == 5 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[5]][row] - elseif queryLength == 6 then - return entityId, + elseif queryLength == 6 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[5]][row], columns[tr[6]][row] - elseif queryLength == 7 then - return entityId, + elseif queryLength == 7 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], @@ -533,8 +547,8 @@ function World.query(world: World, ...: i53): Query columns[tr[5]][row], columns[tr[6]][row], columns[tr[7]][row] - elseif queryLength == 8 then - return entityId, + elseif queryLength == 8 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], @@ -545,7 +559,7 @@ function World.query(world: World, ...: i53): Query columns[tr[8]][row] end - for i in components do + for i in components do queryOutput[i] = tr[i][row] end @@ -556,23 +570,24 @@ function World.query(world: World, ...: i53): Query return setmetatable({}, preparedQuery) :: any end -function World.component(world: World) - local componentId = world.nextComponentId + 1 - if componentId > HI_COMPONENT_ID then - -- IDs are partitioned into ranges because component IDs are not nominal, +function World.component(world: World) + local componentId = world.nextComponentId + 1 + if componentId > HI_COMPONENT_ID then + -- IDs are partitioned into ranges because component IDs are not nominal, -- so it needs to error when IDs intersect into the entity range. - error("Too many components, consider using world:entity() instead to create components.") + error("Too many components, consider using world:entity() instead to create components.") end world.nextComponentId = componentId return componentId end function World.entity(world: World) - world.nextEntityId += 1 - return world.nextEntityId + REST + local nextEntityId = world.nextEntityId + 1 + world.nextEntityId = nextEntityId + return nextEntityId + REST end -function World.delete(world: World, entityId: i53) +function World.delete(world: World, entityId: i53) local entityIndex = world.entityIndex local record = entityIndex[entityId] moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) @@ -584,57 +599,61 @@ function World.delete(world: World, entityId: i53) end function World.observer(world: World, ...) - local componentIds = { ... } - + local componentIds = {...} + local idsCount = #componentIds + local hooks = world.hooks + return { - event = function(event) - local hook = world.hooks[event] - world.hooks[event] = nil + event = function(event) + local hook = hooks[event] + hooks[event] = nil local last, change - return function() + return function() last, change = next(hook, last) - if not last then + if not last then return end local matched = false - - while not matched do + local ids = change.ids + + while not matched do local skip = false - for _, id in change.ids do - if not table.find(componentIds, id) then + for _, id in ids do + if not table.find(componentIds, id) then skip = true break end end - - if skip then + + if skip then last, change = next(hook, last) + ids = change.ids continue end matched = true end - - local queryOutput = {} + + local queryOutput = table.create(idsCount) local row = change.offset local archetype = change.archetype local columns = archetype.columns local archetypeRecords = archetype.records - for _, id in componentIds do - table.insert(queryOutput, columns[archetypeRecords[id]][row]) + for index, id in componentIds do + queryOutput[index] = columns[archetypeRecords[id]][row] end - return archetype.entities[row], unpack(queryOutput, 1, #queryOutput) + return archetype.entities[row], unpack(queryOutput, 1, idsCount) end - end + end; } end return table.freeze({ - World = World, - ON_ADD = ON_ADD, - ON_REMOVE = ON_REMOVE, - ON_SET = ON_SET + World = World; + ON_ADD = ON_ADD; + ON_REMOVE = ON_REMOVE; + ON_SET = ON_SET; }) diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..54227d9 --- /dev/null +++ b/selene.toml @@ -0,0 +1,4 @@ +std = "roblox" + +[lints] +global_usage = "allow" diff --git a/stylua.toml b/stylua.toml new file mode 100644 index 0000000..83e5807 --- /dev/null +++ b/stylua.toml @@ -0,0 +1,5 @@ +column_width = 120 +quote_style = "ForceDouble" + +[sort_requires] +enabled = true diff --git a/testez-companion.toml b/testez-companion.toml new file mode 100644 index 0000000..4de0c23 --- /dev/null +++ b/testez-companion.toml @@ -0,0 +1,3 @@ +roots = ["ServerStorage"] + +[extraOptions] diff --git a/wally.toml b/wally.toml index c4b5be7..5885799 100644 --- a/wally.toml +++ b/wally.toml @@ -10,6 +10,3 @@ include = ["default.project.json", "lib", "wally.toml", "README.md"] TestEZ = "roblox/testez@0.4.1" Matter = "matter-ecs/matter@0.8.0" ecr = "centau/ecr@0.8.0" - - - From c0854e960e8a53a195e6ec0dec7218e4a3a49c52 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 5 May 2024 02:48:34 +0200 Subject: [PATCH 03/46] Move benchmarks (#28) * Update mirror * Remove matter from main --- benches/query.lua | 195 +----- mirror/init.lua | 112 +++- newMatter.lua | 1499 ------------------------------------------- oldMatter.lua | 1567 --------------------------------------------- 4 files changed, 78 insertions(+), 3295 deletions(-) delete mode 100644 newMatter.lua delete mode 100644 oldMatter.lua diff --git a/benches/query.lua b/benches/query.lua index 2c4cd55..8f54596 100644 --- a/benches/query.lua +++ b/benches/query.lua @@ -10,9 +10,6 @@ end local jecs = require("../mirror/init") -local oldMatter = require("../oldMatter") - -local newMatter = require("../newMatter") type i53 = number do @@ -106,194 +103,4 @@ do view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) end -end - -do - TITLE(testkit.color.white_underline("OldMatter query")) - - local ecs = oldMatter.World.new() - local component = oldMatter.component - - do - TITLE("one component in common") - local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) - BENCH("1 component", function() - for _ in world:query(A) do - end - end) - - BENCH("2 component", function() - for _ in world:query(A, B) do - end - end) - - BENCH("4 component", function() - for _ in world:query(A, B, C, D) do - end - end) - - BENCH("8 component", function() - for _ in world:query(A, B, C, D, E, F, G, H) do - end - end) - end - - local D1 = component() - local D2 = component() - local D3 = component() - local D4 = component() - local D5 = component() - local D6 = component() - local D7 = component() - local D8 = component() - - local function flip() - return math.random() >= 0.15 - end - - local added = 0 - local archetypes = {} - for i = 1, 2 ^ 16 - 2 do - local entity = ecs:spawn() - - local combination = "" - - if flip() then - combination ..= "B" - ecs:insert(entity, D2({value = true})) - end - if flip() then - combination ..= "C" - ecs:insert(entity, D3({value = true})) - end - if flip() then - combination ..= "D" - ecs:insert(entity, D4({value = true})) - end - if flip() then - combination ..= "E" - ecs:insert(entity, D5({value = true})) - end - if flip() then - combination ..= "F" - ecs:insert(entity, D6({value = true})) - end - if flip() then - combination ..= "G" - ecs:insert(entity, D7({value = true})) - end - if flip() then - combination ..= "H" - ecs:insert(entity, D8({value = true})) - end - - if #combination == 7 then - added += 1 - ecs:insert(entity, D1({value = true})) - end - archetypes[combination] = true - end - - local a = 0 - for _ in archetypes do - a += 1 - end - - view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) - end -end - -do - TITLE(testkit.color.white_underline("NewMatter query")) - - local ecs = newMatter.World.new() - local component = newMatter.component - - do - TITLE("one component in common") - local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) - BENCH("1 component", function() - for _ in world:query(A) do - end - end) - - BENCH("2 component", function() - for _ in world:query(A, B) do - end - end) - - BENCH("4 component", function() - for _ in world:query(A, B, C, D) do - end - end) - - BENCH("8 component", function() - for _ in world:query(A, B, C, D, E, F, G, H) do - end - end) - end - - local D1 = component() - local D2 = component() - local D3 = component() - local D4 = component() - local D5 = component() - local D6 = component() - local D7 = component() - local D8 = component() - - local function flip() - return math.random() >= 0.15 - end - - local added = 0 - local archetypes = {} - for i = 1, 2 ^ 16 - 2 do - local entity = ecs:spawn() - - local combination = "" - - if flip() then - combination ..= "B" - ecs:insert(entity, D2({value = true})) - end - if flip() then - combination ..= "C" - ecs:insert(entity, D3({value = true})) - end - if flip() then - combination ..= "D" - ecs:insert(entity, D4({value = true})) - end - if flip() then - combination ..= "E" - ecs:insert(entity, D5({value = true})) - end - if flip() then - combination ..= "F" - ecs:insert(entity, D6({value = true})) - end - if flip() then - combination ..= "G" - ecs:insert(entity, D7({value = true})) - end - if flip() then - combination ..= "H" - ecs:insert(entity, D8({value = true})) - end - - if #combination == 7 then - added += 1 - ecs:insert(entity, D1({value = true})) - end - archetypes[combination] = true - end - - local a = 0 - for _ in archetypes do - a += 1 - end - - view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) - end -end +end \ No newline at end of file diff --git a/mirror/init.lua b/mirror/init.lua index e10d9de..35a9b9c 100644 --- a/mirror/init.lua +++ b/mirror/init.lua @@ -51,31 +51,48 @@ local REST = HI_COMPONENT_ID + 4 local function transitionArchetype( entityIndex: EntityIndex, - destinationArchetype: Archetype, + to: Archetype, destinationRow: i24, - sourceArchetype: Archetype, + from: Archetype, sourceRow: i24 ) - local columns = sourceArchetype.columns - local sourceEntities = sourceArchetype.entities - local destinationEntities = destinationArchetype.entities - local destinationColumns = destinationArchetype.columns + local columns = from.columns + local sourceEntities = from.entities + local destinationEntities = to.entities + local destinationColumns = to.columns + local tr = to.records + local types = from.types - for componentId, column in columns do - local targetColumn = destinationColumns[componentId] + for i, column in columns do + -- Retrieves the new column index from the source archetype's record from each component + -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. + local targetColumn = destinationColumns[tr[types[i]]] + + -- Sometimes target column may not exist, e.g. when you remove a component. if targetColumn then targetColumn[destinationRow] = column[sourceRow] end - column[sourceRow] = column[#column] - column[#column] = nil + -- If the entity is the last row in the archetype then swapping it would be meaningless. + local last = #column + if sourceRow ~= last then + -- Swap rempves columns to ensure there are no holes in the archetype. + column[sourceRow] = column[last] + end + column[last] = nil end + -- Move the entity from the source to the destination archetype. destinationEntities[destinationRow] = sourceEntities[sourceRow] entityIndex[sourceEntities[sourceRow]].row = destinationRow + -- Because we have swapped columns we now have to update the records + -- corresponding to the entities' rows that were swapped. local movedAway = #sourceEntities - sourceEntities[sourceRow] = sourceEntities[movedAway] - entityIndex[sourceEntities[movedAway]].row = sourceRow + if sourceRow ~= movedAway then + sourceEntities[sourceRow] = sourceEntities[movedAway] + entityIndex[sourceEntities[movedAway]].row = sourceRow + end + sourceEntities[movedAway] = nil end @@ -145,7 +162,9 @@ local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Arch } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype - createArchetypeRecords(world.componentIndex, archetype, prev) + if #types > 0 then + createArchetypeRecords(world.componentIndex, archetype, prev) + end return archetype end @@ -180,8 +199,6 @@ local function emit(world, eventDescription) }) end - - local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) if #added > 0 then emit(world, { @@ -194,13 +211,13 @@ local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: end end - export type World = typeof(World.new()) local function ensureArchetype(world: World, types, prev) if #types < 1 then return world.ROOT_ARCHETYPE end + local ty = hash(types) local archetype = world.archetypeIndex[ty] if archetype then @@ -226,8 +243,13 @@ end local function findArchetypeWith(world: World, node: Archetype, componentId: i53) local types = node.types + -- Component IDs are added incrementally, so inserting and sorting + -- them each time would be expensive. Instead this insertion sort can find the insertion + -- point in the types array. local at = findInsert(types, componentId) if at == -1 then + -- If it finds a duplicate, it just means it is the same archetype so it can return it + -- directly instead of needing to hash types for a lookup to the archetype. return node end @@ -245,6 +267,7 @@ end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype if not from then + -- If there was no source archetype then it should return the ROOT_ARCHETYPE if not world.ROOT_ARCHETYPE then local ROOT_ARCHETYPE = archetypeOf(world, {}, nil) world.ROOT_ARCHETYPE = ROOT_ARCHETYPE @@ -254,6 +277,8 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet local edge = ensureEdge(from, componentId) if not edge.add then + -- Save an edge using the component ID to the archetype to allow + -- faster traversals to adjacent archetypes. edge.add = findArchetypeWith(world, from, componentId) end @@ -270,26 +295,31 @@ end function World.set(world: World, entityId: i53, componentId: i53, data: unknown) local record = ensureRecord(world.entityIndex, entityId) - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype) + local from = record.archetype + local to = archetypeTraverseAdd(world, componentId, from) - if sourceArchetype == destinationArchetype then - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = data + if from == to then + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. + local archetypeRecord = to.records[componentId] + from.columns[archetypeRecord][record.row] = data + -- Should fire an OnSet event here. return end - if sourceArchetype then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) + if from then + -- If there was a previous archetype, then the entity needs to move the archetype + moveEntity(world.entityIndex, entityId, record, to) else - if #destinationArchetype.types > 0 then - newEntity(entityId, record, destinationArchetype) - onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId }) + if #to.types > 0 then + -- When there is no previous archetype it should create the archetype + newEntity(entityId, record, to) + onNotifyAdd(world, to, from, record.row, { componentId }) end end - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = data + local archetypeRecord = to.records[componentId] + to.columns[archetypeRecord][record.row] = data end local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype @@ -316,9 +346,10 @@ function World.remove(world: World, entityId: i53, componentId: i53) end end +-- Keeping the function as small as possible to enable inlining local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24) local archetype = record.archetype - local archetypeRecord = componentIndex[componentId].sparse[archetype.id] + local archetypeRecord = archetype.records[componentId] if not archetypeRecord then return nil @@ -388,26 +419,24 @@ function World.query(world: World, ...: i53): Query end end - local i = 0 for id in firstArchetypeMap.sparse do local archetype = archetypes[id] local archetypeRecords = archetype.records local indices = {} local skip = false - for j, componentId in components do + for i, componentId in components do local index = archetypeRecords[componentId] if not index then skip = true break end - indices[j] = archetypeRecords[componentId] + indices[i] = archetypeRecords[componentId] end if skip then continue end - i += 1 table.insert(compatibleArchetypes, { archetype, indices }) end @@ -464,7 +493,7 @@ function World.query(world: World, ...: i53): Query local entityId = archetype.entities[row :: number] local columns = archetype.columns local tr = compatibleArchetype[2] - + if queryLength == 1 then return entityId, columns[tr[1]][row] elseif queryLength == 2 then @@ -530,7 +559,9 @@ end function World.component(world: World) local componentId = world.nextComponentId + 1 if componentId > HI_COMPONENT_ID then - error("Too many components") + -- IDs are partitioned into ranges because component IDs are not nominal, + -- so it needs to error when IDs intersect into the entity range. + error("Too many components, consider using world:entity() instead to create components.") end world.nextComponentId = componentId return componentId @@ -541,6 +572,17 @@ function World.entity(world: World) return world.nextEntityId + REST end +function World.delete(world: World, entityId: i53) + local entityIndex = world.entityIndex + local record = entityIndex[entityId] + moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) + -- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from + -- the entities array and delete the record. We know there won't be the hole since + -- we are always removing the last row. + --world.ROOT_ARCHETYPE.entities[record.row] = nil + --entityIndex[entityId] = nil +end + function World.observer(world: World, ...) local componentIds = { ... } diff --git a/newMatter.lua b/newMatter.lua deleted file mode 100644 index 4a50791..0000000 --- a/newMatter.lua +++ /dev/null @@ -1,1499 +0,0 @@ ---!optimize 2 ---!native ---!strict - -local None = {} - -local function merge(one, two) - local new = table.clone(one) - - for key, value in two do - if value == None then - new[key] = nil - else - new[key] = value - end - end - - return new -end - --- https://github.com/freddylist/llama/blob/master/src/List/toSet.lua -local function toSet(list) - local set = {} - - for _, v in ipairs(list) do - set[v] = true - end - - return set -end - --- https://github.com/freddylist/llama/blob/master/src/Dictionary/values.lua -local function values(dictionary) - local valuesList = {} - - local index = 1 - - for _, value in pairs(dictionary) do - valuesList[index] = value - index = index + 1 - end - - return valuesList -end - -local stack = {} - -local function newStackFrame(node) - return { - node = node, - accessedKeys = {}, - } -end - -local function cleanup() - local currentFrame = stack[#stack] - - for baseKey, state in pairs(currentFrame.node.system) do - for key, value in pairs(state.storage) do - if not currentFrame.accessedKeys[baseKey] or not currentFrame.accessedKeys[baseKey][key] then - local cleanupCallback = state.cleanupCallback - - if cleanupCallback then - local shouldAbortCleanup = cleanupCallback(value) - - if shouldAbortCleanup then - continue - end - end - - state.storage[key] = nil - end - end - end -end - -local function start(node, fn) - table.insert(stack, newStackFrame(node)) - fn() - cleanup() - table.remove(stack, #stack) -end - -local function withinTopoContext() - return #stack ~= 0 -end - -local function useFrameState() - return stack[#stack].node.frame -end - -local function useCurrentSystem() - if #stack == 0 then - return - end - - return stack[#stack].node.currentSystem -end - - ---[=[ - @within Matter - - :::tip - **Don't use this function directly in your systems.** - - This function is used for implementing your own topologically-aware functions. It should not be used in your - systems directly. You should use this function to implement your own utilities, similar to `useEvent` and - `useThrottle`. - ::: - - `useHookState` does one thing: it returns a table. An empty, pristine table. Here's the cool thing though: - it always returns the *same* table, based on the script and line where *your function* (the function calling - `useHookState`) was called. - - ### Uniqueness - - If your function is called multiple times from the same line, perhaps within a loop, the default behavior of - `useHookState` is to uniquely identify these by call count, and will return a unique table for each call. - - However, you can override this behavior: you can choose to key by any other value. This means that in addition to - script and line number, the storage will also only return the same table if the unique value (otherwise known as the - "discriminator") is the same. - - ### Cleaning up - As a second optional parameter, you can pass a function that is automatically invoked when your storage is about - to be cleaned up. This happens when your function (and by extension, `useHookState`) ceases to be called again - next frame (keyed by script, line number, and discriminator). - - Your cleanup callback is passed the storage table that's about to be cleaned up. You can then perform cleanup work, - like disconnecting events. - - *Or*, you could return `true`, and abort cleaning up altogether. If you abort cleanup, your storage will stick - around another frame (even if your function wasn't called again). This can be used when you know that the user will - (or might) eventually call your function again, even if they didn't this frame. (For example, caching a value for - a number of seconds). - - If cleanup is aborted, your cleanup function will continue to be called every frame, until you don't abort cleanup, - or the user actually calls your function again. - - ### Example: useThrottle - - This is the entire implementation of the built-in `useThrottle` function: - - ```lua - local function cleanup(storage) - return os.clock() < storage.expiry - end - - local function useThrottle(seconds, discriminator) - local storage = useHookState(discriminator, cleanup) - - if storage.time == nil or os.clock() - storage.time >= seconds then - storage.time = os.clock() - storage.expiry = os.clock() + seconds - return true - end - - return false - end - ``` - - A lot of talk for something so simple, right? - - @param discriminator? any -- A unique value to additionally key by - @param cleanupCallback (storage: {}) -> boolean? -- A function to run when the storage for this hook is cleaned up -]=] -local function useHookState(discriminator, cleanupCallback): {} - local file, line = debug.info(3, "sl") - local fn = debug.info(2, "f") - - local baseKey = string.format("%s:%s:%d", tostring(fn), file, line) - - local currentFrame = stack[#stack] - - if currentFrame == nil then - error("Attempt to access topologically-aware storage outside of a Loop-system context.", 3) - end - - if not currentFrame.accessedKeys[baseKey] then - currentFrame.accessedKeys[baseKey] = {} - end - - local accessedKeys = currentFrame.accessedKeys[baseKey] - - local key = #accessedKeys - - if discriminator ~= nil then - if type(discriminator) == "number" then - discriminator = tostring(discriminator) - end - - key = discriminator - end - - accessedKeys[key] = true - - if not currentFrame.node.system[baseKey] then - currentFrame.node.system[baseKey] = { - storage = {}, - cleanupCallback = cleanupCallback, - } - end - - local storage = currentFrame.node.system[baseKey].storage - - if not storage[key] then - storage[key] = {} - end - - return storage[key] -end - -local topoRuntime = { - start = start, - useHookState = useHookState, - useFrameState = useFrameState, - useCurrentSystem = useCurrentSystem, - withinTopoContext = withinTopoContext, -} - - ---[=[ - @class Component - - A component is a named piece of data that exists on an entity. - Components are created and removed in the [World](/api/World). - - In the docs, the terms "Component" and "ComponentInstance" are used: - - **"Component"** refers to the base class of a specific type of component you've created. - This is what [`Matter.component`](/api/Matter#component) returns. - - **"Component Instance"** refers to an actual piece of data that can exist on an entity. - The metatable of a component instance table is its respective Component table. - - Component instances are *plain-old data*: they do not contain behaviors or methods. - - Since component instances are immutable, one helper function exists on all component instances, `patch`, - which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations. -]=] - ---[=[ - @within Component - @type ComponentInstance {} - - The `ComponentInstance` type refers to an actual piece of data that can exist on an entity. - The metatable of the component instance table is set to its particular Component table. - - A component instance can be created by calling the Component table: - - ```lua - -- Component: - local MyComponent = Matter.component("My component") - - -- component instance: - local myComponentInstance = MyComponent({ - some = "data" - }) - - print(getmetatable(myComponentInstance) == MyComponent) --> true - ``` -]=] - --- This is a special value we set inside the component's metatable that will allow us to detect when --- a Component is accidentally inserted as a Component Instance. --- It should not be accessible through indexing into a component instance directly. -local DIAGNOSTIC_COMPONENT_MARKER = {} - -local nextId = 0 -local function newComponent(name, defaultData) - name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") - assert( - defaultData == nil or type(defaultData) == "table", - "if component default data is specified, it must be a table" - ) - - local component = {} - component.__index = component - - function component.new(data) - data = data or {} - - if defaultData then - data = merge(defaultData, data) - end - - return table.freeze(setmetatable(data, component)) - end - - --[=[ - @within Component - - ```lua - for id, target in world:query(Target) do - if shouldChangeTarget(target) then - world:insert(id, target:patch({ -- modify the existing component - currentTarget = getNewTarget() - })) - end - end - ``` - - A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table - will override those of the existing component instance. - - As all components are immutable and frozen, it is not possible to modify the existing component directly. - - You can use the `Matter.None` constant to remove a value from the component instance: - - ```lua - target:patch({ - currentTarget = Matter.None -- sets currentTarget to nil - }) - ``` - - @param partialNewData {} -- The table to be merged with the existing component data. - @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. - ]=] - function component:patch(partialNewData) - local patch = getmetatable(self).new(merge(self, partialNewData)) - return patch - end - - nextId += 1 - local id = nextId - - setmetatable(component, { - __call = function(_, ...) - return component.new(...) - end, - __tostring = function() - return name - end, - __len = function() - return id - end, - [DIAGNOSTIC_COMPONENT_MARKER] = true, - }) - - return component -end - -local function assertValidType(value, position) - if typeof(value) ~= "table" then - error(string.format("Component #%d is invalid: not a table", position), 3) - end - - local metatable = getmetatable(value) - - if metatable == nil then - error(string.format("Component #%d is invalid: has no metatable", position), 3) - end -end - -local function assertValidComponent(value, position) - assertValidType(value, position) - - local metatable = getmetatable(value) - - if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then - error( - string.format( - "Component #%d is invalid: Component Instance %s was passed instead of the Component itself!", - position, - tostring(metatable) - ), - 3 - ) - end -end - -local function assertValidComponentInstance(value, position) - assertValidType(value, position) - - if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then - error( - string.format( - "Component #%d is invalid: passed a Component instead of a Component instance; " - .. "did you forget to call it as a function?", - position - ), - 3 - ) - end -end - -local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" -local ERROR_DUPLICATE_ENTITY = - "The world already contains an entity with ID %d. Use World:replace instead if this is intentional." -local ERROR_NO_COMPONENTS = "Missing components" - -type i53 = number -type i24 = number - -type Component = { [any]: any } -type ComponentInstance = Component - -type Ty = { i53 } -type ArchetypeId = number - -type Column = { any } - -type Archetype = { - -- Unique identifier of this archetype - id: number, - edges: { - [i24]: { - add: Archetype, - remove: Archetype, - }, - }, - types: Ty, - type: string | number, - entities: { number }, - columns: { Column }, - records: {}, -} - -type Record = { - archetype: Archetype, - row: number, -} - -type EntityIndex = { [i24]: Record } -type ComponentIndex = { [i24]: ArchetypeMap } - -type ArchetypeRecord = number -type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord }, size: number } -type Archetypes = { [ArchetypeId]: Archetype } - -local function transitionArchetype( - entityIndex: EntityIndex, - to: Archetype, - destinationRow: i24, - from: Archetype, - sourceRow: i24 -) - local columns = from.columns - local sourceEntities = from.entities - local destinationEntities = to.entities - local destinationColumns = to.columns - local tr = to.records - local types = from.types - - for componentId, column in columns do - local targetColumn = destinationColumns[tr[types[componentId]]] - if targetColumn then - targetColumn[destinationRow] = column[sourceRow] - end - - if sourceRow ~= #column then - column[sourceRow] = column[#column] - column[#column] = nil - end - end - - destinationEntities[destinationRow] = sourceEntities[sourceRow] - entityIndex[sourceEntities[sourceRow]].row = destinationRow - - local movedAway = #sourceEntities - if sourceRow ~= movedAway then - sourceEntities[sourceRow] = sourceEntities[movedAway] - entityIndex[sourceEntities[movedAway]].row = sourceRow - end - - sourceEntities[movedAway] = nil -end - -local function archetypeAppend(entity: i53, archetype: Archetype): i24 - local entities = archetype.entities - table.insert(entities, entity) - return #entities -end - -local function newEntity(entityId: i53, record: Record, archetype: Archetype) - local row = archetypeAppend(entityId, archetype) - record.archetype = archetype - record.row = row - return record -end - -local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) - local sourceRow = record.row - local from = record.archetype - local destinationRow = archetypeAppend(entityId, to) - transitionArchetype(entityIndex, to, destinationRow, from, sourceRow) - record.archetype = to - record.row = destinationRow -end - -local function hash(arr): string | number - return table.concat(arr, "_") -end - -local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype) - local destinationCount = #to.types - local destinationIds = to.types - - for i = 1, destinationCount do - local destinationId = destinationIds[i] - - if not componentIndex[destinationId] then - componentIndex[destinationId] = { sparse = {}, size = 0 } - end - componentIndex[destinationId].sparse[to.id] = i - to.records[destinationId] = i - end -end - -local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype - local ty = hash(types) - - world.nextArchetypeId = (world.nextArchetypeId :: number) + 1 - local id = world.nextArchetypeId - - local columns = {} :: { any } - - for _ in types do - table.insert(columns, {}) - end - - local archetype = { - id = id, - types = types, - type = ty, - columns = columns, - entities = {}, - edges = {}, - records = {}, - } - - world.archetypeIndex[ty] = archetype - world.archetypes[id] = archetype - - if #types > 0 then - createArchetypeRecords(world.componentIndex, archetype, prev) - end - - return archetype -end - -local World = {} -World.__index = World - -function World.new() - local self = setmetatable({ - entityIndex = {}, - componentIndex = {}, - componentIdToComponent = {}, - archetypes = {}, - archetypeIndex = {}, - nextId = 0, - nextArchetypeId = 0, - _size = 0, - _changedStorage = {}, - }, World) - - self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil) - return self -end - -type World = typeof(World.new()) - -local function ensureArchetype(world: World, types, prev) - if #types < 1 then - return world.ROOT_ARCHETYPE - end - - local ty = hash(types) - local archetype = world.archetypeIndex[ty] - if archetype then - return archetype - end - - return archetypeOf(world, types, prev) -end - -local function findInsert(types: { i53 }, toAdd: i53) - local count = #types - for i = 1, count do - local id = types[i] - if id == toAdd then - return -1 - end - if id > toAdd then - return i - end - end - return count + 1 -end - -local function findArchetypeWith(world: World, node: Archetype, componentId: i53) - local types = node.types - local at = findInsert(types, componentId) - if at == -1 then - return node - end - - local destinationType = table.clone(node.types) - table.insert(destinationType, at, componentId) - return ensureArchetype(world, destinationType, node) -end - -local function ensureEdge(archetype: Archetype, componentId: i53) - if not archetype.edges[componentId] then - archetype.edges[componentId] = {} :: any - end - return archetype.edges[componentId] -end - -local function archetypeTraverseAdd(world: World, componentId: i53, archetype: Archetype?): Archetype - local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype - local edge = ensureEdge(from, componentId) - - if not edge.add then - edge.add = findArchetypeWith(world, from, componentId) - end - - return edge.add -end - -local function componentAdd(world: World, entityId: i53, componentInstance) - local component = getmetatable(componentInstance) - local componentId = #component - - -- TODO: - -- This never gets cleaned up - world.componentIdToComponent[componentId] = component - - local record = world:ensureRecord(entityId) - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype) - - if sourceArchetype == destinationArchetype then - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = componentInstance - return - end - - if sourceArchetype then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) - else - -- if it has any components, then it wont be the root archetype - if #destinationArchetype.types > 0 then - newEntity(entityId, record, destinationArchetype) - end - end - - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = componentInstance -end - -function World.ensureRecord(world: World, entityId: i53) - local entityIndex = world.entityIndex - local id = entityId - if not entityIndex[id] then - entityIndex[id] = {} :: Record - end - return entityIndex[id] -end - -local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype - local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype - local edge = ensureEdge(from, componentId) - - if not edge.remove then - local to = table.clone(from.types) - table.remove(to, table.find(to, componentId)) - edge.remove = ensureArchetype(world, to, from) - end - - return edge.remove -end - -local function get(componentIndex: ComponentIndex, record: Record, componentId: i24): ComponentInstance? - local archetype = record.archetype - if archetype == nil then - return nil - end - - local archetypeRecord = archetype.records[componentId] - if not archetypeRecord then - return nil - end - - return archetype.columns[archetypeRecord][record.row] -end - -local function componentRemove(world: World, entityId: i53, component: Component) - local componentId = #component - local record = world:ensureRecord(entityId) - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) - - -- TODO: - -- There is a better way to get the component for returning - local componentInstance = get(world.componentIndex, record, componentId) - if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) - end - - return componentInstance -end - ---[=[ - Removes a component (or set of components) from an existing entity. - - ```lua - local removedA, removedB = world:remove(entityId, ComponentA, ComponentB) - ``` - - @param entityId number -- The entity ID - @param ... Component -- The components to remove - @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed. -]=] -function World.remove(world: World, entityId: i53, ...) - if not world:contains(entityId) then - error(ERROR_NO_ENTITY, 2) - end - - local length = select("#", ...) - local removed = {} - for i = 1, length do - table.insert(removed, componentRemove(world, entityId, select(i, ...))) - end - - return unpack(removed, 1, length) -end - -function World.get( - world: World, - entityId: i53, - a: Component, - b: Component?, - c: Component?, - d: Component?, - e: Component? -): any - local componentIndex = world.componentIndex - local record = world.entityIndex[entityId] - if not record then - return nil - end - - local va = get(componentIndex, record, #a) - - if b == nil then - return va - elseif c == nil then - return va, get(componentIndex, record, #b) - elseif d == nil then - return va, get(componentIndex, record, #b), get(componentIndex, record, #c) - elseif e == nil then - return va, get(componentIndex, record, #b), get(componentIndex, record, #c), get(componentIndex, record, #d) - else - error("args exceeded") - end -end - -function World.insert(world: World, entityId: i53, ...) - if not world:contains(entityId) then - error(ERROR_NO_ENTITY, 2) - end - - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - local oldComponent = world:get(entityId, metatable) - componentAdd(world, entityId, newComponent) - - world:_trackChanged(metatable, entityId, oldComponent, newComponent) - end -end - -function World.replace(world: World, entityId: i53, ...: ComponentInstance) - error("Replace is unimplemented") - - if not world:contains(entityId) then - error(ERROR_NO_ENTITY, 2) - end - - --moveEntity(entityId, record, world.ROOT_ARCHETYPE) - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - assertValidComponentInstance(newComponent, i) - end -end - -function World.entity(world: World) - world.nextId += 1 - return world.nextId -end - -function World:__iter() - local previous = nil - return function() - local entityId, data = next(self.entityIndex, previous) - previous = entityId - - if entityId == nil then - return nil - end - - local archetype = data.archetype - if not archetype then - return entityId, {} - end - - local columns = archetype.columns - local components = {} - for i, map in columns do - local componentId = archetype.types[i] - components[self.componentIdToComponent[componentId]] = map[data.row] - end - - return entityId, components - end -end - -function World._trackChanged(world: World, metatable, id, old, new) - if not world._changedStorage[metatable] then - return - end - - if old == new then - return - end - - local record = table.freeze({ - old = old, - new = new, - }) - - for _, storage in ipairs(world._changedStorage[metatable]) do - -- If this entity has changed since the last time this system read it, - -- we ensure that the "old" value is whatever the system saw it as last, instead of the - -- "old" value we have here. - if storage[id] then - storage[id] = table.freeze({ old = storage[id].old, new = new }) - else - storage[id] = record - end - end -end - ---[=[ - Spawns a new entity in the world with a specific entity ID and given components. - - The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID. - - @param entityId number -- The entity ID to spawn with - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The same entity ID that was passed in -]=] -function World.spawnAt(world: World, entityId: i53, ...: ComponentInstance) - if world:contains(entityId) then - error(string.format(ERROR_DUPLICATE_ENTITY, entityId), 2) - end - - if entityId >= world.nextId then - world.nextId = entityId + 1 - end - - world._size += 1 - world:ensureRecord(entityId) - - local components = {} - for i = 1, select("#", ...) do - local component = select(i, ...) - assertValidComponentInstance(component, i) - - local metatable = getmetatable(component) - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) - end - - world:_trackChanged(metatable, entityId, nil, component) - - components[metatable] = component - componentAdd(world, entityId, component) - end - - return entityId -end - ---[=[ - Spawns a new entity in the world with the given components. - - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The new entity ID. -]=] -function World.spawn(world: World, ...: ComponentInstance) - return world:spawnAt(world.nextId, ...) -end - -function World.despawn(world: World, entityId: i53) - local entityIndex = world.entityIndex - local record = entityIndex[entityId] - moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) - world.ROOT_ARCHETYPE.entities[record.row] = nil - entityIndex[entityId] = nil - world._size -= 1 -end - -function World.clear(world: World) - world.entityIndex = {} - world.componentIndex = {} - world.archetypes = {} - world.archetypeIndex = {} - world._size = 0 - world.ROOT_ARCHETYPE = archetypeOf(world, {}, nil) -end - -function World.size(world: World) - return world._size -end - -function World.contains(world: World, entityId: i53) - return world.entityIndex[entityId] ~= nil -end - -local function noop(): any - return function() end -end - -local emptyQueryResult = setmetatable({ - next = function() end, - snapshot = function() - return {} - end, - without = function(self) - return self - end, - view = function() - return { - get = function() end, - contains = function() end, - } - end, -}, { - __iter = noop, - __call = noop, -}) - -local function queryResult(compatibleArchetypes, components: { number }, queryLength, ...): any - local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ... - local lastArchetype, archetype = next(compatibleArchetypes) - if not lastArchetype then - return emptyQueryResult - end - - local lastRow - local queryOutput = {} - local function iterate() - local row = next(archetype.entities, lastRow) - while row == nil do - lastArchetype, archetype = next(compatibleArchetypes, lastArchetype) - if lastArchetype == nil then - return - end - row = next(archetype.entities, row) - end - - lastRow = row - - local columns = archetype.columns - local entityId = archetype.entities[row :: number] - local archetypeRecords = archetype.records - - if queryLength == 1 then - return entityId, columns[archetypeRecords[a]][row] - elseif queryLength == 2 then - return entityId, columns[archetypeRecords[a]][row], columns[archetypeRecords[b]][row] - elseif queryLength == 3 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row] - elseif queryLength == 4 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row] - elseif queryLength == 5 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row] - elseif queryLength == 6 then - return entityId, - columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row], - columns[archetypeRecords[f]][row] - elseif queryLength == 7 then - return columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row], - columns[archetypeRecords[f]][row], - columns[archetypeRecords[g]][row] - elseif queryLength == 8 then - return columns[archetypeRecords[a]][row], - columns[archetypeRecords[b]][row], - columns[archetypeRecords[c]][row], - columns[archetypeRecords[d]][row], - columns[archetypeRecords[e]][row], - columns[archetypeRecords[f]][row], - columns[archetypeRecords[g]][row], - columns[archetypeRecords[h]][row] - end - - for i, componentId in components do - queryOutput[i] = columns[archetypeRecords[componentId]][row] - end - - return entityId, unpack(queryOutput, 1, queryLength) - end - --[=[ - @class QueryResult - - A result from the [`World:query`](/api/World#query) function. - - Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the - QueryResult is exhausted and is no longer useful. - - ```lua - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - ``` - ]=] - local QueryResult = {} - QueryResult.__index = QueryResult - - -- TODO: - -- remove in matter 1.0 - function QueryResult:__call() - return iterate() - end - - function QueryResult:__iter() - return function() - return iterate() - end - end - - --[=[ - Returns an iterator that will skip any entities that also have the given components. - - @param ... Component -- The component types to filter against. - @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values - - ```lua - for id in world:query(Target):without(Model) do - -- Do something - end - ``` - ]=] - function QueryResult:without(...) - local components = { ... } - for i, component in components do - components[i] = #component - end - - local compatibleArchetypes = compatibleArchetypes - for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i] - local shouldRemove = false - for _, componentId in components do - if archetype.records[componentId] then - shouldRemove = true - break - end - end - - if shouldRemove then - table.remove(compatibleArchetypes, i) - end - end - - lastArchetype, archetype = next(compatibleArchetypes) - if not lastArchetype then - return emptyQueryResult - end - - return self - end - - --[=[ - Returns the next set of values from the query result. Once all results have been returned, the - QueryResult is exhausted and is no longer useful. - - :::info - This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly - done by the language itself. - ::: - - ```lua - -- Using world:query in this position will make Lua invoke the table as a function. This is conventional. - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - ``` - - If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly - instead of calling the QueryResult as a function. - ```lua - local id, enemy, charge, model = world:query(Enemy, Charge, Model):next() - local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional - ``` - - @return id -- Entity ID - @return ...ComponentInstance -- The requested component values - ]=] - function QueryResult:next() - return iterate() - end - - local function drain() - local entry = table.pack(iterate()) - return if entry.n > 0 then entry else nil - end - - local Snapshot = { - __iter = function(self): any - local i = 0 - return function() - i += 1 - - local data = self[i] :: any - - if data then - return unpack(data, 1, data.n) - end - - return - end - end, - } - - function QueryResult:snapshot() - local list = setmetatable({}, Snapshot) :: any - for entry in drain do - table.insert(list, entry) - end - - return list - end - - --[=[ - Creates a View of the query and does all of the iterator tasks at once at an amortized cost. - This is used for many repeated random access to an entity. If you only need to iterate, just use a query. - - ```lua - local inflicting = world:query(Damage, Hitting, Player):view() - for _, source in world:query(DamagedBy) do - local damage = inflicting:get(source.from) - end - - for _ in world:query(Damage):view() do end -- You can still iterate views if you want! - ``` - - @return View See [View](/api/View) docs. - ]=] - function QueryResult:view() - local fetches = {} - local list = {} :: any - - local View = {} - View.__index = View - - function View:__iter() - local current = list.head - return function() - if not current then - return - end - local entity = current.entity - local fetch = fetches[entity] - current = current.next - - return entity, unpack(fetch, 1, fetch.n) - end - end - - --[=[ - @within View - Retrieve the query results to corresponding `entity` - @param entity number - the entity ID - @return ...ComponentInstance - ]=] - function View:get(entity) - if not self:contains(entity) then - return - end - - local fetch = fetches[entity] - local queryLength = fetch.n - - if queryLength == 1 then - return fetch[1] - elseif queryLength == 2 then - return fetch[1], fetch[2] - elseif queryLength == 3 then - return fetch[1], fetch[2], fetch[3] - elseif queryLength == 4 then - return fetch[1], fetch[2], fetch[3], fetch[4] - elseif queryLength == 5 then - return fetch[1], fetch[2], fetch[3], fetch[4], fetch[5] - end - - return unpack(fetch, 1, fetch.n) - end - - --[=[ - @within View - Equivalent to `world:contains()` - @param entity number - the entity ID - @return boolean - ]=] - function View:contains(entity) - return fetches[entity] ~= nil - end - - for entry in drain do - local entityId = entry[1] - local fetch = table.pack(select(2, unpack(entry))) - local node = { entity = entityId, next = nil } - fetches[entityId] = fetch - - if not list.head then - list.head = node - else - local current = list.head - while current.next do - current = current.next - end - current.next = node - end - end - - return setmetatable({}, View) - end - - return setmetatable({}, QueryResult) -end - ---[=[ - Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over - the results of the query. - - Order of iteration is not guaranteed. - - ```lua - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - - for id in world:query(Target):without(Model) do - -- Again, with feeling - end - ``` - - @param ... Component -- The component types to query. Only entities with *all* of these components will be returned. - @return QueryResult -- See [QueryResult](/api/QueryResult) docs. -]=] -function World.query(world: World, ...: Component): any - local compatibleArchetypes = {} - local components = { ... } - local archetypes = world.archetypes - local queryLength = select("#", ...) - local a: any, b: any, c: any, d: any, e: any, f: any, g: any, h: any = ... - - if queryLength == 0 then - return emptyQueryResult - end - - if queryLength == 1 then - a = #a - components = { a } - -- local archetypesMap = world.componentIndex[a] - -- components = { a } - -- local function single() - -- local id = next(archetypesMap) - -- local archetype = archetypes[id :: number] - -- local lastRow - - -- return function(): any - -- local row, entity = next(archetype.entities, lastRow) - -- while row == nil do - -- id = next(archetypesMap, id) - -- if id == nil then - -- return - -- end - -- archetype = archetypes[id] - -- row = next(archetype.entities, row) - -- end - -- lastRow = row - - -- return entity, archetype.columns[archetype.records[a]] - -- end - -- end - -- return single() - elseif queryLength == 2 then - --print("iter double") - a = #a - b = #b - components = { a, b } - - -- --print(a, b, world.componentIndex) - -- --[[local archetypesMap = world.componentIndex[a] - -- for id in archetypesMap do - -- local archetype = archetypes[id] - -- if archetype.records[b] then - -- table.insert(compatibleArchetypes, archetype) - -- end - -- end - - -- local function double(): () -> (number, any, any) - -- local lastArchetype, archetype = next(compatibleArchetypes) - -- local lastRow - - -- return function() - -- local row = next(archetype.entities, lastRow) - -- while row == nil do - -- lastArchetype, archetype = next(compatibleArchetypes, lastArchetype) - -- if lastArchetype == nil then - -- return - -- end - - -- row = next(archetype.entities, row) - -- end - -- lastRow = row - - -- local entity = archetype.entities[row :: number] - -- local columns = archetype.columns - -- local archetypeRecords = archetype.records - -- return entity, columns[archetypeRecords[a]], columns[archetypeRecords[b]] - -- end - -- end - -- return double() - elseif queryLength == 3 then - a = #a - b = #b - c = #c - components = { a, b, c } - elseif queryLength == 4 then - a = #a - b = #b - c = #c - d = #d - - components = { a, b, c, d } - elseif queryLength == 5 then - a = #a - b = #b - c = #c - d = #d - e = #e - - components = { a, b, c, d, e } - elseif queryLength == 6 then - a = #a - b = #b - c = #c - d = #d - e = #e - f = #f - - components = { a, b, c, d, e, f } - elseif queryLength == 7 then - a = #a - b = #b - c = #c - d = #d - e = #e - f = #f - g = #g - - components = { a, b, c, d, e, f, g } - elseif queryLength == 8 then - a = #a - b = #b - c = #c - d = #d - e = #e - f = #f - g = #g - h = #h - - components = { a, b, c, d, e, f, g, h } - else - for i, component in components do - components[i] = (#component) :: any - end - end - - local firstArchetypeMap - local componentIndex = world.componentIndex - for _, componentId in (components :: any) :: { number } do - local map = componentIndex[componentId] - if not map then - return emptyQueryResult - end - - if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then - firstArchetypeMap = map - end - end - - for id in firstArchetypeMap.sparse do - local archetype = archetypes[id] - local archetypeRecords = archetype.records - local matched = true - for _, componentId in components do - if not archetypeRecords[componentId] then - matched = false - break - end - end - - if matched then - table.insert(compatibleArchetypes, archetype) - end - end - - return queryResult(compatibleArchetypes, components :: any, queryLength, a, b, c, d, e, f, g, h) -end - -local function cleanupQueryChanged(hookState) - local world = hookState.world - local componentToTrack = hookState.componentToTrack - - for index, object in world._changedStorage[componentToTrack] do - if object == hookState.storage then - table.remove(world._changedStorage[componentToTrack], index) - break - end - end - - if next(world._changedStorage[componentToTrack]) == nil then - world._changedStorage[componentToTrack] = nil - end -end - -function World.queryChanged(world: World, componentToTrack, ...: nil) - if ... then - error("World:queryChanged does not take any additional parameters", 2) - end - - local hookState = topoRuntime.useHookState(componentToTrack, cleanupQueryChanged) :: any - if hookState.storage then - return function(): any - local entityId, record = next(hookState.storage) - - if entityId then - hookState.storage[entityId] = nil - - return entityId, record - end - return - end - end - - if not world._changedStorage[componentToTrack] then - world._changedStorage[componentToTrack] = {} - end - - local storage = {} - hookState.storage = storage - hookState.world = world - hookState.componentToTrack = componentToTrack - - table.insert(world._changedStorage[componentToTrack], storage) - - local queryResult = world:query(componentToTrack) - - return function(): any - local entityId, component = queryResult:next() - - if entityId then - return entityId, table.freeze({ new = component }) - end - return - end -end - -return { - World = World, - component = newComponent -} diff --git a/oldMatter.lua b/oldMatter.lua deleted file mode 100644 index 0baf7a7..0000000 --- a/oldMatter.lua +++ /dev/null @@ -1,1567 +0,0 @@ - -local None = {} - -local function merge(one, two) - local new = table.clone(one) - - for key, value in two do - if value == None then - new[key] = nil - else - new[key] = value - end - end - - return new -end - --- https://github.com/freddylist/llama/blob/master/src/List/toSet.lua -local function toSet(list) - local set = {} - - for _, v in ipairs(list) do - set[v] = true - end - - return set -end - --- https://github.com/freddylist/llama/blob/master/src/Dictionary/values.lua -local function values(dictionary) - local valuesList = {} - - local index = 1 - - for _, value in pairs(dictionary) do - valuesList[index] = value - index = index + 1 - end - - return valuesList -end - -local valueIds = {} -local nextValueId = 0 -local compatibilityCache = {} -local archetypeCache = {} - -local function getValueId(value) - local valueId = valueIds[value] - if valueId == nil then - valueIds[value] = nextValueId - valueId = nextValueId - nextValueId += 1 - end - - return valueId -end - -function archetypeOf(...) - local length = select("#", ...) - - local currentNode = archetypeCache - - for i = 1, length do - local nextNode = currentNode[select(i, ...)] - - if not nextNode then - nextNode = {} - currentNode[select(i, ...)] = nextNode - end - - currentNode = nextNode - end - - if currentNode._archetype then - return currentNode._archetype - end - - local list = table.create(length) - - for i = 1, length do - list[i] = getValueId(select(i, ...)) - end - - table.sort(list) - - local archetype = table.concat(list, "_") - - currentNode._archetype = archetype - - return archetype -end - -function negateArchetypeOf(...) - return string.gsub(archetypeOf(...), "_", "x") -end - -function areArchetypesCompatible(queryArchetype, targetArchetype) - local archetypes = string.split(queryArchetype, "x") - local baseArchetype = table.remove(archetypes, 1) - - local cachedCompatibility = compatibilityCache[queryArchetype .. "-" .. targetArchetype] - if cachedCompatibility ~= nil then - return cachedCompatibility - end - - local queryIds = string.split(baseArchetype, "_") - local targetIds = toSet(string.split(targetArchetype, "_")) - local excludeIds = toSet(archetypes) - - for _, queryId in ipairs(queryIds) do - if targetIds[queryId] == nil then - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false - return false - end - end - - for excludeId in excludeIds do - if targetIds[excludeId] then - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = false - return false - end - end - - compatibilityCache[queryArchetype .. "-" .. targetArchetype] = true - - return true - -end - -local stack = {} - -local function newStackFrame(node) - return { - node = node, - accessedKeys = {}, - } -end - -local function cleanup() - local currentFrame = stack[#stack] - - for baseKey, state in pairs(currentFrame.node.system) do - for key, value in pairs(state.storage) do - if not currentFrame.accessedKeys[baseKey] or not currentFrame.accessedKeys[baseKey][key] then - local cleanupCallback = state.cleanupCallback - - if cleanupCallback then - local shouldAbortCleanup = cleanupCallback(value) - - if shouldAbortCleanup then - continue - end - end - - state.storage[key] = nil - end - end - end -end - -local function start(node, fn) - table.insert(stack, newStackFrame(node)) - fn() - cleanup() - table.remove(stack, #stack) -end - -local function withinTopoContext() - return #stack ~= 0 -end - -local function useFrameState() - return stack[#stack].node.frame -end - -local function useCurrentSystem() - if #stack == 0 then - return - end - - return stack[#stack].node.currentSystem -end - - ---[=[ - @within Matter - - :::tip - **Don't use this function directly in your systems.** - - This function is used for implementing your own topologically-aware functions. It should not be used in your - systems directly. You should use this function to implement your own utilities, similar to `useEvent` and - `useThrottle`. - ::: - - `useHookState` does one thing: it returns a table. An empty, pristine table. Here's the cool thing though: - it always returns the *same* table, based on the script and line where *your function* (the function calling - `useHookState`) was called. - - ### Uniqueness - - If your function is called multiple times from the same line, perhaps within a loop, the default behavior of - `useHookState` is to uniquely identify these by call count, and will return a unique table for each call. - - However, you can override this behavior: you can choose to key by any other value. This means that in addition to - script and line number, the storage will also only return the same table if the unique value (otherwise known as the - "discriminator") is the same. - - ### Cleaning up - As a second optional parameter, you can pass a function that is automatically invoked when your storage is about - to be cleaned up. This happens when your function (and by extension, `useHookState`) ceases to be called again - next frame (keyed by script, line number, and discriminator). - - Your cleanup callback is passed the storage table that's about to be cleaned up. You can then perform cleanup work, - like disconnecting events. - - *Or*, you could return `true`, and abort cleaning up altogether. If you abort cleanup, your storage will stick - around another frame (even if your function wasn't called again). This can be used when you know that the user will - (or might) eventually call your function again, even if they didn't this frame. (For example, caching a value for - a number of seconds). - - If cleanup is aborted, your cleanup function will continue to be called every frame, until you don't abort cleanup, - or the user actually calls your function again. - - ### Example: useThrottle - - This is the entire implementation of the built-in `useThrottle` function: - - ```lua - local function cleanup(storage) - return os.clock() < storage.expiry - end - - local function useThrottle(seconds, discriminator) - local storage = useHookState(discriminator, cleanup) - - if storage.time == nil or os.clock() - storage.time >= seconds then - storage.time = os.clock() - storage.expiry = os.clock() + seconds - return true - end - - return false - end - ``` - - A lot of talk for something so simple, right? - - @param discriminator? any -- A unique value to additionally key by - @param cleanupCallback (storage: {}) -> boolean? -- A function to run when the storage for this hook is cleaned up -]=] -local function useHookState(discriminator, cleanupCallback): {} - local file, line = debug.info(3, "sl") - local fn = debug.info(2, "f") - - local baseKey = string.format("%s:%s:%d", tostring(fn), file, line) - - local currentFrame = stack[#stack] - - if currentFrame == nil then - error("Attempt to access topologically-aware storage outside of a Loop-system context.", 3) - end - - if not currentFrame.accessedKeys[baseKey] then - currentFrame.accessedKeys[baseKey] = {} - end - - local accessedKeys = currentFrame.accessedKeys[baseKey] - - local key = #accessedKeys - - if discriminator ~= nil then - if type(discriminator) == "number" then - discriminator = tostring(discriminator) - end - - key = discriminator - end - - accessedKeys[key] = true - - if not currentFrame.node.system[baseKey] then - currentFrame.node.system[baseKey] = { - storage = {}, - cleanupCallback = cleanupCallback, - } - end - - local storage = currentFrame.node.system[baseKey].storage - - if not storage[key] then - storage[key] = {} - end - - return storage[key] -end - -local topoRuntime = { - start = start, - useHookState = useHookState, - useFrameState = useFrameState, - useCurrentSystem = useCurrentSystem, - withinTopoContext = withinTopoContext, -} - - - -local ERROR_NO_ENTITY = "Entity doesn't exist, use world:contains to check if needed" - ---[=[ - @class World - - A World contains entities which have components. - The World is queryable and can be used to get entities with a specific set of components. - Entities are simply ever-increasing integers. -]=] -local World = {} -World.__index = World - ---[=[ - Creates a new World. -]=] -function World.new() - local firstStorage = {} - - return setmetatable({ - -- List of maps from archetype string --> entity ID --> entity data - _storages = { firstStorage }, - -- The most recent storage that has not been dirtied by an iterator - _pristineStorage = firstStorage, - - -- Map from entity ID -> archetype string - _entityArchetypes = {}, - - -- Cache of the component metatables on each entity. Used for generating archetype. - -- Map of entity ID -> array - _entityMetatablesCache = {}, - - -- Cache of what query archetypes are compatible with what component archetypes - _queryCache = {}, - - -- Cache of what entity archetypes have ever existed in the game. This is used for knowing - -- when to update the queryCache. - _entityArchetypeCache = {}, - - -- The next ID that will be assigned with World:spawn - _nextId = 1, - - -- The total number of active entities in the world - _size = 0, - - -- Storage for `queryChanged` - _changedStorage = {}, - }, World) -end - --- Searches all archetype storages for the entity with the given archetype --- Returns the storage that the entity is in if it exists, otherwise nil -function World:_getStorageWithEntity(archetype, id) - for _, storage in self._storages do - local archetypeStorage = storage[archetype] - if archetypeStorage then - if archetypeStorage[id] then - return storage - end - end - end - return nil -end - -function World:_markStorageDirty() - local newStorage = {} - table.insert(self._storages, newStorage) - self._pristineStorage = newStorage - - if topoRuntime.withinTopoContext() then - local frameState = topoRuntime.useFrameState() - - frameState.dirtyWorlds[self] = true - end -end - -function World:_getEntity(id) - local archetype = self._entityArchetypes[id] - local storage = self:_getStorageWithEntity(archetype, id) - - return storage[archetype][id] -end - -function World:_next(last) - local entityId, archetype = next(self._entityArchetypes, last) - - if entityId == nil then - return nil - end - - local storage = self:_getStorageWithEntity(archetype, entityId) - - return entityId, storage[archetype][entityId] -end - ---[=[ - Iterates over all entities in this World. Iteration returns entity ID followed by a dictionary mapping - Component to Component Instance. - - **Usage:** - - ```lua - for entityId, entityData in world do - print(entityId, entityData[Components.Example]) - end - ``` - - @return number - @return {[Component]: ComponentInstance} -]=] -function World:__iter() - return World._next, self -end - ---[=[ - Spawns a new entity in the world with the given components. - - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The new entity ID. -]=] -function World:spawn(...) - return self:spawnAt(self._nextId, ...) -end - ---[=[ - @class Component - - A component is a named piece of data that exists on an entity. - Components are created and removed in the [World](/api/World). - - In the docs, the terms "Component" and "ComponentInstance" are used: - - **"Component"** refers to the base class of a specific type of component you've created. - This is what [`Matter.component`](/api/Matter#component) returns. - - **"Component Instance"** refers to an actual piece of data that can exist on an entity. - The metatable of a component instance table is its respective Component table. - - Component instances are *plain-old data*: they do not contain behaviors or methods. - - Since component instances are immutable, one helper function exists on all component instances, `patch`, - which allows reusing data from an existing component instance to make up for the ergonomic loss of mutations. -]=] - ---[=[ - @within Component - @type ComponentInstance {} - - The `ComponentInstance` type refers to an actual piece of data that can exist on an entity. - The metatable of the component instance table is set to its particular Component table. - - A component instance can be created by calling the Component table: - - ```lua - -- Component: - local MyComponent = Matter.component("My component") - - -- component instance: - local myComponentInstance = MyComponent({ - some = "data" - }) - - print(getmetatable(myComponentInstance) == MyComponent) --> true - ``` -]=] - --- This is a special value we set inside the component's metatable that will allow us to detect when --- a Component is accidentally inserted as a Component Instance. --- It should not be accessible through indexing into a component instance directly. -local DIAGNOSTIC_COMPONENT_MARKER = {} - -local function newComponent(name, defaultData) - name = name or debug.info(2, "s") .. "@" .. debug.info(2, "l") - - assert( - defaultData == nil or type(defaultData) == "table", - "if component default data is specified, it must be a table" - ) - - local component = {} - component.__index = component - - function component.new(data) - data = data or {} - - if defaultData then - data = merge(defaultData, data) - end - - return table.freeze(setmetatable(data, component)) - end - - --[=[ - @within Component - - ```lua - for id, target in world:query(Target) do - if shouldChangeTarget(target) then - world:insert(id, target:patch({ -- modify the existing component - currentTarget = getNewTarget() - })) - end - end - ``` - - A utility function used to immutably modify an existing component instance. Key/value pairs from the passed table - will override those of the existing component instance. - - As all components are immutable and frozen, it is not possible to modify the existing component directly. - - You can use the `Matter.None` constant to remove a value from the component instance: - - ```lua - target:patch({ - currentTarget = Matter.None -- sets currentTarget to nil - }) - ``` - - @param partialNewData {} -- The table to be merged with the existing component data. - @return ComponentInstance -- A copy of the component instance with values from `partialNewData` overriding existing values. - ]=] - function component:patch(partialNewData) - local patch = getmetatable(self).new(merge(self, partialNewData)) - return patch - end - - setmetatable(component, { - __call = function(_, ...) - return component.new(...) - end, - __tostring = function() - return name - end, - [DIAGNOSTIC_COMPONENT_MARKER] = true, - }) - - return component -end - -local function assertValidType(value, position) - if typeof(value) ~= "table" then - error(string.format("Component #%d is invalid: not a table", position), 3) - end - - local metatable = getmetatable(value) - - if metatable == nil then - error(string.format("Component #%d is invalid: has no metatable", position), 3) - end -end - -local function assertValidComponent(value, position) - assertValidType(value, position) - - local metatable = getmetatable(value) - - if getmetatable(metatable) ~= nil and getmetatable(metatable)[DIAGNOSTIC_COMPONENT_MARKER] then - error( - string.format( - "Component #%d is invalid: Component Instance %s was passed instead of the Component itself!", - position, - tostring(metatable) - ), - 3 - ) - end -end - -local function assertValidComponentInstance(value, position) - assertValidType(value, position) - - if getmetatable(value)[DIAGNOSTIC_COMPONENT_MARKER] ~= nil then - error( - string.format( - "Component #%d is invalid: passed a Component instead of a Component instance; " - .. "did you forget to call it as a function?", - position - ), - 3 - ) - end -end - ---[=[ - Spawns a new entity in the world with a specific entity ID and given components. - - The next ID generated from [World:spawn] will be increased as needed to never collide with a manually specified ID. - - @param id number -- The entity ID to spawn with - @param ... ComponentInstance -- The component values to spawn the entity with. - @return number -- The same entity ID that was passed in -]=] -function World:spawnAt(id, ...) - if self:contains(id) then - error( - string.format( - "The world already contains an entity with ID %d. Use World:replace instead if this is intentional.", - id - ), - 2 - ) - end - - self._size += 1 - - if id >= self._nextId then - self._nextId = id + 1 - end - - local components = {} - local metatables = {} - - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) - end - - self:_trackChanged(metatable, id, nil, newComponent) - - components[metatable] = newComponent - table.insert(metatables, metatable) - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, components) - - return id -end - -function World:_newQueryArchetype(queryArchetype) - if self._queryCache[queryArchetype] == nil then - self._queryCache[queryArchetype] = {} - else - return -- Archetype isn't actually new - end - - for _, storage in self._storages do - for entityArchetype in storage do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - self._queryCache[queryArchetype][entityArchetype] = true - end - end - end -end - -function World:_updateQueryCache(entityArchetype) - for queryArchetype, compatibleArchetypes in pairs(self._queryCache) do - if areArchetypesCompatible(queryArchetype, entityArchetype) then - compatibleArchetypes[entityArchetype] = true - end - end -end - -function World:_transitionArchetype(id, components) - local newArchetype = nil - local oldArchetype = self._entityArchetypes[id] - local oldStorage - - if oldArchetype then - oldStorage = self:_getStorageWithEntity(oldArchetype, id) - - if not components then - oldStorage[oldArchetype][id] = nil - end - end - - if components then - newArchetype = archetypeOf(unpack(self._entityMetatablesCache[id])) - - if oldArchetype ~= newArchetype then - if oldStorage then - oldStorage[oldArchetype][id] = nil - end - - if self._pristineStorage[newArchetype] == nil then - self._pristineStorage[newArchetype] = {} - end - - if self._entityArchetypeCache[newArchetype] == nil then - self._entityArchetypeCache[newArchetype] = true - self:_updateQueryCache(newArchetype) - end - self._pristineStorage[newArchetype][id] = components - else - oldStorage[newArchetype][id] = components - end - end - - self._entityArchetypes[id] = newArchetype -end - ---[=[ - Replaces a given entity by ID with an entirely new set of components. - Equivalent to removing all components from an entity, and then adding these ones. - - @param id number -- The entity ID - @param ... ComponentInstance -- The component values to spawn the entity with. -]=] -function World:replace(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local components = {} - local metatables = {} - local entity = self:_getEntity(id) - - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - if components[metatable] then - error(("Duplicate component type at index %d"):format(i), 2) - end - - self:_trackChanged(metatable, id, entity[metatable], newComponent) - - components[metatable] = newComponent - table.insert(metatables, metatable) - end - - for metatable, component in pairs(entity) do - if not components[metatable] then - self:_trackChanged(metatable, id, component, nil) - end - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, components) -end - ---[=[ - Despawns a given entity by ID, removing it and all its components from the world entirely. - - @param id number -- The entity ID -]=] -function World:despawn(id) - local entity = self:_getEntity(id) - - for metatable, component in pairs(entity) do - self:_trackChanged(metatable, id, component, nil) - end - - self._entityMetatablesCache[id] = nil - self:_transitionArchetype(id, nil) - - self._size -= 1 -end - ---[=[ - Removes all entities from the world. - - :::caution - Removing entities in this way is not reported by `queryChanged`. - ::: -]=] -function World:clear() - local firstStorage = {} - self._storages = { firstStorage } - self._pristineStorage = firstStorage - self._entityArchetypes = {} - self._entityMetatablesCache = {} - self._size = 0 - self._changedStorage = {} -end - ---[=[ - Checks if the given entity ID is currently spawned in this world. - - @param id number -- The entity ID - @return bool -- `true` if the entity exists -]=] -function World:contains(id) - return self._entityArchetypes[id] ~= nil -end - ---[=[ - Gets a specific component (or set of components) from a specific entity in this world. - - @param id number -- The entity ID - @param ... Component -- The components to fetch - @return ... -- Returns the component values in the same order they were passed in -]=] -function World:get(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local length = select("#", ...) - - if length == 1 then - assertValidComponent((...), 1) - return entity[...] - end - - local components = {} - for i = 1, length do - local metatable = select(i, ...) - assertValidComponent(metatable, i) - components[i] = entity[metatable] - end - - return unpack(components, 1, length) -end - -local function noop() end - -local noopQuery = setmetatable({ - next = noop, - snapshot = noop, - without = function(self) - return self - end, - view = { - get = noop, - contains = noop, - }, -}, { - __iter = function() - return noop - end, -}) - ---[=[ - @class QueryResult - - A result from the [`World:query`](/api/World#query) function. - - Calling the table or the `next` method allows iteration over the results. Once all results have been returned, the - QueryResult is exhausted and is no longer useful. - - ```lua - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - ``` -]=] - -local QueryResult = {} -QueryResult.__index = QueryResult - -function QueryResult.new(world, expand, queryArchetype, compatibleArchetypes) - return setmetatable({ - world = world, - seenEntities = {}, - currentCompatibleArchetype = next(compatibleArchetypes), - compatibleArchetypes = compatibleArchetypes, - storageIndex = 1, - _expand = expand, - _queryArchetype = queryArchetype, - }, QueryResult) -end - -local function nextItem(query) - local world = query.world - local currentCompatibleArchetype = query.currentCompatibleArchetype - local seenEntities = query.seenEntities - local compatibleArchetypes = query.compatibleArchetypes - - local entityId, entityData - - local storages = world._storages - repeat - local nextStorage = storages[query.storageIndex] - local currently = nextStorage[currentCompatibleArchetype] - if currently then - entityId, entityData = next(currently, query.lastEntityId) - end - - while entityId == nil do - currentCompatibleArchetype = next(compatibleArchetypes, currentCompatibleArchetype) - - if currentCompatibleArchetype == nil then - query.storageIndex += 1 - - nextStorage = storages[query.storageIndex] - - if nextStorage == nil or next(nextStorage) == nil then - return - end - - currentCompatibleArchetype = nil - - if world._pristineStorage == nextStorage then - world:_markStorageDirty() - end - - continue - elseif nextStorage[currentCompatibleArchetype] == nil then - continue - end - - entityId, entityData = next(nextStorage[currentCompatibleArchetype]) - end - - query.lastEntityId = entityId - - until seenEntities[entityId] == nil - - query.currentCompatibleArchetype = currentCompatibleArchetype - - seenEntities[entityId] = true - - return entityId, entityData -end - -function QueryResult:__iter() - return function() - return self._expand(nextItem(self)) - end -end - -function QueryResult:__call() - return self._expand(nextItem(self)) -end - ---[=[ - Returns the next set of values from the query result. Once all results have been returned, the - QueryResult is exhausted and is no longer useful. - - :::info - This function is equivalent to calling the QueryResult as a function. When used in a for loop, this is implicitly - done by the language itself. - ::: - - ```lua - -- Using world:query in this position will make Lua invoke the table as a function. This is conventional. - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - ``` - - If you wanted to iterate over the QueryResult without a for loop, it's recommended that you call `next` directly - instead of calling the QueryResult as a function. - ```lua - local id, enemy, charge, model = world:query(Enemy, Charge, Model):next() - local id, enemy, charge, model = world:query(Enemy, Charge, Model)() -- Possible, but unconventional - ``` - - @return id -- Entity ID - @return ...ComponentInstance -- The requested component values -]=] -function QueryResult:next() - return self._expand(nextItem(self)) -end - -local snapshot = { - __iter = function(self): any - local i = 0 - return function() - i += 1 - - local data = self[i] - - if data then - return unpack(data, 1, data.n) - end - return - end - end, -} - ---[=[ - Creates a "snapshot" of this query, draining this QueryResult and returning a list containing all of its results. - - By default, iterating over a QueryResult happens in "real time": it iterates over the actual data in the ECS, so - changes that occur during the iteration will affect future results. - - By contrast, `QueryResult:snapshot()` creates a list of all of the results of this query at the moment it is called, - so changes made while iterating over the result of `QueryResult:snapshot` do not affect future results of the - iteration. - - Of course, this comes with a cost: we must allocate a new list and iterate over everything returned from the - QueryResult in advance, so using this method is slower than iterating over a QueryResult directly. - - The table returned from this method has a custom `__iter` method, which lets you use it as you would use QueryResult - directly: - - ```lua - for entityId, health, player in world:query(Health, Player):snapshot() do - - end - ``` - - However, the table itself is just a list of sub-tables structured like `{entityId, component1, component2, ...etc}`. - - @return {{entityId: number, component: ComponentInstance, component: ComponentInstance, component: ComponentInstance, ...}} -]=] -function QueryResult:snapshot() - local list = setmetatable({}, snapshot) - - local function iter() - return nextItem(self) - end - - for entityId, entityData in iter do - if entityId then - table.insert(list, table.pack(self._expand(entityId, entityData))) - end - end - - return list -end - ---[=[ - Returns an iterator that will skip any entities that also have the given components. - - :::tip - This is essentially equivalent to querying normally, using `World:get` to check if a component is present, - and using Lua's `continue` keyword to skip this iteration (though, using `:without` is faster). - - This means that you should avoid queries that return a very large amount of results only to filter them down - to a few with `:without`. If you can, always prefer adding components and making your query more specific. - ::: - - @param ... Component -- The component types to filter against. - @return () -> (id, ...ComponentInstance) -- Iterator of entity ID followed by the requested component values - - ```lua - for id in world:query(Target):without(Model) do - -- Do something - end - ``` -]=] - -function QueryResult:without(...) - local world = self.world - local filter = negateArchetypeOf(...) - - local negativeArchetype = `{self._queryArchetype}x{filter}` - - if world._queryCache[negativeArchetype] == nil then - world:_newQueryArchetype(negativeArchetype) - end - - local compatibleArchetypes = world._queryCache[negativeArchetype] - - self.compatibleArchetypes = compatibleArchetypes - self.currentCompatibleArchetype = next(compatibleArchetypes) - return self -end - ---[=[ - @class View - - Provides random access to the results of a query. - - Calling the View is equivalent to iterating a query. - - ```lua - for id, player, health, poison in world:query(Player, Health, Poison):view() do - -- Do something - end - ``` -]=] - ---[=[ - Creates a View of the query and does all of the iterator tasks at once at an amortized cost. - This is used for many repeated random access to an entity. If you only need to iterate, just use a query. - - ```lua - local inflicting = world:query(Damage, Hitting, Player):view() - for _, source in world:query(DamagedBy) do - local damage = inflicting:get(source.from) - end - - for _ in world:query(Damage):view() do end -- You can still iterate views if you want! - ``` - - @return View See [View](/api/View) docs. -]=] - -function QueryResult:view() - local function iter() - return nextItem(self) - end - - local fetches = {} - local list = {} :: any - - local View = {} - View.__index = View - - function View:__iter() - local current = list.head - return function() - if not current then - return - end - local entity = current.entity - local fetch = fetches[entity] - current = current.next - - return entity, unpack(fetch, 1, fetch.n) - end - end - - --[=[ - @within View - Retrieve the query results to corresponding `entity` - @param entity number - the entity ID - @return ...ComponentInstance - ]=] - function View:get(entity) - if not self:contains(entity) then - return - end - - local fetch = fetches[entity] - local queryLength = fetch.n - - if queryLength == 1 then - return fetch[1] - elseif queryLength == 2 then - return fetch[1], fetch[2] - elseif queryLength == 3 then - return fetch[1], fetch[2], fetch[3] - elseif queryLength == 4 then - return fetch[1], fetch[2], fetch[3], fetch[4] - elseif queryLength == 5 then - return fetch[1], fetch[2], fetch[3], fetch[4], fetch[5] - end - - return unpack(fetch, 1, fetch.n) - end - - --[=[ - @within View - Equivalent to `world:contains()` - @param entity number - the entity ID - @return boolean - ]=] - - function View:contains(entity) - return fetches[entity] ~= nil - end - - for entityId, entityData in iter do - if entityId then - -- We start at 2 on Select since we don't need want to pack the entity id. - local fetch = table.pack(select(2, self._expand(entityId, entityData))) - local node = { entity = entityId, next = nil } - - fetches[entityId] = fetch - - if not list.head then - list.head = node - else - local current = list.head - while current.next do - current = current.next - end - current.next = node - end - end - end - - return setmetatable({}, View) -end - ---[=[ - Performs a query against the entities in this World. Returns a [QueryResult](/api/QueryResult), which iterates over - the results of the query. - - Order of iteration is not guaranteed. - - ```lua - for id, enemy, charge, model in world:query(Enemy, Charge, Model) do - -- Do something - end - - for id in world:query(Target):without(Model) do - -- Again, with feeling - end - ``` - - @param ... Component -- The component types to query. Only entities with *all* of these components will be returned. - @return QueryResult -- See [QueryResult](/api/QueryResult) docs. -]=] - -function World:query(...) - assertValidComponent((...), 1) - - local metatables = { ... } - local queryLength = select("#", ...) - - local archetype = archetypeOf(...) - - if self._queryCache[archetype] == nil then - self:_newQueryArchetype(archetype) - end - - local compatibleArchetypes = self._queryCache[archetype] - - if next(compatibleArchetypes) == nil then - -- If there are no compatible storages avoid creating our complicated iterator - return noopQuery - end - - local queryOutput = table.create(queryLength) - - local function expand(entityId, entityData) - if not entityId then - return - end - - if queryLength == 1 then - return entityId, entityData[metatables[1]] - elseif queryLength == 2 then - return entityId, entityData[metatables[1]], entityData[metatables[2]] - elseif queryLength == 3 then - return entityId, entityData[metatables[1]], entityData[metatables[2]], entityData[metatables[3]] - elseif queryLength == 4 then - return entityId, - entityData[metatables[1]], - entityData[metatables[2]], - entityData[metatables[3]], - entityData[metatables[4]] - elseif queryLength == 5 then - return entityId, - entityData[metatables[1]], - entityData[metatables[2]], - entityData[metatables[3]], - entityData[metatables[4]], - entityData[metatables[5]] - end - - for i, metatable in ipairs(metatables) do - queryOutput[i] = entityData[metatable] - end - - return entityId, unpack(queryOutput, 1, queryLength) - end - - if self._pristineStorage == self._storages[1] then - self:_markStorageDirty() - end - - return QueryResult.new(self, expand, archetype, compatibleArchetypes) -end - -local function cleanupQueryChanged(hookState) - local world = hookState.world - local componentToTrack = hookState.componentToTrack - - for index, object in world._changedStorage[componentToTrack] do - if object == hookState.storage then - table.remove(world._changedStorage[componentToTrack], index) - break - end - end - - if next(world._changedStorage[componentToTrack]) == nil then - world._changedStorage[componentToTrack] = nil - end -end - ---[=[ - @interface ChangeRecord - @within World - .new? ComponentInstance -- The new value of the component. Nil if just removed. - .old? ComponentInstance -- The former value of the component. Nil if just added. -]=] - ---[=[ - :::info Topologically-aware function - This function is only usable if called within the context of [`Loop:begin`](/api/Loop#begin). - ::: - - Queries for components that have changed **since the last time your system ran `queryChanged`**. - - Only one changed record is returned per entity, even if the same entity changed multiple times. The order - in which changed records are returned is not guaranteed to be the order that the changes occurred in. - - It should be noted that `queryChanged` does not have the same iterator invalidation concerns as `World:query`. - - :::tip - The first time your system runs (i.e., on the first frame), all existing entities in the world that match your query - are returned as "new" change records. - ::: - - :::info - Calling this function from your system creates storage internally for your system. Then, changes meeting your - criteria are pushed into your storage. Calling `queryChanged` again each frame drains this storage. - - If your system isn't called every frame, the storage will continually fill up and does not empty unless you drain - it. - - If you stop calling `queryChanged` in your system, changes will stop being tracked. - ::: - - ### Returns - `queryChanged` returns an iterator function, so you call it in a for loop just like `World:query`. - - The iterator returns the entity ID, followed by a [`ChangeRecord`](#ChangeRecord). - - The `ChangeRecord` type is a table that contains two fields, `new` and `old`, respectively containing the new - component instance, and the old component instance. `new` and `old` will never be the same value. - - `new` will be nil if the component was removed (or the entity was despawned), and `old` will be nil if the - component was just added. - - The `old` field will be the value of the component the last time this system observed it, not - necessarily the value it changed from most recently. - - The `ChangeRecord` table is potentially shared with multiple systems tracking changes for this component, so it - cannot be modified. - - ```lua - for id, record in world:queryChanged(Model) do - if record.new == nil then - -- Model was removed - - if enemy.type == "this is a made up example" then - world:remove(id, Enemy) - end - end - end - ``` - - @param componentToTrack Component -- The component you want to listen to changes for. - @return () -> (id, ChangeRecord) -- Iterator of entity ID and change record -]=] -function World:queryChanged(componentToTrack, ...: nil) - if ... then - error("World:queryChanged does not take any additional parameters", 2) - end - - local hookState = topoRuntime.useHookState(componentToTrack, cleanupQueryChanged) - - if hookState.storage then - return function(): any - local entityId, record = next(hookState.storage) - - if entityId then - hookState.storage[entityId] = nil - - return entityId, record - end - return - end - end - - if not self._changedStorage[componentToTrack] then - self._changedStorage[componentToTrack] = {} - end - - local storage = {} - hookState.storage = storage - hookState.world = self - hookState.componentToTrack = componentToTrack - - table.insert(self._changedStorage[componentToTrack], storage) - - local queryResult = self:query(componentToTrack) - - return function(): any - local entityId, component = queryResult:next() - - if entityId then - return entityId, table.freeze({ new = component }) - end - return - end -end - -function World:_trackChanged(metatable, id, old, new) - if not self._changedStorage[metatable] then - return - end - - if old == new then - return - end - - local record = table.freeze({ - old = old, - new = new, - }) - - for _, storage in ipairs(self._changedStorage[metatable]) do - -- If this entity has changed since the last time this system read it, - -- we ensure that the "old" value is whatever the system saw it as last, instead of the - -- "old" value we have here. - if storage[id] then - storage[id] = table.freeze({ old = storage[id].old, new = new }) - else - storage[id] = record - end - end -end - ---[=[ - Inserts a component (or set of components) into an existing entity. - - If another instance of a given component already exists on this entity, it is replaced. - - ```lua - world:insert( - entityId, - ComponentA({ - foo = "bar" - }), - ComponentB({ - baz = "qux" - }) - ) - ``` - - @param id number -- The entity ID - @param ... ComponentInstance -- The component values to insert -]=] -function World:insert(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local wasNew = false - for i = 1, select("#", ...) do - local newComponent = select(i, ...) - - assertValidComponentInstance(newComponent, i) - - local metatable = getmetatable(newComponent) - - local oldComponent = entity[metatable] - - if not oldComponent then - wasNew = true - - table.insert(self._entityMetatablesCache[id], metatable) - end - - self:_trackChanged(metatable, id, oldComponent, newComponent) - - entity[metatable] = newComponent - end - - if wasNew then -- wasNew - self:_transitionArchetype(id, entity) - end -end - ---[=[ - Removes a component (or set of components) from an existing entity. - - ```lua - local removedA, removedB = world:remove(entityId, ComponentA, ComponentB) - ``` - - @param id number -- The entity ID - @param ... Component -- The components to remove - @return ...ComponentInstance -- Returns the component instance values that were removed in the order they were passed. -]=] -function World:remove(id, ...) - if not self:contains(id) then - error(ERROR_NO_ENTITY, 2) - end - - local entity = self:_getEntity(id) - - local length = select("#", ...) - local removed = {} - - for i = 1, length do - local metatable = select(i, ...) - - assertValidComponent(metatable, i) - - local oldComponent = entity[metatable] - - removed[i] = oldComponent - - self:_trackChanged(metatable, id, oldComponent, nil) - - entity[metatable] = nil - end - - -- Rebuild entity metatable cache - local metatables = {} - - for metatable in pairs(entity) do - table.insert(metatables, metatable) - end - - self._entityMetatablesCache[id] = metatables - - self:_transitionArchetype(id, entity) - - return unpack(removed, 1, length) -end - ---[=[ - Returns the number of entities currently spawned in the world. -]=] -function World:size() - return self._size -end - ---[=[ - :::tip - [Loop] automatically calls this function on your World(s), so there is no need to call it yourself if you're using - a Loop. - ::: - - If you are not using a Loop, you should call this function at a regular interval (i.e., once per frame) to optimize - the internal storage for queries. - - This is part of a strategy to eliminate iterator invalidation when modifying the World while inside a query from - [World:query]. While inside a query, any changes to the World are stored in a separate location from the rest of - the World. Calling this function combines the separate storage back into the main storage, which speeds things up - again. -]=] -function World:optimizeQueries() - if #self._storages == 1 then - return - end - - local firstStorage = self._storages[1] - - for i = 2, #self._storages do - local storage = self._storages[i] - - for archetype, entities in storage do - if firstStorage[archetype] == nil then - firstStorage[archetype] = entities - else - for entityId, entityData in entities do - if firstStorage[archetype][entityId] then - error("Entity ID already exists in first storage...") - end - firstStorage[archetype][entityId] = entityData - end - end - end - end - - table.clear(self._storages) - - self._storages[1] = firstStorage - self._pristineStorage = firstStorage -end - -return { - World = World, - component = newComponent -} From 5476491c5d6d62cf80f69927f744d2611140315e Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 5 May 2024 03:25:34 +0200 Subject: [PATCH 04/46] Should index into column --- lib/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/init.lua b/lib/init.lua index b3e31bc..6d9c1fe 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -560,7 +560,7 @@ function World.query(world: World, ...: i53): Query end for i in components do - queryOutput[i] = tr[i][row] + queryOutput[i] = columns[tr[i]][row] end return entityId, unpack(queryOutput, 1, queryLength) From 7ad1ef37f0823879e7da95a1f856547cda3b701d Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 5 May 2024 03:45:38 +0200 Subject: [PATCH 05/46] Add exhaustive benchmarks for operations --- benches/exhaustive.lua | 372 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 benches/exhaustive.lua diff --git a/benches/exhaustive.lua b/benches/exhaustive.lua new file mode 100644 index 0000000..3095c70 --- /dev/null +++ b/benches/exhaustive.lua @@ -0,0 +1,372 @@ +local testkit = require("../testkit") +local jecs = require("../lib/init") +local ecr = require("../DevPackages/_Index/centau_ecr@0.8.0/ecr/src/ecr") + + +local BENCH, START = testkit.benchmark() + +local function TITLE(title: string) + print() + print(testkit.color.white(title)) +end + +local N = 2^16-2 + +type i53 = number + +do TITLE "create" + BENCH("entity", function() + local world = jecs.World.new() + for i = 1, START(N) do + world:entity() + end + end) +end + +--- component benchmarks + +--todo: perform the same benchmarks for multiple components.? +-- these kind of operations only support 1 component at a time, which is +-- a shame, especially for archetypes where moving components is expensive. + +do TITLE "set" + BENCH("add 1 component", function() + local world = jecs.World.new() + local entities = {} + + local A = world:component() + + for i = 1, N do + entities[i] = world:entity() + end + + for i = 1, START(N) do + world:set(entities[i], A, i) + end + end) + + BENCH("change 1 component", function() + local world = jecs.World.new() + local entities = {} + + local A = world:component() + local e = world:entity() + world:set(e, A, 1) + + for i = 1, START(N) do + world:set(e, A, 2) + end + end) + +end + +do TITLE "remove" + BENCH("1 component", function() + local world = jecs.World.new() + local entities = {} + + local A = world:component() + + for i = 1, N do + local id = world:entity() + entities[i] = id + world:set(id, A, true) + end + + for i = 1, START(N) do + world:remove(entities[i], A) + end + + end) +end + +do TITLE "get" + BENCH("1 component", function() + local world = jecs.World.new() + local entities = {} + + local A = world:component() + + for i = 1, N do + local id = world:entity() + entities[i] = id + world:set(id, A, true) + end + + for i = 1, START(N) do + -- ? curious why the overhead is roughly 80 ns. + world:get(entities[i], A) + end + + end) + + BENCH("2 component", function() + local world = jecs.World.new() + local entities = {} + + local A = world:component() + local B = world:component() + + for i = 1, N do + local id = world:entity() + entities[i] = id + world:set(id, A, true) + world:set(id, B, true) + end + + for i = 1, START(N) do + world:get(entities[i], A, B) + end + + end) + + BENCH("3 component", function() + local world = jecs.World.new() + local entities = {} + + local A = world:component() + local B = world:component() + local C = world:component() + + for i = 1, N do + local id = world:entity() + entities[i] = id + world:set(id, A, true) + world:set(id, B, true) + world:set(id, C, true) + end + + for i = 1, START(N) do + world:get(entities[i], A, B, C) + end + + end) + + BENCH("4 component", function() + local world = jecs.World.new() + local entities = {} + + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + + for i = 1, N do + local id = world:entity() + entities[i] = id + world:set(id, A, true) + world:set(id, B, true) + world:set(id, C, true) + world:set(id, D, true) + end + + for i = 1, START(N) do + world:get(entities[i], A, B, C, D) + end + + end) +end + +do TITLE (testkit.color.white_underline("Jecs query")) + + local function count(query: () -> ()) + local n = 0 + for _ in query do + n += 1 + end + return n + end + + local function flip() + return math.random() > 0.5 + end + + local function view_bench( + world: jecs.World, + A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53 + ) + + BENCH("1 component", function() + START(count(world:query(A))) + for _ in world:query(A) do end + end) + + BENCH("2 component", function() + START(count(world:query(A, B))) + for _ in world:query(A, B) do end + end) + + BENCH("4 component", function() + START(count(world:query(A, B, C, D))) + for _ in world:query(A, B, C, D) do end + end) + + BENCH("8 component", function() + START(count(world:query(A, B, C, D, E, F, G, H))) + for _ in world:query(A, B, C, D, E, F, G, H) do end + end) + end + + do TITLE "random components" + + local world = jecs.World.new() + + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + local E = world:component() + local F = world:component() + local G = world:component() + local H = world:component() + local I = world:component() + + for i = 1, N do + local id = world:entity() + if flip() then world:set(id, A, true) end + if flip() then world:set(id, B, true) end + if flip() then world:set(id, C, true) end + if flip() then world:set(id, D, true) end + if flip() then world:set(id, E, true) end + if flip() then world:set(id, F, true) end + if flip() then world:set(id, G, true) end + if flip() then world:set(id, H, true) end + if flip() then world:set(id, I, true) end + + end + + view_bench(world, A, B, C, D, E, F, G, H, I) + + end + + do TITLE "one component in common" + + local world = jecs.World.new() + + local A = world:component() + local B = world:component() + local C = world:component() + local D = world:component() + local E = world:component() + local F = world:component() + local G = world:component() + local H = world:component() + local I = world:component() + + for i = 1, N do + local id = world:entity() + local a = true + if flip() then world:set(id, B, true) else a = false end + if flip() then world:set(id, C, true) else a = false end + if flip() then world:set(id, D, true) else a = false end + if flip() then world:set(id, E, true) else a = false end + if flip() then world:set(id, F, true) else a = false end + if flip() then world:set(id, G, true) else a = false end + if flip() then world:set(id, H, true) else a = false end + if flip() then world:set(id, I, true) else a = false end + if a then world:set(id, A, true) end + + end + + view_bench(world, A, B, C, D, E, F, G, H, I) + + end + +end + +do TITLE (testkit.color.white_underline("ECR query")) + + local A = ecr.component() + local B = ecr.component() + local C = ecr.component() + local D = ecr.component() + local E = ecr.component() + local F = ecr.component() + local G = ecr.component() + local H = ecr.component() + local I = ecr.component() + + local function count(query: () -> ()) + local n = 0 + for _ in query do + n += 1 + end + return n + end + + local function flip() + return math.random() > 0.5 + end + + local function view_bench( + world: ecr.Registry, + A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53 + ) + + BENCH("1 component", function() + START(count(world:view(A))) + for _ in world:view(A) do end + end) + + BENCH("2 component", function() + START(count(world:view(A, B))) + for _ in world:view(A, B) do end + end) + + BENCH("4 component", function() + START(count(world:view(A, B, C, D))) + for _ in world:view(A, B, C, D) do end + end) + + BENCH("8 component", function() + START(count(world:view(A, B, C, D, E, F, G, H))) + for _ in world:view(A, B, C, D, E, F, G, H) do end + end) + end + + + do TITLE "random components" + local world = ecr.registry() + + for i = 1, N do + local id = world.create() + if flip() then world:set(id, A, true) end + if flip() then world:set(id, B, true) end + if flip() then world:set(id, C, true) end + if flip() then world:set(id, D, true) end + if flip() then world:set(id, E, true) end + if flip() then world:set(id, F, true) end + if flip() then world:set(id, G, true) end + if flip() then world:set(id, H, true) end + if flip() then world:set(id, I, true) end + + end + + view_bench(world, A, B, C, D, E, F, G, H, I) + + end + + do TITLE "one component in common" + + local world = ecr.registry() + + for i = 1, N do + local id = world.create() + local a = true + if flip() then world:set(id, B, true) else a = false end + if flip() then world:set(id, C, true) else a = false end + if flip() then world:set(id, D, true) else a = false end + if flip() then world:set(id, E, true) else a = false end + if flip() then world:set(id, F, true) else a = false end + if flip() then world:set(id, G, true) else a = false end + if flip() then world:set(id, H, true) else a = false end + if flip() then world:set(id, I, true) else a = false end + if a then world:set(id, A, true) end + + end + + view_bench(world, A, B, C, D, E, F, G, H, I) + + end + +end \ No newline at end of file From 7cb610b097fde21e7eef56dba041016ff3d5e3ee Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 5 May 2024 15:06:57 +0200 Subject: [PATCH 06/46] Update mirror --- benches/query.lua | 96 +++++++++- mirror/init.lua | 435 ++++++++++++++++++++++++---------------------- 2 files changed, 322 insertions(+), 209 deletions(-) diff --git a/benches/query.lua b/benches/query.lua index 8f54596..195e9c6 100644 --- a/benches/query.lua +++ b/benches/query.lua @@ -8,7 +8,8 @@ local function TITLE(title: string) print(testkit.color.white(title)) end -local jecs = require("../mirror/init") +local jecs = require("../lib/init") +local mirror = require("../mirror/init") type i53 = number @@ -101,6 +102,99 @@ do a += 1 end + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end + +do + TITLE(testkit.color.white_underline("Mirror query")) + local ecs = mirror.World.new() + do + TITLE("one component in common") + + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("1 component", function() + for _ in world:query(A) do + end + end) + + BENCH("2 component", function() + for _ in world:query(A, B) do + end + end) + + BENCH("4 component", function() + for _ in world:query(A, B, C, D) do + end + end) + + BENCH("8 component", function() + for _ in world:query(A, B, C, D, E, F, G, H) do + end + end) + end + + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() + + local function flip() + return math.random() >= 0.15 + end + + local added = 0 + local archetypes = {} + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() + + local combination = "" + + if flip() then + combination ..= "B" + ecs:set(entity, D2, {value = true}) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, {value = true}) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, {value = true}) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, {value = true}) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, {value = true}) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, {value = true}) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, {value = true}) + end + + if #combination == 7 then + added += 1 + ecs:set(entity, D1, {value = true}) + end + archetypes[combination] = true + end + + local a = 0 + for _ in archetypes do + a += 1 + end + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) end end \ No newline at end of file diff --git a/mirror/init.lua b/mirror/init.lua index 35a9b9c..6d9c1fe 100644 --- a/mirror/init.lua +++ b/mirror/init.lua @@ -6,10 +6,10 @@ type i53 = number type i24 = number -type Ty = { i53 } +type Ty = {i53} type ArchetypeId = number -type Column = { any } +type Column = {any} type Archetype = { id: number, @@ -20,9 +20,9 @@ type Archetype = { }, }, types: Ty, - type: string | number, - entities: { number }, - columns: { Column }, + type: string | number, + entities: {number}, + columns: {Column}, records: {}, } @@ -31,13 +31,13 @@ type Record = { row: number, } -type EntityIndex = { [i24]: Record } -type ComponentIndex = { [i24]: ArchetypeMap} +type EntityIndex = {[i24]: Record} +type ComponentIndex = {[i24]: ArchetypeMap} type ArchetypeRecord = number -type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number } -type Archetypes = { [ArchetypeId]: Archetype } - +type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number} +type Archetypes = {[ArchetypeId]: Archetype} + type ArchetypeDiff = { added: Ty, removed: Ty, @@ -64,17 +64,17 @@ local function transitionArchetype( local types = from.types for i, column in columns do - -- Retrieves the new column index from the source archetype's record from each component + -- Retrieves the new column index from the source archetype's record from each component -- We have to do this because the columns are tightly packed and indexes may not correspond to each other. local targetColumn = destinationColumns[tr[types[i]]] -- Sometimes target column may not exist, e.g. when you remove a component. - if targetColumn then + if targetColumn then targetColumn[destinationRow] = column[sourceRow] end -- If the entity is the last row in the archetype then swapping it would be meaningless. local last = #column - if sourceRow ~= last then + if sourceRow ~= last then -- Swap rempves columns to ensure there are no holes in the archetype. column[sourceRow] = column[last] end @@ -82,24 +82,27 @@ local function transitionArchetype( end -- Move the entity from the source to the destination archetype. - destinationEntities[destinationRow] = sourceEntities[sourceRow] - entityIndex[sourceEntities[sourceRow]].row = destinationRow + local atSourceRow = sourceEntities[sourceRow] + destinationEntities[destinationRow] = atSourceRow + entityIndex[atSourceRow].row = destinationRow -- Because we have swapped columns we now have to update the records -- corresponding to the entities' rows that were swapped. local movedAway = #sourceEntities - if sourceRow ~= movedAway then - sourceEntities[sourceRow] = sourceEntities[movedAway] - entityIndex[sourceEntities[movedAway]].row = sourceRow + if sourceRow ~= movedAway then + local atMovedAway = sourceEntities[movedAway] + sourceEntities[sourceRow] = atMovedAway + entityIndex[atMovedAway].row = sourceRow end - + sourceEntities[movedAway] = nil end -local function archetypeAppend(entity: i53, archetype: Archetype): i24 +local function archetypeAppend(entity: number, archetype: Archetype): number local entities = archetype.entities - table.insert(entities, entity) - return #entities + local length = #entities + 1 + entities[length] = entity + return length end local function newEntity(entityId: i53, record: Record, archetype: Archetype) @@ -122,47 +125,49 @@ local function hash(arr): string | number return table.concat(arr, "_") end -local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?) - local destinationCount = #to.types +local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?) local destinationIds = to.types + local records = to.records + local id = to.id - for i = 1, destinationCount do - local destinationId = destinationIds[i] + for i, destinationId in destinationIds do + local archetypesMap = componentIndex[destinationId] - if not componentIndex[destinationId] then - componentIndex[destinationId] = { size = 0, sparse = {} } + if not archetypesMap then + archetypesMap = {size = 0, sparse = {}} + componentIndex[destinationId] = archetypesMap end - local archetypesMap = componentIndex[destinationId] - archetypesMap.sparse[to.id] = i - to.records[destinationId] = i + archetypesMap.sparse[id] = i + records[destinationId] = i end end -local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype +local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype local ty = hash(types) - world.nextArchetypeId = (world.nextArchetypeId::number)+ 1 - local id = world.nextArchetypeId + local id = world.nextArchetypeId + 1 + world.nextArchetypeId = id - local columns = {} :: { any } + local length = #types + local columns = table.create(length) :: {any} - for _ in types do - table.insert(columns, {}) + for index in types do + columns[index] = {} end local archetype = { - id = id, - types = types, - type = ty, - columns = columns, - entities = {}, - edges = {}, - records = {}, + columns = columns; + edges = {}; + entities = {}; + id = id; + records = {}; + type = ty; + types = types; } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype - if #types > 0 then + if length > 0 then createArchetypeRecords(world.componentIndex, archetype, prev) end @@ -171,42 +176,42 @@ end local World = {} World.__index = World -function World.new() +function World.new() local self = setmetatable({ - entityIndex = {}, - componentIndex = {}, - archetypes = {}, - archetypeIndex = {}, - ROOT_ARCHETYPE = (nil :: any) :: Archetype, - nextEntityId = 0, - nextComponentId = 0, - nextArchetypeId = 0, + archetypeIndex = {}; + archetypes = {}; + componentIndex = {}; + entityIndex = {}; hooks = { - [ON_ADD] = {} - } + [ON_ADD] = {}; + }; + nextArchetypeId = 0; + nextComponentId = 0; + nextEntityId = 0; + ROOT_ARCHETYPE = (nil :: any) :: Archetype; }, World) - return self + return self end -local function emit(world, eventDescription) +local function emit(world, eventDescription) local event = eventDescription.event table.insert(world.hooks[event], { - ids = eventDescription.ids, - archetype = eventDescription.archetype, - otherArchetype = eventDescription.otherArchetype, - offset = eventDescription.offset + archetype = eventDescription.archetype; + ids = eventDescription.ids; + offset = eventDescription.offset; + otherArchetype = eventDescription.otherArchetype; }) end -local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) - if #added > 0 then +local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) + if #added > 0 then emit(world, { - event = ON_ADD, - ids = added, - archetype = archetype, - otherArchetype = otherArchetype, - offset = row, + archetype = archetype; + event = ON_ADD; + ids = added; + offset = row; + otherArchetype = otherArchetype; }) end end @@ -217,7 +222,7 @@ local function ensureArchetype(world: World, types, prev) if #types < 1 then return world.ROOT_ARCHETYPE end - + local ty = hash(types) local archetype = world.archetypeIndex[ty] if archetype then @@ -227,10 +232,8 @@ local function ensureArchetype(world: World, types, prev) return archetypeOf(world, types, prev) end -local function findInsert(types: { i53 }, toAdd: i53) - local count = #types - for i = 1, count do - local id = types[i] +local function findInsert(types: {i53}, toAdd: i53) + for i, id in types do if id == toAdd then return -1 end @@ -238,13 +241,13 @@ local function findInsert(types: { i53 }, toAdd: i53) return i end end - return count + 1 + return #types + 1 end local function findArchetypeWith(world: World, node: Archetype, componentId: i53) local types = node.types -- Component IDs are added incrementally, so inserting and sorting - -- them each time would be expensive. Instead this insertion sort can find the insertion + -- them each time would be expensive. Instead this insertion sort can find the insertion -- point in the types array. local at = findInsert(types, componentId) if at == -1 then @@ -259,48 +262,57 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53 end local function ensureEdge(archetype: Archetype, componentId: i53) - if not archetype.edges[componentId] then - archetype.edges[componentId] = {} :: any + local edges = archetype.edges + local edge = edges[componentId] + if not edge then + edge = {} :: any + edges[componentId] = edge end - return archetype.edges[componentId] + return edge end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype - if not from then + if not from then -- If there was no source archetype then it should return the ROOT_ARCHETYPE - if not world.ROOT_ARCHETYPE then - local ROOT_ARCHETYPE = archetypeOf(world, {}, nil) - world.ROOT_ARCHETYPE = ROOT_ARCHETYPE - end - from = world.ROOT_ARCHETYPE + local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE + if not ROOT_ARCHETYPE then + ROOT_ARCHETYPE = archetypeOf(world, {}, nil) + world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never + end + from = ROOT_ARCHETYPE end + local edge = ensureEdge(from, componentId) - - if not edge.add then - -- Save an edge using the component ID to the archetype to allow + local add = edge.add + if not add then + -- Save an edge using the component ID to the archetype to allow -- faster traversals to adjacent archetypes. - edge.add = findArchetypeWith(world, from, componentId) + add = findArchetypeWith(world, from, componentId) + edge.add = add :: never end - return edge.add + return add end local function ensureRecord(entityIndex, entityId: i53): Record - local id = entityId - if not entityIndex[id] then - entityIndex[id] = {} + local record = entityIndex[entityId] + + if not record then + record = {} + entityIndex[entityId] = record end - return entityIndex[id] :: Record + + return record :: Record end -function World.set(world: World, entityId: i53, componentId: i53, data: unknown) +function World.set(world: World, entityId: i53, componentId: i53, data: unknown) local record = ensureRecord(world.entityIndex, entityId) local from = record.archetype local to = archetypeTraverseAdd(world, componentId, from) - if from == to then - -- If the archetypes are the same it can avoid moving the entity - -- and just set the data directly. + if from == to then + -- If the archetypes are the same it can avoid moving the entity + -- and just set the data directly. local archetypeRecord = to.records[componentId] from.columns[archetypeRecord][record.row] = data -- Should fire an OnSet event here. @@ -308,13 +320,13 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) end if from then - -- If there was a previous archetype, then the entity needs to move the archetype + -- If there was a previous archetype, then the entity needs to move the archetype moveEntity(world.entityIndex, entityId, record, to) else if #to.types > 0 then -- When there is no previous archetype it should create the archetype newEntity(entityId, record, to) - onNotifyAdd(world, to, from, record.row, { componentId }) + onNotifyAdd(world, to, from, record.row, {componentId}) end end @@ -326,28 +338,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype local edge = ensureEdge(from, componentId) - - if not edge.remove then - local to = table.clone(from.types) + local remove = edge.remove + if not remove then + local to = table.clone(from.types) table.remove(to, table.find(to, componentId)) - edge.remove = ensureArchetype(world, to, from) + remove = ensureArchetype(world, to, from) + edge.remove = remove :: never end - return edge.remove + return remove end -function World.remove(world: World, entityId: i53, componentId: i53) - local record = ensureRecord(world.entityIndex, entityId) +function World.remove(world: World, entityId: i53, componentId: i53) + local entityIndex = world.entityIndex + local record = ensureRecord(entityIndex, entityId) local sourceArchetype = record.archetype local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) - if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) + if sourceArchetype and not (sourceArchetype == destinationArchetype) then + moveEntity(entityIndex, entityId, record, destinationArchetype) end end -- Keeping the function as small as possible to enable inlining -local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24) +local function get(record: Record, componentId: i24) local archetype = record.archetype local archetypeRecord = archetype.records[componentId] @@ -360,35 +374,35 @@ end function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) local id = entityId - local componentIndex = world.componentIndex local record = world.entityIndex[id] if not record then return nil end - local va = get(componentIndex, record, a) + local va = get(record, a) if b == nil then return va elseif c == nil then - return va, get(componentIndex, record, b) + return va, get(record, b) elseif d == nil then - return va, get(componentIndex, record, b), get(componentIndex, record, c) + return va, get(record, b), get(record, c) elseif e == nil then - return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d) + return va, get(record, b), get(record, c), get(record, d) else error("args exceeded") end end -local function noop(self: Query, ...: i53): () -> (number, ...any) - return function() - end :: any +-- the less creation the better +local function actualNoOperation() end +local function noop(_self: Query, ...: i53): () -> (number, ...any) + return actualNoOperation :: any end local EmptyQuery = { - __iter = noop, - without = noop + __iter = noop; + without = noop; } EmptyQuery.__index = EmptyQuery setmetatable(EmptyQuery, EmptyQuery) @@ -396,25 +410,28 @@ setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) function World.query(world: World, ...: i53): Query - local compatibleArchetypes = {} - local components = { ... } - local archetypes = world.archetypes - local queryLength = #components - - if queryLength == 0 then + -- breaking? + if (...) == nil then error("Missing components") end + local compatibleArchetypes = {} + local length = 0 + + local components = {...} + local archetypes = world.archetypes + local queryLength = #components + local firstArchetypeMap local componentIndex = world.componentIndex - for i, componentId in components do + for _, componentId in components do local map = componentIndex[componentId] if not map then return EmptyQuery end - if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then + if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then firstArchetypeMap = map end end @@ -422,110 +439,107 @@ function World.query(world: World, ...: i53): Query for id in firstArchetypeMap.sparse do local archetype = archetypes[id] local archetypeRecords = archetype.records - local indices = {} + local indices = {} local skip = false - - for i, componentId in components do + + for i, componentId in components do local index = archetypeRecords[componentId] - if not index then + if not index then skip = true break end - indices[i] = archetypeRecords[componentId] + indices[i] = index end - if skip then + if skip then continue end - table.insert(compatibleArchetypes, { archetype, indices }) + + length += 1 + compatibleArchetypes[length] = {archetype, indices} end local lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then + if not lastArchetype then return EmptyQuery end - + local preparedQuery = {} preparedQuery.__index = preparedQuery - function preparedQuery:without(...) - local components = { ... } - for i = #compatibleArchetypes, 1, -1 do + function preparedQuery:without(...) + local withoutComponents = {...} + for i = #compatibleArchetypes, 1, -1 do local archetype = compatibleArchetypes[i][1] + local records = archetype.records local shouldRemove = false - for _, componentId in components do - if archetype.records[componentId] then + + for _, componentId in withoutComponents do + if records[componentId] then shouldRemove = true break end end - if shouldRemove then + + if shouldRemove then table.remove(compatibleArchetypes, i) end - end + end lastArchetype, compatibleArchetype = next(compatibleArchetypes) - if not lastArchetype then + if not lastArchetype then return EmptyQuery end - + return self end local lastRow local queryOutput = {} - - function preparedQuery:__iter() - return function() + function preparedQuery:__iter() + return function() local archetype = compatibleArchetype[1] local row = next(archetype.entities, lastRow) - while row == nil do + while row == nil do lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) - if lastArchetype == nil then - return + if lastArchetype == nil then + return end archetype = compatibleArchetype[1] row = next(archetype.entities, row) end lastRow = row - + local entityId = archetype.entities[row :: number] local columns = archetype.columns local tr = compatibleArchetype[2] - - if queryLength == 1 then + + if queryLength == 1 then return entityId, columns[tr[1]][row] - elseif queryLength == 2 then + elseif queryLength == 2 then return entityId, columns[tr[1]][row], columns[tr[2]][row] - elseif queryLength == 3 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row] - elseif queryLength == 4 then - return entityId, - columns[tr[1]][row], - columns[tr[2]][row], - columns[tr[3]][row], - columns[tr[4]][row] - elseif queryLength == 5 then - return entityId, + elseif queryLength == 3 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] + elseif queryLength == 4 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row] + elseif queryLength == 5 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[5]][row] - elseif queryLength == 6 then - return entityId, + elseif queryLength == 6 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row], columns[tr[5]][row], columns[tr[6]][row] - elseif queryLength == 7 then - return entityId, + elseif queryLength == 7 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], @@ -533,8 +547,8 @@ function World.query(world: World, ...: i53): Query columns[tr[5]][row], columns[tr[6]][row], columns[tr[7]][row] - elseif queryLength == 8 then - return entityId, + elseif queryLength == 8 then + return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], @@ -545,8 +559,8 @@ function World.query(world: World, ...: i53): Query columns[tr[8]][row] end - for i in components do - queryOutput[i] = tr[i][row] + for i in components do + queryOutput[i] = columns[tr[i]][row] end return entityId, unpack(queryOutput, 1, queryLength) @@ -556,23 +570,24 @@ function World.query(world: World, ...: i53): Query return setmetatable({}, preparedQuery) :: any end -function World.component(world: World) - local componentId = world.nextComponentId + 1 - if componentId > HI_COMPONENT_ID then - -- IDs are partitioned into ranges because component IDs are not nominal, +function World.component(world: World) + local componentId = world.nextComponentId + 1 + if componentId > HI_COMPONENT_ID then + -- IDs are partitioned into ranges because component IDs are not nominal, -- so it needs to error when IDs intersect into the entity range. - error("Too many components, consider using world:entity() instead to create components.") + error("Too many components, consider using world:entity() instead to create components.") end world.nextComponentId = componentId return componentId end function World.entity(world: World) - world.nextEntityId += 1 - return world.nextEntityId + REST + local nextEntityId = world.nextEntityId + 1 + world.nextEntityId = nextEntityId + return nextEntityId + REST end -function World.delete(world: World, entityId: i53) +function World.delete(world: World, entityId: i53) local entityIndex = world.entityIndex local record = entityIndex[entityId] moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) @@ -584,57 +599,61 @@ function World.delete(world: World, entityId: i53) end function World.observer(world: World, ...) - local componentIds = { ... } - + local componentIds = {...} + local idsCount = #componentIds + local hooks = world.hooks + return { - event = function(event) - local hook = world.hooks[event] - world.hooks[event] = nil + event = function(event) + local hook = hooks[event] + hooks[event] = nil local last, change - return function() + return function() last, change = next(hook, last) - if not last then + if not last then return end local matched = false - - while not matched do + local ids = change.ids + + while not matched do local skip = false - for _, id in change.ids do - if not table.find(componentIds, id) then + for _, id in ids do + if not table.find(componentIds, id) then skip = true break end end - - if skip then + + if skip then last, change = next(hook, last) + ids = change.ids continue end matched = true end - - local queryOutput = {} + + local queryOutput = table.create(idsCount) local row = change.offset local archetype = change.archetype local columns = archetype.columns local archetypeRecords = archetype.records - for _, id in componentIds do - table.insert(queryOutput, columns[archetypeRecords[id]][row]) + for index, id in componentIds do + queryOutput[index] = columns[archetypeRecords[id]][row] end - return archetype.entities[row], unpack(queryOutput, 1, #queryOutput) + return archetype.entities[row], unpack(queryOutput, 1, idsCount) end - end + end; } end return table.freeze({ - World = World, - ON_ADD = ON_ADD, - ON_REMOVE = ON_REMOVE, - ON_SET = ON_SET + World = World; + ON_ADD = ON_ADD; + ON_REMOVE = ON_REMOVE; + ON_SET = ON_SET; }) From d5414f1bc453394e0bf045a12e262c06984debdd Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 5 May 2024 15:22:02 +0200 Subject: [PATCH 07/46] Add iter method (#20) --- lib/init.lua | 33 ++++++++++++++++++++++++++++++++- lib/init.spec.lua | 33 +++++++++++++++++++++++++++++++++ tests/test1.lua | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 1 deletion(-) diff --git a/lib/init.lua b/lib/init.lua index 6d9c1fe..8c0b5bd 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -178,7 +178,7 @@ local World = {} World.__index = World function World.new() local self = setmetatable({ - archetypeIndex = {}; + archetypeIndex = {}; archetypes = {}; componentIndex = {}; entityIndex = {}; @@ -651,6 +651,37 @@ function World.observer(world: World, ...) } end +function World.__iter(world: World): () -> (number?, unknown?) + local entityIndex = world.entityIndex + local last + + return function() + local entity, record = next(entityIndex, last) + if not entity then + return + end + last = entity + + local archetype = record.archetype + if not archetype then + -- Returns only the entity id as an entity without data should not return + -- data and allow the user to get an error if they don't handle the case. + return entity + end + + local row = record.row + local types = archetype.types + local columns = archetype.columns + local entityData = {} + for i, column in columns do + -- We use types because the key should be the component ID not the column index + entityData[types[i]] = column[row] + end + + return entity, entityData + end +end + return table.freeze({ World = World; ON_ADD = ON_ADD; diff --git a/lib/init.spec.lua b/lib/init.spec.lua index fdc8331..98f485b 100644 --- a/lib/init.spec.lua +++ b/lib/init.spec.lua @@ -299,5 +299,38 @@ return function() expect(world:get(id, Poison)).to.never.be.ok() expect(world:get(id, Health)).to.never.be.ok() end) + + it("should allow iterating the whole world", function() + local world = jecs.World.new() + + local A, B = world:entity(), world:entity() + + local eA = world:entity() + world:set(eA, A, true) + local eB = world:entity() + world:set(eB, B, true) + local eAB = world:entity() + world:set(eAB, A, true) + world:set(eAB, B, true) + + local count = 0 + for id, data in world do + count += 1 + if id == eA then + expect(data[A]).to.be.ok() + expect(data[B]).to.never.be.ok() + elseif id == eB then + expect(data[B]).to.be.ok() + expect(data[A]).to.never.be.ok() + elseif id == eAB then + expect(data[A]).to.be.ok() + expect(data[B]).to.be.ok() + else + error("unknown entity", id) + end + end + + expect(count).to.equal(3) + end) end) end \ No newline at end of file diff --git a/tests/test1.lua b/tests/test1.lua index 0b031d3..7ff3b5a 100644 --- a/tests/test1.lua +++ b/tests/test1.lua @@ -110,6 +110,39 @@ TEST("world:query", function() CHECK(world:get(id, Health) == nil) end + do CASE "Should allow iterating the whole world" + local world = jecs.World.new() + + local A, B = world:entity(), world:entity() + + local eA = world:entity() + world:set(eA, A, true) + local eB = world:entity() + world:set(eB, B, true) + local eAB = world:entity() + world:set(eAB, A, true) + world:set(eAB, B, true) + + local count = 0 + for id, data in world do + count += 1 + if id == eA then + CHECK(data[A] == true) + CHECK(data[B] == nil) + elseif id == eB then + CHECK(data[B] == true) + CHECK(data[A] == nil) + elseif id == eAB then + CHECK(data[A] == true) + CHECK(data[B] == true) + else + error("unknown entity", id) + end + end + + CHECK(count == 3) + end + end) FINISH() \ No newline at end of file From 517dbb99c0eb8ae54f731ee6742e96daf50400eb Mon Sep 17 00:00:00 2001 From: alicesaidhi <166900055+alicesaidhi@users.noreply.github.com> Date: Tue, 7 May 2024 18:37:14 +0200 Subject: [PATCH 08/46] Add svg images (#18) * svg logo * fix light mode * Create logo_old.png --- README.md | 3 ++- jecs_darkmode.svg | 6 ++++++ jecs_lightmode.svg | 6 ++++++ logo.png => logo_old.png | Bin 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 jecs_darkmode.svg create mode 100644 jecs_lightmode.svg rename logo.png => logo_old.png (100%) diff --git a/README.md b/README.md index 96f82a8..0386756 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@

- + +

[![License: Apache 2.0](https://img.shields.io/badge/License-Apache-blue.svg?style=for-the-badge)](LICENSE-APACHE) diff --git a/jecs_darkmode.svg b/jecs_darkmode.svg new file mode 100644 index 0000000..f64b173 --- /dev/null +++ b/jecs_darkmode.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/jecs_lightmode.svg b/jecs_lightmode.svg new file mode 100644 index 0000000..dbcd08c --- /dev/null +++ b/jecs_lightmode.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/logo.png b/logo_old.png similarity index 100% rename from logo.png rename to logo_old.png From 887c892c2ebb4406e5e6f7f7d3be4e018b031046 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 7 May 2024 21:30:36 +0200 Subject: [PATCH 09/46] Move root archetype (#33) --- lib/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/init.lua b/lib/init.lua index 8c0b5bd..ce0951e 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -190,6 +190,7 @@ function World.new() nextEntityId = 0; ROOT_ARCHETYPE = (nil :: any) :: Archetype; }, World) + self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil) return self end @@ -272,6 +273,7 @@ local function ensureEdge(archetype: Archetype, componentId: i53) end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype + from = from or world.ROOT_ARCHETYPE if not from then -- If there was no source archetype then it should return the ROOT_ARCHETYPE local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE From bf5908a8f5da83641be19bff397bdf54e852b1a4 Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 7 May 2024 21:32:56 +0200 Subject: [PATCH 10/46] Adds symmetic and idempotent function add (#26) * Adds symmetic function add * Should be componentId not entityId --- lib/init.lua | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index ce0951e..250a56f 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -274,15 +274,6 @@ end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype from = from or world.ROOT_ARCHETYPE - if not from then - -- If there was no source archetype then it should return the ROOT_ARCHETYPE - local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE - if not ROOT_ARCHETYPE then - ROOT_ARCHETYPE = archetypeOf(world, {}, nil) - world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never - end - from = ROOT_ARCHETYPE - end local edge = ensureEdge(from, componentId) local add = edge.add @@ -307,7 +298,23 @@ local function ensureRecord(entityIndex, entityId: i53): Record return record :: Record end -function World.set(world: World, entityId: i53, componentId: i53, data: unknown) + +function World.add(world: World, entityId: i53, componentId: i53) + local record = ensureRecord(world.entityIndex, entityId) + local from = record.archetype + local to = archetypeTraverseAdd(world, componentId, from) + if from then + moveEntity(world.entityIndex, entityId, record, to) + else + if #to.types > 0 then + newEntity(entityId, record, to) + onNotifyAdd(world, to, from, record.row, { componentId }) + end + end +end + +-- Symmetric like `World.add` but idempotent +function World.set(world: World, entityId: i53, componentId: i53, data: unknown) local record = ensureRecord(world.entityIndex, entityId) local from = record.archetype local to = archetypeTraverseAdd(world, componentId, from) @@ -331,7 +338,7 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) onNotifyAdd(world, to, from, record.row, {componentId}) end end - + local archetypeRecord = to.records[componentId] to.columns[archetypeRecord][record.row] = data end From e8b78f7b50033d02c33fc755c7ec584da12c3acb Mon Sep 17 00:00:00 2001 From: Marcus Date: Tue, 7 May 2024 21:33:42 +0200 Subject: [PATCH 11/46] Add world.delete (#22) --- lib/init.lua | 50 +++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 250a56f..defc1b3 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -596,15 +596,51 @@ function World.entity(world: World) return nextEntityId + REST end -function World.delete(world: World, entityId: i53) +-- should reuse this logic in World.set instead of swap removing in transition archetype +local function destructColumns(columns, count, row) + if row == count then + for _, column in columns do + column[count] = nil + end + else + for _, column in columns do + column[row] = column[count] + column[count] = nil + end + end +end + +local function archetypeDelete(entityIndex, archetype: Archetype, row: i24, destruct: boolean) + local entities = archetype.entities + local last = #entities + + local entityToMove = entities[last] + --local entityToDelete = entities[row] + entities[row] = entityToMove + entities[last] = nil + + if row ~= last then + local recordToMove = entityIndex[entityToMove] + if recordToMove then + recordToMove.row = row + end + end + + local columns = archetype.columns + + if not destruct then + return + end + + destructColumns(columns, last, row) +end + +function World.delete(world: World, entityId: i53) local entityIndex = world.entityIndex local record = entityIndex[entityId] - moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) - -- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from - -- the entities array and delete the record. We know there won't be the hole since - -- we are always removing the last row. - --world.ROOT_ARCHETYPE.entities[record.row] = nil - --entityIndex[entityId] = nil + local archetype = record.archetype + archetypeDelete(entityIndex, archetype, record.row, true) + entityIndex[entityId] = nil end function World.observer(world: World, ...) From 91d3fcabc3d63d54ed3362da659d262fa9806286 Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 8 May 2024 00:57:22 +0200 Subject: [PATCH 12/46] Add case for when component is not found in archetype (#25) * Add case for when component is not found in archetype * Check only destination archetype first * Omit onNotifyAdd --- .gitignore | 3 --- lib/init.lua | 51 ++++++++++++++++++++++++++++++------------------- tests/test1.lua | 21 ++++++++++++++++++-- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 9143a00..a43fa5f 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,3 @@ WallyPatches roblox.toml sourcemap.json drafts/*.lua - -*.code-workspace -roblox.yml diff --git a/lib/init.lua b/lib/init.lua index defc1b3..a57ae9e 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -287,23 +287,31 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet return add end -local function ensureRecord(entityIndex, entityId: i53): Record +local function ensureRecord(world, entityId: i53): Record + local entityIndex = world.entityIndex local record = entityIndex[entityId] - if not record then - record = {} - entityIndex[entityId] = record + if record then + return record end - return record :: Record + local ROOT = world.ROOT_ARCHETYPE + local row = #ROOT.entities + 1 + ROOT.entities[row] = entityId + record = { + archetype = ROOT, + row = row + } + entityIndex[entityId] = record + return record end function World.add(world: World, entityId: i53, componentId: i53) - local record = ensureRecord(world.entityIndex, entityId) + local record = ensureRecord(world, entityId) local from = record.archetype local to = archetypeTraverseAdd(world, componentId, from) - if from then + if from and not (from == world.ROOT_ARCHETYPE) then moveEntity(world.entityIndex, entityId, record, to) else if #to.types > 0 then @@ -315,19 +323,20 @@ end -- Symmetric like `World.add` but idempotent function World.set(world: World, entityId: i53, componentId: i53, data: unknown) - local record = ensureRecord(world.entityIndex, entityId) + local record = ensureRecord(world, entityId) local from = record.archetype - local to = archetypeTraverseAdd(world, componentId, from) - if from == to then + local archetypeRecord = from.records[componentId] + if archetypeRecord then -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. - local archetypeRecord = to.records[componentId] from.columns[archetypeRecord][record.row] = data -- Should fire an OnSet event here. return end + local to = archetypeTraverseAdd(world, componentId, from) + if from then -- If there was a previous archetype, then the entity needs to move the archetype moveEntity(world.entityIndex, entityId, record, to) @@ -335,22 +344,25 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) if #to.types > 0 then -- When there is no previous archetype it should create the archetype newEntity(entityId, record, to) - onNotifyAdd(world, to, from, record.row, {componentId}) + --onNotifyAdd(world, to, from, record.row, {componentId}) end end - local archetypeRecord = to.records[componentId] + archetypeRecord = to.records[componentId] to.columns[archetypeRecord][record.row] = data end -local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype - local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype +local function archetypeTraverseRemove(world: World, componentId: i53, from: Archetype): Archetype local edge = ensureEdge(from, componentId) local remove = edge.remove if not remove then local to = table.clone(from.types) - table.remove(to, table.find(to, componentId)) + local at = table.find(to, componentId) + if not at then + return from + end + table.remove(to, at) remove = ensureArchetype(world, to, from) edge.remove = remove :: never end @@ -359,13 +371,12 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype end function World.remove(world: World, entityId: i53, componentId: i53) - local entityIndex = world.entityIndex - local record = ensureRecord(entityIndex, entityId) + local record = ensureRecord(world, entityId) local sourceArchetype = record.archetype local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(entityIndex, entityId, record, destinationArchetype) + moveEntity(world.entityIndex, entityId, record, destinationArchetype) end end @@ -732,4 +743,4 @@ return table.freeze({ ON_ADD = ON_ADD; ON_REMOVE = ON_REMOVE; ON_SET = ON_SET; -}) +}) \ No newline at end of file diff --git a/tests/test1.lua b/tests/test1.lua index 7ff3b5a..3fe86da 100644 --- a/tests/test1.lua +++ b/tests/test1.lua @@ -35,14 +35,17 @@ TEST("world:query", function() local world = jecs.World.new() local A = world:component() local B = world:component() + local C = world:component() local entities = {} for i = 1, N do local id = world:entity() - world:set(id, A, true) + -- specifically put them in disorder to track regression + -- https://github.com/Ukendio/jecs/pull/15 world:set(id, B, true) - if i > 5 then world:remove(id, B, true) end + world:set(id, A, true) + if i > 5 then world:remove(id, B) end entities[i] = id end @@ -110,6 +113,20 @@ TEST("world:query", function() CHECK(world:get(id, Health) == nil) end + do CASE "show allow remove that doesn't exist on entity" + local world = jecs.World.new() + + local Health = world:entity() + local Poison = world:component() + + local id = world:entity() + world:set(id, Health, 50) + world:remove(id, Poison) + + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == 50) + end + do CASE "Should allow iterating the whole world" local world = jecs.World.new() From 1de41447b62caae571bd1b34a254ea9518b69311 Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 8 May 2024 01:04:11 +0200 Subject: [PATCH 13/46] Remove observer for now (#34) * Add case for when component is not found in archetype * Check only destination archetype first * Omit onNotifyAdd * Remove observers --- lib/init.lua | 53 ----------------------------------------------- lib/init.spec.lua | 16 -------------- 2 files changed, 69 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index a57ae9e..29396a5 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -654,59 +654,6 @@ function World.delete(world: World, entityId: i53) entityIndex[entityId] = nil end -function World.observer(world: World, ...) - local componentIds = {...} - local idsCount = #componentIds - local hooks = world.hooks - - return { - event = function(event) - local hook = hooks[event] - hooks[event] = nil - - local last, change - return function() - last, change = next(hook, last) - if not last then - return - end - - local matched = false - local ids = change.ids - - while not matched do - local skip = false - for _, id in ids do - if not table.find(componentIds, id) then - skip = true - break - end - end - - if skip then - last, change = next(hook, last) - ids = change.ids - continue - end - - matched = true - end - - local queryOutput = table.create(idsCount) - local row = change.offset - local archetype = change.archetype - local columns = archetype.columns - local archetypeRecords = archetype.records - for index, id in componentIds do - queryOutput[index] = columns[archetypeRecords[id]][row] - end - - return archetype.entities[row], unpack(queryOutput, 1, idsCount) - end - end; - } -end - function World.__iter(world: World): () -> (number?, unknown?) local entityIndex = world.entityIndex local last diff --git a/lib/init.spec.lua b/lib/init.spec.lua index 98f485b..553c9a4 100644 --- a/lib/init.spec.lua +++ b/lib/init.spec.lua @@ -176,22 +176,6 @@ return function() expect(added).to.equal(0) end) - it("track changes", function() - local Position = world:entity() - - local moving = world:entity() - world:set(moving, Position, Vector3.new(1, 2, 3)) - - local count = 0 - - for e, position in world:observer(Position).event(jecs.ON_ADD) do - count += 1 - expect(e).to.equal(moving) - expect(position).to.equal(Vector3.new(1, 2, 3)) - end - expect(count).to.equal(1) - end) - it("should query all matching entities", function() local world = jecs.World.new() From 87d49e513422a47d5022932c1c8143a4dbd0504a Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 9 May 2024 02:20:54 +0200 Subject: [PATCH 14/46] Reorganize file --- lib/a.lua | 5 +++ lib/init.lua | 94 ++++++++++++++++++++++++++-------------------------- 2 files changed, 52 insertions(+), 47 deletions(-) create mode 100644 lib/a.lua diff --git a/lib/a.lua b/lib/a.lua new file mode 100644 index 0000000..68b844c --- /dev/null +++ b/lib/a.lua @@ -0,0 +1,5 @@ +local test = { + ez = "godo" +} + + test.ez = "good" \ No newline at end of file diff --git a/lib/init.lua b/lib/init.lua index 29396a5..93e5021 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -143,14 +143,14 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet end end -local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype +local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype local ty = hash(types) local id = world.nextArchetypeId + 1 world.nextArchetypeId = id local length = #types - local columns = table.create(length) :: {any} + local columns = table.create(length) for index in types do columns[index] = {} @@ -174,51 +174,6 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet return archetype end -local World = {} -World.__index = World -function World.new() - local self = setmetatable({ - archetypeIndex = {}; - archetypes = {}; - componentIndex = {}; - entityIndex = {}; - hooks = { - [ON_ADD] = {}; - }; - nextArchetypeId = 0; - nextComponentId = 0; - nextEntityId = 0; - ROOT_ARCHETYPE = (nil :: any) :: Archetype; - }, World) - self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil) - return self -end - -local function emit(world, eventDescription) - local event = eventDescription.event - - table.insert(world.hooks[event], { - archetype = eventDescription.archetype; - ids = eventDescription.ids; - offset = eventDescription.offset; - otherArchetype = eventDescription.otherArchetype; - }) -end - -local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) - if #added > 0 then - emit(world, { - archetype = archetype; - event = ON_ADD; - ids = added; - offset = row; - otherArchetype = otherArchetype; - }) - end -end - -export type World = typeof(World.new()) - local function ensureArchetype(world: World, types, prev) if #types < 1 then return world.ROOT_ARCHETYPE @@ -306,6 +261,51 @@ local function ensureRecord(world, entityId: i53): Record return record end +local World = {} +World.__index = World +function World.new() + local self = setmetatable({ + archetypeIndex = {}; + archetypes = {}; + componentIndex = {}; + entityIndex = {}; + hooks = { + [ON_ADD] = {}; + }; + nextArchetypeId = 0; + nextComponentId = 0; + nextEntityId = 0; + ROOT_ARCHETYPE = (nil :: any) :: Archetype; + }, World) + self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil) + return self +end + +local function emit(world, eventDescription) + local event = eventDescription.event + + table.insert(world.hooks[event], { + archetype = eventDescription.archetype; + ids = eventDescription.ids; + offset = eventDescription.offset; + otherArchetype = eventDescription.otherArchetype; + }) +end + +local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) + if #added > 0 then + emit(world, { + archetype = archetype; + event = ON_ADD; + ids = added; + offset = row; + otherArchetype = otherArchetype; + }) + end +end + +export type World = typeof(World.new()) + function World.add(world: World, entityId: i53, componentId: i53) local record = ensureRecord(world, entityId) From 6775601e21611720699375e2ece2c6d9452c9e3c Mon Sep 17 00:00:00 2001 From: Marcus Date: Fri, 10 May 2024 14:27:38 +0200 Subject: [PATCH 15/46] Delete lib/a.lua --- lib/a.lua | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 lib/a.lua diff --git a/lib/a.lua b/lib/a.lua deleted file mode 100644 index 68b844c..0000000 --- a/lib/a.lua +++ /dev/null @@ -1,5 +0,0 @@ -local test = { - ez = "godo" -} - - test.ez = "good" \ No newline at end of file From d63de48546dc8d499750764adf9db387d8335a24 Mon Sep 17 00:00:00 2001 From: Marcus Date: Fri, 10 May 2024 17:59:57 +0200 Subject: [PATCH 16/46] Relationships (#31) * Sparse set for entity records * Swap dense indexes * Improve inlining * Add benchmarks * Add tests for relations * Add REST * Merge upstream changes * Add back symmetric and non idempotent add function * Only swap when not last row * Assert that the entity is alive * Update example with relations --- README.md | 22 +- benches/query.lua | 46 ++++ benches/visual/insertion.bench.lua | 36 ++- lib/init.lua | 411 +++++++++++++++++------------ tests/{test1.lua => world.lua} | 107 +++++--- 5 files changed, 414 insertions(+), 208 deletions(-) rename tests/{test1.lua => world.lua} (56%) diff --git a/README.md b/README.md index 0386756..2d8d210 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,18 @@ jecs is a stupidly fast Entity Component System (ECS). ### Example ```lua -local world = Jecs.World.new() - -local Health = world:component() -local Damage = world:component() -local Position = world:component() +local world = World.new() local player = world:entity() local opponent = world:entity() +local Health = world:component() +local Position = world:component() +-- Notice how components can just be entities as well? +-- It allows you to model relationships easily! +local Damage = world:entity() +local DamagedBy = world:entity() + world:set(player, Health, 100) world:set(player, Damage, 8) world:set(player, Position, Vector3.new(0, 5, 0)) @@ -38,17 +41,20 @@ world:set(opponent, Position, Vector3.new(0, 5, 3)) for playerId, playerPosition, health in world:query(Position, Health) do local totalDamage = 0 - for _, opponentPosition, damage in world:query(Position, Damage) do + for opponentId, opponentPosition, damage in world:query(Position, Damage) do if (playerPosition - opponentPosition).Magnitude < 5 then totalDamage += damage end + world:set(playerId, ECS_PAIR(DamagedBy, opponentId), totalDamage) end +end - world:set(playerId, Health, health - totalDamage) +-- Gets the damage inflicted by our specific opponent! +for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, opponent)) do + world:set(playerId, health - inflicted) end assert(world:get(playerId, Health) == 79) -assert(world:get(opponentId, Health) == 92) ``` 125 archetypes, 4 random components queried. diff --git a/benches/query.lua b/benches/query.lua index 195e9c6..34b63de 100644 --- a/benches/query.lua +++ b/benches/query.lua @@ -39,6 +39,29 @@ do for _ in world:query(A, B, C, D, E, F, G, H) do end end) + + local e = world:entity() + world:set(e, A, true) + world:set(e, B, true) + world:set(e, C, true) + world:set(e, D, true) + world:set(e, E, true) + world:set(e, F, true) + world:set(e, G, true) + world:set(e, H, true) + + BENCH("Update Data", function() + for _ = 1, 100 do + world:set(e, A, false) + world:set(e, B, false) + world:set(e, C, false) + world:set(e, D, false) + world:set(e, E, false) + world:set(e, F, false) + world:set(e, G, false) + world:set(e, H, false) + end + end) end local D1 = ecs:component() @@ -132,6 +155,29 @@ do for _ in world:query(A, B, C, D, E, F, G, H) do end end) + + local e = world:entity() + world:set(e, A, true) + world:set(e, B, true) + world:set(e, C, true) + world:set(e, D, true) + world:set(e, E, true) + world:set(e, F, true) + world:set(e, G, true) + world:set(e, H, true) + + BENCH("Update Data", function() + for _ = 1, 100 do + world:set(e, A, false) + world:set(e, B, false) + world:set(e, C, false) + world:set(e, D, false) + world:set(e, E, false) + world:set(e, F, false) + world:set(e, G, false) + world:set(e, H, false) + end + end) end local D1 = ecs:component() diff --git a/benches/visual/insertion.bench.lua b/benches/visual/insertion.bench.lua index 3f7415a..e8e50be 100644 --- a/benches/visual/insertion.bench.lua +++ b/benches/visual/insertion.bench.lua @@ -8,6 +8,8 @@ local jecs = require(ReplicatedStorage.Lib) local ecr = require(ReplicatedStorage.DevPackages.ecr) local newWorld = Matter.World.new() local ecs = jecs.World.new() +local mirror = require(ReplicatedStorage.mirror) +local mcs = mirror.World.new() local A1 = Matter.component() local A2 = Matter.component() @@ -35,6 +37,15 @@ local C5 = ecs:entity() local C6 = ecs:entity() local C7 = ecs:entity() local C8 = ecs:entity() +local E1 = mcs:entity() +local E2 = mcs:entity() +local E3 = mcs:entity() +local E4 = mcs:entity() +local E5 = mcs:entity() +local E6 = mcs:entity() +local E7 = mcs:entity() +local E8 = mcs:entity() + local registry2 = ecr.registry() return { @@ -44,7 +55,7 @@ return { Functions = { Matter = function() - for i = 1, 50 do + for i = 1, 500 do newWorld:spawn( A1({ value = true }), A2({ value = true }), @@ -60,8 +71,8 @@ return { ECR = function() - for i = 1, 50 do - local e = registry2.create() + local e = registry2.create() + for i = 1, 500 do registry2:set(e, B1, {value = false}) registry2:set(e, B2, {value = false}) registry2:set(e, B3, {value = false}) @@ -78,7 +89,7 @@ return { local e = ecs:entity() - for i = 1, 50 do + for i = 1, 500 do ecs:set(e, C1, {value = false}) ecs:set(e, C2, {value = false}) @@ -89,6 +100,23 @@ return { ecs:set(e, C7, {value = false}) ecs:set(e, C8, {value = false}) + end + end, + Mirror = function() + + local e = ecs:entity() + + for i = 1, 500 do + + mcs:set(e, E1, {value = false}) + mcs:set(e, E2, {value = false}) + mcs:set(e, E3, {value = false}) + mcs:set(e, E4, {value = false}) + mcs:set(e, E5, {value = false}) + mcs:set(e, E6, {value = false}) + mcs:set(e, E7, {value = false}) + mcs:set(e, E8, {value = false}) + end end diff --git a/lib/init.lua b/lib/init.lua index 93e5021..166e3b6 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -29,9 +29,10 @@ type Archetype = { type Record = { archetype: Archetype, row: number, + dense: i24, } -type EntityIndex = {[i24]: Record} +type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}} type ComponentIndex = {[i24]: ArchetypeMap} type ArchetypeRecord = number @@ -81,21 +82,27 @@ local function transitionArchetype( column[last] = nil end - -- Move the entity from the source to the destination archetype. - local atSourceRow = sourceEntities[sourceRow] - destinationEntities[destinationRow] = atSourceRow - entityIndex[atSourceRow].row = destinationRow + local sparse = entityIndex.sparse + local movedAway = #sourceEntities + -- Move the entity from the source to the destination archetype. -- Because we have swapped columns we now have to update the records -- corresponding to the entities' rows that were swapped. - local movedAway = #sourceEntities - if sourceRow ~= movedAway then - local atMovedAway = sourceEntities[movedAway] - sourceEntities[sourceRow] = atMovedAway - entityIndex[atMovedAway].row = sourceRow + local e1 = sourceEntities[sourceRow] + local e2 = sourceEntities[movedAway] + + if sourceRow ~= movedAway then + sourceEntities[sourceRow] = e2 end sourceEntities[movedAway] = nil + destinationEntities[destinationRow] = e1 + + local record1 = sparse[e1] + local record2 = sparse[e2] + + record1.row = destinationRow + record2.row = sourceRow end local function archetypeAppend(entity: number, archetype: Archetype): number @@ -143,14 +150,14 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet end end -local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype +local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype local ty = hash(types) local id = world.nextArchetypeId + 1 world.nextArchetypeId = id local length = #types - local columns = table.create(length) + local columns = table.create(length) :: {any} for index in types do columns[index] = {} @@ -174,6 +181,194 @@ local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetyp return archetype end +local World = {} +World.__index = World +function World.new() + local self = setmetatable({ + archetypeIndex = {}; + archetypes = {}; + componentIndex = {}; + entityIndex = { + dense = {}, + sparse = {} + } :: EntityIndex; + hooks = { + [ON_ADD] = {}; + }; + nextArchetypeId = 0; + nextComponentId = 0; + nextEntityId = 0; + ROOT_ARCHETYPE = (nil :: any) :: Archetype; + }, World) + return self +end + +local FLAGS_PAIR = 0x8 + +local function addFlags(flags) + local typeFlags = 0x0 + if flags.isPair then + typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. + end + if false then + typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. + end + + return typeFlags +end + +local ECS_ID_FLAGS_MASK = 0x10 + +-- ECS_ENTITY_MASK (0xFFFFFFFFull << 28) +local ECS_ENTITY_MASK = bit32.lshift(1, 24) + +-- ECS_GENERATION_MASK (0xFFFFull << 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) + +local function newId(source: number, target: number) + local e = source * 2^28 + target * ECS_ID_FLAGS_MASK + return e +end + +local function isPair(e: number) + return (e % 2^4) // FLAGS_PAIR ~= 0 +end + +function separate(entity: number) + local _typeFlags = entity % 0x10 + entity //= ECS_ID_FLAGS_MASK + return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags +end + +-- HIGH 24 bits LOW 24 bits +local function ECS_GENERATION(e: i53) + e //= 0x10 + return e % ECS_GENERATION_MASK +end + +local function ECS_ID(e: i53) + e //= 0x10 + return e // ECS_ENTITY_MASK +end + +local function ECS_GENERATION_INC(e: i53) + local id, generation, flags = separate(e) + + return newId(id, generation + 1) + flags +end + +-- gets the high ID +local function ECS_PAIR_FIRST(entity: i53): i24 + entity //= 0x10 + local first = entity % ECS_ENTITY_MASK + return first +end + +-- gets the low ID +local ECS_PAIR_SECOND = ECS_ID + +local function ECS_PAIR(source: number, target: number) + local id = newId(ECS_PAIR_SECOND(target), ECS_PAIR_SECOND(source)) + addFlags({ isPair = true }) + return id +end + +local function getAlive(entityIndex: EntityIndex, id: i53) + return assert(entityIndex.dense[id], id .. "is not alive") +end + +local function ecs_get_source(entityIndex, e) + assert(isPair(e)) + return getAlive(entityIndex, ECS_PAIR_FIRST(e)) +end +local function ecs_get_target(entityIndex, e) + assert(isPair(e)) + return getAlive(entityIndex, ECS_PAIR_SECOND(e)) +end + +function World.component(world: World) + local componentId = world.nextComponentId + 1 + if componentId > HI_COMPONENT_ID then + -- IDs are partitioned into ranges because component IDs are not nominal, + -- so it needs to error when IDs intersect into the entity range. + error("Too many components, consider using world:entity() instead to create components.") + end + world.nextComponentId = componentId + return componentId +end + +function World.entity(world: World) + local nextEntityId = world.nextEntityId + 1 + world.nextEntityId = nextEntityId + local index = nextEntityId + REST + local id = newId(index, 0) + local entityIndex = world.entityIndex + entityIndex.sparse[id] = { + dense = index + } :: Record + entityIndex.dense[index] = id + + return id +end + +-- should reuse this logic in World.set instead of swap removing in transition archetype +local function destructColumns(columns, count, row) + if row == count then + for _, column in columns do + column[count] = nil + end + else + for _, column in columns do + column[row] = column[count] + column[count] = nil + end + end +end + +local function archetypeDelete(entityIndex, record: Record, entityId: i53, destruct: boolean) + local sparse, dense = entityIndex.sparse, entityIndex.dense + local archetype = record.archetype + local row = record.row + local entities = archetype.entities + local last = #entities + + local entityToMove = entities[last] + + if row ~= last then + dense[record.dense] = entityToMove + sparse[entityToMove] = record + end + + sparse[entityId] = nil + dense[#dense] = nil + + entities[row], entities[last] = entities[last], nil + + local columns = archetype.columns + + if not destruct then + return + end + + destructColumns(columns, last, row) +end + +function World.delete(world: World, entityId: i53) + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entityId] + if not record then + return + end + archetypeDelete(entityIndex, record, entityId, true) +end + +export type World = typeof(World.new()) + local function ensureArchetype(world: World, types, prev) if #types < 1 then return world.ROOT_ARCHETYPE @@ -228,7 +423,15 @@ local function ensureEdge(archetype: Archetype, componentId: i53) end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype - from = from or world.ROOT_ARCHETYPE + if not from then + -- If there was no source archetype then it should return the ROOT_ARCHETYPE + local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE + if not ROOT_ARCHETYPE then + ROOT_ARCHETYPE = archetypeOf(world, {}, nil) + world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never + end + from = ROOT_ARCHETYPE + end local edge = ensureEdge(from, componentId) local add = edge.add @@ -242,101 +445,35 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet return add end -local function ensureRecord(world, entityId: i53): Record - local entityIndex = world.entityIndex - local record = entityIndex[entityId] - - if record then - return record - end - - local ROOT = world.ROOT_ARCHETYPE - local row = #ROOT.entities + 1 - ROOT.entities[row] = entityId - record = { - archetype = ROOT, - row = row - } - entityIndex[entityId] = record - return record -end - -local World = {} -World.__index = World -function World.new() - local self = setmetatable({ - archetypeIndex = {}; - archetypes = {}; - componentIndex = {}; - entityIndex = {}; - hooks = { - [ON_ADD] = {}; - }; - nextArchetypeId = 0; - nextComponentId = 0; - nextEntityId = 0; - ROOT_ARCHETYPE = (nil :: any) :: Archetype; - }, World) - self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil) - return self -end - -local function emit(world, eventDescription) - local event = eventDescription.event - - table.insert(world.hooks[event], { - archetype = eventDescription.archetype; - ids = eventDescription.ids; - offset = eventDescription.offset; - otherArchetype = eventDescription.otherArchetype; - }) -end - -local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) - if #added > 0 then - emit(world, { - archetype = archetype; - event = ON_ADD; - ids = added; - offset = row; - otherArchetype = otherArchetype; - }) - end -end - -export type World = typeof(World.new()) - - function World.add(world: World, entityId: i53, componentId: i53) - local record = ensureRecord(world, entityId) + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entityId] local from = record.archetype local to = archetypeTraverseAdd(world, componentId, from) if from and not (from == world.ROOT_ARCHETYPE) then - moveEntity(world.entityIndex, entityId, record, to) + moveEntity(entityIndex, entityId, record, to) else if #to.types > 0 then newEntity(entityId, record, to) - onNotifyAdd(world, to, from, record.row, { componentId }) end end end -- Symmetric like `World.add` but idempotent -function World.set(world: World, entityId: i53, componentId: i53, data: unknown) - local record = ensureRecord(world, entityId) +function World.set(world: World, entityId: i53, componentId: i53, data: unknown) + local record = world.entityIndex.sparse[entityId] local from = record.archetype + local to = archetypeTraverseAdd(world, componentId, from) - local archetypeRecord = from.records[componentId] - if archetypeRecord then + if from == to then -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. + local archetypeRecord = to.records[componentId] from.columns[archetypeRecord][record.row] = data -- Should fire an OnSet event here. return end - local to = archetypeTraverseAdd(world, componentId, from) - if from then -- If there was a previous archetype, then the entity needs to move the archetype moveEntity(world.entityIndex, entityId, record, to) @@ -344,11 +481,10 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) if #to.types > 0 then -- When there is no previous archetype it should create the archetype newEntity(entityId, record, to) - --onNotifyAdd(world, to, from, record.row, {componentId}) end end - - archetypeRecord = to.records[componentId] + + local archetypeRecord = to.records[componentId] to.columns[archetypeRecord][record.row] = data end @@ -371,12 +507,13 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc end function World.remove(world: World, entityId: i53, componentId: i53) - local record = ensureRecord(world, entityId) + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entityId] local sourceArchetype = record.archetype local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) if sourceArchetype and not (sourceArchetype == destinationArchetype) then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) + moveEntity(entityIndex, entityId, record, destinationArchetype) end end @@ -394,7 +531,7 @@ end function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) local id = entityId - local record = world.entityIndex[id] + local record = world.entityIndex.sparse[id] if not record then return nil end @@ -590,86 +727,24 @@ function World.query(world: World, ...: i53): Query return setmetatable({}, preparedQuery) :: any end -function World.component(world: World) - local componentId = world.nextComponentId + 1 - if componentId > HI_COMPONENT_ID then - -- IDs are partitioned into ranges because component IDs are not nominal, - -- so it needs to error when IDs intersect into the entity range. - error("Too many components, consider using world:entity() instead to create components.") - end - world.nextComponentId = componentId - return componentId -end - -function World.entity(world: World) - local nextEntityId = world.nextEntityId + 1 - world.nextEntityId = nextEntityId - return nextEntityId + REST -end - --- should reuse this logic in World.set instead of swap removing in transition archetype -local function destructColumns(columns, count, row) - if row == count then - for _, column in columns do - column[count] = nil - end - else - for _, column in columns do - column[row] = column[count] - column[count] = nil - end - end -end - -local function archetypeDelete(entityIndex, archetype: Archetype, row: i24, destruct: boolean) - local entities = archetype.entities - local last = #entities - - local entityToMove = entities[last] - --local entityToDelete = entities[row] - entities[row] = entityToMove - entities[last] = nil - - if row ~= last then - local recordToMove = entityIndex[entityToMove] - if recordToMove then - recordToMove.row = row - end - end - - local columns = archetype.columns - - if not destruct then - return - end - - destructColumns(columns, last, row) -end - -function World.delete(world: World, entityId: i53) - local entityIndex = world.entityIndex - local record = entityIndex[entityId] - local archetype = record.archetype - archetypeDelete(entityIndex, archetype, record.row, true) - entityIndex[entityId] = nil -end - function World.__iter(world: World): () -> (number?, unknown?) - local entityIndex = world.entityIndex + local dense = world.entityIndex.dense + local sparse = world.entityIndex.sparse local last return function() - local entity, record = next(entityIndex, last) - if not entity then + local lastEntity, entityId = next(dense, last) + if not lastEntity then return end - last = entity + last = lastEntity + local record = sparse[entityId] local archetype = record.archetype if not archetype then -- Returns only the entity id as an entity without data should not return -- data and allow the user to get an error if they don't handle the case. - return entity + return entityId end local row = record.row @@ -681,7 +756,7 @@ function World.__iter(world: World): () -> (number?, unknown?) entityData[types[i]] = column[row] end - return entity, entityData + return entityId, entityData end end @@ -690,4 +765,12 @@ return table.freeze({ ON_ADD = ON_ADD; ON_REMOVE = ON_REMOVE; ON_SET = ON_SET; -}) \ No newline at end of file + ECS_ID = ECS_ID, + IS_PAIR = isPair, + ECS_PAIR = ECS_PAIR, + ECS_GENERATION = ECS_GENERATION, + ECS_GENERATION_INC = ECS_GENERATION_INC, + getAlive = getAlive, + ecs_get_target = ecs_get_target, + ecs_get_source = ecs_get_source +}) diff --git a/tests/test1.lua b/tests/world.lua similarity index 56% rename from tests/test1.lua rename to tests/world.lua index 3fe86da..5dd3f95 100644 --- a/tests/test1.lua +++ b/tests/world.lua @@ -1,11 +1,52 @@ local testkit = require("../testkit") local jecs = require("../lib/init") +local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION +local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC +local IS_PAIR = jecs.IS_PAIR +local ECS_PAIR = jecs.ECS_PAIR +local getAlive = jecs.getAlive +local ecs_pair_first = jecs.ecs_pair_first +local ecs_pair_second = jecs.ecs_pair_second +local REST = 256 + 4 local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local N = 10 -TEST("world:query", function() +TEST("world", function() + do CASE "should be iterable" + local world = jecs.World.new() + local A = world:component() + local B = world:component() + + local eA = world:entity() + world:set(eA, A, true) + local eB = world:entity() + world:set(eB, B, true) + local eAB = world:entity() + world:set(eAB, A, true) + world:set(eAB, B, true) + + local count = 0 + for id, data in world do + count += 1 + if id == eA then + CHECK(data[A] == true) + CHECK(data[B] == nil) + elseif id == eB then + CHECK(data[A] == nil) + CHECK(data[B] == true) + elseif id == eAB then + CHECK(data[A] == true) + CHECK(data[B] == true) + else + error("unknown entity", id) + end + end + + CHECK(count == 3) + end + do CASE "should query all matching entities" local world = jecs.World.new() @@ -16,7 +57,6 @@ TEST("world:query", function() for i = 1, N do local id = world:entity() - world:set(id, A, true) if i > 5 then world:set(id, B, true) end entities[i] = id @@ -98,7 +138,7 @@ TEST("world:query", function() CHECK(world:get(id, Poison) == 5) end - do CASE "Should allow deleting components" + do CASE "should allow deleting components" local world = jecs.World.new() local Health = world:entity() @@ -107,13 +147,20 @@ TEST("world:query", function() local id = world:entity() world:set(id, Poison, 5) world:set(id, Health, 50) + local id1 = world:entity() + world:set(id1, Poison, 500) + world:set(id1, Health, 50) + world:delete(id) CHECK(world:get(id, Poison) == nil) CHECK(world:get(id, Health) == nil) + CHECK(world:get(id1, Poison) == 500) + CHECK(world:get(id1, Health) == 50) + end - do CASE "show allow remove that doesn't exist on entity" + do CASE "should allow remove that doesn't exist on entity" local world = jecs.World.new() local Health = world:entity() @@ -124,40 +171,36 @@ TEST("world:query", function() world:remove(id, Poison) CHECK(world:get(id, Poison) == nil) + print(world:get(id, Health)) CHECK(world:get(id, Health) == 50) end - - do CASE "Should allow iterating the whole world" + + do CASE "should increment generation" local world = jecs.World.new() + local e = world:entity() + CHECK(ECS_ID(e) == 1 + REST) + CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) + CHECK(ECS_GENERATION(e) == 0) -- 0 + e = ECS_GENERATION_INC(e) + CHECK(ECS_GENERATION(e) == 1) -- 1 + end - local A, B = world:entity(), world:entity() + do CASE "relations" + local world = jecs.World.new() + local _e = world:entity() + local e2 = world:entity() + local e3 = world:entity() + CHECK(ECS_ID(e2) == 2 + REST) + CHECK(ECS_ID(e3) == 3 + REST) + CHECK(ECS_GENERATION(e2) == 0) + CHECK(ECS_GENERATION(e3) == 0) - local eA = world:entity() - world:set(eA, A, true) - local eB = world:entity() - world:set(eB, B, true) - local eAB = world:entity() - world:set(eAB, A, true) - world:set(eAB, B, true) + CHECK(IS_PAIR(world:entity()) == false) - local count = 0 - for id, data in world do - count += 1 - if id == eA then - CHECK(data[A] == true) - CHECK(data[B] == nil) - elseif id == eB then - CHECK(data[B] == true) - CHECK(data[A] == nil) - elseif id == eAB then - CHECK(data[A] == true) - CHECK(data[B] == true) - else - error("unknown entity", id) - end - end - - CHECK(count == 3) + local pair = ECS_PAIR(e2, e3) + CHECK(IS_PAIR(pair) == true) + CHECK(ecs_pair_first(world.entityIndex, pair) == e2) + CHECK(ecs_pair_second(world.entityIndex, pair) == e3) end end) From cfee0e9861df9e316b872ae57a31f4fc8bf45a6e Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 10 May 2024 18:02:23 +0200 Subject: [PATCH 17/46] Add relations as first class citizens --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2d8d210..bb62bdf 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ jecs is a stupidly fast Entity Component System (ECS). - Zero-dependency Luau package - Optimized for column-major operations - Cache friendly archetype/SoA storage +- Entity Relationships as first class citizens ### Example From 10a54c368caac1dcf78c892fd48bc4b2a9959fa9 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 10 May 2024 18:09:34 +0200 Subject: [PATCH 18/46] Update list --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb62bdf..af675da 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,13 @@ Just an ECS jecs is a stupidly fast Entity Component System (ECS). +- Entity Relationships as first class citizens - Process tens of thousands of entities with ease every frame -- Zero-dependency Luau package +- Type-safe [Luau](https://luau-lang.org/) API +- Zero-dependency package - Optimized for column-major operations - Cache friendly archetype/SoA storage -- Entity Relationships as first class citizens +- Unit tested for stability ### Example From d087df35946a57a1f473b3d7336c2371fac84976 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 10 May 2024 18:10:30 +0200 Subject: [PATCH 19/46] 0.1.0 --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index 5885799..9e0b760 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.0.0-prototype.rc.3" +version = "0.1.0-rc.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" exclude = ["**"] From 107d260abf8070e1fbf7e57a13d0e9758cc9db5b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 10 May 2024 18:13:22 +0200 Subject: [PATCH 20/46] Remove dependencies --- wally.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wally.toml b/wally.toml index 9e0b760..ffc6841 100644 --- a/wally.toml +++ b/wally.toml @@ -7,6 +7,4 @@ exclude = ["**"] include = ["default.project.json", "lib", "wally.toml", "README.md"] [dev-dependencies] -TestEZ = "roblox/testez@0.4.1" -Matter = "matter-ecs/matter@0.8.0" -ecr = "centau/ecr@0.8.0" +TestEZ = "roblox/testez@0.4.1" \ No newline at end of file From 076f0ca436749ec552e0570f9638a1cd6fbcaa39 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 10 May 2024 18:20:02 +0200 Subject: [PATCH 21/46] Bump to rc.4 and include lib --- wally.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/wally.toml b/wally.toml index ffc6841..fc77a24 100644 --- a/wally.toml +++ b/wally.toml @@ -1,10 +1,9 @@ [package] name = "ukendio/jecs" -version = "0.1.0-rc.0" +version = "0.1.0-rc.4" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" +include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] exclude = ["**"] -include = ["default.project.json", "lib", "wally.toml", "README.md"] -[dev-dependencies] -TestEZ = "roblox/testez@0.4.1" \ No newline at end of file +[dev-dependencies] \ No newline at end of file From 87711eff19ff94686e51dd548d9363b1e52f0bb3 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 10 May 2024 20:35:41 +0200 Subject: [PATCH 22/46] Register components as entities --- lib/init.lua | 26 ++++++++++++++------------ tests/world.lua | 14 +++++++------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 166e3b6..ea1154b 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -291,6 +291,16 @@ local function ecs_get_target(entityIndex, e) return getAlive(entityIndex, ECS_PAIR_SECOND(e)) end +local function nextEntityId(entityIndex, index: i24) + local id = newId(index, 0) + entityIndex.sparse[id] = { + dense = index + } :: Record + entityIndex.dense[index] = id + + return id +end + function World.component(world: World) local componentId = world.nextComponentId + 1 if componentId > HI_COMPONENT_ID then @@ -299,21 +309,13 @@ function World.component(world: World) error("Too many components, consider using world:entity() instead to create components.") end world.nextComponentId = componentId - return componentId + return nextEntityId(world.entityIndex, componentId) end function World.entity(world: World) - local nextEntityId = world.nextEntityId + 1 - world.nextEntityId = nextEntityId - local index = nextEntityId + REST - local id = newId(index, 0) - local entityIndex = world.entityIndex - entityIndex.sparse[id] = { - dense = index - } :: Record - entityIndex.dense[index] = id - - return id + local entityId = world.nextEntityId + 1 + world.nextEntityId = entityId + return nextEntityId(world.entityIndex, entityId + REST) end -- should reuse this logic in World.set instead of swap removing in transition archetype diff --git a/tests/world.lua b/tests/world.lua index 5dd3f95..d95097d 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -5,8 +5,8 @@ local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local IS_PAIR = jecs.IS_PAIR local ECS_PAIR = jecs.ECS_PAIR local getAlive = jecs.getAlive -local ecs_pair_first = jecs.ecs_pair_first -local ecs_pair_second = jecs.ecs_pair_second +local ecs_get_source = jecs.ecs_get_source +local ecs_get_target = jecs.ecs_get_target local REST = 256 + 4 local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() @@ -39,12 +39,12 @@ TEST("world", function() elseif id == eAB then CHECK(data[A] == true) CHECK(data[B] == true) - else - error("unknown entity", id) end end - CHECK(count == 3) + -- components are registered in the entity index as well + -- so this test has to add 2 to account for them + CHECK(count == 3 + 2) end do CASE "should query all matching entities" @@ -199,8 +199,8 @@ TEST("world", function() local pair = ECS_PAIR(e2, e3) CHECK(IS_PAIR(pair) == true) - CHECK(ecs_pair_first(world.entityIndex, pair) == e2) - CHECK(ecs_pair_second(world.entityIndex, pair) == e3) + CHECK(ecs_get_source(world.entityIndex, pair) == e2) + CHECK(ecs_get_target(world.entityIndex, pair) == e3) end end) From 4c105fa72ce314c79ff79c273e3a8d023b4d7afe Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 10 May 2024 20:36:01 +0200 Subject: [PATCH 23/46] Bump version --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index fc77a24..7102c41 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.1.0-rc.4" +version = "0.1.0-rc.5" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] From e6b16e91ae76a6a448dee5108aa5b880442cb4b6 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 11 May 2024 02:10:04 +0200 Subject: [PATCH 24/46] Remove assert --- lib/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/init.lua b/lib/init.lua index ea1154b..ca6a573 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -279,7 +279,7 @@ local function ECS_PAIR(source: number, target: number) end local function getAlive(entityIndex: EntityIndex, id: i53) - return assert(entityIndex.dense[id], id .. "is not alive") + return entityIndex.dense[id] end local function ecs_get_source(entityIndex, e) From 582b09be6645dd64242abc880693a2e99f6d59fc Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sat, 11 May 2024 02:12:47 +0200 Subject: [PATCH 25/46] Update readme --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af675da..6bb4607 100644 --- a/README.md +++ b/README.md @@ -45,9 +45,14 @@ world:set(opponent, Position, Vector3.new(0, 5, 3)) for playerId, playerPosition, health in world:query(Position, Health) do local totalDamage = 0 for opponentId, opponentPosition, damage in world:query(Position, Damage) do + if playerId == opponentId then + continue + end if (playerPosition - opponentPosition).Magnitude < 5 then totalDamage += damage end + -- We create a pair between the relation component `DamagedBy` and the entity id of the opponent. + -- This will allow us to specifically query for damage exerted by a specific opponent. world:set(playerId, ECS_PAIR(DamagedBy, opponentId), totalDamage) end end @@ -57,7 +62,7 @@ for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, oppon world:set(playerId, health - inflicted) end -assert(world:get(playerId, Health) == 79) +assert(world:get(player, Health) == 79) ``` 125 archetypes, 4 random components queried. From 2df5f3f18e494d5177a17af398e833f60d8d0c3a Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 13 May 2024 00:53:51 +0200 Subject: [PATCH 26/46] Add wildcards (#37) * Fix export * Initial commit * Uncomment cases * Rename case * Add tests for wildcards * Support wildcards in records * Add tests for relation data * Add shorthands * Change casing of exports * Change function signatures * Improve inlining of ECS_PAIR * Delete whitespace * Create root archetype * Add back tests * Fix tests --- lib/init.lua | 299 ++++++++++++++++++++++++---------------------- lib/init.spec.lua | 68 ++++++++++- test.project.json | 6 +- tests/world.lua | 77 ++++++++++-- wally.toml | 3 +- 5 files changed, 292 insertions(+), 161 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index ca6a573..b7486e4 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -44,11 +44,118 @@ type ArchetypeDiff = { removed: Ty, } +local FLAGS_PAIR = 0x8 local HI_COMPONENT_ID = 256 local ON_ADD = HI_COMPONENT_ID + 1 local ON_REMOVE = HI_COMPONENT_ID + 2 local ON_SET = HI_COMPONENT_ID + 3 -local REST = HI_COMPONENT_ID + 4 +local WILDCARD = HI_COMPONENT_ID + 4 +local REST = HI_COMPONENT_ID + 5 + +local ECS_ID_FLAGS_MASK = 0x10 +local ECS_ENTITY_MASK = bit32.lshift(1, 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) + +local function addFlags(isPair: boolean) + local typeFlags = 0x0 + + if isPair then + typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. + end + if false then + typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. + end + + return typeFlags +end + +local function newId(source: number, target: number) + local e = source * 2^28 + target * ECS_ID_FLAGS_MASK + return e +end + +local function ECS_IS_PAIR(e: number) + return (e % 2^4) // FLAGS_PAIR ~= 0 +end + +function separate(entity: number) + local _typeFlags = entity % 0x10 + entity //= ECS_ID_FLAGS_MASK + return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags +end + +-- HIGH 24 bits LOW 24 bits +local function ECS_GENERATION(e: i53) + e //= 0x10 + return e % ECS_GENERATION_MASK +end + +local function ECS_ID(e: i53) + e //= 0x10 + return e // ECS_ENTITY_MASK +end + +local function ECS_GENERATION_INC(e: i53) + local id, generation, flags = separate(e) + + return newId(id, generation + 1) + flags +end + +-- gets the high ID +local function ECS_PAIR_FIRST(entity: i53): i24 + entity //= 0x10 + local first = entity % ECS_ENTITY_MASK + return first +end + +-- gets the low ID +local ECS_PAIR_SECOND = ECS_ID + +local function ECS_PAIR(first: number, second: number) + local target = WILDCARD + local relation + + if first == WILDCARD then + relation = second + elseif second == WILDCARD then + relation = first + else + relation = second + target = ECS_PAIR_SECOND(first) + end + + return newId( + ECS_PAIR_SECOND(relation), target) + addFlags(--[[isPair]] true) +end + +local function getAlive(entityIndex: EntityIndex, id: i53) + return entityIndex.dense[id] +end + +local function ecs_get_source(entityIndex, e) + assert(ECS_IS_PAIR(e)) + return getAlive(entityIndex, ECS_PAIR_FIRST(e)) +end +local function ecs_get_target(entityIndex, e) + assert(ECS_IS_PAIR(e)) + return getAlive(entityIndex, ECS_PAIR_SECOND(e)) +end + +local function nextEntityId(entityIndex, index: i24) + local id = newId(index, 0) + entityIndex.sparse[id] = { + dense = index + } :: Record + entityIndex.dense[index] = id + + return id +end local function transitionArchetype( entityIndex: EntityIndex, @@ -132,22 +239,14 @@ local function hash(arr): string | number return table.concat(arr, "_") end -local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?) - local destinationIds = to.types - local records = to.records - local id = to.id +local function createArchetypeRecord(componentIndex, id, componentId, i) + local archetypesMap = componentIndex[componentId] - for i, destinationId in destinationIds do - local archetypesMap = componentIndex[destinationId] - - if not archetypesMap then - archetypesMap = {size = 0, sparse = {}} - componentIndex[destinationId] = archetypesMap - end - - archetypesMap.sparse[id] = i - records[destinationId] = i + if not archetypesMap then + archetypesMap = {size = 0, sparse = {}} + componentIndex[componentId] = archetypesMap end + archetypesMap.sparse[id] = i end local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype @@ -157,10 +256,26 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet world.nextArchetypeId = id local length = #types - local columns = table.create(length) :: {any} + local columns = table.create(length) - for index in types do - columns[index] = {} + local records = {} + local componentIndex = world.componentIndex + local entityIndex = world.entityIndex + for i, componentId in types do + createArchetypeRecord(componentIndex, id, componentId, i) + records[componentId] = i + columns[i] = {} + + if ECS_IS_PAIR(componentId) then + local first = ecs_get_source(entityIndex, componentId) + local second = ecs_get_target(entityIndex, componentId) + local firstPair = ECS_PAIR(first, WILDCARD) + local secondPair = ECS_PAIR(WILDCARD, second) + createArchetypeRecord(componentIndex, id, firstPair, i) + createArchetypeRecord(componentIndex, id, secondPair, i) + records[firstPair] = i + records[secondPair] = i + end end local archetype = { @@ -168,15 +283,12 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet edges = {}; entities = {}; id = id; - records = {}; + records = records; type = ty; types = types; } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype - if length > 0 then - createArchetypeRecords(world.componentIndex, archetype, prev) - end return archetype end @@ -186,8 +298,8 @@ World.__index = World function World.new() local self = setmetatable({ archetypeIndex = {}; - archetypes = {}; - componentIndex = {}; + archetypes = {} :: Archetypes; + componentIndex = {} :: ComponentIndex; entityIndex = { dense = {}, sparse = {} @@ -200,107 +312,10 @@ function World.new() nextEntityId = 0; ROOT_ARCHETYPE = (nil :: any) :: Archetype; }, World) + self.ROOT_ARCHETYPE = archetypeOf(self, {}) return self end -local FLAGS_PAIR = 0x8 - -local function addFlags(flags) - local typeFlags = 0x0 - if flags.isPair then - typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. - end - if false then - typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true - end - if false then - typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true - end - if false then - typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. - end - - return typeFlags -end - -local ECS_ID_FLAGS_MASK = 0x10 - --- ECS_ENTITY_MASK (0xFFFFFFFFull << 28) -local ECS_ENTITY_MASK = bit32.lshift(1, 24) - --- ECS_GENERATION_MASK (0xFFFFull << 24) -local ECS_GENERATION_MASK = bit32.lshift(1, 16) - -local function newId(source: number, target: number) - local e = source * 2^28 + target * ECS_ID_FLAGS_MASK - return e -end - -local function isPair(e: number) - return (e % 2^4) // FLAGS_PAIR ~= 0 -end - -function separate(entity: number) - local _typeFlags = entity % 0x10 - entity //= ECS_ID_FLAGS_MASK - return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags -end - --- HIGH 24 bits LOW 24 bits -local function ECS_GENERATION(e: i53) - e //= 0x10 - return e % ECS_GENERATION_MASK -end - -local function ECS_ID(e: i53) - e //= 0x10 - return e // ECS_ENTITY_MASK -end - -local function ECS_GENERATION_INC(e: i53) - local id, generation, flags = separate(e) - - return newId(id, generation + 1) + flags -end - --- gets the high ID -local function ECS_PAIR_FIRST(entity: i53): i24 - entity //= 0x10 - local first = entity % ECS_ENTITY_MASK - return first -end - --- gets the low ID -local ECS_PAIR_SECOND = ECS_ID - -local function ECS_PAIR(source: number, target: number) - local id = newId(ECS_PAIR_SECOND(target), ECS_PAIR_SECOND(source)) + addFlags({ isPair = true }) - return id -end - -local function getAlive(entityIndex: EntityIndex, id: i53) - return entityIndex.dense[id] -end - -local function ecs_get_source(entityIndex, e) - assert(isPair(e)) - return getAlive(entityIndex, ECS_PAIR_FIRST(e)) -end -local function ecs_get_target(entityIndex, e) - assert(isPair(e)) - return getAlive(entityIndex, ECS_PAIR_SECOND(e)) -end - -local function nextEntityId(entityIndex, index: i24) - local id = newId(index, 0) - entityIndex.sparse[id] = { - dense = index - } :: Record - entityIndex.dense[index] = id - - return id -end - function World.component(world: World) local componentId = world.nextComponentId + 1 if componentId > HI_COMPONENT_ID then @@ -402,15 +417,16 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53 -- Component IDs are added incrementally, so inserting and sorting -- them each time would be expensive. Instead this insertion sort can find the insertion -- point in the types array. + + local destinationType = table.clone(node.types) local at = findInsert(types, componentId) if at == -1 then -- If it finds a duplicate, it just means it is the same archetype so it can return it -- directly instead of needing to hash types for a lookup to the archetype. return node end - - local destinationType = table.clone(node.types) table.insert(destinationType, at, componentId) + return ensureArchetype(world, destinationType, node) end @@ -425,15 +441,7 @@ local function ensureEdge(archetype: Archetype, componentId: i53) end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype - if not from then - -- If there was no source archetype then it should return the ROOT_ARCHETYPE - local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE - if not ROOT_ARCHETYPE then - ROOT_ARCHETYPE = archetypeOf(world, {}, nil) - world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never - end - from = ROOT_ARCHETYPE - end + from = from or world.ROOT_ARCHETYPE local edge = ensureEdge(from, componentId) local add = edge.add @@ -659,14 +667,14 @@ function World.query(world: World, ...: i53): Query function preparedQuery:__iter() return function() local archetype = compatibleArchetype[1] - local row = next(archetype.entities, lastRow) + local row: number = next(archetype.entities, lastRow) :: number while row == nil do lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) if lastArchetype == nil then return end archetype = compatibleArchetype[1] - row = next(archetype.entities, row) + row = next(archetype.entities, row) :: number end lastRow = row @@ -764,15 +772,22 @@ end return table.freeze({ World = World; - ON_ADD = ON_ADD; - ON_REMOVE = ON_REMOVE; - ON_SET = ON_SET; + + OnAdd = ON_ADD; + OnRemove = ON_REMOVE; + OnSet = ON_SET; + Wildcard = WILDCARD, + w = WILDCARD, + Rest = REST, + ECS_ID = ECS_ID, - IS_PAIR = isPair, + IS_PAIR = ECS_IS_PAIR, ECS_PAIR = ECS_PAIR, - ECS_GENERATION = ECS_GENERATION, ECS_GENERATION_INC = ECS_GENERATION_INC, - getAlive = getAlive, + ECS_GENERATION = ECS_GENERATION, ecs_get_target = ecs_get_target, - ecs_get_source = ecs_get_source + ecs_get_source = ecs_get_source, + + pair = ECS_PAIR, + getAlive = getAlive, }) diff --git a/lib/init.spec.lua b/lib/init.spec.lua index 553c9a4..8de8de9 100644 --- a/lib/init.spec.lua +++ b/lib/init.spec.lua @@ -309,12 +309,74 @@ return function() elseif id == eAB then expect(data[A]).to.be.ok() expect(data[B]).to.be.ok() - else - error("unknown entity", id) end end - expect(count).to.equal(3) + expect(count).to.equal(5) end) + + it("should allow querying for relations", function() + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:set(bob, jecs.pair(Eats, Apples), true) + for e, bool in world:query(jecs.pair(Eats, Apples)) do + expect(e).to.equal(bob) + expect(bool).to.equal(bool) + end + end) + + it("should allow wildcards in queries", function() + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:set(bob, jecs.pair(Eats, Apples), "bob eats apples") + for e, data in world:query(jecs.pair(Eats, jecs.w)) do + expect(e).to.equal(bob) + expect(data).to.equal("bob eats apples") + end + for e, data in world:query(jecs.pair(jecs.w, Apples)) do + expect(e).to.equal(bob) + expect(data).to.equal("bob eats apples") + end + end) + + it("should match against multiple pairs", function() + local world = jecs.World.new() + local pair = jecs.pair + local Eats = world:entity() + local Apples = world:entity() + local Oranges =world:entity() + local bob = world:entity() + local alice = world:entity() + + world:set(bob, pair(Eats, Apples), "bob eats apples") + world:set(alice, pair(Eats, Oranges), "alice eats oranges") + + local w = jecs.Wildcard + + local count = 0 + for e, data in world:query(pair(Eats, w)) do + count += 1 + if e == bob then + expect(data).to.equal("bob eats apples") + else + expect(data).to.equal("alice eats oranges") + end + end + + expect(count).to.equal(2) + count = 0 + + for e, data in world:query(pair(w, Apples)) do + count += 1 + expect(data).to.equal("bob eats apples") + end + expect(count).to.equal(1) + end) end) end \ No newline at end of file diff --git a/test.project.json b/test.project.json index b931a84..bdcbd0b 100644 --- a/test.project.json +++ b/test.project.json @@ -11,9 +11,6 @@ }, "ReplicatedStorage": { "$className": "ReplicatedStorage", - "DevPackages": { - "$path": "DevPackages" - }, "Lib": { "$path": "lib" }, @@ -25,6 +22,9 @@ }, "mirror": { "$path": "mirror" + }, + "DevPackages": { + "$path": "DevPackages" } }, "TestService": { diff --git a/tests/world.lua b/tests/world.lua index d95097d..1aff493 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -7,7 +7,6 @@ local ECS_PAIR = jecs.ECS_PAIR local getAlive = jecs.getAlive local ecs_get_source = jecs.ecs_get_source local ecs_get_target = jecs.ecs_get_target -local REST = 256 + 4 local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() @@ -18,7 +17,6 @@ TEST("world", function() local world = jecs.World.new() local A = world:component() local B = world:component() - local eA = world:entity() world:set(eA, A, true) local eB = world:entity() @@ -48,7 +46,6 @@ TEST("world", function() end do CASE "should query all matching entities" - local world = jecs.World.new() local A = world:component() local B = world:component() @@ -71,7 +68,6 @@ TEST("world", function() end do CASE "should query all matching entities when irrelevant component is removed" - local world = jecs.World.new() local A = world:component() local B = world:component() @@ -99,7 +95,6 @@ TEST("world", function() end do CASE "should query all entities without B" - local world = jecs.World.new() local A = world:component() local B = world:component() @@ -171,29 +166,24 @@ TEST("world", function() world:remove(id, Poison) CHECK(world:get(id, Poison) == nil) - print(world:get(id, Health)) CHECK(world:get(id, Health) == 50) end do CASE "should increment generation" local world = jecs.World.new() local e = world:entity() - CHECK(ECS_ID(e) == 1 + REST) + CHECK(ECS_ID(e) == 1 + jecs.Rest) CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) CHECK(ECS_GENERATION(e) == 0) -- 0 e = ECS_GENERATION_INC(e) CHECK(ECS_GENERATION(e) == 1) -- 1 end - do CASE "relations" + do CASE "should get alive from index in the dense array" local world = jecs.World.new() local _e = world:entity() local e2 = world:entity() local e3 = world:entity() - CHECK(ECS_ID(e2) == 2 + REST) - CHECK(ECS_ID(e3) == 3 + REST) - CHECK(ECS_GENERATION(e2) == 0) - CHECK(ECS_GENERATION(e3) == 0) CHECK(IS_PAIR(world:entity()) == false) @@ -203,6 +193,69 @@ TEST("world", function() CHECK(ecs_get_target(world.entityIndex, pair) == e3) end + do CASE "should allow querying for relations" + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:set(bob, ECS_PAIR(Eats, Apples), true) + for e, bool in world:query(ECS_PAIR(Eats, Apples)) do + CHECK(e == bob) + CHECK(bool) + end + end + + do CASE "should allow wildcards in queries" + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + + local w = jecs.Wildcard + for e, data in world:query(ECS_PAIR(Eats, w)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + for e, data in world:query(ECS_PAIR(w, Apples)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + end + + do CASE "should match against multiple pairs" + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges =world:entity() + local bob = world:entity() + local alice = world:entity() + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + + local w = jecs.Wildcard + local count = 0 + for e, data in world:query(ECS_PAIR(Eats, w)) do + count += 1 + if e == bob then + CHECK(data == "bob eats apples") + else + CHECK(data == "alice eats oranges") + end + end + + CHECK(count == 2) + count = 0 + + for e, data in world:query(ECS_PAIR(w, Apples)) do + count += 1 + CHECK(data == "bob eats apples") + end + CHECK(count == 1) + end end) FINISH() \ No newline at end of file diff --git a/wally.toml b/wally.toml index 7102c41..f17e660 100644 --- a/wally.toml +++ b/wally.toml @@ -6,4 +6,5 @@ realm = "shared" include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] exclude = ["**"] -[dev-dependencies] \ No newline at end of file +[dev-dependencies] +TestEZ = "roblox/testez@0.4.1" \ No newline at end of file From d6b6caf07afb176842ebe83801987eb1239378f9 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 13 May 2024 02:26:00 +0200 Subject: [PATCH 27/46] 0.1.0-rc.6 --- wally.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wally.toml b/wally.toml index f17e660..41797b9 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.1.0-rc.5" +version = "0.1.0-rc.6" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] From 85e2621cce9a7c788fb904aca7240b1227a1e81b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 13 May 2024 20:15:09 +0200 Subject: [PATCH 28/46] CompatibleArchetype as a map --- lib/init.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index b7486e4..f45e842 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -623,7 +623,10 @@ function World.query(world: World, ...: i53): Query end length += 1 - compatibleArchetypes[length] = {archetype, indices} + compatibleArchetypes[length] = { + archetype = archetype, + indices = indices + } end local lastArchetype, compatibleArchetype = next(compatibleArchetypes) @@ -637,7 +640,7 @@ function World.query(world: World, ...: i53): Query function preparedQuery:without(...) local withoutComponents = {...} for i = #compatibleArchetypes, 1, -1 do - local archetype = compatibleArchetypes[i][1] + local archetype = compatibleArchetypes[i].archetype local records = archetype.records local shouldRemove = false @@ -666,21 +669,21 @@ function World.query(world: World, ...: i53): Query function preparedQuery:__iter() return function() - local archetype = compatibleArchetype[1] + local archetype = compatibleArchetype.archetype local row: number = next(archetype.entities, lastRow) :: number while row == nil do lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) if lastArchetype == nil then return end - archetype = compatibleArchetype[1] + archetype = compatibleArchetype.archetype row = next(archetype.entities, row) :: number end lastRow = row local entityId = archetype.entities[row :: number] local columns = archetype.columns - local tr = compatibleArchetype[2] + local tr = compatibleArchetype.indices if queryLength == 1 then return entityId, columns[tr[1]][row] From 6710e3cdcb9ac103dc7ba5beb9c2358d4da96c67 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 14 May 2024 03:22:05 +0200 Subject: [PATCH 29/46] Bump wally to 0.3.2 --- aftman.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aftman.toml b/aftman.toml index ace6bc1..56cddbd 100644 --- a/aftman.toml +++ b/aftman.toml @@ -1,5 +1,5 @@ [tools] -wally = "upliftgames/wally@0.3.1" +wally = "upliftgames/wally@0.3.2" rojo = "rojo-rbx/rojo@7.4.1" stylua = "johnnymorganz/stylua@0.19.1" selene = "kampfkarren/selene@0.26.1" From e86b4c7f4c803d41209680ad53781a8d0ca668a3 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 14 May 2024 17:52:41 +0200 Subject: [PATCH 30/46] Rename functions --- lib/init.lua | 74 ++++++++++++++++++++++++------------------------- tests/world.lua | 8 +++--- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index f45e842..aabfbed 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -75,7 +75,7 @@ local function addFlags(isPair: boolean) return typeFlags end -local function newId(source: number, target: number) +local function ECS_COMBINE(source: number, target: number): i53 local e = source * 2^28 + target * ECS_ID_FLAGS_MASK return e end @@ -96,7 +96,7 @@ local function ECS_GENERATION(e: i53) return e % ECS_GENERATION_MASK end -local function ECS_ID(e: i53) +local function ECS_ENTITY_T_LO(e: i53) e //= 0x10 return e // ECS_ENTITY_MASK end @@ -104,51 +104,51 @@ end local function ECS_GENERATION_INC(e: i53) local id, generation, flags = separate(e) - return newId(id, generation + 1) + flags + return ECS_COMBINE(id, generation + 1) + flags end -- gets the high ID -local function ECS_PAIR_FIRST(entity: i53): i24 +local function ECS_ENTITY_T_HI(entity: i53): i24 entity //= 0x10 local first = entity % ECS_ENTITY_MASK return first end --- gets the low ID -local ECS_PAIR_SECOND = ECS_ID +local function ECS_PAIR(pred: number, obj: number) + local first + local second: number = WILDCARD -local function ECS_PAIR(first: number, second: number) - local target = WILDCARD - local relation - - if first == WILDCARD then - relation = second - elseif second == WILDCARD then - relation = first + if pred == WILDCARD then + first = obj + elseif obj == WILDCARD then + first = pred else - relation = second - target = ECS_PAIR_SECOND(first) + first = obj + second = ECS_ENTITY_T_LO(pred) end - return newId( - ECS_PAIR_SECOND(relation), target) + addFlags(--[[isPair]] true) + return ECS_COMBINE( + ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) end local function getAlive(entityIndex: EntityIndex, id: i53) return entityIndex.dense[id] end -local function ecs_get_source(entityIndex, e) +-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits +local function ECS_PAIR_RELATION(entityIndex, e) assert(ECS_IS_PAIR(e)) - return getAlive(entityIndex, ECS_PAIR_FIRST(e)) -end -local function ecs_get_target(entityIndex, e) - assert(ECS_IS_PAIR(e)) - return getAlive(entityIndex, ECS_PAIR_SECOND(e)) + return getAlive(entityIndex, ECS_ENTITY_T_HI(e)) end -local function nextEntityId(entityIndex, index: i24) - local id = newId(index, 0) +-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits +local function ECS_PAIR_OBJECT(entityIndex, e) + assert(ECS_IS_PAIR(e)) + return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) +end + +local function nextEntityId(entityIndex, index: i24): i53 + local id = ECS_COMBINE(index, 0) entityIndex.sparse[id] = { dense = index } :: Record @@ -267,14 +267,14 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet columns[i] = {} if ECS_IS_PAIR(componentId) then - local first = ecs_get_source(entityIndex, componentId) - local second = ecs_get_target(entityIndex, componentId) - local firstPair = ECS_PAIR(first, WILDCARD) - local secondPair = ECS_PAIR(WILDCARD, second) - createArchetypeRecord(componentIndex, id, firstPair, i) - createArchetypeRecord(componentIndex, id, secondPair, i) - records[firstPair] = i - records[secondPair] = i + local pred = ECS_PAIR_RELATION(entityIndex, componentId) + local obj = ECS_PAIR_OBJECT(entityIndex, componentId) + local first = ECS_PAIR(pred, WILDCARD) + local second = ECS_PAIR(WILDCARD, obj) + createArchetypeRecord(componentIndex, id, first, i) + createArchetypeRecord(componentIndex, id, second, i) + records[first] = i + records[second] = i end end @@ -783,13 +783,13 @@ return table.freeze({ w = WILDCARD, Rest = REST, - ECS_ID = ECS_ID, IS_PAIR = ECS_IS_PAIR, + ECS_ID = ECS_ENTITY_T_LO, ECS_PAIR = ECS_PAIR, ECS_GENERATION_INC = ECS_GENERATION_INC, ECS_GENERATION = ECS_GENERATION, - ecs_get_target = ecs_get_target, - ecs_get_source = ecs_get_source, + ECS_PAIR_RELATION = ECS_PAIR_RELATION, + ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, pair = ECS_PAIR, getAlive = getAlive, diff --git a/tests/world.lua b/tests/world.lua index 1aff493..cf7f47f 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -5,8 +5,8 @@ local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local IS_PAIR = jecs.IS_PAIR local ECS_PAIR = jecs.ECS_PAIR local getAlive = jecs.getAlive -local ecs_get_source = jecs.ecs_get_source -local ecs_get_target = jecs.ecs_get_target +local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION +local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() @@ -189,8 +189,8 @@ TEST("world", function() local pair = ECS_PAIR(e2, e3) CHECK(IS_PAIR(pair) == true) - CHECK(ecs_get_source(world.entityIndex, pair) == e2) - CHECK(ecs_get_target(world.entityIndex, pair) == e3) + CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) + CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) end do CASE "should allow querying for relations" From cf0683cf0307b2dd5fb9a74315023967c5d5c4a3 Mon Sep 17 00:00:00 2001 From: Marcus Date: Fri, 17 May 2024 00:17:53 +0200 Subject: [PATCH 31/46] Add World:target (#39) --- README.md | 59 +++++++--------- lib/init.lua | 184 ++++++++++++++++++++++++++++++++++-------------- tests/world.lua | 74 +++++++++++++++++++ 3 files changed, 232 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 6bb4607..8740429 100644 --- a/README.md +++ b/README.md @@ -22,47 +22,38 @@ jecs is a stupidly fast Entity Component System (ECS). ### Example ```lua -local world = World.new() +local world = jecs.World.new() +local pair = jecs.pair -local player = world:entity() -local opponent = world:entity() +local ChildOf = world:component() +local Name = world:component() -local Health = world:component() -local Position = world:component() --- Notice how components can just be entities as well? --- It allows you to model relationships easily! -local Damage = world:entity() -local DamagedBy = world:entity() +local function parent(entity) + return world:target(entity, ChildOf) +end +local function name() -world:set(player, Health, 100) -world:set(player, Damage, 8) -world:set(player, Position, Vector3.new(0, 5, 0)) +local alice = world:entity() +world:set(alice, Name, "alice") -world:set(opponent, Health, 100) -world:set(opponent, Damage, 21) -world:set(opponent, Position, Vector3.new(0, 5, 3)) +local bob = world:entity() +world:add(bob, pair(ChildOf, alice)) +world:set(bob, Name, "bob") -for playerId, playerPosition, health in world:query(Position, Health) do - local totalDamage = 0 - for opponentId, opponentPosition, damage in world:query(Position, Damage) do - if playerId == opponentId then - continue - end - if (playerPosition - opponentPosition).Magnitude < 5 then - totalDamage += damage - end - -- We create a pair between the relation component `DamagedBy` and the entity id of the opponent. - -- This will allow us to specifically query for damage exerted by a specific opponent. - world:set(playerId, ECS_PAIR(DamagedBy, opponentId), totalDamage) - end +local sara = world:entity() +world:add(sara, pair(ChildOf, alice)) +world:set(sara, Name, "sara") + +print(getName(parent(sara))) + +for e in world:query(pair(ChildOf, alice)) do + print(getName(e), "is the child of alice") end --- Gets the damage inflicted by our specific opponent! -for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, opponent)) do - world:set(playerId, health - inflicted) -end - -assert(world:get(player, Health) == 79) +-- Output +-- "alice" +-- bob is the child of alice +-- sara is the child of alice ``` 125 archetypes, 4 random components queried. diff --git a/lib/init.lua b/lib/init.lua index aabfbed..5f11982 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -14,7 +14,7 @@ type Column = {any} type Archetype = { id: number, edges: { - [i24]: { + [i53]: { add: Archetype, remove: Archetype, }, @@ -26,17 +26,37 @@ type Archetype = { records: {}, } + type Record = { archetype: Archetype, row: number, dense: i24, + componentRecord: ArchetypeMap } type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}} -type ComponentIndex = {[i24]: ArchetypeMap} type ArchetypeRecord = number -type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number} +--[[ +TODO: +{ + index: number, + count: number, + column: number +} + +]] + +type ArchetypeMap = { + cache: {[number]: ArchetypeRecord}, + first: ArchetypeMap, + second: ArchetypeMap, + parent: ArchetypeMap, + size: number +} + +type ComponentIndex = {[i24]: ArchetypeMap} + type Archetypes = {[ArchetypeId]: Archetype} type ArchetypeDiff = { @@ -96,6 +116,7 @@ local function ECS_GENERATION(e: i53) return e % ECS_GENERATION_MASK end +-- SECOND local function ECS_ENTITY_T_LO(e: i53) e //= 0x10 return e // ECS_ENTITY_MASK @@ -107,7 +128,7 @@ local function ECS_GENERATION_INC(e: i53) return ECS_COMBINE(id, generation + 1) + flags end --- gets the high ID +-- FIRST gets the high ID local function ECS_ENTITY_T_HI(entity: i53): i24 entity //= 0x10 local first = entity % ECS_ENTITY_MASK @@ -131,8 +152,13 @@ local function ECS_PAIR(pred: number, obj: number) ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) end -local function getAlive(entityIndex: EntityIndex, id: i53) - return entityIndex.dense[id] +local function getAlive(entityIndex: EntityIndex, id: i24) + local entityId = entityIndex.dense[id] + local record = entityIndex.sparse[entityIndex.dense[id]] + if not record then + error(id.." is not alive") + end + return entityId end -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits @@ -239,17 +265,29 @@ local function hash(arr): string | number return table.concat(arr, "_") end -local function createArchetypeRecord(componentIndex, id, componentId, i) +local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap local archetypesMap = componentIndex[componentId] if not archetypesMap then - archetypesMap = {size = 0, sparse = {}} + archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap componentIndex[componentId] = archetypesMap end - archetypesMap.sparse[id] = i + + archetypesMap.cache[archetypeId] = i + archetypesMap.size += 1 + + return archetypesMap end -local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype +local function ECS_ID_IS_WILDCARD(e) + assert(ECS_IS_PAIR(e)) + local first = ECS_ENTITY_T_HI(e) + local second = ECS_ENTITY_T_LO(e) + return first == WILDCARD or second == WILDCARD +end + + +local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype local ty = hash(types) local id = world.nextArchetypeId + 1 @@ -257,25 +295,27 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet local length = #types local columns = table.create(length) + local componentIndex = world.componentIndex local records = {} - local componentIndex = world.componentIndex - local entityIndex = world.entityIndex for i, componentId in types do - createArchetypeRecord(componentIndex, id, componentId, i) + ensureComponentRecord(componentIndex, id, componentId, i) records[componentId] = i - columns[i] = {} - if ECS_IS_PAIR(componentId) then - local pred = ECS_PAIR_RELATION(entityIndex, componentId) - local obj = ECS_PAIR_OBJECT(entityIndex, componentId) - local first = ECS_PAIR(pred, WILDCARD) - local second = ECS_PAIR(WILDCARD, obj) - createArchetypeRecord(componentIndex, id, first, i) - createArchetypeRecord(componentIndex, id, second, i) - records[first] = i - records[second] = i + local relation = ECS_PAIR_RELATION(world.entityIndex, componentId) + local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) + + local idr_r = ECS_PAIR(relation, WILDCARD) + ensureComponentRecord( + componentIndex, id, idr_r, i) + records[idr_r] = i + + local idr_t = ECS_PAIR(WILDCARD, object) + ensureComponentRecord( + componentIndex, id, idr_t, i) + records[idr_t] = i end + columns[i] = {} end local archetype = { @@ -333,6 +373,29 @@ function World.entity(world: World) return nextEntityId(world.entityIndex, entityId + REST) end +-- TODO: +-- should have an additional `index` parameter which selects the nth target +-- this is important when an entity can have multiple relationships with the same target +function World.target(world: World, entity: i53, relation: i24): i24? + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entity] + local archetype = record.archetype + if not archetype then + return nil + end + local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)] + if not componentRecord then + return nil + end + + local archetypeRecord = componentRecord.cache[archetype.id] + if not archetypeRecord then + return nil + end + + return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord]) +end + -- should reuse this logic in World.set instead of swap removing in transition archetype local function destructColumns(columns, count, row) if row == count then @@ -347,41 +410,54 @@ local function destructColumns(columns, count, row) end end -local function archetypeDelete(entityIndex, record: Record, entityId: i53, destruct: boolean) +local function archetypeDelete(world: World, id: i53) + local componentIndex = world.componentIndex + local archetypesMap = componentIndex[id] + local archetypes = world.archetypes + if archetypesMap then + for archetypeId in archetypesMap.cache do + for _, entity in archetypes[archetypeId].entities do + world:remove(entity, id) + end + end + + componentIndex[id] = nil + end +end + +function World.delete(world: World, entityId: i53) + local record = world.entityIndex.sparse[entityId] + if not record then + return + end + local entityIndex = world.entityIndex local sparse, dense = entityIndex.sparse, entityIndex.dense local archetype = record.archetype local row = record.row - local entities = archetype.entities - local last = #entities - local entityToMove = entities[last] + archetypeDelete(world, entityId) + archetypeDelete(world, ECS_PAIR(entityId, WILDCARD)) + archetypeDelete(world, ECS_PAIR(WILDCARD, entityId)) - if row ~= last then - dense[record.dense] = entityToMove - sparse[entityToMove] = record + if archetype then + local entities = archetype.entities + local last = #entities + + if row ~= last then + local entityToMove = entities[last] + dense[record.dense] = entityToMove + sparse[entityToMove] = record + end + + entities[row], entities[last] = entities[last], nil + + local columns = archetype.columns + + destructColumns(columns, last, row) end sparse[entityId] = nil dense[#dense] = nil - - entities[row], entities[last] = entities[last], nil - - local columns = archetype.columns - - if not destruct then - return - end - - destructColumns(columns, last, row) -end - -function World.delete(world: World, entityId: i53) - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entityId] - if not record then - return - end - archetypeDelete(entityIndex, record, entityId, true) end export type World = typeof(World.new()) @@ -530,6 +606,10 @@ end -- Keeping the function as small as possible to enable inlining local function get(record: Record, componentId: i24) local archetype = record.archetype + if not archetype then + return nil + end + local archetypeRecord = archetype.records[componentId] if not archetypeRecord then @@ -575,7 +655,7 @@ EmptyQuery.__index = EmptyQuery setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) - +local testkit = require("../testkit") function World.query(world: World, ...: i53): Query -- breaking? if (...) == nil then @@ -603,9 +683,10 @@ function World.query(world: World, ...: i53): Query end end - for id in firstArchetypeMap.sparse do + for id in firstArchetypeMap.cache do local archetype = archetypes[id] local archetypeRecords = archetype.records + local indices = {} local skip = false @@ -615,6 +696,7 @@ function World.query(world: World, ...: i53): Query skip = true break end + -- index should be index.offset indices[i] = index end diff --git a/tests/world.lua b/tests/world.lua index cf7f47f..2d4bb6d 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -1,5 +1,6 @@ local testkit = require("../testkit") local jecs = require("../lib/init") +local __ = jecs.Wildcard local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local IS_PAIR = jecs.IS_PAIR @@ -9,7 +10,16 @@ local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() +local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) + local ok, err: string? = pcall(fn, ...) + if not CHECK(not ok, 2) then + local i = string.find(err :: string, " ") + assert(i) + local msg = string.sub(err :: string, i+1) + CHECK(msg == s, 2) + end +end local N = 10 TEST("world", function() @@ -256,6 +266,70 @@ TEST("world", function() end CHECK(count == 1) end + + do CASE "should only relate alive entities" + + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + + world:delete(Apples) + local Wildcard = jecs.Wildcard + + local count = 0 + for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do + count += 1 + end + + CHECK(count == 0) + end + + do CASE "should error when setting invalid pair" + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:delete(Apples) + + CHECK_NO_ERR("Apples should be dead", function() + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + end) + end + + do CASE "should find target for ChildOf" + local world = jecs.World.new() + + local ChildOf = world:component() + local Name = world:component() + + local function parent(entity) + return world:target(entity, ChildOf) + end + + local bob = world:entity() + local alice = world:entity() + local sara = world:entity() + + world:add(bob, ECS_PAIR(ChildOf, alice)) + world:set(bob, Name, "bob") + world:add(sara, ECS_PAIR(ChildOf, alice)) + world:set(sara, Name, "sara") + CHECK(parent(bob) == alice) -- O(1) + + local count = 0 + for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do + print(name) + count += 1 + end + CHECK(count == 2) + end end) FINISH() \ No newline at end of file From 659b858f5a48c4b57fc41a9e5f8fe07cf37f8c59 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 17 May 2024 00:19:53 +0200 Subject: [PATCH 32/46] Fix name function --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8740429..2bb28e2 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ local Name = world:component() local function parent(entity) return world:target(entity, ChildOf) end -local function name() +local function name(entity) + return world:get(entity, Name) +end local alice = world:entity() world:set(alice, Name, "alice") From 5533cd1c649d3c2698c0b76d1c8921712d8e7f79 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 17 May 2024 00:37:47 +0200 Subject: [PATCH 33/46] Remove random testkit call --- lib/init.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/init.lua b/lib/init.lua index 5f11982..aaa975a 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -655,7 +655,7 @@ EmptyQuery.__index = EmptyQuery setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) -local testkit = require("../testkit") + function World.query(world: World, ...: i53): Query -- breaking? if (...) == nil then From 6c2f47bf701122421cb89e3a4ff21f0144844901 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Fri, 17 May 2024 01:05:40 +0200 Subject: [PATCH 34/46] Add todo --- lib/init.lua | 5 +++-- tests/world.lua | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index aaa975a..5cf38ea 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -420,7 +420,7 @@ local function archetypeDelete(world: World, id: i53) world:remove(entity, id) end end - + componentIndex[id] = nil end end @@ -436,9 +436,10 @@ function World.delete(world: World, entityId: i53) local row = record.row archetypeDelete(world, entityId) + -- TODO: should traverse linked )component records to pairs including entityId archetypeDelete(world, ECS_PAIR(entityId, WILDCARD)) archetypeDelete(world, ECS_PAIR(WILDCARD, entityId)) - + if archetype then local entities = archetype.entities local last = #entities diff --git a/tests/world.lua b/tests/world.lua index 2d4bb6d..f0eff7d 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -276,6 +276,7 @@ TEST("world", function() local bob = world:entity() local alice = world:entity() + world:set(bob, Apples, "apples") world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") @@ -283,11 +284,14 @@ TEST("world", function() local Wildcard = jecs.Wildcard local count = 0 - for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do + for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do count += 1 end + + world:delete(ECS_PAIR(Eats, Apples)) CHECK(count == 0) + CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) end do CASE "should error when setting invalid pair" From f55993180b01f7e334c80262048d565a764ce0b3 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 19 May 2024 04:17:22 +0200 Subject: [PATCH 35/46] Release (#43) * Add release * Release 0.1.0 --- .github/workflows/release.yaml | 73 ++++++++++++++++++++++++++++++++++ wally.toml | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/release.yaml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..b485f17 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,73 @@ +name: Release + +on: + push: + tags: ["v*"] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout Project + uses: actions/checkout@v3 + + - name: Install Aftman + uses: ok-nick/setup-aftman@v0.3.0 + + - name: Install Dependencies + run: wally install + + - name: Build + run: rojo build --output build.rbxm default.project.json + + - name: Upload Build Artifact + uses: actions/upload-artifact@v3 + with: + name: build + path: build.rbxm + + release: + name: Release + needs: [build] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout Project + uses: actions/checkout@v3 + + - name: Download Jecs Build + uses: actions/download-artifact@v3 + with: + name: build + path: build + + - name: Rename Build + run: mv build/build.rbxm jecs.rbxm + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: Matter ${{ github.ref_name }} + body: | + Matter ${{ github.ref_name }} is now available! + files: | + jecs.rbxm + + publish: + name: Publish + needs: [release] + runs-on: ubuntu-latest + steps: + - name: Checkout Project + uses: actions/checkout@v3 + + - name: Install Aftman + uses: ok-nick/setup-aftman@v0.3.0 + + - name: Wally Login + run: wally login --token ${{ secrets.WALLY_AUTH_TOKEN }} + + - name: Publish + run: wally publish \ No newline at end of file diff --git a/wally.toml b/wally.toml index 41797b9..879b3e7 100644 --- a/wally.toml +++ b/wally.toml @@ -1,6 +1,6 @@ [package] name = "ukendio/jecs" -version = "0.1.0-rc.6" +version = "0.1.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] From b8f7bed84cd7001949a3789a92ba680ef19f9ffc Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 19 May 2024 04:30:20 +0200 Subject: [PATCH 36/46] Release (#44) * Add release * Release 0.1.0 * Move the benches to bench.project.json --- bench.project.json | 31 +++ benches/exhaustive.lua | 372 ---------------------------- benches/visual/insertion.bench.lua | 1 - benches/visual/wally.toml | 11 + lib/init.spec.lua | 382 ----------------------------- test.project.json | 12 - tests.server.lua | 9 - wally.toml | 5 +- 8 files changed, 43 insertions(+), 780 deletions(-) create mode 100644 bench.project.json delete mode 100644 benches/exhaustive.lua create mode 100644 benches/visual/wally.toml delete mode 100644 lib/init.spec.lua delete mode 100644 tests.server.lua diff --git a/bench.project.json b/bench.project.json new file mode 100644 index 0000000..e55b3ec --- /dev/null +++ b/bench.project.json @@ -0,0 +1,31 @@ +{ + "name": "jecs-test", + "tree": { + "$className": "DataModel", + "StarterPlayer": { + "$className": "StarterPlayer", + "StarterPlayerScripts": { + "$className": "StarterPlayerScripts", + "$path": "tests" + } + }, + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "Lib": { + "$path": "lib" + }, + "rgb": { + "$path": "rgb.lua" + }, + "benches": { + "$path": "benches" + }, + "mirror": { + "$path": "mirror" + }, + "DevPackages": { + "$path": "benches/visual/DevPackages" + } + } + } +} \ No newline at end of file diff --git a/benches/exhaustive.lua b/benches/exhaustive.lua deleted file mode 100644 index 3095c70..0000000 --- a/benches/exhaustive.lua +++ /dev/null @@ -1,372 +0,0 @@ -local testkit = require("../testkit") -local jecs = require("../lib/init") -local ecr = require("../DevPackages/_Index/centau_ecr@0.8.0/ecr/src/ecr") - - -local BENCH, START = testkit.benchmark() - -local function TITLE(title: string) - print() - print(testkit.color.white(title)) -end - -local N = 2^16-2 - -type i53 = number - -do TITLE "create" - BENCH("entity", function() - local world = jecs.World.new() - for i = 1, START(N) do - world:entity() - end - end) -end - ---- component benchmarks - ---todo: perform the same benchmarks for multiple components.? --- these kind of operations only support 1 component at a time, which is --- a shame, especially for archetypes where moving components is expensive. - -do TITLE "set" - BENCH("add 1 component", function() - local world = jecs.World.new() - local entities = {} - - local A = world:component() - - for i = 1, N do - entities[i] = world:entity() - end - - for i = 1, START(N) do - world:set(entities[i], A, i) - end - end) - - BENCH("change 1 component", function() - local world = jecs.World.new() - local entities = {} - - local A = world:component() - local e = world:entity() - world:set(e, A, 1) - - for i = 1, START(N) do - world:set(e, A, 2) - end - end) - -end - -do TITLE "remove" - BENCH("1 component", function() - local world = jecs.World.new() - local entities = {} - - local A = world:component() - - for i = 1, N do - local id = world:entity() - entities[i] = id - world:set(id, A, true) - end - - for i = 1, START(N) do - world:remove(entities[i], A) - end - - end) -end - -do TITLE "get" - BENCH("1 component", function() - local world = jecs.World.new() - local entities = {} - - local A = world:component() - - for i = 1, N do - local id = world:entity() - entities[i] = id - world:set(id, A, true) - end - - for i = 1, START(N) do - -- ? curious why the overhead is roughly 80 ns. - world:get(entities[i], A) - end - - end) - - BENCH("2 component", function() - local world = jecs.World.new() - local entities = {} - - local A = world:component() - local B = world:component() - - for i = 1, N do - local id = world:entity() - entities[i] = id - world:set(id, A, true) - world:set(id, B, true) - end - - for i = 1, START(N) do - world:get(entities[i], A, B) - end - - end) - - BENCH("3 component", function() - local world = jecs.World.new() - local entities = {} - - local A = world:component() - local B = world:component() - local C = world:component() - - for i = 1, N do - local id = world:entity() - entities[i] = id - world:set(id, A, true) - world:set(id, B, true) - world:set(id, C, true) - end - - for i = 1, START(N) do - world:get(entities[i], A, B, C) - end - - end) - - BENCH("4 component", function() - local world = jecs.World.new() - local entities = {} - - local A = world:component() - local B = world:component() - local C = world:component() - local D = world:component() - - for i = 1, N do - local id = world:entity() - entities[i] = id - world:set(id, A, true) - world:set(id, B, true) - world:set(id, C, true) - world:set(id, D, true) - end - - for i = 1, START(N) do - world:get(entities[i], A, B, C, D) - end - - end) -end - -do TITLE (testkit.color.white_underline("Jecs query")) - - local function count(query: () -> ()) - local n = 0 - for _ in query do - n += 1 - end - return n - end - - local function flip() - return math.random() > 0.5 - end - - local function view_bench( - world: jecs.World, - A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53 - ) - - BENCH("1 component", function() - START(count(world:query(A))) - for _ in world:query(A) do end - end) - - BENCH("2 component", function() - START(count(world:query(A, B))) - for _ in world:query(A, B) do end - end) - - BENCH("4 component", function() - START(count(world:query(A, B, C, D))) - for _ in world:query(A, B, C, D) do end - end) - - BENCH("8 component", function() - START(count(world:query(A, B, C, D, E, F, G, H))) - for _ in world:query(A, B, C, D, E, F, G, H) do end - end) - end - - do TITLE "random components" - - local world = jecs.World.new() - - local A = world:component() - local B = world:component() - local C = world:component() - local D = world:component() - local E = world:component() - local F = world:component() - local G = world:component() - local H = world:component() - local I = world:component() - - for i = 1, N do - local id = world:entity() - if flip() then world:set(id, A, true) end - if flip() then world:set(id, B, true) end - if flip() then world:set(id, C, true) end - if flip() then world:set(id, D, true) end - if flip() then world:set(id, E, true) end - if flip() then world:set(id, F, true) end - if flip() then world:set(id, G, true) end - if flip() then world:set(id, H, true) end - if flip() then world:set(id, I, true) end - - end - - view_bench(world, A, B, C, D, E, F, G, H, I) - - end - - do TITLE "one component in common" - - local world = jecs.World.new() - - local A = world:component() - local B = world:component() - local C = world:component() - local D = world:component() - local E = world:component() - local F = world:component() - local G = world:component() - local H = world:component() - local I = world:component() - - for i = 1, N do - local id = world:entity() - local a = true - if flip() then world:set(id, B, true) else a = false end - if flip() then world:set(id, C, true) else a = false end - if flip() then world:set(id, D, true) else a = false end - if flip() then world:set(id, E, true) else a = false end - if flip() then world:set(id, F, true) else a = false end - if flip() then world:set(id, G, true) else a = false end - if flip() then world:set(id, H, true) else a = false end - if flip() then world:set(id, I, true) else a = false end - if a then world:set(id, A, true) end - - end - - view_bench(world, A, B, C, D, E, F, G, H, I) - - end - -end - -do TITLE (testkit.color.white_underline("ECR query")) - - local A = ecr.component() - local B = ecr.component() - local C = ecr.component() - local D = ecr.component() - local E = ecr.component() - local F = ecr.component() - local G = ecr.component() - local H = ecr.component() - local I = ecr.component() - - local function count(query: () -> ()) - local n = 0 - for _ in query do - n += 1 - end - return n - end - - local function flip() - return math.random() > 0.5 - end - - local function view_bench( - world: ecr.Registry, - A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53 - ) - - BENCH("1 component", function() - START(count(world:view(A))) - for _ in world:view(A) do end - end) - - BENCH("2 component", function() - START(count(world:view(A, B))) - for _ in world:view(A, B) do end - end) - - BENCH("4 component", function() - START(count(world:view(A, B, C, D))) - for _ in world:view(A, B, C, D) do end - end) - - BENCH("8 component", function() - START(count(world:view(A, B, C, D, E, F, G, H))) - for _ in world:view(A, B, C, D, E, F, G, H) do end - end) - end - - - do TITLE "random components" - local world = ecr.registry() - - for i = 1, N do - local id = world.create() - if flip() then world:set(id, A, true) end - if flip() then world:set(id, B, true) end - if flip() then world:set(id, C, true) end - if flip() then world:set(id, D, true) end - if flip() then world:set(id, E, true) end - if flip() then world:set(id, F, true) end - if flip() then world:set(id, G, true) end - if flip() then world:set(id, H, true) end - if flip() then world:set(id, I, true) end - - end - - view_bench(world, A, B, C, D, E, F, G, H, I) - - end - - do TITLE "one component in common" - - local world = ecr.registry() - - for i = 1, N do - local id = world.create() - local a = true - if flip() then world:set(id, B, true) else a = false end - if flip() then world:set(id, C, true) else a = false end - if flip() then world:set(id, D, true) else a = false end - if flip() then world:set(id, E, true) else a = false end - if flip() then world:set(id, F, true) else a = false end - if flip() then world:set(id, G, true) else a = false end - if flip() then world:set(id, H, true) else a = false end - if flip() then world:set(id, I, true) else a = false end - if a then world:set(id, A, true) end - - end - - view_bench(world, A, B, C, D, E, F, G, H, I) - - end - -end \ No newline at end of file diff --git a/benches/visual/insertion.bench.lua b/benches/visual/insertion.bench.lua index e8e50be..8e24f29 100644 --- a/benches/visual/insertion.bench.lua +++ b/benches/visual/insertion.bench.lua @@ -2,7 +2,6 @@ --!native local ReplicatedStorage = game:GetService("ReplicatedStorage") -local rgb = require(ReplicatedStorage.rgb) local Matter = require(ReplicatedStorage.DevPackages.Matter) local jecs = require(ReplicatedStorage.Lib) local ecr = require(ReplicatedStorage.DevPackages.ecr) diff --git a/benches/visual/wally.toml b/benches/visual/wally.toml new file mode 100644 index 0000000..cb0f731 --- /dev/null +++ b/benches/visual/wally.toml @@ -0,0 +1,11 @@ +[package] +name = "private/private" +version = "0.1.0-rc.6" +registry = "https://github.com/UpliftGames/wally-index" +realm = "shared" +include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] +exclude = ["**"] + +[dev-dependencies] +Matter = "matter-ecs/matter@0.8.0" +ecr = "centau/ecr@0.8.0" \ No newline at end of file diff --git a/lib/init.spec.lua b/lib/init.spec.lua deleted file mode 100644 index 8de8de9..0000000 --- a/lib/init.spec.lua +++ /dev/null @@ -1,382 +0,0 @@ -local jecs = require(script.Parent) -local world = jecs.World.new() - -local A, B, C, D = world:entity(), world:entity(), world:entity(), world:entity() -local E, F, G, H = world:entity(), world:entity(), world:entity(), world:entity() -print("A", A) -print("B", B) -print("C", C) -print("D", D) -print("E", E) -print("F", F) -print("G", G) -print("H", H) - -local common = 0 -local N = 2^16-2 -local archetypes = {} -local function flip() - return math.random() >= 0.5 -end - -local amountOfCombination = 0 -for i = 1, N do - local entity = world:entity() - local combination = "" - - if flip() then - combination ..= "2_" - world:set(entity, B, { value = true}) - end - if flip() then - combination ..= "3_" - world:set(entity, C, { value = true}) - end - if flip() then - combination ..= "4_" - world:set(entity, D, { value = true}) - end - if flip() then - combination ..= "5_" - world:set(entity, E, { value = true}) - end - if flip() then - combination ..= "6_" - world:set(entity, F, { value = true}) - end - if flip() then - combination ..= "7_" - world:set(entity, G, { value = true}) - end - if flip() then - combination ..= "8" - world:set(entity, H, { value = true}) - end - - if #combination == 7 then - combination = "1_" .. combination - common += 1 - world:set(entity, A, { value = true}) - end - - if combination:find("2") - and combination:find("3") - and combination:find("4") - and combination:find("6") - then - amountOfCombination += 1 - end - archetypes[combination] = true -end - -return function() - describe("World", function() - it("should add component", function() - local id = world:entity() - world:set(id, A, true) - world:set(id, B, 1) - - local id1 = world:entity() - world:set(id1, A, "hello") - expect(world:get(id, A)).to.equal(true) - expect(world:get(id, B)).to.equal(1) - expect(world:get(id1, A)).to.equal("hello") - end) - - it("should remove component", function() - local Tag = world:entity() - local entities = {} - for i = 1, 10 do - local entity = world:entity() - entities[i] = entity - world:set(entity, Tag) - end - - for i = 1, 10 do - local entity = entities[i] - expect(world:get(entity, Tag)).to.equal(nil) - world:remove(entity, Tag) - end - - end) - - it("should override component data", function() - - local id = world:entity() - world:set(id, A, true) - expect(world:get(id, A)).to.equal(true) - - world:set(id, A, false) - expect(world:get(id, A)).to.equal(false) - - end) - - it("should not query a removed component", function() - local Tag = world:entity() - local AnotherTag = world:entity() - - local entity = world:entity() - world:set(entity, Tag) - world:set(entity, AnotherTag) - world:remove(entity, AnotherTag) - - local added = 0 - for e, t, a in world:query(Tag, AnotherTag) do - added += 1 - end - expect(added).to.equal(0) - end) - - it("should query correct number of compatible archetypes", function() - local added = 0 - for _ in world:query(B, C, D, F) do - added += 1 - end - expect(added).to.equal(amountOfCombination) - end) - - it("should not query poisoned players", function() - local Player = world:entity() - local Health = world:entity() - local Poison = world:entity() - - local one = world:entity() - world:set(one, Player, { name = "alice"}) - world:set(one, Health, 100) - world:set(one, Poison) - - local two = world:entity() - world:set(two, Player, { name = "bob"}) - world:set(two, Health, 90) - - local withoutCount = 0 - for _id, _player in world:query(Player):without(Poison) do - withoutCount += 1 - end - - expect(withoutCount).to.equal(1) - end) - - it("should allow calling world:entity before world:component", function() - for _ = 1, 256 do - world:entity() - end - expect(world:component()).to.be.ok() - end) - - it("should skip iteration", function() - local Position, Velocity = world:entity(), world:entity() - local e = world:entity() - world:set(e, Position, Vector3.zero) - world:set(e, Velocity, Vector3.one) - local added = 0 - for i in world:query(Position):without(Velocity) do - added += 1 - end - expect(added).to.equal(0) - end) - - it("should query all matching entities", function() - - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local entities = {} - for i = 1, N do - local id = world:entity() - - - world:set(id, A, true) - if i > 5 then world:set(id, B, true) end - entities[i] = id - end - - for id in world:query(A) do - local i = table.find(entities, id) - expect(i).to.be.ok() - table.remove(entities, i) - end - - expect(#entities).to.equal(0) - end) - - it("should query all matching entities when irrelevant component is removed", function() - - - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local entities = {} - for i = 1, N do - local id = world:entity() - - world:set(id, A, true) - world:set(id, B, true) - if i > 5 then world:remove(id, B, true) end - entities[i] = id - end - - local added = 0 - for id in world:query(A) do - added += 1 - local i = table.find(entities, id) - expect(i).to.be.ok() - table.remove(entities, i) - end - - expect(added).to.equal(N) - end) - - it("should query all entities without B", function() - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local entities = {} - for i = 1, N do - local id = world:entity() - - world:set(id, A, true) - if i < 5 then - entities[i] = id - else - world:set(id, B, true) - end - - end - - for id in world:query(A):without(B) do - local i = table.find(entities, id) - expect(i).to.be.ok() - table.remove(entities, i) - end - - expect(#entities).to.equal(0) - end) - - it("should allow setting components in arbitrary order", function() - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - - expect(world:get(id, Poison)).to.equal(5) - end) - - it("Should allow deleting components", function() - local world = jecs.World.new() - - local Health = world:entity() - local Poison = world:component() - - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - world:delete(id) - - expect(world:get(id, Poison)).to.never.be.ok() - expect(world:get(id, Health)).to.never.be.ok() - end) - - it("should allow iterating the whole world", function() - local world = jecs.World.new() - - local A, B = world:entity(), world:entity() - - local eA = world:entity() - world:set(eA, A, true) - local eB = world:entity() - world:set(eB, B, true) - local eAB = world:entity() - world:set(eAB, A, true) - world:set(eAB, B, true) - - local count = 0 - for id, data in world do - count += 1 - if id == eA then - expect(data[A]).to.be.ok() - expect(data[B]).to.never.be.ok() - elseif id == eB then - expect(data[B]).to.be.ok() - expect(data[A]).to.never.be.ok() - elseif id == eAB then - expect(data[A]).to.be.ok() - expect(data[B]).to.be.ok() - end - end - - expect(count).to.equal(5) - end) - - it("should allow querying for relations", function() - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, jecs.pair(Eats, Apples), true) - for e, bool in world:query(jecs.pair(Eats, Apples)) do - expect(e).to.equal(bob) - expect(bool).to.equal(bool) - end - end) - - it("should allow wildcards in queries", function() - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, jecs.pair(Eats, Apples), "bob eats apples") - for e, data in world:query(jecs.pair(Eats, jecs.w)) do - expect(e).to.equal(bob) - expect(data).to.equal("bob eats apples") - end - for e, data in world:query(jecs.pair(jecs.w, Apples)) do - expect(e).to.equal(bob) - expect(data).to.equal("bob eats apples") - end - end) - - it("should match against multiple pairs", function() - local world = jecs.World.new() - local pair = jecs.pair - local Eats = world:entity() - local Apples = world:entity() - local Oranges =world:entity() - local bob = world:entity() - local alice = world:entity() - - world:set(bob, pair(Eats, Apples), "bob eats apples") - world:set(alice, pair(Eats, Oranges), "alice eats oranges") - - local w = jecs.Wildcard - - local count = 0 - for e, data in world:query(pair(Eats, w)) do - count += 1 - if e == bob then - expect(data).to.equal("bob eats apples") - else - expect(data).to.equal("alice eats oranges") - end - end - - expect(count).to.equal(2) - count = 0 - - for e, data in world:query(pair(w, Apples)) do - count += 1 - expect(data).to.equal("bob eats apples") - end - expect(count).to.equal(1) - end) - end) -end \ No newline at end of file diff --git a/test.project.json b/test.project.json index bdcbd0b..0a3901a 100644 --- a/test.project.json +++ b/test.project.json @@ -22,18 +22,6 @@ }, "mirror": { "$path": "mirror" - }, - "DevPackages": { - "$path": "DevPackages" - } - }, - "TestService": { - "$properties": { - "ExecuteWithStudioRun": true - }, - "$className": "TestService", - "run": { - "$path": "tests.server.lua" } } } diff --git a/tests.server.lua b/tests.server.lua deleted file mode 100644 index 683913d..0000000 --- a/tests.server.lua +++ /dev/null @@ -1,9 +0,0 @@ -local ReplicatedStorage = game:GetService("ReplicatedStorage") - -require(ReplicatedStorage.DevPackages.TestEZ).TestBootstrap:run({ - ReplicatedStorage.Lib, - nil, - { - noXpcallByDefault = true, - }, -}) diff --git a/wally.toml b/wally.toml index 879b3e7..a19b86f 100644 --- a/wally.toml +++ b/wally.toml @@ -4,7 +4,4 @@ version = "0.1.0" registry = "https://github.com/UpliftGames/wally-index" realm = "shared" include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"] -exclude = ["**"] - -[dev-dependencies] -TestEZ = "roblox/testez@0.4.1" \ No newline at end of file +exclude = ["**"] \ No newline at end of file From f1ba9c4a5579ba539891c8020b53c6e3bea6e0a6 Mon Sep 17 00:00:00 2001 From: Marcus Date: Fri, 24 May 2024 02:58:33 +0200 Subject: [PATCH 37/46] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2bb28e2..059e172 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Just an ECS jecs is a stupidly fast Entity Component System (ECS). - Entity Relationships as first class citizens -- Process tens of thousands of entities with ease every frame +- Iterate 350,000 entities at 60 frames per second - Type-safe [Luau](https://luau-lang.org/) API - Zero-dependency package - Optimized for column-major operations From 0567856a5998fc8e9c0a0bfb8debe0ff16ccba92 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 27 May 2024 03:39:20 +0200 Subject: [PATCH 38/46] Add docs (#45) * Initial commit * Add section for standalone * Fix docs * Add pages to docs * Remove redundant files --- README.md | 2 +- docs/api-types.md | 45 ++ docs/tutorials/quick-start/getting-started.md | 19 + docs/tutorials/quick-start/rbxm.png | Bin 0 -> 34402 bytes lib/init.lua | 273 +++++---- mkdocs.yml | 186 ++++++ tests/world.lua | 552 +++++++++--------- 7 files changed, 665 insertions(+), 412 deletions(-) create mode 100644 docs/api-types.md create mode 100644 docs/tutorials/quick-start/getting-started.md create mode 100644 docs/tutorials/quick-start/rbxm.png create mode 100644 mkdocs.yml diff --git a/README.md b/README.md index 059e172..c5ece64 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ local Name = world:component() local function parent(entity) return world:target(entity, ChildOf) end -local function name(entity) +local function getName(entity) return world:get(entity, Name) end diff --git a/docs/api-types.md b/docs/api-types.md new file mode 100644 index 0000000..b446999 --- /dev/null +++ b/docs/api-types.md @@ -0,0 +1,45 @@ +# World + +A World contains all ECS data +Games can have multiple worlds, although typically only one is necessary. These worlds are isolated from each other, meaning they donot share the same entities nor component IDs. + +--- + +# Entity + +An unique id. + +Entities consist out of a number unique to the entity in the lower 32 bits, and a counter used to track entity liveliness in the upper 32 bits. When an id is recycled, its generation count is increased. This causes recycled ids to be very large (>4 billion), which is normal. + +--- + +# QueryIter + +A result from the `World:query` function. + +Queries are used to iterate over entities that match against the set collection of components. + +Calling it in a loop will allow iteration over the results. + +```lua +for id, enemy, charge, model in world:query(Enemy, Charge, Model) do + -- Do something +end +``` + +### QueryIter.without + +QueryIter.without(iter: QueryIter + ...: [Entity](../api-types/Entity)): QueryIter + + +Create a new Query Iterator from the filter + +#### Parameters + world The world. + ... The collection of components to filter archetypes against. + +#### Returns + +The new query iterator. + diff --git a/docs/tutorials/quick-start/getting-started.md b/docs/tutorials/quick-start/getting-started.md new file mode 100644 index 0000000..bd702d2 --- /dev/null +++ b/docs/tutorials/quick-start/getting-started.md @@ -0,0 +1,19 @@ +# Getting Started +This section will provide a walk through setting up your development environment and a quick overview of the different features and concepts in Jecs with short examples. + +## Installing Jecs + +To use Jecs, you will need to add the library to your project's source folder. + +## Installing as standalone +Head over to the [Releases](https://github.com/ukendio/jecs/releases/latest) page and install the rbxm file. +![jecs.rbxm](rbxm.png) + +## Installing with Wally +Jecs is available as a package on [wally.run](https://wally.run/package/ukendio/jecs) + +Add it to your project's Wally.toml like this: +```toml +[dependencies] +jecs = "0.1.0" # Make sure this is the latest version +``` \ No newline at end of file diff --git a/docs/tutorials/quick-start/rbxm.png b/docs/tutorials/quick-start/rbxm.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8f38d5b842c87789249317e03a70c24f45ab48 GIT binary patch literal 34402 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

J;wRKgqu9eJayK1e z&$AVytU`@SAnaioMNr@;{ouuamPluxB~H|%&-?gOOH5F|#~1C+>D+ViJ>nTRXxkbt zIyHasbYvF$#9}I6QXIVsJfkJ%YoVW{aw7kU(F~YIAbQdbIDZ_A#%uC;Pd>q@*h?$q zM~olW;yp`L6RFy6fTwpI68_=r%=2#(al7Q%p6<)mru)FCgu7=WC3mc|V>rV%nN(9m z-Stm5P6($YvOortT#dB~Rj7F#?vPTF)U4TBBID`%-BXA}xc_h^lKe#e#!dUzMd;nfpLyjmu*jPWfq_Sb(~V;vA5AbQYCi8?PmI2zrUE%e61hY zN1TH7X{3hey*k}XOS?yEB09Iq%=t>#ob;V+5WStfns$`7D7BR`&LmjKdX()`JC%e} z)3WgfkqR>S*99TsL00vpqBy1{_bdD>H*+?(4f72Zew5j-Lp=KVe?R!R7L6oa|9xj& z|CQ6}hSJ1+dm!785Y=}5$jH_Yuk{AjAvGSXl8_ZJg)J)5u4-?c6UA>LU!q=m;}@$G z(7WoSbk1*z2(Y$q=DMJ>Hz`u~QMx(?o@-V7KPv{Zbmw8m?`n8`AfRe}Do;f3pC9WN zSDCiTNjbaK@1tWWK7vMlkt6!$A`DC85q0S@QUn+^c+#iBm6@&qe; zZ-q4nlgt7;v?clAD5)v^=?hp7{lNOPchSU6K%24-Qln&fr>h?>3hHk5!L%82-HHf@ zbagbBW`y-B9tlQq4pL7b)c@fS()lLp+~%KT!ewnZtKA;5pN12$gl|9Ng8G4RA=5z)H9uFX zCpxQSL}PL<>^F~HGm%rM7#!34p@H8gslH!oI>OqaFA-;AQS8?J>!OpR5GeZhG$A;@ zB3AF*p$QUTYmrDAlyG#+X%y1!4{6ph+1a08w0CZPs3-!aewW%RzhC@9ToSE2-r8GP zCH;?w=LtJ-Dp3maeK}D&WsmkL;Q{)we(!qxI?~M43r1~B6uARB9=OZRo zcVz=aiRQNME2YDSjXQuR1JV7-R+rdTL+N<(?lIF=lAcJ(WpCx#oP3>I+n(|slpLK+ zOgx@xs6VnUWqeNWOyQ-|_R>wr`k!uQ z@Ac^HEepgVS~NQ|oo=G*fMK(7RYi~7Oee6NwM|Hf9Z1<$^brq9e0UDK~^OyxR|)B>e7Fy2MHR z#xp<*!f`AVIh)qmvj-GLvcdfx zfKusWY$=f`)b6B^RU5vJ_RCHE$Av{0B_7uUdChy}RF2OO$O5^(55kxmM*cxPkNyS; zY^lV2y3FwGC!qDW!x%KId|{aw&p|(!|8}6i+TWM>M_-xz?Y!PH5xez`{~wL}x8vA< zb^W5vNrJ^0zer$~-hVZ#!@CM24@~M%_zls8{0}g!yOfS749u?GWco?vV##yL!l{Vk zADpJp4WDmepaK&3HHZWEGkwApHAE;J>QX5~9{3r;G7kR|8Ee8L~|w4;n(n9e!RX1lJ`4QzdWfq1$eXwlqP@A zcK{DO0rQtB9!K9T#TMU8A=?q^1Atk+LpSf!FL#!(rm*hl96z+kM7RCs)ic1qXVOYi J`A-br{vYTowmAR* literal 0 HcmV?d00001 diff --git a/lib/init.lua b/lib/init.lua index 5cf38ea..9ef0b10 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -6,10 +6,10 @@ type i53 = number type i24 = number -type Ty = {i53} +type Ty = { i53 } type ArchetypeId = number -type Column = {any} +type Column = { any } type Archetype = { id: number, @@ -21,20 +21,19 @@ type Archetype = { }, types: Ty, type: string | number, - entities: {number}, - columns: {Column}, + entities: { number }, + columns: { Column }, records: {}, } - type Record = { archetype: Archetype, row: number, dense: i24, - componentRecord: ArchetypeMap + componentRecord: ArchetypeMap, } -type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}} +type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } } type ArchetypeRecord = number --[[ @@ -48,16 +47,16 @@ TODO: ]] type ArchetypeMap = { - cache: {[number]: ArchetypeRecord}, + cache: { [number]: ArchetypeRecord }, first: ArchetypeMap, second: ArchetypeMap, parent: ArchetypeMap, - size: number + size: number, } -type ComponentIndex = {[i24]: ArchetypeMap} +type ComponentIndex = { [i24]: ArchetypeMap } -type Archetypes = {[ArchetypeId]: Archetype} +type Archetypes = { [ArchetypeId]: Archetype } type ArchetypeDiff = { added: Ty, @@ -70,114 +69,102 @@ local ON_ADD = HI_COMPONENT_ID + 1 local ON_REMOVE = HI_COMPONENT_ID + 2 local ON_SET = HI_COMPONENT_ID + 3 local WILDCARD = HI_COMPONENT_ID + 4 -local REST = HI_COMPONENT_ID + 5 +local REST = HI_COMPONENT_ID + 5 local ECS_ID_FLAGS_MASK = 0x10 local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_GENERATION_MASK = bit32.lshift(1, 16) -local function addFlags(isPair: boolean) - local typeFlags = 0x0 +local function addFlags(isPair: boolean) + local typeFlags = 0x0 - if isPair then - typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. - end + if isPair then + typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. + end if false then - typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true - end - if false then - typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true - end - if false then - typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. - end + typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. + end - return typeFlags + return typeFlags end local function ECS_COMBINE(source: number, target: number): i53 - local e = source * 2^28 + target * ECS_ID_FLAGS_MASK - return e + local e = source * 268435456 + target * ECS_ID_FLAGS_MASK + return e end -local function ECS_IS_PAIR(e: number) - return (e % 2^4) // FLAGS_PAIR ~= 0 -end - -function separate(entity: number) - local _typeFlags = entity % 0x10 - entity //= ECS_ID_FLAGS_MASK - return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags +local function ECS_IS_PAIR(e: number) + return (e % 2 ^ 4) // FLAGS_PAIR ~= 0 end -- HIGH 24 bits LOW 24 bits local function ECS_GENERATION(e: i53) - e //= 0x10 - return e % ECS_GENERATION_MASK -end - --- SECOND -local function ECS_ENTITY_T_LO(e: i53) - e //= 0x10 - return e // ECS_ENTITY_MASK + e = e // 0x10 + return e % ECS_GENERATION_MASK end local function ECS_GENERATION_INC(e: i53) - local id, generation, flags = separate(e) + local flags = e // 0x10 + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK - return ECS_COMBINE(id, generation + 1) + flags + return ECS_COMBINE(id, generation + 1) + flags end -- FIRST gets the high ID -local function ECS_ENTITY_T_HI(entity: i53): i24 - entity //= 0x10 - local first = entity % ECS_ENTITY_MASK - return first +local function ECS_ENTITY_T_HI(e: i53): i24 + e = e // 0x10 + return e % ECS_ENTITY_MASK end -local function ECS_PAIR(pred: number, obj: number) - local first +-- SECOND +local function ECS_ENTITY_T_LO(e: i53) + e = e // 0x10 + return e // ECS_ENTITY_MASK +end + +local function ECS_PAIR(pred: i53, obj: i53): i53 + local first local second: number = WILDCARD - if pred == WILDCARD then + if pred == WILDCARD then first = obj elseif obj == WILDCARD then first = pred else - first = obj + first = obj second = ECS_ENTITY_T_LO(pred) end - return ECS_COMBINE( - ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) -end + return ECS_COMBINE(ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) +end -local function getAlive(entityIndex: EntityIndex, id: i24) +local function getAlive(entityIndex: EntityIndex, id: i24) local entityId = entityIndex.dense[id] - local record = entityIndex.sparse[entityIndex.dense[id]] - if not record then - error(id.." is not alive") - end - return entityId + return entityId end -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -local function ECS_PAIR_RELATION(entityIndex, e) - assert(ECS_IS_PAIR(e)) - return getAlive(entityIndex, ECS_ENTITY_T_HI(e)) +local function ECS_PAIR_RELATION(entityIndex, e) + return getAlive(entityIndex, ECS_ENTITY_T_HI(e)) end -- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -local function ECS_PAIR_OBJECT(entityIndex, e) - assert(ECS_IS_PAIR(e)) - return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) +local function ECS_PAIR_OBJECT(entityIndex, e) + return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) end local function nextEntityId(entityIndex, index: i24): i53 local id = ECS_COMBINE(index, 0) entityIndex.sparse[id] = { - dense = index - } :: Record + dense = index, + } :: Record entityIndex.dense[index] = id return id @@ -224,7 +211,7 @@ local function transitionArchetype( local e1 = sourceEntities[sourceRow] local e2 = sourceEntities[movedAway] - if sourceRow ~= movedAway then + if sourceRow ~= movedAway then sourceEntities[sourceRow] = e2 end @@ -252,7 +239,7 @@ local function newEntity(entityId: i53, record: Record, archetype: Archetype) return record end -local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) +local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Record, to: Archetype) local sourceRow = record.row local from = record.archetype local destinationRow = archetypeAppend(entityId, to) @@ -265,11 +252,16 @@ local function hash(arr): string | number return table.concat(arr, "_") end -local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap +local function ensureComponentRecord( + componentIndex: ComponentIndex, + archetypeId: number, + componentId: number, + i: number +): ArchetypeMap local archetypesMap = componentIndex[componentId] if not archetypesMap then - archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap + archetypesMap = { size = 0, cache = {}, first = {}, second = {} } :: ArchetypeMap componentIndex[componentId] = archetypesMap end @@ -279,15 +271,14 @@ local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId return archetypesMap end -local function ECS_ID_IS_WILDCARD(e) +local function ECS_ID_IS_WILDCARD(e) assert(ECS_IS_PAIR(e)) local first = ECS_ENTITY_T_HI(e) local second = ECS_ENTITY_T_LO(e) return first == WILDCARD or second == WILDCARD end - -local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype +local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype local ty = hash(types) local id = world.nextArchetypeId + 1 @@ -301,31 +292,29 @@ local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetyp for i, componentId in types do ensureComponentRecord(componentIndex, id, componentId, i) records[componentId] = i - if ECS_IS_PAIR(componentId) then + if ECS_IS_PAIR(componentId) then local relation = ECS_PAIR_RELATION(world.entityIndex, componentId) local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) - + local idr_r = ECS_PAIR(relation, WILDCARD) - ensureComponentRecord( - componentIndex, id, idr_r, i) + ensureComponentRecord(componentIndex, id, idr_r, i) records[idr_r] = i - + local idr_t = ECS_PAIR(WILDCARD, object) - ensureComponentRecord( - componentIndex, id, idr_t, i) + ensureComponentRecord(componentIndex, id, idr_t, i) records[idr_t] = i end columns[i] = {} end local archetype = { - columns = columns; - edges = {}; - entities = {}; - id = id; - records = records; - type = ty; - types = types; + columns = columns, + edges = {}, + entities = {}, + id = id, + records = records, + type = ty, + types = types, } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype @@ -337,20 +326,20 @@ local World = {} World.__index = World function World.new() local self = setmetatable({ - archetypeIndex = {}; - archetypes = {} :: Archetypes; - componentIndex = {} :: ComponentIndex; + archetypeIndex = {}, + archetypes = {} :: Archetypes, + componentIndex = {} :: ComponentIndex, entityIndex = { dense = {}, - sparse = {} - } :: EntityIndex; + sparse = {}, + } :: EntityIndex, hooks = { - [ON_ADD] = {}; - }; - nextArchetypeId = 0; - nextComponentId = 0; - nextEntityId = 0; - ROOT_ARCHETYPE = (nil :: any) :: Archetype; + [ON_ADD] = {}, + }, + nextArchetypeId = 0, + nextComponentId = 0, + nextEntityId = 0, + ROOT_ARCHETYPE = (nil :: any) :: Archetype, }, World) self.ROOT_ARCHETYPE = archetypeOf(self, {}) return self @@ -380,16 +369,16 @@ function World.target(world: World, entity: i53, relation: i24): i24? local entityIndex = world.entityIndex local record = entityIndex.sparse[entity] local archetype = record.archetype - if not archetype then + if not archetype then return nil end local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)] - if not componentRecord then + if not componentRecord then return nil end local archetypeRecord = componentRecord.cache[archetype.id] - if not archetypeRecord then + if not archetypeRecord then return nil end @@ -397,37 +386,37 @@ function World.target(world: World, entity: i53, relation: i24): i24? end -- should reuse this logic in World.set instead of swap removing in transition archetype -local function destructColumns(columns, count, row) - if row == count then - for _, column in columns do +local function destructColumns(columns, count, row) + if row == count then + for _, column in columns do column[count] = nil end else - for _, column in columns do + for _, column in columns do column[row] = column[count] column[count] = nil end end end -local function archetypeDelete(world: World, id: i53) - local componentIndex = world.componentIndex +local function archetypeDelete(world: World, id: i53) + local componentIndex = world.componentIndex local archetypesMap = componentIndex[id] local archetypes = world.archetypes - if archetypesMap then - for archetypeId in archetypesMap.cache do - for _, entity in archetypes[archetypeId].entities do + if archetypesMap then + for archetypeId in archetypesMap.cache do + for _, entity in archetypes[archetypeId].entities do world:remove(entity, id) end end - + componentIndex[id] = nil end end -function World.delete(world: World, entityId: i53) +function World.delete(world: World, entityId: i53) local record = world.entityIndex.sparse[entityId] - if not record then + if not record then return end local entityIndex = world.entityIndex @@ -439,12 +428,12 @@ function World.delete(world: World, entityId: i53) -- TODO: should traverse linked )component records to pairs including entityId archetypeDelete(world, ECS_PAIR(entityId, WILDCARD)) archetypeDelete(world, ECS_PAIR(WILDCARD, entityId)) - - if archetype then + + if archetype then local entities = archetype.entities local last = #entities - if row ~= last then + if row ~= last then local entityToMove = entities[last] dense[record.dense] = entityToMove sparse[entityToMove] = record @@ -477,7 +466,7 @@ local function ensureArchetype(world: World, types, prev) return archetypeOf(world, types, prev) end -local function findInsert(types: {i53}, toAdd: i53) +local function findInsert(types: { i53 }, toAdd: i53) for i, id in types do if id == toAdd then return -1 @@ -494,7 +483,7 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53 -- Component IDs are added incrementally, so inserting and sorting -- them each time would be expensive. Instead this insertion sort can find the insertion -- point in the types array. - + local destinationType = table.clone(node.types) local at = findInsert(types, componentId) if at == -1 then @@ -532,7 +521,7 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet return add end -function World.add(world: World, entityId: i53, componentId: i53) +function World.add(world: World, entityId: i53, componentId: i53) local entityIndex = world.entityIndex local record = entityIndex.sparse[entityId] local from = record.archetype @@ -582,7 +571,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc if not remove then local to = table.clone(from.types) local at = table.find(to, componentId) - if not at then + if not at then return from end table.remove(to, at) @@ -607,7 +596,7 @@ end -- Keeping the function as small as possible to enable inlining local function get(record: Record, componentId: i24) local archetype = record.archetype - if not archetype then + if not archetype then return nil end @@ -644,20 +633,20 @@ end -- the less creation the better local function actualNoOperation() end -local function noop(_self: Query, ...: i53): () -> (number, ...any) +local function noop(_self: Query, ...): () -> () return actualNoOperation :: any end local EmptyQuery = { - __iter = noop; - without = noop; + __iter = noop, + without = noop, } EmptyQuery.__index = EmptyQuery setmetatable(EmptyQuery, EmptyQuery) export type Query = typeof(EmptyQuery) -function World.query(world: World, ...: i53): Query +function World.query(world: World, ...): Query -- breaking? if (...) == nil then error("Missing components") @@ -666,7 +655,7 @@ function World.query(world: World, ...: i53): Query local compatibleArchetypes = {} local length = 0 - local components = {...} + local components = { ... } local archetypes = world.archetypes local queryLength = #components @@ -707,8 +696,8 @@ function World.query(world: World, ...: i53): Query length += 1 compatibleArchetypes[length] = { - archetype = archetype, - indices = indices + archetype = archetype, + indices = indices, } end @@ -721,7 +710,7 @@ function World.query(world: World, ...: i53): Query preparedQuery.__index = preparedQuery function preparedQuery:without(...) - local withoutComponents = {...} + local withoutComponents = { ... } for i = #compatibleArchetypes, 1, -1 do local archetype = compatibleArchetypes[i].archetype local records = archetype.records @@ -828,16 +817,16 @@ function World.__iter(world: World): () -> (number?, unknown?) local sparse = world.entityIndex.sparse local last - return function() + return function() local lastEntity, entityId = next(dense, last) - if not lastEntity then + if not lastEntity then return end last = lastEntity local record = sparse[entityId] local archetype = record.archetype - if not archetype then + if not archetype then -- Returns only the entity id as an entity without data should not return -- data and allow the user to get an error if they don't handle the case. return entityId @@ -851,17 +840,17 @@ function World.__iter(world: World): () -> (number?, unknown?) -- We use types because the key should be the component ID not the column index entityData[types[i]] = column[row] end - + return entityId, entityData end end return table.freeze({ - World = World; + World = World, - OnAdd = ON_ADD; - OnRemove = ON_REMOVE; - OnSet = ON_SET; + OnAdd = ON_ADD, + OnRemove = ON_REMOVE, + OnSet = ON_SET, Wildcard = WILDCARD, w = WILDCARD, Rest = REST, diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..5029861 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,186 @@ +site_name: Fusion +site_url: https://elttob.uk/Fusion/ +repo_name: dphfox/Fusion +repo_url: https://github.com/dphfox/Fusion + +extra: + version: + provider: mike + +theme: + name: material + custom_dir: docs/assets/overrides + logo: assets/logo + favicon: assets/logo-dark.svg + palette: + - media: "(prefers-color-scheme: dark)" + scheme: fusiondoc-dark + toggle: + icon: octicons/sun-24 + title: Switch to light theme + - media: "(prefers-color-scheme: light)" + scheme: fusiondoc-light + toggle: + icon: octicons/moon-24 + title: Switch to dark theme + font: + text: Plus Jakarta Sans + code: JetBrains Mono + features: + - navigation.tabs + - navigation.top + - navigation.sections + - navigation.instant + - navigation.indexes + - search.suggest + - search.highlight + icon: + repo: octicons/mark-github-16 + +extra_css: + - assets/theme/fusiondoc.css + - assets/theme/colours.css + - assets/theme/code.css + - assets/theme/paragraph.css + - assets/theme/page.css + - assets/theme/admonition.css + - assets/theme/404.css + - assets/theme/api-reference.css + - assets/theme/dev-tools.css + +extra_javascript: + - assets/scripts/smooth-scroll.js + +nav: + - Home: index.md + - Tutorials: + - Get Started: tutorials/index.md + - Installing Fusion: tutorials/get-started/installing-fusion.md + - Developer Tools: tutorials/get-started/developer-tools.md + - Getting Help: tutorials/get-started/getting-help.md + - Fundamentals: + - Scopes: tutorials/fundamentals/scopes.md + - Values: tutorials/fundamentals/values.md + - Observers: tutorials/fundamentals/observers.md + - Computeds: tutorials/fundamentals/computeds.md + - Tables: + - ForValues: tutorials/tables/forvalues.md + - ForKeys: tutorials/tables/forkeys.md + - ForPairs: tutorials/tables/forpairs.md + - Animation: + - Tweens: tutorials/animation/tweens.md + - Springs: tutorials/animation/springs.md + - Roblox: + - Hydration: tutorials/roblox/hydration.md + - New Instances: tutorials/roblox/new-instances.md + - Parenting: tutorials/roblox/parenting.md + - Events: tutorials/roblox/events.md + - Change Events: tutorials/roblox/change-events.md + - Outputs: tutorials/roblox/outputs.md + - References: tutorials/roblox/references.md + - Best Practices: + - Components: tutorials/best-practices/components.md + - Instance Handling: tutorials/best-practices/instance-handling.md + - Callbacks: tutorials/best-practices/callbacks.md + - State: tutorials/best-practices/state.md + - Sharing Values: tutorials/best-practices/sharing-values.md + - Error Safety: tutorials/best-practices/error-safety.md + - Optimisation: tutorials/best-practices/optimisation.md + + - Examples: + - Home: examples/index.md + - Cookbook: + - examples/cookbook/index.md + - Player List: examples/cookbook/player-list.md + - Animated Computed: examples/cookbook/animated-computed.md + - Fetch Data From Server: examples/cookbook/fetch-data-from-server.md + - Light & Dark Theme: examples/cookbook/light-and-dark-theme.md + - Button Component: examples/cookbook/button-component.md + - Loading Spinner: examples/cookbook/loading-spinner.md + - Drag & Drop: examples/cookbook/drag-and-drop.md + - API Reference: + - api-reference/index.md + - General: + - Errors: api-reference/general/errors.md + - Types: + - Contextual: api-reference/general/types/contextual.md + - Version: api-reference/general/types/version.md + - Members: + - Contextual: api-reference/general/members/contextual.md + - Safe: api-reference/general/members/safe.md + - version: api-reference/general/members/version.md + - Memory: + - Types: + - Scope: api-reference/memory/types/scope.md + - ScopedObject: api-reference/memory/types/scopedobject.md + - Task: api-reference/memory/types/task.md + - Members: + - deriveScope: api-reference/memory/members/derivescope.md + - doCleanup: api-reference/memory/members/docleanup.md + - scoped: api-reference/memory/members/scoped.md + - State: + - Types: + - UsedAs: api-reference/state/types/usedas.md + - Computed: api-reference/state/types/computed.md + - Dependency: api-reference/state/types/dependency.md + - Dependent: api-reference/state/types/dependent.md + - For: api-reference/state/types/for.md + - Observer: api-reference/state/types/observer.md + - StateObject: api-reference/state/types/stateobject.md + - Use: api-reference/state/types/use.md + - Value: api-reference/state/types/value.md + - Members: + - Computed: api-reference/state/members/computed.md + - ForKeys: api-reference/state/members/forkeys.md + - ForPairs: api-reference/state/members/forpairs.md + - ForValues: api-reference/state/members/forvalues.md + - Observer: api-reference/state/members/observer.md + - peek: api-reference/state/members/peek.md + - Value: api-reference/state/members/value.md + - Roblox: + - Types: + - Child: api-reference/roblox/types/child.md + - PropertyTable: api-reference/roblox/types/propertytable.md + - SpecialKey: api-reference/roblox/types/specialkey.md + - Members: + - Attribute: api-reference/roblox/members/attribute.md + - AttributeChange: api-reference/roblox/members/attributechange.md + - AttributeOut: api-reference/roblox/members/attributeout.md + - Children: api-reference/roblox/members/children.md + - Hydrate: api-reference/roblox/members/hydrate.md + - New: api-reference/roblox/members/new.md + - OnChange: api-reference/roblox/members/onchange.md + - OnEvent: api-reference/roblox/members/onevent.md + - Out: api-reference/roblox/members/out.md + - Ref: api-reference/roblox/members/ref.md + - Animation: + - Types: + - Animatable: api-reference/animation/types/animatable.md + - Spring: api-reference/animation/types/spring.md + - Tween: api-reference/animation/types/tween.md + - Members: + - Tween: api-reference/animation/members/tween.md + - Spring: api-reference/animation/members/spring.md + - Extras: + - Home: extras/index.md + - Backgrounds: extras/backgrounds.md + - Brand Guidelines: extras/brand-guidelines.md + +markdown_extensions: + - admonition + - attr_list + - meta + - md_in_html + - pymdownx.superfences + - pymdownx.betterem + - pymdownx.details + - pymdownx.tabbed: + alternate_style: true + - pymdownx.inlinehilite + - toc: + permalink: true + - pymdownx.highlight: + guess_lang: false + - pymdownx.emoji: + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg diff --git a/tests/world.lua b/tests/world.lua index f0eff7d..79a2686 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -1,5 +1,5 @@ -local testkit = require("../testkit") local jecs = require("../lib/init") +local testkit = require("../testkit") local __ = jecs.Wildcard local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC @@ -11,329 +11,343 @@ local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) - local ok, err: string? = pcall(fn, ...) + local ok, err: string? = pcall(fn, ...) - if not CHECK(not ok, 2) then - local i = string.find(err :: string, " ") - assert(i) - local msg = string.sub(err :: string, i+1) - CHECK(msg == s, 2) - end + if not CHECK(not ok, 2) then + local i = string.find(err :: string, " ") + assert(i) + local msg = string.sub(err :: string, i + 1) + CHECK(msg == s, 2) + end end local N = 10 -TEST("world", function() - do CASE "should be iterable" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local eA = world:entity() - world:set(eA, A, true) - local eB = world:entity() - world:set(eB, B, true) - local eAB = world:entity() - world:set(eAB, A, true) - world:set(eAB, B, true) +TEST("world", function() + do + CASE("should be iterable") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local eA = world:entity() + world:set(eA, A, true) + local eB = world:entity() + world:set(eB, B, true) + local eAB = world:entity() + world:set(eAB, A, true) + world:set(eAB, B, true) - local count = 0 - for id, data in world do - count += 1 - if id == eA then - CHECK(data[A] == true) - CHECK(data[B] == nil) - elseif id == eB then - CHECK(data[A] == nil) - CHECK(data[B] == true) - elseif id == eAB then - CHECK(data[A] == true) - CHECK(data[B] == true) - end - end + local count = 0 + for id, data in world do + count += 1 + if id == eA then + CHECK(data[A] == true) + CHECK(data[B] == nil) + elseif id == eB then + CHECK(data[A] == nil) + CHECK(data[B] == true) + elseif id == eAB then + CHECK(data[A] == true) + CHECK(data[B] == true) + end + end - -- components are registered in the entity index as well - -- so this test has to add 2 to account for them - CHECK(count == 3 + 2) - end + -- components are registered in the entity index as well + -- so this test has to add 2 to account for them + CHECK(count == 3 + 2) + end - do CASE "should query all matching entities" - local world = jecs.World.new() - local A = world:component() - local B = world:component() + do + CASE("should query all matching entities") + local world = jecs.World.new() + local A = world:component() + local B = world:component() - local entities = {} - for i = 1, N do - local id = world:entity() + local entities = {} + for i = 1, N do + local id = world:entity() - world:set(id, A, true) - if i > 5 then world:set(id, B, true) end - entities[i] = id - end + world:set(id, A, true) + if i > 5 then + world:set(id, B, true) + end + entities[i] = id + end - for id in world:query(A) do - table.remove(entities, CHECK(table.find(entities, id))) - end + for id in world:query(A) do + table.remove(entities, CHECK(table.find(entities, id))) + end - CHECK(#entities == 0) + CHECK(#entities == 0) + end - end + do + CASE("should query all matching entities when irrelevant component is removed") + local world = jecs.World.new() + local A = world:component() + local B = world:component() + local C = world:component() - do CASE "should query all matching entities when irrelevant component is removed" - local world = jecs.World.new() - local A = world:component() - local B = world:component() - local C = world:component() + local entities = {} + for i = 1, N do + local id = world:entity() - local entities = {} - for i = 1, N do - local id = world:entity() + -- specifically put them in disorder to track regression + -- https://github.com/Ukendio/jecs/pull/15 + world:set(id, B, true) + world:set(id, A, true) + if i > 5 then + world:remove(id, B) + end + entities[i] = id + end - -- specifically put them in disorder to track regression - -- https://github.com/Ukendio/jecs/pull/15 - world:set(id, B, true) - world:set(id, A, true) - if i > 5 then world:remove(id, B) end - entities[i] = id - end + local added = 0 + for id in world:query(A) do + added += 1 + table.remove(entities, CHECK(table.find(entities, id))) + end - local added = 0 - for id in world:query(A) do - added += 1 - table.remove(entities, CHECK(table.find(entities, id))) - end + CHECK(added == N) + end - CHECK(added == N) - end + do + CASE("should query all entities without B") + local world = jecs.World.new() + local A = world:component() + local B = world:component() - do CASE "should query all entities without B" - local world = jecs.World.new() - local A = world:component() - local B = world:component() + local entities = {} + for i = 1, N do + local id = world:entity() - local entities = {} - for i = 1, N do - local id = world:entity() + world:set(id, A, true) + if i < 5 then + entities[i] = id + else + world:set(id, B, true) + end + end - world:set(id, A, true) - if i < 5 then - entities[i] = id - else - world:set(id, B, true) - end - - end + for id in world:query(A):without(B) do + table.remove(entities, CHECK(table.find(entities, id))) + end - for id in world:query(A):without(B) do - table.remove(entities, CHECK(table.find(entities, id))) - end + CHECK(#entities == 0) + end - CHECK(#entities == 0) + do + CASE("should allow setting components in arbitrary order") + local world = jecs.World.new() - end + local Health = world:entity() + local Poison = world:component() - do CASE "should allow setting components in arbitrary order" - local world = jecs.World.new() + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) - local Health = world:entity() - local Poison = world:component() + CHECK(world:get(id, Poison) == 5) + end - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) + do + CASE("should allow deleting components") + local world = jecs.World.new() - CHECK(world:get(id, Poison) == 5) - end + local Health = world:entity() + local Poison = world:component() - do CASE "should allow deleting components" - local world = jecs.World.new() + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + local id1 = world:entity() + world:set(id1, Poison, 500) + world:set(id1, Health, 50) - local Health = world:entity() - local Poison = world:component() + world:delete(id) - local id = world:entity() - world:set(id, Poison, 5) - world:set(id, Health, 50) - local id1 = world:entity() - world:set(id1, Poison, 500) - world:set(id1, Health, 50) + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == nil) + CHECK(world:get(id1, Poison) == 500) + CHECK(world:get(id1, Health) == 50) + end - world:delete(id) + do + CASE("should allow remove that doesn't exist on entity") + local world = jecs.World.new() - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == nil) - CHECK(world:get(id1, Poison) == 500) - CHECK(world:get(id1, Health) == 50) + local Health = world:entity() + local Poison = world:component() - end + local id = world:entity() + world:set(id, Health, 50) + world:remove(id, Poison) - do CASE "should allow remove that doesn't exist on entity" - local world = jecs.World.new() + CHECK(world:get(id, Poison) == nil) + CHECK(world:get(id, Health) == 50) + end - local Health = world:entity() - local Poison = world:component() + do + CASE("should increment generation") + local world = jecs.World.new() + local e = world:entity() + CHECK(ECS_ID(e) == 1 + jecs.Rest) + CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) + CHECK(ECS_GENERATION(e) == 0) -- 0 + e = ECS_GENERATION_INC(e) + CHECK(ECS_GENERATION(e) == 1) -- 1 + end - local id = world:entity() - world:set(id, Health, 50) - world:remove(id, Poison) + do + CASE("should get alive from index in the dense array") + local world = jecs.World.new() + local _e = world:entity() + local e2 = world:entity() + local e3 = world:entity() - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == 50) - end + CHECK(IS_PAIR(world:entity()) == false) - do CASE "should increment generation" - local world = jecs.World.new() - local e = world:entity() - CHECK(ECS_ID(e) == 1 + jecs.Rest) - CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) - CHECK(ECS_GENERATION(e) == 0) -- 0 - e = ECS_GENERATION_INC(e) - CHECK(ECS_GENERATION(e) == 1) -- 1 - end + local pair = ECS_PAIR(e2, e3) + CHECK(IS_PAIR(pair) == true) + CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) + CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) + end - do CASE "should get alive from index in the dense array" - local world = jecs.World.new() - local _e = world:entity() - local e2 = world:entity() - local e3 = world:entity() + do + CASE("should allow querying for relations") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() - CHECK(IS_PAIR(world:entity()) == false) + world:set(bob, ECS_PAIR(Eats, Apples), true) + for e, bool in world:query(ECS_PAIR(Eats, Apples)) do + CHECK(e == bob) + CHECK(bool) + end + end - local pair = ECS_PAIR(e2, e3) - CHECK(IS_PAIR(pair) == true) - CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) - CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) - end + do + CASE("should allow wildcards in queries") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() - do CASE "should allow querying for relations" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), true) - for e, bool in world:query(ECS_PAIR(Eats, Apples)) do - CHECK(e == bob) - CHECK(bool) - end - end - - do CASE "should allow wildcards in queries" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - - local w = jecs.Wildcard - for e, data in world:query(ECS_PAIR(Eats, w)) do - CHECK(e == bob) - CHECK(data == "bob eats apples") - end - for e, data in world:query(ECS_PAIR(w, Apples)) do - CHECK(e == bob) - CHECK(data == "bob eats apples") - end - end + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - do CASE "should match against multiple pairs" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local Oranges =world:entity() - local bob = world:entity() - local alice = world:entity() - - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - - local w = jecs.Wildcard - local count = 0 - for e, data in world:query(ECS_PAIR(Eats, w)) do - count += 1 - if e == bob then - CHECK(data == "bob eats apples") - else - CHECK(data == "alice eats oranges") - end - end + local w = jecs.Wildcard + for e, data in world:query(ECS_PAIR(Eats, w)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + for e, data in world:query(ECS_PAIR(w, Apples)) do + CHECK(e == bob) + CHECK(data == "bob eats apples") + end + end - CHECK(count == 2) - count = 0 + do + CASE("should match against multiple pairs") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() - for e, data in world:query(ECS_PAIR(w, Apples)) do - count += 1 - CHECK(data == "bob eats apples") - end - CHECK(count == 1) - end + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - do CASE "should only relate alive entities" - - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local Oranges = world:entity() - local bob = world:entity() - local alice = world:entity() - - world:set(bob, Apples, "apples") - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + local w = jecs.Wildcard + local count = 0 + for e, data in world:query(ECS_PAIR(Eats, w)) do + count += 1 + if e == bob then + CHECK(data == "bob eats apples") + else + CHECK(data == "alice eats oranges") + end + end - world:delete(Apples) - local Wildcard = jecs.Wildcard - - local count = 0 - for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do - count += 1 - end + CHECK(count == 2) + count = 0 - world:delete(ECS_PAIR(Eats, Apples)) - - CHECK(count == 0) - CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) - end + for e, data in world:query(ECS_PAIR(w, Apples)) do + count += 1 + CHECK(data == "bob eats apples") + end + CHECK(count == 1) + end - do CASE "should error when setting invalid pair" - local world = jecs.World.new() - local Eats = world:entity() - local Apples = world:entity() - local bob = world:entity() + do + CASE("should only relate alive entities") - world:delete(Apples) + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local Oranges = world:entity() + local bob = world:entity() + local alice = world:entity() - CHECK_NO_ERR("Apples should be dead", function() - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - end) - end + world:set(bob, Apples, "apples") + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") - do CASE "should find target for ChildOf" - local world = jecs.World.new() + world:delete(Apples) + local Wildcard = jecs.Wildcard - local ChildOf = world:component() - local Name = world:component() + local count = 0 + for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do + count += 1 + end - local function parent(entity) - return world:target(entity, ChildOf) - end + world:delete(ECS_PAIR(Eats, Apples)) - local bob = world:entity() - local alice = world:entity() - local sara = world:entity() - - world:add(bob, ECS_PAIR(ChildOf, alice)) - world:set(bob, Name, "bob") - world:add(sara, ECS_PAIR(ChildOf, alice)) - world:set(sara, Name, "sara") - CHECK(parent(bob) == alice) -- O(1) + CHECK(count == 0) + CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) + end - local count = 0 - for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do - print(name) - count += 1 - end - CHECK(count == 2) - end + do + CASE("should error when setting invalid pair") + local world = jecs.World.new() + local Eats = world:entity() + local Apples = world:entity() + local bob = world:entity() + + world:delete(Apples) + + world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + end + + do + CASE("should find target for ChildOf") + local world = jecs.World.new() + + local ChildOf = world:component() + local Name = world:component() + + local function parent(entity) + return world:target(entity, ChildOf) + end + + local bob = world:entity() + local alice = world:entity() + local sara = world:entity() + + world:add(bob, ECS_PAIR(ChildOf, alice)) + world:set(bob, Name, "bob") + world:add(sara, ECS_PAIR(ChildOf, alice)) + world:set(sara, Name, "sara") + CHECK(parent(bob) == alice) -- O(1) + + local count = 0 + for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do + print(name) + count += 1 + end + CHECK(count == 2) + end end) -FINISH() \ No newline at end of file +FINISH() + From c0594c7e75b97a156d031a29f91cbc1cef63e3f8 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 03:50:46 +0200 Subject: [PATCH 39/46] Fix docs --- docs/api-types.md | 2 +- docs/api/world.md | 188 ++++++++++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 8 +- 3 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 docs/api/world.md diff --git a/docs/api-types.md b/docs/api-types.md index b446999..cce3856 100644 --- a/docs/api-types.md +++ b/docs/api-types.md @@ -30,7 +30,7 @@ end ### QueryIter.without QueryIter.without(iter: QueryIter - ...: [Entity](../api-types/Entity)): QueryIter + ...: [Entity](#Entity)): QueryIter Create a new Query Iterator from the filter diff --git a/docs/api/world.md b/docs/api/world.md new file mode 100644 index 0000000..06a8a66 --- /dev/null +++ b/docs/api/world.md @@ -0,0 +1,188 @@ +# World + +### World.new + +World.new(): [World](../api-types/World) + +Create a new world. + +#### Returns +A new world + +--- + +### World.entity + +World.entity(world: [World](../api-types/World)): [Entity](../api-types/Entity) + +Creates an entity in the world. + +#### Returns +A new entiity id + +--- + +### World.target + +World.target(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + rel: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Get the target of a relationship. + +This will return a target (second element of a pair) of the entity for the specified relationship. + +#### Parameters + world The world. + entity The entity. + rel The relationship between the entity and the target. + +#### Returns + +The first target for the relationship + +--- + +### World.add + +World.add(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Add a (component) id to an entity. + +This operation adds a single (component) id to an entity. +If the entity already has the id, this operation will have no side effects. + +#### Parameters + world The world. + entity The entity. + id The id to add. + +--- + +### World.remove + +World.remove(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Remove a (component) id to an entity. + +This operation removes a single (component) id to an entity. +If the entity already has the id, this operation will have no side effects. + +#### Parameters + world The world. + entity The entity. + id The id to add. + +--- + +### World.get + +World.get(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity)): any + +Gets the component data. + +#### Parameters + world The world. + entity The entity. + id The id of component to get. + +#### Returns +The component data, nil if the entity does not have the componnet. + +--- + +### World.set + +World.set(world: [World](../api-types/World), + entity: [Entity](../api-types/Entity), + id: [Entity](../api-types/Entity) + data: any) + +Set the value of a component. + +#### Parameters + world The world. + entity The entity. + id The id of the componment set. + data The data to the component. + +--- + +### World.query + +World.query(world: [World](../api-types/World), + ...: [Entity](../api-types/Entity)): [QueryIter](../api-types/QueryIter) + +Create a QueryIter from the list of filters. + +#### Parameters + world The world. + ... The collection of components to match entities against. + +#### Returns + +The query iterator. + +--- + +# Pair + +### pair + +pair(first: [Entity](../api-types/Entity), + second: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) + +Creates a composite key. + +#### Parameters + first The first element. + second The second element. + +#### Returns + +The pair of the two elements + +--- + +### IS_PAIR + +jecs.IS_PAIR(id: [Entity](../api-types/Entity)): boolean + +Creates a composite key. + +#### Parameters + id The id to check. + +#### Returns + +If id is a pair. + +--- + +# Constants + +### OnAdd + +--- + +### OnRemove + +--- + +### Rest + +--- + +### OnSet + +--- + +### Wildcard + +Matches any id, returns all matches. \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index 5029861..1a47333 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,7 +1,7 @@ -site_name: Fusion -site_url: https://elttob.uk/Fusion/ -repo_name: dphfox/Fusion -repo_url: https://github.com/dphfox/Fusion +site_name: Jecs +site_url: jecs.github.io/jecs +repo_name: ukendio/jecs +repo_url: https://github.com/ukendio/jecs extra: version: From 3d689ebdd84e9aa115115ca33215a49eada3fd4d Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 11:33:59 +0200 Subject: [PATCH 40/46] Fix links --- docs/api/world.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/api/world.md b/docs/api/world.md index 06a8a66..39097bc 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -2,7 +2,7 @@ ### World.new -World.new(): [World](../api-types/World) +World.new(): [World](../api-types#World) Create a new world. @@ -13,7 +13,7 @@ A new world ### World.entity -World.entity(world: [World](../api-types/World)): [Entity](../api-types/Entity) +World.entity(world: [World](../api-types#World)): [Entity](../api-types#Entity) Creates an entity in the world. @@ -24,9 +24,9 @@ A new entiity id ### World.target -World.target(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - rel: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +World.target(world: [World](../api-types#World), + entity: [Entity](../api-types#Entity), + rel: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) Get the target of a relationship. @@ -45,9 +45,9 @@ The first target for the relationship ### World.add -World.add(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +World.add(world: [World](../api-types#World), + entity: [Entity](../api-types#Entity), + id: [Entity](../api-types#Entity)): [Entity](..#api-types/Entity) Add a (component) id to an entity. @@ -63,9 +63,9 @@ If the entity already has the id, this operation will have no side effects. ### World.remove -World.remove(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +World.remove(world: [World](../api-types#World), + entity: [Entity](../api-types#Entity), + id: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) Remove a (component) id to an entity. @@ -135,8 +135,8 @@ The query iterator. ### pair -pair(first: [Entity](../api-types/Entity), - second: [Entity](../api-types/Entity)): [Entity](../api-types/Entity) +pair(first: [Entity](../api-types#Entity), + second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) Creates a composite key. @@ -152,7 +152,7 @@ The pair of the two elements ### IS_PAIR -jecs.IS_PAIR(id: [Entity](../api-types/Entity)): boolean +jecs.IS_PAIR(id: [Entity](../api-types#Entity)): boolean Creates a composite key. @@ -185,4 +185,4 @@ If id is a pair. ### Wildcard -Matches any id, returns all matches. \ No newline at end of file +Matches any id, returns all matches. From 94cc3ee8ea8f87bba1e96b5b82a80bc6f6742a80 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 11:45:41 +0200 Subject: [PATCH 41/46] Fix links --- docs/api/world.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/api/world.md b/docs/api/world.md index 39097bc..fa52403 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -2,7 +2,7 @@ ### World.new -World.new(): [World](../api-types#World) +World.new(): [World](../api-typesk.md#World) Create a new world. @@ -13,7 +13,7 @@ A new world ### World.entity -World.entity(world: [World](../api-types#World)): [Entity](../api-types#Entity) +World.entity(world: [World](../api-types.md#World)): [Entity](../api-types.md#Entity) Creates an entity in the world. @@ -24,9 +24,9 @@ A new entiity id ### World.target -World.target(world: [World](../api-types#World), - entity: [Entity](../api-types#Entity), - rel: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) +World.target(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + rel: [Entity](../api-types.md#Entity)): [Entity](../api-types.md#Entity) Get the target of a relationship. @@ -45,9 +45,9 @@ The first target for the relationship ### World.add -World.add(world: [World](../api-types#World), - entity: [Entity](../api-types#Entity), - id: [Entity](../api-types#Entity)): [Entity](..#api-types/Entity) +World.add(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + id: [Entity](../api-types.md#Entity)): [Entity](..#api-types.md#Entity) Add a (component) id to an entity. @@ -81,9 +81,9 @@ If the entity already has the id, this operation will have no side effects. ### World.get -World.get(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity)): any +World.get(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + id: [Entity](../api-types.md#Entity)): any Gets the component data. @@ -99,9 +99,9 @@ The component data, nil if the entity does not have the componnet. ### World.set -World.set(world: [World](../api-types/World), - entity: [Entity](../api-types/Entity), - id: [Entity](../api-types/Entity) +World.set(world: [World](../api-types.md#World), + entity: [Entity](../api-types.md#Entity), + id: [Entity](../api-types.md#Entity) data: any) Set the value of a component. @@ -116,8 +116,8 @@ Set the value of a component. ### World.query -World.query(world: [World](../api-types/World), - ...: [Entity](../api-types/Entity)): [QueryIter](../api-types/QueryIter) +World.query(world: [World](../api-types.md#World), + ...: [Entity](../api-types.mdEntity)): [QueryIter](../api-types.md#QueryIter) Create a QueryIter from the list of filters. From 1498a28c3fd665b4a7215789031eb6e9a6d550e0 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 27 May 2024 11:46:42 +0200 Subject: [PATCH 42/46] Update world.md --- docs/api/world.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/world.md b/docs/api/world.md index fa52403..93f4e7c 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -2,7 +2,7 @@ ### World.new -World.new(): [World](../api-typesk.md#World) +World.new(): [World](../api-types.md#World) Create a new world. From 05d61bd6da988b079ebb0fb210773f9d7b044d7e Mon Sep 17 00:00:00 2001 From: Ukendio Date: Mon, 27 May 2024 20:09:22 +0200 Subject: [PATCH 43/46] Fix doc --- docs/api/world.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api/world.md b/docs/api/world.md index fa52403..c7f251b 100644 --- a/docs/api/world.md +++ b/docs/api/world.md @@ -135,8 +135,7 @@ The query iterator. ### pair -pair(first: [Entity](../api-types#Entity), - second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) +pair(first: [Entity](../api-types#Entity), second: [Entity](../api-types#Entity)): [Entity](../api-types#Entity) Creates a composite key. From b1a2354adb6fa061204cdcffd37ad74cfba5dbf0 Mon Sep 17 00:00:00 2001 From: alice <166900055+alicesaidhi@users.noreply.github.com> Date: Wed, 5 Jun 2024 20:30:34 +0200 Subject: [PATCH 44/46] implement types --- lib/init.lua | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/init.lua b/lib/init.lua index 9ef0b10..58f081e 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -845,8 +845,51 @@ function World.__iter(world: World): () -> (number?, unknown?) end end +export type QueryShim = typeof(setmetatable( + {} :: { + --- Excludes the given selection from the query + without: (QueryShim, U...) -> QueryShim + }, + {} :: { + __iter: () -> (number, T...) + } +)) + +export type WorldShim = typeof(setmetatable( + {} :: { + + --- Creates a new entity + entity: (WorldShim) -> T, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (WorldShim) -> T, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent))`, you will obtain the parent entity. + target: (WorldShim, id: unknown, relation: unknown) -> T?, + --- Deletes an entity and all it's related components and relationships. + delete: (WorldShim, id: unknown) -> (), + + --- Adds a component to the entity with no value + add: (WorldShim, id: unknown, component: T) -> (), + --- Assigns a value to a component on the given entity + set: (WorldShim, id: unknown, component: T, data: T) -> (), + --- Removes a component from the given entity + remove: (WorldShim, id: unknown, component: unknown) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: (WorldShim, id: unknown, T...) -> T..., + + --- Searches the world for entities that match a given query + query: (WorldShim, T...) -> Query + + }, + {} :: { + __iter: (world: World) -> () -> (number, {[unknown]: unknown?}) + } + +)) + return table.freeze({ - World = World, + World = World :: {new: () -> WorldShim}, OnAdd = ON_ADD, OnRemove = ON_REMOVE, From 7a0a8b3fe0c9566f139283e640d637447795b16f Mon Sep 17 00:00:00 2001 From: alice <166900055+alicesaidhi@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:05:27 +0200 Subject: [PATCH 45/46] make types more specific --- lib/init.lua | 81 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 58f081e..75a7227 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -845,13 +845,20 @@ function World.__iter(world: World): () -> (number?, unknown?) end end +-- __nominal_type_dont_use could not be any or T as it causes a type error +-- or produces a union +export type Component = T & {__nominal_type_dont_use: never} +export type Entity = Component +export type Relationship = Component +type ctype = Component + export type QueryShim = typeof(setmetatable( {} :: { --- Excludes the given selection from the query without: (QueryShim, U...) -> QueryShim }, {} :: { - __iter: () -> (number, T...) + __iter: (QueryShim) -> () -> (Entity, T...) } )) @@ -859,43 +866,73 @@ export type WorldShim = typeof(setmetatable( {} :: { --- Creates a new entity - entity: (WorldShim) -> T, + entity: (WorldShim) -> Entity, --- Creates a new entity located in the first 256 ids. --- These should be used for static components for fast access. - component: (WorldShim) -> T, + component: (WorldShim) -> Component, --- Gets the target of an relationship. For example, when a user calls --- `world:target(id, ChildOf(parent))`, you will obtain the parent entity. - target: (WorldShim, id: unknown, relation: unknown) -> T?, + target: (WorldShim, id: Entity, relation: Relationship) -> Entity?, --- Deletes an entity and all it's related components and relationships. - delete: (WorldShim, id: unknown) -> (), + delete: (WorldShim, id: Entity) -> (), --- Adds a component to the entity with no value - add: (WorldShim, id: unknown, component: T) -> (), + add: (WorldShim, id: Entity, component: Component) -> (), --- Assigns a value to a component on the given entity - set: (WorldShim, id: unknown, component: T, data: T) -> (), + set: (WorldShim, id: Entity, component: Component, data: T) -> (), --- Removes a component from the given entity - remove: (WorldShim, id: unknown, component: unknown) -> (), + remove: (WorldShim, id: Entity, component: Component) -> (), --- Retrieves the value of up to 4 components. These values may be nil. - get: (WorldShim, id: unknown, T...) -> T..., + get: + ((WorldShim, id: any, ctype) -> A) + & ((WorldShim, id: Entity, ctype, ctype) -> (A, B)) + & ((WorldShim, id: Entity, ctype, ctype, ctype) -> (A, B, C)) + & (WorldShim, id: Entity, ctype, ctype, ctype, ctype) -> (A, B, C, D), --- Searches the world for entities that match a given query - query: (WorldShim, T...) -> Query - + query: + ((WorldShim, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ...ctype) -> QueryShim) + }, {} :: { - __iter: (world: World) -> () -> (number, {[unknown]: unknown?}) + __iter: (world: WorldShim) -> () -> (number, {[unknown]: unknown?}) } )) -return table.freeze({ - World = World :: {new: () -> WorldShim}, +return { + World = (World :: any) :: {new: () -> WorldShim}, - OnAdd = ON_ADD, - OnRemove = ON_REMOVE, - OnSet = ON_SET, - Wildcard = WILDCARD, - w = WILDCARD, + OnAdd = (ON_ADD :: any) :: Component, + OnRemove = (ON_REMOVE :: any) :: Component, + OnSet = (ON_SET :: any) :: Component, + Wildcard = (WILDCARD :: any) :: Component, + w = (WILDCARD :: any) :: Component, Rest = REST, IS_PAIR = ECS_IS_PAIR, @@ -906,6 +943,6 @@ return table.freeze({ ECS_PAIR_RELATION = ECS_PAIR_RELATION, ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, - pair = ECS_PAIR, - getAlive = getAlive, -}) + pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> Relationship, + getAlive = getAlive +} From 55a8d78f5427f454ac594c95e3da88c9b8ad08f6 Mon Sep 17 00:00:00 2001 From: alice <166900055+alicesaidhi@users.noreply.github.com> Date: Wed, 5 Jun 2024 23:13:10 +0200 Subject: [PATCH 46/46] reduce query to 10 generics --- lib/init.lua | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/lib/init.lua b/lib/init.lua index 75a7227..e1f4549 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -901,22 +901,7 @@ export type WorldShim = typeof(setmetatable( & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype) -> QueryShim) - & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype

, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ...ctype) -> QueryShim) + & ((WorldShim, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ctype, ...ctype) -> QueryShim) }, {} :: { @@ -925,7 +910,7 @@ export type WorldShim = typeof(setmetatable( )) -return { +return table.freeze({ World = (World :: any) :: {new: () -> WorldShim}, OnAdd = (ON_ADD :: any) :: Component, @@ -945,4 +930,4 @@ return { pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> Relationship, getAlive = getAlive -} +})