Merge branch 'main' into add-world.add

This commit is contained in:
Marcus 2024-05-07 21:32:49 +02:00 committed by GitHub
commit e154798be4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 1178 additions and 3776 deletions

3
.gitignore vendored
View file

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

View file

@ -1,6 +1,7 @@
<p align="center">
<img src="logo.png" />
<img src="jecs_darkmode.svg#gh-dark-mode-only" width=50%/>
<img src="jecs_lightmode.svg#gh-light-mode-only" width=50%/>
</p>
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache-blue.svg?style=for-the-badge)](LICENSE-APACHE)

372
benches/exhaustive.lua Normal file
View file

@ -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

View file

@ -1,35 +1,33 @@
--!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))
end
local jecs = require("../mirror/init")
local jecs = require("../lib/init")
local mirror = require("../mirror/init")
local oldMatter = require("../oldMatter")
local newMatter = require("../newMatter")
type i53 = number
do TITLE (testkit.color.white_underline("Jecs query"))
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
)
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
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
for _ in world:query(A, B) do
end
end)
BENCH("4 component", function()
@ -38,7 +36,8 @@ do TITLE (testkit.color.white_underline("Jecs query"))
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
for _ in world:query(A, B, C, D, E, F, G, H) do
end
end)
end
@ -89,7 +88,6 @@ do TITLE (testkit.color.white_underline("Jecs query"))
if flip() then
combination ..= "H"
ecs:set(entity, D8, {value = true})
end
if #combination == 7 then
@ -100,29 +98,29 @@ do TITLE (testkit.color.white_underline("Jecs query"))
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
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
)
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
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
for _ in world:query(A, B) do
end
end)
BENCH("4 component", function()
@ -131,18 +129,19 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
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 = 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
@ -151,149 +150,51 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
local added = 0
local archetypes = {}
for i = 1, 2 ^ 16 - 2 do
local entity = ecs:spawn()
local entity = ecs:entity()
local combination = ""
if flip() then
combination ..= "B"
ecs:insert(entity, D2({value = true}))
ecs:set(entity, D2, {value = true})
end
if flip() then
combination ..= "C"
ecs:insert(entity, D3({value = true}))
ecs:set(entity, D3, {value = true})
end
if flip() then
combination ..= "D"
ecs:insert(entity, D4({value = true}))
ecs:set(entity, D4, {value = true})
end
if flip() then
combination ..= "E"
ecs:insert(entity, D5({value = true}))
ecs:set(entity, D5, {value = true})
end
if flip() then
combination ..= "F"
ecs:insert(entity, D6({value = true}))
ecs:set(entity, D6, {value = true})
end
if flip() then
combination ..= "G"
ecs:insert(entity, D7({value = true}))
ecs:set(entity, D7, {value = true})
end
if flip() then
combination ..= "H"
ecs:insert(entity, D8({value = true}))
ecs:set(entity, D8, {value = true})
end
if #combination == 7 then
added += 1
ecs:insert(entity, D1({value = true}))
ecs:set(entity, D1, {value = true})
end
archetypes[combination] = true
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
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

View file

@ -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()
return registry2
end,
end;
Functions = {
Matter = function()
for i = 1, 1000 do
newWorld:spawn()
end
end,
end;
ECR = function(_, registry2)
for i = 1, 1000 do
registry2.create()
end
end,
end;
Jecs = function()
for i = 1, 1000 do
ecs:entity()
end
end
},
end;
};
}

6
jecs_darkmode.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="white"/>
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

6
jecs_lightmode.svg Normal file
View file

@ -0,0 +1,6 @@
<svg width="47" height="18" viewBox="0 0 47 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 14C5.8 14 6 13.3333 6 13V4H0V0H6H10V13C10 17 6.66667 18 5 18H0V14H5Z" fill="black"/>
<path d="M46.5 4V0H39C37.1667 0 33.5 1.1 33.5 5.5C33.5 9.9 36.8333 11 38.5 11H41C41.5 11 42.5 11.3 42.5 12.5C42.5 13.7 41.5 14 41 14H33.5V18H41.5C43.1667 18 46.5 16.9 46.5 12.5C46.5 8.1 43.1667 7 41.5 7H39C38.5 7 37.5 6.7 37.5 5.5C37.5 4.3 38.5 4 39 4H46.5Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.5 0V4H30.5C28.5 4 24.5 5 24.5 9C24.5 11.0835 25.5853 12.3531 26.9078 13.0914L22.4606 14.661C21.2893 13.3156 20.5 11.4775 20.5 9C20.5 1.8 27.1667 0 30.5 0H32.5ZM24.4656 16.3357C26.5037 17.5803 28.8905 18 30.5 18H32.5V14H31.0833L24.4656 16.3357Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.3793 0C24.766 0.241156 24.1568 0.53354 23.571 0.885014C22.1712 1.72492 20.9038 2.91123 20.0606 4.5H11V0H25.3793ZM25.5 4.39421C25.445 4.42876 25.3906 4.46402 25.3368 4.5H25.5V4.39421ZM20.0606 13.5C20.9038 15.0888 22.1712 16.2751 23.571 17.115C24.1568 17.4665 24.766 17.7588 25.3793 18H11V13.5H20.0606ZM19.1854 7C19.0649 7.62348 19 8.28956 19 9C19 9.71044 19.0649 10.3765 19.1854 11H11V7H19.1854Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -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
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 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
@ -173,20 +178,19 @@ local World = {}
World.__index = World
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)
local ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
self.ROOT_ARCHETYPE = ROOT_ARCHETYPE
self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
return self
end
@ -194,21 +198,21 @@ 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
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
@ -230,9 +234,7 @@ local function ensureArchetype(world: World, types, prev)
end
local function findInsert(types: {i53}, toAdd: i53)
local count = #types
for i = 1, count do
local id = types[i]
for i, id in types do
if id == toAdd then
return -1
end
@ -240,7 +242,7 @@ 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)
@ -261,35 +263,42 @@ 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
from = from or world.ROOT_ARCHETYPE
local edge = ensureEdge(from, componentId)
if not edge.add then
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] = {}
end
return entityIndex[id] :: Record
local record = entityIndex[entityId]
if not record then
record = {}
entityIndex[entityId] = record
end
-- Symmetric
return record :: Record
end
function World.add(world: World, entityId: i53, componentId: i53)
local record = ensureRecord(world.entityIndex, entityId)
local from = record.archetype
@ -338,28 +347,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 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)
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)
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]
@ -372,35 +383,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)
@ -408,19 +419,22 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
-- breaking?
if (...) == nil then
error("Missing components")
end
local compatibleArchetypes = {}
local length = 0
local components = {...}
local archetypes = world.archetypes
local queryLength = #components
if queryLength == 0 then
error("Missing components")
end
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
@ -443,13 +457,15 @@ function World.query(world: World, ...: i53): Query
skip = true
break
end
indices[i] = archetypeRecords[componentId]
indices[i] = index
end
if skip then
continue
end
table.insert(compatibleArchetypes, { archetype, indices })
length += 1
compatibleArchetypes[length] = {archetype, indices}
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
@ -461,16 +477,19 @@ function World.query(world: World, ...: i53): Query
preparedQuery.__index = preparedQuery
function preparedQuery:without(...)
local components = { ... }
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
table.remove(compatibleArchetypes, i)
end
@ -487,7 +506,6 @@ function World.query(world: World, ...: i53): Query
local lastRow
local queryOutput = {}
function preparedQuery:__iter()
return function()
local archetype = compatibleArchetype[1]
@ -511,16 +529,9 @@ function World.query(world: World, ...: i53): Query
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]
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]
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],
@ -558,7 +569,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)
@ -580,8 +591,9 @@ function World.component(world: World)
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)
@ -597,11 +609,13 @@ end
function World.observer(world: World, ...)
local componentIds = {...}
local idsCount = #componentIds
local hooks = world.hooks
return {
event = function(event)
local hook = world.hooks[event]
world.hooks[event] = nil
local hook = hooks[event]
hooks[event] = nil
local last, change
return function()
@ -611,10 +625,11 @@ function World.observer(world: World, ...)
end
local matched = false
local ids = change.ids
while not matched do
local skip = false
for _, id in change.ids do
for _, id in ids do
if not table.find(componentIds, id) then
skip = true
break
@ -623,30 +638,62 @@ function World.observer(world: World, ...)
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)
end
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end
end;
}
end
function World.__iter(world: World): () -> (number?, unknown?)
local entityIndex = world.entityIndex
local last
return function()
local entity, record = next(entityIndex, last)
if not entity then
return
end
last = entity
local archetype = record.archetype
if not archetype then
-- Returns only the entity id as an entity without data should not return
-- data and allow the user to get an error if they don't handle the case.
return entity
end
local row = record.row
local types = archetype.types
local columns = archetype.columns
local entityData = {}
for i, column in columns do
-- We use types because the key should be the component ID not the column index
entityData[types[i]] = column[row]
end
return entity, entityData
end
end
return table.freeze({
World = World,
ON_ADD = ON_ADD,
ON_REMOVE = ON_REMOVE,
ON_SET = ON_SET
World = World;
ON_ADD = ON_ADD;
ON_REMOVE = ON_REMOVE;
ON_SET = ON_SET;
})

View file

@ -299,5 +299,38 @@ return function()
expect(world:get(id, Poison)).to.never.be.ok()
expect(world:get(id, Health)).to.never.be.ok()
end)
it("should allow iterating the whole world", function()
local world = jecs.World.new()
local A, B = world:entity(), world:entity()
local eA = world:entity()
world:set(eA, A, true)
local eB = world:entity()
world:set(eB, B, true)
local eAB = world:entity()
world:set(eAB, A, true)
world:set(eAB, B, true)
local count = 0
for id, data in world do
count += 1
if id == eA then
expect(data[A]).to.be.ok()
expect(data[B]).to.never.be.ok()
elseif id == eB then
expect(data[B]).to.be.ok()
expect(data[A]).to.never.be.ok()
elseif id == eAB then
expect(data[A]).to.be.ok()
expect(data[B]).to.be.ok()
else
error("unknown entity", id)
end
end
expect(count).to.equal(3)
end)
end)
end

View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -51,38 +51,58 @@ 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
destinationEntities[destinationRow] = sourceEntities[sourceRow]
entityIndex[sourceEntities[sourceRow]].row = destinationRow
-- Move the entity from the source to the destination archetype.
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
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)
@ -105,47 +125,51 @@ 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 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 length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev)
end
return archetype
end
@ -154,17 +178,17 @@ local World = {}
World.__index = World
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
end
@ -173,34 +197,32 @@ 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
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
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
@ -211,9 +233,7 @@ local function ensureArchetype(world: World, types, prev)
end
local function findInsert(types: {i53}, toAdd: i53)
local count = #types
for i = 1, count do
local id = types[i]
for i, id in types do
if id == toAdd then
return -1
end
@ -221,13 +241,18 @@ 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
-- 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
@ -237,88 +262,108 @@ 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 world.ROOT_ARCHETYPE then
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
if not ROOT_ARCHETYPE then
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
end
from = world.ROOT_ARCHETYPE
from = ROOT_ARCHETYPE
end
local edge = ensureEdge(from, componentId)
if not edge.add then
edge.add = findArchetypeWith(world, from, componentId)
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.
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)
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
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
local edge = ensureEdge(from, componentId)
if not edge.remove then
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)
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)
moveEntity(entityIndex, entityId, record, destinationArchetype)
end
end
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
-- Keeping the function as small as possible to enable inlining
local function get(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
@ -329,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)
@ -365,19 +410,22 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
-- breaking?
if (...) == nil then
error("Missing components")
end
local compatibleArchetypes = {}
local length = 0
local components = {...}
local archetypes = world.archetypes
local queryLength = #components
if queryLength == 0 then
error("Missing components")
end
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
@ -388,27 +436,27 @@ 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] = index
end
if skip then
continue
end
i += 1
table.insert(compatibleArchetypes, { archetype, indices })
length += 1
compatibleArchetypes[length] = {archetype, indices}
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
@ -420,16 +468,19 @@ function World.query(world: World, ...: i53): Query
preparedQuery.__index = preparedQuery
function preparedQuery:without(...)
local components = { ... }
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
table.remove(compatibleArchetypes, i)
end
@ -446,7 +497,6 @@ function World.query(world: World, ...: i53): Query
local lastRow
local queryOutput = {}
function preparedQuery:__iter()
return function()
local archetype = compatibleArchetype[1]
@ -470,16 +520,9 @@ function World.query(world: World, ...: i53): Query
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]
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]
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],
@ -517,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)
@ -530,24 +573,40 @@ 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
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)
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 = {...}
local idsCount = #componentIds
local hooks = world.hooks
return {
event = function(event)
local hook = world.hooks[event]
world.hooks[event] = nil
local hook = hooks[event]
hooks[event] = nil
local last, change
return function()
@ -557,10 +616,11 @@ function World.observer(world: World, ...)
end
local matched = false
local ids = change.ids
while not matched do
local skip = false
for _, id in change.ids do
for _, id in ids do
if not table.find(componentIds, id) then
skip = true
break
@ -569,30 +629,31 @@ function World.observer(world: World, ...)
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)
end
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
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;
})

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

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

@ -110,6 +110,39 @@ TEST("world:query", function()
CHECK(world:get(id, Health) == nil)
end
do CASE "Should allow iterating the whole world"
local world = jecs.World.new()
local A, B = world:entity(), world:entity()
local eA = world:entity()
world:set(eA, A, true)
local eB = world:entity()
world:set(eB, B, true)
local eAB = world:entity()
world:set(eAB, A, true)
world:set(eAB, B, true)
local count = 0
for id, data in world do
count += 1
if id == eA then
CHECK(data[A] == true)
CHECK(data[B] == nil)
elseif id == eB then
CHECK(data[B] == true)
CHECK(data[A] == nil)
elseif id == eAB then
CHECK(data[A] == true)
CHECK(data[B] == true)
else
error("unknown entity", id)
end
end
CHECK(count == 3)
end
end)
FINISH()

View file

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