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] 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" - - -