From cda04ce5a99af606133c8c18013d264d92985b6a Mon Sep 17 00:00:00 2001 From: Michael Date: Fri, 3 May 2024 12:14:45 -0400 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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 6/6] 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; })