Fix style and add some micro optimizations (#27)

This commit is contained in:
howmanysmall 2024-05-04 17:52:01 -06:00 committed by GitHub
parent cda04ce5a9
commit 283243350f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 496 additions and 469 deletions

3
.gitignore vendored
View file

@ -50,3 +50,6 @@ WallyPatches
roblox.toml roblox.toml
sourcemap.json sourcemap.json
drafts/*.lua drafts/*.lua
*.code-workspace
roblox.yml

View file

@ -3,4 +3,4 @@ wally = "upliftgames/wally@0.3.1"
rojo = "rojo-rbx/rojo@7.4.1" rojo = "rojo-rbx/rojo@7.4.1"
stylua = "johnnymorganz/stylua@0.19.1" stylua = "johnnymorganz/stylua@0.19.1"
selene = "kampfkarren/selene@0.26.1" selene = "kampfkarren/selene@0.26.1"
wally-patch-package="Barocena/wally-patch-package@1.2.1" wally-patch-package = "Barocena/wally-patch-package@1.2.1"

View file

@ -1,11 +1,11 @@
--!optimize 2 --!optimize 2
--!native --!native
local testkit = require('../testkit') local testkit = require("../testkit")
local BENCH, START = testkit.benchmark() local BENCH, START = testkit.benchmark()
local function TITLE(title: string) local function TITLE(title: string)
print() print()
print(testkit.color.white(title)) print(testkit.color.white(title))
end end
local jecs = require("../mirror/init") local jecs = require("../mirror/init")
@ -15,285 +15,285 @@ local oldMatter = require("../oldMatter")
local newMatter = require("../newMatter") local newMatter = require("../newMatter")
type i53 = number type i53 = number
do TITLE (testkit.color.white_underline("Jecs query")) do
local ecs = jecs.World.new() TITLE(testkit.color.white_underline("Jecs query"))
do TITLE "one component in common" local ecs = jecs.World.new()
do
TITLE("one component in common")
local function view_bench( local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
world: jecs.World, BENCH("1 component", function()
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53 for _ in world:query(A) do
) end
end)
BENCH("1 component", function() BENCH("2 component", function()
for _ in world:query(A) do end for _ in world:query(A, B) do
end) end
end)
BENCH("2 component", function() BENCH("4 component", function()
for _ in world:query(A, B) do end for _ in world:query(A, B, C, D) do
end) end
end)
BENCH("4 component", function() BENCH("8 component", function()
for _ in world:query(A, B, C, D) do for _ in world:query(A, B, C, D, E, F, G, H) do
end end
end) end)
end
BENCH("8 component", function() local D1 = ecs:component()
for _ in world:query(A, B, C, D, E, F, G, H) do end local D2 = ecs:component()
end) local D3 = ecs:component()
end 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 function flip()
local D2 = ecs:component() return math.random() >= 0.15
local D3 = ecs:component() end
local D4 = ecs:component()
local D5 = ecs:component()
local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local function flip() local added = 0
return math.random() >= 0.15
end
local added = 0
local archetypes = {} local archetypes = {}
for i = 1, 2^16-2 do for i = 1, 2 ^ 16 - 2 do
local entity = ecs:entity() local entity = ecs:entity()
local combination = "" local combination = ""
if flip() then if flip() then
combination ..= "B" combination ..= "B"
ecs:set(entity, D2, {value = true}) ecs:set(entity, D2, {value = true})
end end
if flip() then if flip() then
combination ..= "C" combination ..= "C"
ecs:set(entity, D3, { value = true }) ecs:set(entity, D3, {value = true})
end end
if flip() then if flip() then
combination ..= "D" combination ..= "D"
ecs:set(entity, D4, { value = true}) ecs:set(entity, D4, {value = true})
end end
if flip() then if flip() then
combination ..= "E" combination ..= "E"
ecs:set(entity, D5, { value = true}) ecs:set(entity, D5, {value = true})
end end
if flip() then if flip() then
combination ..= "F" combination ..= "F"
ecs:set(entity, D6, {value = true}) ecs:set(entity, D6, {value = true})
end end
if flip() then if flip() then
combination ..= "G" combination ..= "G"
ecs:set(entity, D7, { value = true}) ecs:set(entity, D7, {value = true})
end end
if flip() then if flip() then
combination ..= "H" combination ..= "H"
ecs:set(entity, D8, {value = true}) ecs:set(entity, D8, {value = true})
end
end if #combination == 7 then
added += 1
if #combination == 7 then ecs:set(entity, D1, {value = true})
added += 1 end
ecs:set(entity, D1, { value = true})
end
archetypes[combination] = true archetypes[combination] = true
end end
local a = 0 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) view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end end
end end
do TITLE(testkit.color.white_underline("OldMatter query")) do
TITLE(testkit.color.white_underline("OldMatter query"))
local ecs = oldMatter.World.new() local ecs = oldMatter.World.new()
local component = oldMatter.component local component = oldMatter.component
do TITLE "one component in common" do
local function view_bench( TITLE("one component in common")
world: jecs.World, local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
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() BENCH("2 component", function()
for _ in world:query(A) do end for _ in world:query(A, B) do
end) end
end)
BENCH("2 component", function() BENCH("4 component", function()
for _ in world:query(A, B) do end for _ in world:query(A, B, C, D) do
end) end
end)
BENCH("4 component", function() BENCH("8 component", function()
for _ in world:query(A, B, C, D) do for _ in world:query(A, B, C, D, E, F, G, H) do
end end
end) end)
end
BENCH("8 component", function() local D1 = component()
for _ in world:query(A, B, C, D, E, F, G, H) do end local D2 = component()
end) local D3 = component()
end local D4 = component()
local D5 = component()
local D6 = component()
local D7 = component()
local D8 = component()
local D1 = component() local function flip()
local D2 = component() return math.random() >= 0.15
local D3 = component() end
local D4 = component()
local D5 = component()
local D6 = component()
local D7 = component()
local D8 = component()
local function flip() local added = 0
return math.random() >= 0.15
end
local added = 0
local archetypes = {} local archetypes = {}
for i = 1, 2^16-2 do for i = 1, 2 ^ 16 - 2 do
local entity = ecs:spawn() local entity = ecs:spawn()
local combination = "" local combination = ""
if flip() then if flip() then
combination ..= "B" combination ..= "B"
ecs:insert(entity, D2({value = true})) ecs:insert(entity, D2({value = true}))
end end
if flip() then if flip() then
combination ..= "C" combination ..= "C"
ecs:insert(entity, D3({value = true})) ecs:insert(entity, D3({value = true}))
end end
if flip() then if flip() then
combination ..= "D" combination ..= "D"
ecs:insert(entity, D4({value = true})) ecs:insert(entity, D4({value = true}))
end end
if flip() then if flip() then
combination ..= "E" combination ..= "E"
ecs:insert(entity, D5({value = true})) ecs:insert(entity, D5({value = true}))
end end
if flip() then if flip() then
combination ..= "F" combination ..= "F"
ecs:insert(entity, D6({value = true})) 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 #combination == 7 then
if flip() then added += 1
combination ..= "G" ecs:insert(entity, D1({value = true}))
ecs:insert(entity, D7({value = true})) end
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 archetypes[combination] = true
end end
local a = 0 local a = 0
for _ in archetypes do a+= 1 end for _ in archetypes do
a += 1
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) end
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end end
do TITLE(testkit.color.white_underline("NewMatter query")) do
TITLE(testkit.color.white_underline("NewMatter query"))
local ecs = newMatter.World.new() local ecs = newMatter.World.new()
local component = newMatter.component local component = newMatter.component
do TITLE "one component in common" do
local function view_bench( TITLE("one component in common")
world: jecs.World, local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
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() BENCH("2 component", function()
for _ in world:query(A) do end for _ in world:query(A, B) do
end) end
end)
BENCH("2 component", function() BENCH("4 component", function()
for _ in world:query(A, B) do end for _ in world:query(A, B, C, D) do
end) end
end)
BENCH("4 component", function() BENCH("8 component", function()
for _ in world:query(A, B, C, D) do for _ in world:query(A, B, C, D, E, F, G, H) do
end end
end) end)
end
BENCH("8 component", function() local D1 = component()
for _ in world:query(A, B, C, D, E, F, G, H) do end local D2 = component()
end) local D3 = component()
end local D4 = component()
local D5 = component()
local D6 = component()
local D7 = component()
local D8 = component()
local D1 = component() local function flip()
local D2 = component() return math.random() >= 0.15
local D3 = component() end
local D4 = component()
local D5 = component()
local D6 = component()
local D7 = component()
local D8 = component()
local function flip() local added = 0
return math.random() >= 0.15
end
local added = 0
local archetypes = {} local archetypes = {}
for i = 1, 2^16-2 do for i = 1, 2 ^ 16 - 2 do
local entity = ecs:spawn() local entity = ecs:spawn()
local combination = "" local combination = ""
if flip() then if flip() then
combination ..= "B" combination ..= "B"
ecs:insert(entity, D2({value = true})) ecs:insert(entity, D2({value = true}))
end end
if flip() then if flip() then
combination ..= "C" combination ..= "C"
ecs:insert(entity, D3({value = true})) ecs:insert(entity, D3({value = true}))
end end
if flip() then if flip() then
combination ..= "D" combination ..= "D"
ecs:insert(entity, D4({value = true})) ecs:insert(entity, D4({value = true}))
end end
if flip() then if flip() then
combination ..= "E" combination ..= "E"
ecs:insert(entity, D5({value = true})) ecs:insert(entity, D5({value = true}))
end end
if flip() then if flip() then
combination ..= "F" combination ..= "F"
ecs:insert(entity, D6({value = true})) 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 #combination == 7 then
if flip() then added += 1
combination ..= "G" ecs:insert(entity, D1({value = true}))
ecs:insert(entity, D7({value = true})) end
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 archetypes[combination] = true
end end
local a = 0 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) view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end end
end
end

View file

@ -2,41 +2,37 @@
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local rgb = require(ReplicatedStorage.rgb)
local Matter = require(ReplicatedStorage.DevPackages.Matter) local Matter = require(ReplicatedStorage.DevPackages.Matter)
local jecs = require(ReplicatedStorage.Lib)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local rgb = require(ReplicatedStorage.rgb)
local newWorld = Matter.World.new() local newWorld = Matter.World.new()
local ecs = jecs.World.new() local ecs = jecs.World.new()
return { return {
ParameterGenerator = function() ParameterGenerator = function()
local registry2 = ecr.registry() local registry2 = ecr.registry()
return registry2 return registry2
end, end;
Functions = { Functions = {
Matter = function() Matter = function()
for i = 1, 1000 do for i = 1, 1000 do
newWorld:spawn() newWorld:spawn()
end end
end, end;
ECR = function(_, registry2)
for i = 1, 1000 do
registry2.create()
end
end;
ECR = function(_, registry2) Jecs = function()
for i = 1, 1000 do for i = 1, 1000 do
registry2.create() ecs:entity()
end end
end, end;
};
Jecs = function()
for i = 1, 1000 do
ecs:entity()
end
end
},
} }

View file

@ -6,10 +6,10 @@
type i53 = number type i53 = number
type i24 = number type i24 = number
type Ty = { i53 } type Ty = {i53}
type ArchetypeId = number type ArchetypeId = number
type Column = { any } type Column = {any}
type Archetype = { type Archetype = {
id: number, id: number,
@ -20,9 +20,9 @@ type Archetype = {
}, },
}, },
types: Ty, types: Ty,
type: string | number, type: string | number,
entities: { number }, entities: {number},
columns: { Column }, columns: {Column},
records: {}, records: {},
} }
@ -31,13 +31,13 @@ type Record = {
row: number, row: number,
} }
type EntityIndex = { [i24]: Record } type EntityIndex = {[i24]: Record}
type ComponentIndex = { [i24]: ArchetypeMap} type ComponentIndex = {[i24]: ArchetypeMap}
type ArchetypeRecord = number type ArchetypeRecord = number
type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number } type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
type Archetypes = { [ArchetypeId]: Archetype } type Archetypes = {[ArchetypeId]: Archetype}
type ArchetypeDiff = { type ArchetypeDiff = {
added: Ty, added: Ty,
removed: Ty, removed: Ty,
@ -64,17 +64,17 @@ local function transitionArchetype(
local types = from.types local types = from.types
for i, column in columns do 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. -- 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]]] local targetColumn = destinationColumns[tr[types[i]]]
-- Sometimes target column may not exist, e.g. when you remove a component. -- Sometimes target column may not exist, e.g. when you remove a component.
if targetColumn then if targetColumn then
targetColumn[destinationRow] = column[sourceRow] targetColumn[destinationRow] = column[sourceRow]
end end
-- If the entity is the last row in the archetype then swapping it would be meaningless. -- If the entity is the last row in the archetype then swapping it would be meaningless.
local last = #column local last = #column
if sourceRow ~= last then if sourceRow ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype. -- Swap rempves columns to ensure there are no holes in the archetype.
column[sourceRow] = column[last] column[sourceRow] = column[last]
end end
@ -82,24 +82,27 @@ local function transitionArchetype(
end end
-- Move the entity from the source to the destination archetype. -- Move the entity from the source to the destination archetype.
destinationEntities[destinationRow] = sourceEntities[sourceRow] local atSourceRow = sourceEntities[sourceRow]
entityIndex[sourceEntities[sourceRow]].row = destinationRow destinationEntities[destinationRow] = atSourceRow
entityIndex[atSourceRow].row = destinationRow
-- Because we have swapped columns we now have to update the records -- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped. -- corresponding to the entities' rows that were swapped.
local movedAway = #sourceEntities local movedAway = #sourceEntities
if sourceRow ~= movedAway then if sourceRow ~= movedAway then
sourceEntities[sourceRow] = sourceEntities[movedAway] local atMovedAway = sourceEntities[movedAway]
entityIndex[sourceEntities[movedAway]].row = sourceRow sourceEntities[sourceRow] = atMovedAway
entityIndex[atMovedAway].row = sourceRow
end end
sourceEntities[movedAway] = nil sourceEntities[movedAway] = nil
end end
local function archetypeAppend(entity: i53, archetype: Archetype): i24 local function archetypeAppend(entity: number, archetype: Archetype): number
local entities = archetype.entities local entities = archetype.entities
table.insert(entities, entity) local length = #entities + 1
return #entities entities[length] = entity
return length
end end
local function newEntity(entityId: i53, record: Record, archetype: Archetype) local function newEntity(entityId: i53, record: Record, archetype: Archetype)
@ -122,47 +125,49 @@ local function hash(arr): string | number
return table.concat(arr, "_") return table.concat(arr, "_")
end end
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?) local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
local destinationCount = #to.types
local destinationIds = to.types local destinationIds = to.types
local records = to.records
local id = to.id
for i = 1, destinationCount do for i, destinationId in destinationIds do
local destinationId = destinationIds[i] local archetypesMap = componentIndex[destinationId]
if not componentIndex[destinationId] then if not archetypesMap then
componentIndex[destinationId] = { size = 0, sparse = {} } archetypesMap = {size = 0, sparse = {}}
componentIndex[destinationId] = archetypesMap
end end
local archetypesMap = componentIndex[destinationId] archetypesMap.sparse[id] = i
archetypesMap.sparse[to.id] = i records[destinationId] = i
to.records[destinationId] = i
end end
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) local ty = hash(types)
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1 local id = world.nextArchetypeId + 1
local id = world.nextArchetypeId world.nextArchetypeId = id
local columns = {} :: { any } local length = #types
local columns = table.create(length) :: {any}
for _ in types do for index in types do
table.insert(columns, {}) columns[index] = {}
end end
local archetype = { local archetype = {
id = id, columns = columns;
types = types, edges = {};
type = ty, entities = {};
columns = columns, id = id;
entities = {}, records = {};
edges = {}, type = ty;
records = {}, types = types;
} }
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype world.archetypes[id] = archetype
if #types > 0 then if length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev) createArchetypeRecords(world.componentIndex, archetype, prev)
end end
@ -171,42 +176,42 @@ end
local World = {} local World = {}
World.__index = World World.__index = World
function World.new() function World.new()
local self = setmetatable({ local self = setmetatable({
entityIndex = {}, archetypeIndex = {};
componentIndex = {}, archetypes = {};
archetypes = {}, componentIndex = {};
archetypeIndex = {}, entityIndex = {};
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
nextEntityId = 0,
nextComponentId = 0,
nextArchetypeId = 0,
hooks = { hooks = {
[ON_ADD] = {} [ON_ADD] = {};
} };
nextArchetypeId = 0;
nextComponentId = 0;
nextEntityId = 0;
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World) }, World)
return self return self
end end
local function emit(world, eventDescription) local function emit(world, eventDescription)
local event = eventDescription.event local event = eventDescription.event
table.insert(world.hooks[event], { table.insert(world.hooks[event], {
ids = eventDescription.ids, archetype = eventDescription.archetype;
archetype = eventDescription.archetype, ids = eventDescription.ids;
otherArchetype = eventDescription.otherArchetype, offset = eventDescription.offset;
offset = eventDescription.offset otherArchetype = eventDescription.otherArchetype;
}) })
end end
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
if #added > 0 then if #added > 0 then
emit(world, { emit(world, {
event = ON_ADD, archetype = archetype;
ids = added, event = ON_ADD;
archetype = archetype, ids = added;
otherArchetype = otherArchetype, offset = row;
offset = row, otherArchetype = otherArchetype;
}) })
end end
end end
@ -217,7 +222,7 @@ local function ensureArchetype(world: World, types, prev)
if #types < 1 then if #types < 1 then
return world.ROOT_ARCHETYPE return world.ROOT_ARCHETYPE
end end
local ty = hash(types) local ty = hash(types)
local archetype = world.archetypeIndex[ty] local archetype = world.archetypeIndex[ty]
if archetype then if archetype then
@ -227,10 +232,8 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev) return archetypeOf(world, types, prev)
end end
local function findInsert(types: { i53 }, toAdd: i53) local function findInsert(types: {i53}, toAdd: i53)
local count = #types for i, id in types do
for i = 1, count do
local id = types[i]
if id == toAdd then if id == toAdd then
return -1 return -1
end end
@ -238,13 +241,13 @@ local function findInsert(types: { i53 }, toAdd: i53)
return i return i
end end
end end
return count + 1 return #types + 1
end end
local function findArchetypeWith(world: World, node: Archetype, componentId: i53) local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
local types = node.types local types = node.types
-- Component IDs are added incrementally, so inserting and sorting -- 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. -- point in the types array.
local at = findInsert(types, componentId) local at = findInsert(types, componentId)
if at == -1 then if at == -1 then
@ -259,48 +262,57 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
end end
local function ensureEdge(archetype: Archetype, componentId: i53) local function ensureEdge(archetype: Archetype, componentId: i53)
if not archetype.edges[componentId] then local edges = archetype.edges
archetype.edges[componentId] = {} :: any local edge = edges[componentId]
if not edge then
edge = {} :: any
edges[componentId] = edge
end end
return archetype.edges[componentId] return edge
end end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype 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 there was no source archetype then it should return the ROOT_ARCHETYPE
if not world.ROOT_ARCHETYPE then local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil) if not ROOT_ARCHETYPE then
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
end world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
from = world.ROOT_ARCHETYPE end
from = ROOT_ARCHETYPE
end end
local edge = ensureEdge(from, componentId) local edge = ensureEdge(from, componentId)
local add = edge.add
if not edge.add then if not add then
-- Save an edge using the component ID to the archetype to allow -- Save an edge using the component ID to the archetype to allow
-- faster traversals to adjacent archetypes. -- faster traversals to adjacent archetypes.
edge.add = findArchetypeWith(world, from, componentId) add = findArchetypeWith(world, from, componentId)
edge.add = add :: never
end end
return edge.add return add
end end
local function ensureRecord(entityIndex, entityId: i53): Record local function ensureRecord(entityIndex, entityId: i53): Record
local id = entityId local record = entityIndex[entityId]
if not entityIndex[id] then
entityIndex[id] = {} if not record then
record = {}
entityIndex[entityId] = record
end end
return entityIndex[id] :: Record
return record :: Record
end 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 record = ensureRecord(world.entityIndex, entityId)
local from = record.archetype local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from) local to = archetypeTraverseAdd(world, componentId, from)
if from == to then if from == to then
-- If the archetypes are the same it can avoid moving the entity -- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly. -- and just set the data directly.
local archetypeRecord = to.records[componentId] local archetypeRecord = to.records[componentId]
from.columns[archetypeRecord][record.row] = data from.columns[archetypeRecord][record.row] = data
-- Should fire an OnSet event here. -- Should fire an OnSet event here.
@ -308,13 +320,13 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
end end
if from then 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) moveEntity(world.entityIndex, entityId, record, to)
else else
if #to.types > 0 then if #to.types > 0 then
-- When there is no previous archetype it should create the archetype -- When there is no previous archetype it should create the archetype
newEntity(entityId, record, to) newEntity(entityId, record, to)
onNotifyAdd(world, to, from, record.row, { componentId }) onNotifyAdd(world, to, from, record.row, {componentId})
end end
end end
@ -326,28 +338,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
local edge = ensureEdge(from, componentId) local edge = ensureEdge(from, componentId)
local remove = edge.remove
if not edge.remove then if not remove then
local to = table.clone(from.types) local to = table.clone(from.types)
table.remove(to, table.find(to, componentId)) table.remove(to, table.find(to, componentId))
edge.remove = ensureArchetype(world, to, from) remove = ensureArchetype(world, to, from)
edge.remove = remove :: never
end end
return edge.remove return remove
end end
function World.remove(world: World, entityId: i53, componentId: i53) function World.remove(world: World, entityId: i53, componentId: i53)
local record = ensureRecord(world.entityIndex, entityId) local entityIndex = world.entityIndex
local record = ensureRecord(entityIndex, entityId)
local sourceArchetype = record.archetype local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then if sourceArchetype and not (sourceArchetype == destinationArchetype) then
moveEntity(world.entityIndex, entityId, record, destinationArchetype) moveEntity(entityIndex, entityId, record, destinationArchetype)
end end
end end
-- Keeping the function as small as possible to enable inlining -- 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 archetype = record.archetype
local archetypeRecord = archetype.records[componentId] 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?) function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId local id = entityId
local componentIndex = world.componentIndex
local record = world.entityIndex[id] local record = world.entityIndex[id]
if not record then if not record then
return nil return nil
end end
local va = get(componentIndex, record, a) local va = get(record, a)
if b == nil then if b == nil then
return va return va
elseif c == nil then elseif c == nil then
return va, get(componentIndex, record, b) return va, get(record, b)
elseif d == nil then 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 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 else
error("args exceeded") error("args exceeded")
end end
end end
local function noop(self: Query, ...: i53): () -> (number, ...any) -- the less creation the better
return function() local function actualNoOperation() end
end :: any local function noop(_self: Query, ...: i53): () -> (number, ...any)
return actualNoOperation :: any
end end
local EmptyQuery = { local EmptyQuery = {
__iter = noop, __iter = noop;
without = noop without = noop;
} }
EmptyQuery.__index = EmptyQuery EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery) setmetatable(EmptyQuery, EmptyQuery)
@ -396,25 +410,28 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery) export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query function World.query(world: World, ...: i53): Query
local compatibleArchetypes = {} -- breaking?
local components = { ... } if (...) == nil then
local archetypes = world.archetypes
local queryLength = #components
if queryLength == 0 then
error("Missing components") error("Missing components")
end end
local compatibleArchetypes = {}
local length = 0
local components = {...}
local archetypes = world.archetypes
local queryLength = #components
local firstArchetypeMap local firstArchetypeMap
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
for i, componentId in components do for _, componentId in components do
local map = componentIndex[componentId] local map = componentIndex[componentId]
if not map then if not map then
return EmptyQuery return EmptyQuery
end end
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map firstArchetypeMap = map
end end
end end
@ -422,110 +439,107 @@ function World.query(world: World, ...: i53): Query
for id in firstArchetypeMap.sparse do for id in firstArchetypeMap.sparse do
local archetype = archetypes[id] local archetype = archetypes[id]
local archetypeRecords = archetype.records local archetypeRecords = archetype.records
local indices = {} local indices = {}
local skip = false local skip = false
for i, componentId in components do for i, componentId in components do
local index = archetypeRecords[componentId] local index = archetypeRecords[componentId]
if not index then if not index then
skip = true skip = true
break break
end end
indices[i] = archetypeRecords[componentId] indices[i] = index
end end
if skip then if skip then
continue continue
end end
table.insert(compatibleArchetypes, { archetype, indices })
length += 1
compatibleArchetypes[length] = {archetype, indices}
end end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes) local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then if not lastArchetype then
return EmptyQuery return EmptyQuery
end end
local preparedQuery = {} local preparedQuery = {}
preparedQuery.__index = preparedQuery preparedQuery.__index = preparedQuery
function preparedQuery:without(...) function preparedQuery:without(...)
local components = { ... } local withoutComponents = {...}
for i = #compatibleArchetypes, 1, -1 do for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i][1] local archetype = compatibleArchetypes[i][1]
local records = archetype.records
local shouldRemove = false local shouldRemove = false
for _, componentId in components do
if archetype.records[componentId] then for _, componentId in withoutComponents do
if records[componentId] then
shouldRemove = true shouldRemove = true
break break
end end
end end
if shouldRemove then
if shouldRemove then
table.remove(compatibleArchetypes, i) table.remove(compatibleArchetypes, i)
end end
end end
lastArchetype, compatibleArchetype = next(compatibleArchetypes) lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then if not lastArchetype then
return EmptyQuery return EmptyQuery
end end
return self return self
end end
local lastRow local lastRow
local queryOutput = {} local queryOutput = {}
function preparedQuery:__iter()
function preparedQuery:__iter() return function()
return function()
local archetype = compatibleArchetype[1] local archetype = compatibleArchetype[1]
local row = next(archetype.entities, lastRow) local row = next(archetype.entities, lastRow)
while row == nil do while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
if lastArchetype == nil then if lastArchetype == nil then
return return
end end
archetype = compatibleArchetype[1] archetype = compatibleArchetype[1]
row = next(archetype.entities, row) row = next(archetype.entities, row)
end end
lastRow = row lastRow = row
local entityId = archetype.entities[row :: number] local entityId = archetype.entities[row :: number]
local columns = archetype.columns local columns = archetype.columns
local tr = compatibleArchetype[2] local tr = compatibleArchetype[2]
if queryLength == 1 then if queryLength == 1 then
return entityId, columns[tr[1]][row] return entityId, columns[tr[1]][row]
elseif queryLength == 2 then elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row] return entityId, columns[tr[1]][row], columns[tr[2]][row]
elseif queryLength == 3 then elseif queryLength == 3 then
return entityId, return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
columns[tr[1]][row], elseif queryLength == 4 then
columns[tr[2]][row], return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
columns[tr[3]][row] elseif queryLength == 5 then
elseif queryLength == 4 then return entityId,
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[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
columns[tr[4]][row], columns[tr[4]][row],
columns[tr[5]][row] columns[tr[5]][row]
elseif queryLength == 6 then elseif queryLength == 6 then
return entityId, return entityId,
columns[tr[1]][row], columns[tr[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
columns[tr[4]][row], columns[tr[4]][row],
columns[tr[5]][row], columns[tr[5]][row],
columns[tr[6]][row] columns[tr[6]][row]
elseif queryLength == 7 then elseif queryLength == 7 then
return entityId, return entityId,
columns[tr[1]][row], columns[tr[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
@ -533,8 +547,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[5]][row], columns[tr[5]][row],
columns[tr[6]][row], columns[tr[6]][row],
columns[tr[7]][row] columns[tr[7]][row]
elseif queryLength == 8 then elseif queryLength == 8 then
return entityId, return entityId,
columns[tr[1]][row], columns[tr[1]][row],
columns[tr[2]][row], columns[tr[2]][row],
columns[tr[3]][row], columns[tr[3]][row],
@ -545,7 +559,7 @@ function World.query(world: World, ...: i53): Query
columns[tr[8]][row] columns[tr[8]][row]
end end
for i in components do for i in components do
queryOutput[i] = tr[i][row] queryOutput[i] = tr[i][row]
end end
@ -556,23 +570,24 @@ function World.query(world: World, ...: i53): Query
return setmetatable({}, preparedQuery) :: any return setmetatable({}, preparedQuery) :: any
end end
function World.component(world: World) function World.component(world: World)
local componentId = world.nextComponentId + 1 local componentId = world.nextComponentId + 1
if componentId > HI_COMPONENT_ID then if componentId > HI_COMPONENT_ID then
-- IDs are partitioned into ranges because component IDs are not nominal, -- IDs are partitioned into ranges because component IDs are not nominal,
-- so it needs to error when IDs intersect into the entity range. -- 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 end
world.nextComponentId = componentId world.nextComponentId = componentId
return componentId return componentId
end end
function World.entity(world: World) function World.entity(world: World)
world.nextEntityId += 1 local nextEntityId = world.nextEntityId + 1
return world.nextEntityId + REST world.nextEntityId = nextEntityId
return nextEntityId + REST
end end
function World.delete(world: World, entityId: i53) function World.delete(world: World, entityId: i53)
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
local record = entityIndex[entityId] local record = entityIndex[entityId]
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE) moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
@ -584,57 +599,61 @@ function World.delete(world: World, entityId: i53)
end end
function World.observer(world: World, ...) function World.observer(world: World, ...)
local componentIds = { ... } local componentIds = {...}
local idsCount = #componentIds
local hooks = world.hooks
return { return {
event = function(event) event = function(event)
local hook = world.hooks[event] local hook = hooks[event]
world.hooks[event] = nil hooks[event] = nil
local last, change local last, change
return function() return function()
last, change = next(hook, last) last, change = next(hook, last)
if not last then if not last then
return return
end end
local matched = false local matched = false
local ids = change.ids
while not matched do
while not matched do
local skip = false local skip = false
for _, id in change.ids do for _, id in ids do
if not table.find(componentIds, id) then if not table.find(componentIds, id) then
skip = true skip = true
break break
end end
end end
if skip then if skip then
last, change = next(hook, last) last, change = next(hook, last)
ids = change.ids
continue continue
end end
matched = true matched = true
end end
local queryOutput = {} local queryOutput = table.create(idsCount)
local row = change.offset local row = change.offset
local archetype = change.archetype local archetype = change.archetype
local columns = archetype.columns local columns = archetype.columns
local archetypeRecords = archetype.records local archetypeRecords = archetype.records
for _, id in componentIds do for index, id in componentIds do
table.insert(queryOutput, columns[archetypeRecords[id]][row]) queryOutput[index] = columns[archetypeRecords[id]][row]
end end
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput) return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end end
end end;
} }
end end
return table.freeze({ return table.freeze({
World = World, World = World;
ON_ADD = ON_ADD, ON_ADD = ON_ADD;
ON_REMOVE = ON_REMOVE, ON_REMOVE = ON_REMOVE;
ON_SET = ON_SET ON_SET = ON_SET;
}) })

4
selene.toml Normal file
View file

@ -0,0 +1,4 @@
std = "roblox"
[lints]
global_usage = "allow"

5
stylua.toml Normal file
View file

@ -0,0 +1,5 @@
column_width = 120
quote_style = "ForceDouble"
[sort_requires]
enabled = true

3
testez-companion.toml Normal file
View file

@ -0,0 +1,3 @@
roots = ["ServerStorage"]
[extraOptions]

View file

@ -10,6 +10,3 @@ include = ["default.project.json", "lib", "wally.toml", "README.md"]
TestEZ = "roblox/testez@0.4.1" TestEZ = "roblox/testez@0.4.1"
Matter = "matter-ecs/matter@0.8.0" Matter = "matter-ecs/matter@0.8.0"
ecr = "centau/ecr@0.8.0" ecr = "centau/ecr@0.8.0"