[](LICENSE-APACHE)
@@ -10,23 +11,29 @@ Just an ECS
jecs is a stupidly fast Entity Component System (ECS).
+- Entity Relationships as first class citizens
- Process tens of thousands of entities with ease every frame
-- Zero-dependency Luau package
+- Type-safe [Luau](https://luau-lang.org/) API
+- Zero-dependency package
- Optimized for column-major operations
- Cache friendly archetype/SoA storage
+- Unit tested for stability
### Example
```lua
-local world = Jecs.World.new()
-
-local Health = world:component()
-local Damage = world:component()
-local Position = world:component()
+local world = World.new()
local player = world:entity()
local opponent = world:entity()
+local Health = world:component()
+local Position = world:component()
+-- Notice how components can just be entities as well?
+-- It allows you to model relationships easily!
+local Damage = world:entity()
+local DamagedBy = world:entity()
+
world:set(player, Health, 100)
world:set(player, Damage, 8)
world:set(player, Position, Vector3.new(0, 5, 0))
@@ -37,17 +44,25 @@ world:set(opponent, Position, Vector3.new(0, 5, 3))
for playerId, playerPosition, health in world:query(Position, Health) do
local totalDamage = 0
- for _, opponentPosition, damage in world:query(Position, Damage) do
+ for opponentId, opponentPosition, damage in world:query(Position, Damage) do
+ if playerId == opponentId then
+ continue
+ end
if (playerPosition - opponentPosition).Magnitude < 5 then
totalDamage += damage
end
+ -- We create a pair between the relation component `DamagedBy` and the entity id of the opponent.
+ -- This will allow us to specifically query for damage exerted by a specific opponent.
+ world:set(playerId, ECS_PAIR(DamagedBy, opponentId), totalDamage)
end
-
- world:set(playerId, Health, health - totalDamage)
end
-assert(world:get(playerId, Health) == 79)
-assert(world:get(opponentId, Health) == 92)
+-- Gets the damage inflicted by our specific opponent!
+for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, opponent)) do
+ world:set(playerId, health - inflicted)
+end
+
+assert(world:get(player, Health) == 79)
```
125 archetypes, 4 random components queried.
diff --git a/benches/exhaustive.lua b/benches/exhaustive.lua
new file mode 100644
index 0000000..3095c70
--- /dev/null
+++ b/benches/exhaustive.lua
@@ -0,0 +1,372 @@
+local testkit = require("../testkit")
+local jecs = require("../lib/init")
+local ecr = require("../DevPackages/_Index/centau_ecr@0.8.0/ecr/src/ecr")
+
+
+local BENCH, START = testkit.benchmark()
+
+local function TITLE(title: string)
+ print()
+ print(testkit.color.white(title))
+end
+
+local N = 2^16-2
+
+type i53 = number
+
+do TITLE "create"
+ BENCH("entity", function()
+ local world = jecs.World.new()
+ for i = 1, START(N) do
+ world:entity()
+ end
+ end)
+end
+
+--- component benchmarks
+
+--todo: perform the same benchmarks for multiple components.?
+-- these kind of operations only support 1 component at a time, which is
+-- a shame, especially for archetypes where moving components is expensive.
+
+do TITLE "set"
+ BENCH("add 1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+
+ for i = 1, N do
+ entities[i] = world:entity()
+ end
+
+ for i = 1, START(N) do
+ world:set(entities[i], A, i)
+ end
+ end)
+
+ BENCH("change 1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local e = world:entity()
+ world:set(e, A, 1)
+
+ for i = 1, START(N) do
+ world:set(e, A, 2)
+ end
+ end)
+
+end
+
+do TITLE "remove"
+ BENCH("1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ end
+
+ for i = 1, START(N) do
+ world:remove(entities[i], A)
+ end
+
+ end)
+end
+
+do TITLE "get"
+ BENCH("1 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ end
+
+ for i = 1, START(N) do
+ -- ? curious why the overhead is roughly 80 ns.
+ world:get(entities[i], A)
+ end
+
+ end)
+
+ BENCH("2 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local B = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ world:set(id, B, true)
+ end
+
+ for i = 1, START(N) do
+ world:get(entities[i], A, B)
+ end
+
+ end)
+
+ BENCH("3 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ world:set(id, B, true)
+ world:set(id, C, true)
+ end
+
+ for i = 1, START(N) do
+ world:get(entities[i], A, B, C)
+ end
+
+ end)
+
+ BENCH("4 component", function()
+ local world = jecs.World.new()
+ local entities = {}
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+ local D = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ entities[i] = id
+ world:set(id, A, true)
+ world:set(id, B, true)
+ world:set(id, C, true)
+ world:set(id, D, true)
+ end
+
+ for i = 1, START(N) do
+ world:get(entities[i], A, B, C, D)
+ end
+
+ end)
+end
+
+do TITLE (testkit.color.white_underline("Jecs query"))
+
+ local function count(query: () -> ())
+ local n = 0
+ for _ in query do
+ n += 1
+ end
+ return n
+ end
+
+ local function flip()
+ return math.random() > 0.5
+ end
+
+ local function view_bench(
+ world: jecs.World,
+ A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53
+ )
+
+ BENCH("1 component", function()
+ START(count(world:query(A)))
+ for _ in world:query(A) do end
+ end)
+
+ BENCH("2 component", function()
+ START(count(world:query(A, B)))
+ for _ in world:query(A, B) do end
+ end)
+
+ BENCH("4 component", function()
+ START(count(world:query(A, B, C, D)))
+ for _ in world:query(A, B, C, D) do end
+ end)
+
+ BENCH("8 component", function()
+ START(count(world:query(A, B, C, D, E, F, G, H)))
+ for _ in world:query(A, B, C, D, E, F, G, H) do end
+ end)
+ end
+
+ do TITLE "random components"
+
+ local world = jecs.World.new()
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+ local D = world:component()
+ local E = world:component()
+ local F = world:component()
+ local G = world:component()
+ local H = world:component()
+ local I = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ if flip() then world:set(id, A, true) end
+ if flip() then world:set(id, B, true) end
+ if flip() then world:set(id, C, true) end
+ if flip() then world:set(id, D, true) end
+ if flip() then world:set(id, E, true) end
+ if flip() then world:set(id, F, true) end
+ if flip() then world:set(id, G, true) end
+ if flip() then world:set(id, H, true) end
+ if flip() then world:set(id, I, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+ do TITLE "one component in common"
+
+ local world = jecs.World.new()
+
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+ local D = world:component()
+ local E = world:component()
+ local F = world:component()
+ local G = world:component()
+ local H = world:component()
+ local I = world:component()
+
+ for i = 1, N do
+ local id = world:entity()
+ local a = true
+ if flip() then world:set(id, B, true) else a = false end
+ if flip() then world:set(id, C, true) else a = false end
+ if flip() then world:set(id, D, true) else a = false end
+ if flip() then world:set(id, E, true) else a = false end
+ if flip() then world:set(id, F, true) else a = false end
+ if flip() then world:set(id, G, true) else a = false end
+ if flip() then world:set(id, H, true) else a = false end
+ if flip() then world:set(id, I, true) else a = false end
+ if a then world:set(id, A, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+end
+
+do TITLE (testkit.color.white_underline("ECR query"))
+
+ local A = ecr.component()
+ local B = ecr.component()
+ local C = ecr.component()
+ local D = ecr.component()
+ local E = ecr.component()
+ local F = ecr.component()
+ local G = ecr.component()
+ local H = ecr.component()
+ local I = ecr.component()
+
+ local function count(query: () -> ())
+ local n = 0
+ for _ in query do
+ n += 1
+ end
+ return n
+ end
+
+ local function flip()
+ return math.random() > 0.5
+ end
+
+ local function view_bench(
+ world: ecr.Registry,
+ A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53, I: i53
+ )
+
+ BENCH("1 component", function()
+ START(count(world:view(A)))
+ for _ in world:view(A) do end
+ end)
+
+ BENCH("2 component", function()
+ START(count(world:view(A, B)))
+ for _ in world:view(A, B) do end
+ end)
+
+ BENCH("4 component", function()
+ START(count(world:view(A, B, C, D)))
+ for _ in world:view(A, B, C, D) do end
+ end)
+
+ BENCH("8 component", function()
+ START(count(world:view(A, B, C, D, E, F, G, H)))
+ for _ in world:view(A, B, C, D, E, F, G, H) do end
+ end)
+ end
+
+
+ do TITLE "random components"
+ local world = ecr.registry()
+
+ for i = 1, N do
+ local id = world.create()
+ if flip() then world:set(id, A, true) end
+ if flip() then world:set(id, B, true) end
+ if flip() then world:set(id, C, true) end
+ if flip() then world:set(id, D, true) end
+ if flip() then world:set(id, E, true) end
+ if flip() then world:set(id, F, true) end
+ if flip() then world:set(id, G, true) end
+ if flip() then world:set(id, H, true) end
+ if flip() then world:set(id, I, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+ do TITLE "one component in common"
+
+ local world = ecr.registry()
+
+ for i = 1, N do
+ local id = world.create()
+ local a = true
+ if flip() then world:set(id, B, true) else a = false end
+ if flip() then world:set(id, C, true) else a = false end
+ if flip() then world:set(id, D, true) else a = false end
+ if flip() then world:set(id, E, true) else a = false end
+ if flip() then world:set(id, F, true) else a = false end
+ if flip() then world:set(id, G, true) else a = false end
+ if flip() then world:set(id, H, true) else a = false end
+ if flip() then world:set(id, I, true) else a = false end
+ if a then world:set(id, A, true) end
+
+ end
+
+ view_bench(world, A, B, C, D, E, F, G, H, I)
+
+ end
+
+end
\ No newline at end of file
diff --git a/benches/query.lua b/benches/query.lua
index 2c4cd55..acdb896 100644
--- a/benches/query.lua
+++ b/benches/query.lua
@@ -8,7 +8,8 @@ local function TITLE(title: string)
print(testkit.color.white(title))
end
-local jecs = require("../mirror/init")
+local jecs = require("../lib/init")
+local mirror = require("../mirror/init")
local oldMatter = require("../oldMatter")
@@ -41,6 +42,145 @@ do
for _ in world:query(A, B, C, D, E, F, G, H) do
end
end)
+
+ local e = world:entity()
+ world:set(e, A, true)
+ world:set(e, B, true)
+ world:set(e, C, true)
+ world:set(e, D, true)
+ world:set(e, E, true)
+ world:set(e, F, true)
+ world:set(e, G, true)
+ world:set(e, H, true)
+
+ BENCH("Update Data", function()
+ for _ = 1, 100 do
+ world:set(e, A, false)
+ world:set(e, B, false)
+ world:set(e, C, false)
+ world:set(e, D, false)
+ world:set(e, E, false)
+ world:set(e, F, false)
+ world:set(e, G, false)
+ world:set(e, H, false)
+ end
+ end)
+ end
+
+ local D1 = ecs:component()
+ local D2 = ecs:component()
+ local D3 = ecs:component()
+ local D4 = ecs:component()
+ local D5 = ecs:component()
+ local D6 = ecs:component()
+ local D7 = ecs:component()
+ local D8 = ecs:component()
+
+ local function flip()
+ return math.random() >= 0.15
+ end
+
+ local added = 0
+ local archetypes = {}
+ for i = 1, 2 ^ 16 - 2 do
+ local entity = ecs:entity()
+
+ local combination = ""
+
+ if flip() then
+ combination ..= "B"
+ ecs:set(entity, D2, {value = true})
+ end
+ if flip() then
+ combination ..= "C"
+ ecs:set(entity, D3, {value = true})
+ end
+ if flip() then
+ combination ..= "D"
+ ecs:set(entity, D4, {value = true})
+ end
+ if flip() then
+ combination ..= "E"
+ ecs:set(entity, D5, {value = true})
+ end
+ if flip() then
+ combination ..= "F"
+ ecs:set(entity, D6, {value = true})
+ end
+ if flip() then
+ combination ..= "G"
+ ecs:set(entity, D7, {value = true})
+ end
+ if flip() then
+ combination ..= "H"
+ ecs:set(entity, D8, {value = true})
+ end
+
+ if #combination == 7 then
+ added += 1
+ ecs:set(entity, D1, {value = true})
+ end
+ archetypes[combination] = true
+ end
+
+ local a = 0
+ for _ in archetypes do
+ a += 1
+ end
+
+ view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
+ end
+end
+
+do
+ TITLE(testkit.color.white_underline("Mirror query"))
+ local ecs = mirror.World.new()
+ do
+ TITLE("one component in common")
+
+ local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
+ BENCH("1 component", function()
+ for _ in world:query(A) do
+ end
+ end)
+
+ BENCH("2 component", function()
+ for _ in world:query(A, B) do
+ end
+ end)
+
+ BENCH("4 component", function()
+ for _ in world:query(A, B, C, D) do
+ end
+ end)
+
+ BENCH("8 component", function()
+ for _ in world:query(A, B, C, D, E, F, G, H) do
+ end
+ end)
+
+ local e = world:entity()
+ world:set(e, A, true)
+ world:set(e, B, true)
+ world:set(e, C, true)
+ world:set(e, D, true)
+ world:set(e, E, true)
+ world:set(e, F, true)
+ world:set(e, G, true)
+ world:set(e, H, true)
+
+ BENCH("Update Data", function()
+ for _ = 1, 100 do
+ world:set(e, A, false)
+ world:set(e, B, false)
+ world:set(e, C, false)
+ world:set(e, D, false)
+ world:set(e, E, false)
+ world:set(e, F, false)
+ world:set(e, G, false)
+ world:set(e, H, false)
+ end
+ end)
end
local D1 = ecs:component()
diff --git a/benches/visual/insertion.bench.lua b/benches/visual/insertion.bench.lua
index 3f7415a..e8e50be 100644
--- a/benches/visual/insertion.bench.lua
+++ b/benches/visual/insertion.bench.lua
@@ -8,6 +8,8 @@ local jecs = require(ReplicatedStorage.Lib)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local newWorld = Matter.World.new()
local ecs = jecs.World.new()
+local mirror = require(ReplicatedStorage.mirror)
+local mcs = mirror.World.new()
local A1 = Matter.component()
local A2 = Matter.component()
@@ -35,6 +37,15 @@ local C5 = ecs:entity()
local C6 = ecs:entity()
local C7 = ecs:entity()
local C8 = ecs:entity()
+local E1 = mcs:entity()
+local E2 = mcs:entity()
+local E3 = mcs:entity()
+local E4 = mcs:entity()
+local E5 = mcs:entity()
+local E6 = mcs:entity()
+local E7 = mcs:entity()
+local E8 = mcs:entity()
+
local registry2 = ecr.registry()
return {
@@ -44,7 +55,7 @@ return {
Functions = {
Matter = function()
- for i = 1, 50 do
+ for i = 1, 500 do
newWorld:spawn(
A1({ value = true }),
A2({ value = true }),
@@ -60,8 +71,8 @@ return {
ECR = function()
- for i = 1, 50 do
- local e = registry2.create()
+ local e = registry2.create()
+ for i = 1, 500 do
registry2:set(e, B1, {value = false})
registry2:set(e, B2, {value = false})
registry2:set(e, B3, {value = false})
@@ -78,7 +89,7 @@ return {
local e = ecs:entity()
- for i = 1, 50 do
+ for i = 1, 500 do
ecs:set(e, C1, {value = false})
ecs:set(e, C2, {value = false})
@@ -89,6 +100,23 @@ return {
ecs:set(e, C7, {value = false})
ecs:set(e, C8, {value = false})
+ end
+ end,
+ Mirror = function()
+
+ local e = ecs:entity()
+
+ for i = 1, 500 do
+
+ mcs:set(e, E1, {value = false})
+ mcs:set(e, E2, {value = false})
+ mcs:set(e, E3, {value = false})
+ mcs:set(e, E4, {value = false})
+ mcs:set(e, E5, {value = false})
+ mcs:set(e, E6, {value = false})
+ mcs:set(e, E7, {value = false})
+ mcs:set(e, E8, {value = false})
+
end
end
diff --git a/jecs_darkmode.svg b/jecs_darkmode.svg
new file mode 100644
index 0000000..f64b173
--- /dev/null
+++ b/jecs_darkmode.svg
@@ -0,0 +1,6 @@
+
diff --git a/jecs_lightmode.svg b/jecs_lightmode.svg
new file mode 100644
index 0000000..dbcd08c
--- /dev/null
+++ b/jecs_lightmode.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/init.lua b/lib/init.lua
index b3e31bc..f45e842 100644
--- a/lib/init.lua
+++ b/lib/init.lua
@@ -29,9 +29,10 @@ type Archetype = {
type Record = {
archetype: Archetype,
row: number,
+ dense: i24,
}
-type EntityIndex = {[i24]: Record}
+type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}}
type ComponentIndex = {[i24]: ArchetypeMap}
type ArchetypeRecord = number
@@ -43,11 +44,118 @@ type ArchetypeDiff = {
removed: Ty,
}
+local FLAGS_PAIR = 0x8
local HI_COMPONENT_ID = 256
local ON_ADD = HI_COMPONENT_ID + 1
local ON_REMOVE = HI_COMPONENT_ID + 2
local ON_SET = HI_COMPONENT_ID + 3
-local REST = HI_COMPONENT_ID + 4
+local WILDCARD = HI_COMPONENT_ID + 4
+local REST = HI_COMPONENT_ID + 5
+
+local ECS_ID_FLAGS_MASK = 0x10
+local ECS_ENTITY_MASK = bit32.lshift(1, 24)
+local ECS_GENERATION_MASK = bit32.lshift(1, 16)
+
+local function addFlags(isPair: boolean)
+ local typeFlags = 0x0
+
+ if isPair then
+ typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID.
+ end
+ if false then
+ typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true
+ end
+ if false then
+ typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true
+ end
+ if false then
+ typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID.
+ end
+
+ return typeFlags
+end
+
+local function newId(source: number, target: number)
+ local e = source * 2^28 + target * ECS_ID_FLAGS_MASK
+ return e
+end
+
+local function ECS_IS_PAIR(e: number)
+ return (e % 2^4) // FLAGS_PAIR ~= 0
+end
+
+function separate(entity: number)
+ local _typeFlags = entity % 0x10
+ entity //= ECS_ID_FLAGS_MASK
+ return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags
+end
+
+-- HIGH 24 bits LOW 24 bits
+local function ECS_GENERATION(e: i53)
+ e //= 0x10
+ return e % ECS_GENERATION_MASK
+end
+
+local function ECS_ID(e: i53)
+ e //= 0x10
+ return e // ECS_ENTITY_MASK
+end
+
+local function ECS_GENERATION_INC(e: i53)
+ local id, generation, flags = separate(e)
+
+ return newId(id, generation + 1) + flags
+end
+
+-- gets the high ID
+local function ECS_PAIR_FIRST(entity: i53): i24
+ entity //= 0x10
+ local first = entity % ECS_ENTITY_MASK
+ return first
+end
+
+-- gets the low ID
+local ECS_PAIR_SECOND = ECS_ID
+
+local function ECS_PAIR(first: number, second: number)
+ local target = WILDCARD
+ local relation
+
+ if first == WILDCARD then
+ relation = second
+ elseif second == WILDCARD then
+ relation = first
+ else
+ relation = second
+ target = ECS_PAIR_SECOND(first)
+ end
+
+ return newId(
+ ECS_PAIR_SECOND(relation), target) + addFlags(--[[isPair]] true)
+end
+
+local function getAlive(entityIndex: EntityIndex, id: i53)
+ return entityIndex.dense[id]
+end
+
+local function ecs_get_source(entityIndex, e)
+ assert(ECS_IS_PAIR(e))
+ return getAlive(entityIndex, ECS_PAIR_FIRST(e))
+end
+local function ecs_get_target(entityIndex, e)
+ assert(ECS_IS_PAIR(e))
+ return getAlive(entityIndex, ECS_PAIR_SECOND(e))
+end
+
+local function nextEntityId(entityIndex, index: i24)
+ local id = newId(index, 0)
+ entityIndex.sparse[id] = {
+ dense = index
+ } :: Record
+ entityIndex.dense[index] = id
+
+ return id
+end
local function transitionArchetype(
entityIndex: EntityIndex,
@@ -81,21 +189,27 @@ local function transitionArchetype(
column[last] = nil
end
- -- Move the entity from the source to the destination archetype.
- local atSourceRow = sourceEntities[sourceRow]
- destinationEntities[destinationRow] = atSourceRow
- entityIndex[atSourceRow].row = destinationRow
+ local sparse = entityIndex.sparse
+ local movedAway = #sourceEntities
+ -- Move the entity from the source to the destination archetype.
-- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped.
- local movedAway = #sourceEntities
- if sourceRow ~= movedAway then
- local atMovedAway = sourceEntities[movedAway]
- sourceEntities[sourceRow] = atMovedAway
- entityIndex[atMovedAway].row = sourceRow
+ local e1 = sourceEntities[sourceRow]
+ local e2 = sourceEntities[movedAway]
+
+ if sourceRow ~= movedAway then
+ sourceEntities[sourceRow] = e2
end
sourceEntities[movedAway] = nil
+ destinationEntities[destinationRow] = e1
+
+ local record1 = sparse[e1]
+ local record2 = sparse[e2]
+
+ record1.row = destinationRow
+ record2.row = sourceRow
end
local function archetypeAppend(entity: number, archetype: Archetype): number
@@ -125,22 +239,14 @@ local function hash(arr): string | number
return table.concat(arr, "_")
end
-local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
- local destinationIds = to.types
- local records = to.records
- local id = to.id
+local function createArchetypeRecord(componentIndex, id, componentId, i)
+ local archetypesMap = componentIndex[componentId]
- for i, destinationId in destinationIds do
- local archetypesMap = componentIndex[destinationId]
-
- if not archetypesMap then
- archetypesMap = {size = 0, sparse = {}}
- componentIndex[destinationId] = archetypesMap
- end
-
- archetypesMap.sparse[id] = i
- records[destinationId] = i
+ if not archetypesMap then
+ archetypesMap = {size = 0, sparse = {}}
+ componentIndex[componentId] = archetypesMap
end
+ archetypesMap.sparse[id] = i
end
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
@@ -150,10 +256,26 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet
world.nextArchetypeId = id
local length = #types
- local columns = table.create(length) :: {any}
+ local columns = table.create(length)
- for index in types do
- columns[index] = {}
+ local records = {}
+ local componentIndex = world.componentIndex
+ local entityIndex = world.entityIndex
+ for i, componentId in types do
+ createArchetypeRecord(componentIndex, id, componentId, i)
+ records[componentId] = i
+ columns[i] = {}
+
+ if ECS_IS_PAIR(componentId) then
+ local first = ecs_get_source(entityIndex, componentId)
+ local second = ecs_get_target(entityIndex, componentId)
+ local firstPair = ECS_PAIR(first, WILDCARD)
+ local secondPair = ECS_PAIR(WILDCARD, second)
+ createArchetypeRecord(componentIndex, id, firstPair, i)
+ createArchetypeRecord(componentIndex, id, secondPair, i)
+ records[firstPair] = i
+ records[secondPair] = i
+ end
end
local archetype = {
@@ -161,15 +283,12 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet
edges = {};
entities = {};
id = id;
- records = {};
+ records = records;
type = ty;
types = types;
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
- if length > 0 then
- createArchetypeRecords(world.componentIndex, archetype, prev)
- end
return archetype
end
@@ -179,9 +298,12 @@ World.__index = World
function World.new()
local self = setmetatable({
archetypeIndex = {};
- archetypes = {};
- componentIndex = {};
- entityIndex = {};
+ archetypes = {} :: Archetypes;
+ componentIndex = {} :: ComponentIndex;
+ entityIndex = {
+ dense = {},
+ sparse = {}
+ } :: EntityIndex;
hooks = {
[ON_ADD] = {};
};
@@ -190,32 +312,78 @@ function World.new()
nextEntityId = 0;
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World)
+ self.ROOT_ARCHETYPE = archetypeOf(self, {})
return self
end
-local function emit(world, eventDescription)
- local event = eventDescription.event
-
- table.insert(world.hooks[event], {
- archetype = eventDescription.archetype;
- ids = eventDescription.ids;
- offset = eventDescription.offset;
- otherArchetype = eventDescription.otherArchetype;
- })
+function World.component(world: World)
+ local componentId = world.nextComponentId + 1
+ if componentId > HI_COMPONENT_ID then
+ -- IDs are partitioned into ranges because component IDs are not nominal,
+ -- so it needs to error when IDs intersect into the entity range.
+ error("Too many components, consider using world:entity() instead to create components.")
+ end
+ world.nextComponentId = componentId
+ return nextEntityId(world.entityIndex, componentId)
end
-local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
- if #added > 0 then
- emit(world, {
- archetype = archetype;
- event = ON_ADD;
- ids = added;
- offset = row;
- otherArchetype = otherArchetype;
- })
+function World.entity(world: World)
+ local entityId = world.nextEntityId + 1
+ world.nextEntityId = entityId
+ return nextEntityId(world.entityIndex, entityId + REST)
+end
+
+-- should reuse this logic in World.set instead of swap removing in transition archetype
+local function destructColumns(columns, count, row)
+ if row == count then
+ for _, column in columns do
+ column[count] = nil
+ end
+ else
+ for _, column in columns do
+ column[row] = column[count]
+ column[count] = nil
+ end
end
end
+local function archetypeDelete(entityIndex, record: Record, entityId: i53, destruct: boolean)
+ local sparse, dense = entityIndex.sparse, entityIndex.dense
+ local archetype = record.archetype
+ local row = record.row
+ local entities = archetype.entities
+ local last = #entities
+
+ local entityToMove = entities[last]
+
+ if row ~= last then
+ dense[record.dense] = entityToMove
+ sparse[entityToMove] = record
+ end
+
+ sparse[entityId] = nil
+ dense[#dense] = nil
+
+ entities[row], entities[last] = entities[last], nil
+
+ local columns = archetype.columns
+
+ if not destruct then
+ return
+ end
+
+ destructColumns(columns, last, row)
+end
+
+function World.delete(world: World, entityId: i53)
+ local entityIndex = world.entityIndex
+ local record = entityIndex.sparse[entityId]
+ if not record then
+ return
+ end
+ archetypeDelete(entityIndex, record, entityId, true)
+end
+
export type World = typeof(World.new())
local function ensureArchetype(world: World, types, prev)
@@ -249,15 +417,16 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
-- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
+
+ local destinationType = table.clone(node.types)
local at = findInsert(types, componentId)
if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype.
return node
end
-
- local destinationType = table.clone(node.types)
table.insert(destinationType, at, componentId)
+
return ensureArchetype(world, destinationType, node)
end
@@ -272,15 +441,7 @@ local function ensureEdge(archetype: Archetype, componentId: i53)
end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
- if not from then
- -- If there was no source archetype then it should return the ROOT_ARCHETYPE
- local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
- if not ROOT_ARCHETYPE then
- ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
- world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
- end
- from = ROOT_ARCHETYPE
- end
+ from = from or world.ROOT_ARCHETYPE
local edge = ensureEdge(from, componentId)
local add = edge.add
@@ -294,19 +455,23 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
return add
end
-local function ensureRecord(entityIndex, entityId: i53): Record
- local record = entityIndex[entityId]
-
- if not record then
- record = {}
- entityIndex[entityId] = record
+function World.add(world: World, entityId: i53, componentId: i53)
+ local entityIndex = world.entityIndex
+ local record = entityIndex.sparse[entityId]
+ local from = record.archetype
+ local to = archetypeTraverseAdd(world, componentId, from)
+ if from and not (from == world.ROOT_ARCHETYPE) then
+ moveEntity(entityIndex, entityId, record, to)
+ else
+ if #to.types > 0 then
+ newEntity(entityId, record, to)
+ end
end
-
- return record :: Record
end
+-- Symmetric like `World.add` but idempotent
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
- local record = ensureRecord(world.entityIndex, entityId)
+ local record = world.entityIndex.sparse[entityId]
local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from)
@@ -326,7 +491,6 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
if #to.types > 0 then
-- When there is no previous archetype it should create the archetype
newEntity(entityId, record, to)
- onNotifyAdd(world, to, from, record.row, {componentId})
end
end
@@ -334,14 +498,17 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
to.columns[archetypeRecord][record.row] = data
end
-local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
- local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
+local function archetypeTraverseRemove(world: World, componentId: i53, from: Archetype): Archetype
local edge = ensureEdge(from, componentId)
local remove = edge.remove
if not remove then
local to = table.clone(from.types)
- table.remove(to, table.find(to, componentId))
+ local at = table.find(to, componentId)
+ if not at then
+ return from
+ end
+ table.remove(to, at)
remove = ensureArchetype(world, to, from)
edge.remove = remove :: never
end
@@ -351,7 +518,7 @@ end
function World.remove(world: World, entityId: i53, componentId: i53)
local entityIndex = world.entityIndex
- local record = ensureRecord(entityIndex, entityId)
+ local record = entityIndex.sparse[entityId]
local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
@@ -374,7 +541,7 @@ end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId
- local record = world.entityIndex[id]
+ local record = world.entityIndex.sparse[id]
if not record then
return nil
end
@@ -456,7 +623,10 @@ function World.query(world: World, ...: i53): Query
end
length += 1
- compatibleArchetypes[length] = {archetype, indices}
+ compatibleArchetypes[length] = {
+ archetype = archetype,
+ indices = indices
+ }
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
@@ -470,7 +640,7 @@ function World.query(world: World, ...: i53): Query
function preparedQuery:without(...)
local withoutComponents = {...}
for i = #compatibleArchetypes, 1, -1 do
- local archetype = compatibleArchetypes[i][1]
+ local archetype = compatibleArchetypes[i].archetype
local records = archetype.records
local shouldRemove = false
@@ -499,21 +669,21 @@ function World.query(world: World, ...: i53): Query
function preparedQuery:__iter()
return function()
- local archetype = compatibleArchetype[1]
- local row = next(archetype.entities, lastRow)
+ local archetype = compatibleArchetype.archetype
+ local row: number = next(archetype.entities, lastRow) :: number
while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
if lastArchetype == nil then
return
end
- archetype = compatibleArchetype[1]
- row = next(archetype.entities, row)
+ archetype = compatibleArchetype.archetype
+ row = next(archetype.entities, row) :: number
end
lastRow = row
local entityId = archetype.entities[row :: number]
local columns = archetype.columns
- local tr = compatibleArchetype[2]
+ local tr = compatibleArchetype.indices
if queryLength == 1 then
return entityId, columns[tr[1]][row]
@@ -560,7 +730,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)
@@ -570,90 +740,57 @@ function World.query(world: World, ...: i53): Query
return setmetatable({}, preparedQuery) :: any
end
-function World.component(world: World)
- local componentId = world.nextComponentId + 1
- if componentId > HI_COMPONENT_ID then
- -- IDs are partitioned into ranges because component IDs are not nominal,
- -- so it needs to error when IDs intersect into the entity range.
- error("Too many components, consider using world:entity() instead to create components.")
+function World.__iter(world: World): () -> (number?, unknown?)
+ local dense = world.entityIndex.dense
+ local sparse = world.entityIndex.sparse
+ local last
+
+ return function()
+ local lastEntity, entityId = next(dense, last)
+ if not lastEntity then
+ return
+ end
+ last = lastEntity
+
+ local record = sparse[entityId]
+ local archetype = record.archetype
+ if not archetype then
+ -- Returns only the entity id as an entity without data should not return
+ -- data and allow the user to get an error if they don't handle the case.
+ return entityId
+ 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 entityId, entityData
end
- world.nextComponentId = componentId
- return componentId
-end
-
-function World.entity(world: World)
- 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 = hooks[event]
- hooks[event] = nil
-
- local last, change
- return function()
- last, change = next(hook, last)
- if not last then
- return
- end
-
- local matched = false
- local ids = change.ids
-
- while not matched do
- local skip = false
- for _, id in ids do
- if not table.find(componentIds, id) then
- skip = true
- break
- end
- end
-
- if skip then
- last, change = next(hook, last)
- ids = change.ids
- continue
- end
-
- matched = true
- end
-
- local queryOutput = table.create(idsCount)
- local row = change.offset
- local archetype = change.archetype
- local columns = archetype.columns
- local archetypeRecords = archetype.records
- for index, id in componentIds do
- queryOutput[index] = columns[archetypeRecords[id]][row]
- end
-
- return archetype.entities[row], unpack(queryOutput, 1, idsCount)
- end
- end;
- }
end
return table.freeze({
World = World;
- ON_ADD = ON_ADD;
- ON_REMOVE = ON_REMOVE;
- ON_SET = ON_SET;
+
+ OnAdd = ON_ADD;
+ OnRemove = ON_REMOVE;
+ OnSet = ON_SET;
+ Wildcard = WILDCARD,
+ w = WILDCARD,
+ Rest = REST,
+
+ ECS_ID = ECS_ID,
+ IS_PAIR = ECS_IS_PAIR,
+ ECS_PAIR = ECS_PAIR,
+ ECS_GENERATION_INC = ECS_GENERATION_INC,
+ ECS_GENERATION = ECS_GENERATION,
+ ecs_get_target = ecs_get_target,
+ ecs_get_source = ecs_get_source,
+
+ pair = ECS_PAIR,
+ getAlive = getAlive,
})
diff --git a/lib/init.spec.lua b/lib/init.spec.lua
index fdc8331..8de8de9 100644
--- a/lib/init.spec.lua
+++ b/lib/init.spec.lua
@@ -176,22 +176,6 @@ return function()
expect(added).to.equal(0)
end)
- it("track changes", function()
- local Position = world:entity()
-
- local moving = world:entity()
- world:set(moving, Position, Vector3.new(1, 2, 3))
-
- local count = 0
-
- for e, position in world:observer(Position).event(jecs.ON_ADD) do
- count += 1
- expect(e).to.equal(moving)
- expect(position).to.equal(Vector3.new(1, 2, 3))
- end
- expect(count).to.equal(1)
- end)
-
it("should query all matching entities", function()
local world = jecs.World.new()
@@ -299,5 +283,100 @@ 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()
+ end
+ end
+
+ expect(count).to.equal(5)
+ end)
+
+ it("should allow querying for relations", function()
+ local world = jecs.World.new()
+ local Eats = world:entity()
+ local Apples = world:entity()
+ local bob = world:entity()
+
+ world:set(bob, jecs.pair(Eats, Apples), true)
+ for e, bool in world:query(jecs.pair(Eats, Apples)) do
+ expect(e).to.equal(bob)
+ expect(bool).to.equal(bool)
+ end
+ end)
+
+ it("should allow wildcards in queries", function()
+ local world = jecs.World.new()
+ local Eats = world:entity()
+ local Apples = world:entity()
+ local bob = world:entity()
+
+ world:set(bob, jecs.pair(Eats, Apples), "bob eats apples")
+ for e, data in world:query(jecs.pair(Eats, jecs.w)) do
+ expect(e).to.equal(bob)
+ expect(data).to.equal("bob eats apples")
+ end
+ for e, data in world:query(jecs.pair(jecs.w, Apples)) do
+ expect(e).to.equal(bob)
+ expect(data).to.equal("bob eats apples")
+ end
+ end)
+
+ it("should match against multiple pairs", function()
+ local world = jecs.World.new()
+ local pair = jecs.pair
+ local Eats = world:entity()
+ local Apples = world:entity()
+ local Oranges =world:entity()
+ local bob = world:entity()
+ local alice = world:entity()
+
+ world:set(bob, pair(Eats, Apples), "bob eats apples")
+ world:set(alice, pair(Eats, Oranges), "alice eats oranges")
+
+ local w = jecs.Wildcard
+
+ local count = 0
+ for e, data in world:query(pair(Eats, w)) do
+ count += 1
+ if e == bob then
+ expect(data).to.equal("bob eats apples")
+ else
+ expect(data).to.equal("alice eats oranges")
+ end
+ end
+
+ expect(count).to.equal(2)
+ count = 0
+
+ for e, data in world:query(pair(w, Apples)) do
+ count += 1
+ expect(data).to.equal("bob eats apples")
+ end
+ expect(count).to.equal(1)
+ end)
end)
end
\ No newline at end of file
diff --git a/logo.png b/logo_old.png
similarity index 100%
rename from logo.png
rename to logo_old.png
diff --git a/mirror/init.lua b/mirror/init.lua
index 35a9b9c..6d9c1fe 100644
--- a/mirror/init.lua
+++ b/mirror/init.lua
@@ -6,10 +6,10 @@
type i53 = number
type i24 = number
-type Ty = { i53 }
+type Ty = {i53}
type ArchetypeId = number
-type Column = { any }
+type Column = {any}
type Archetype = {
id: number,
@@ -20,9 +20,9 @@ type Archetype = {
},
},
types: Ty,
- type: string | number,
- entities: { number },
- columns: { Column },
+ type: string | number,
+ entities: {number},
+ columns: {Column},
records: {},
}
@@ -31,13 +31,13 @@ type Record = {
row: number,
}
-type EntityIndex = { [i24]: Record }
-type ComponentIndex = { [i24]: ArchetypeMap}
+type EntityIndex = {[i24]: Record}
+type ComponentIndex = {[i24]: ArchetypeMap}
type ArchetypeRecord = number
-type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
-type Archetypes = { [ArchetypeId]: Archetype }
-
+type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
+type Archetypes = {[ArchetypeId]: Archetype}
+
type ArchetypeDiff = {
added: Ty,
removed: Ty,
@@ -64,17 +64,17 @@ local function transitionArchetype(
local types = from.types
for i, column in columns do
- -- Retrieves the new column index from the source archetype's record from each component
+ -- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local targetColumn = destinationColumns[tr[types[i]]]
-- Sometimes target column may not exist, e.g. when you remove a component.
- if targetColumn then
+ if targetColumn then
targetColumn[destinationRow] = column[sourceRow]
end
-- If the entity is the last row in the archetype then swapping it would be meaningless.
local last = #column
- if sourceRow ~= last then
+ if sourceRow ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype.
column[sourceRow] = column[last]
end
@@ -82,24 +82,27 @@ local function transitionArchetype(
end
-- Move the entity from the source to the destination archetype.
- destinationEntities[destinationRow] = sourceEntities[sourceRow]
- entityIndex[sourceEntities[sourceRow]].row = destinationRow
+ local atSourceRow = sourceEntities[sourceRow]
+ destinationEntities[destinationRow] = atSourceRow
+ entityIndex[atSourceRow].row = destinationRow
-- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped.
local movedAway = #sourceEntities
- if sourceRow ~= movedAway then
- sourceEntities[sourceRow] = sourceEntities[movedAway]
- entityIndex[sourceEntities[movedAway]].row = sourceRow
+ if sourceRow ~= movedAway then
+ local atMovedAway = sourceEntities[movedAway]
+ sourceEntities[sourceRow] = atMovedAway
+ entityIndex[atMovedAway].row = sourceRow
end
-
+
sourceEntities[movedAway] = nil
end
-local function archetypeAppend(entity: i53, archetype: Archetype): i24
+local function archetypeAppend(entity: number, archetype: Archetype): number
local entities = archetype.entities
- table.insert(entities, entity)
- return #entities
+ local length = #entities + 1
+ entities[length] = entity
+ return length
end
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
@@ -122,47 +125,49 @@ local function hash(arr): string | number
return table.concat(arr, "_")
end
-local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
- local destinationCount = #to.types
+local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
local destinationIds = to.types
+ local records = to.records
+ local id = to.id
- for i = 1, destinationCount do
- local destinationId = destinationIds[i]
+ for i, destinationId in destinationIds do
+ local archetypesMap = componentIndex[destinationId]
- if not componentIndex[destinationId] then
- componentIndex[destinationId] = { size = 0, sparse = {} }
+ if not archetypesMap then
+ archetypesMap = {size = 0, sparse = {}}
+ componentIndex[destinationId] = archetypesMap
end
- local archetypesMap = componentIndex[destinationId]
- archetypesMap.sparse[to.id] = i
- to.records[destinationId] = i
+ archetypesMap.sparse[id] = i
+ records[destinationId] = i
end
end
-local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
+local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
local ty = hash(types)
- world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
- local id = world.nextArchetypeId
+ local id = world.nextArchetypeId + 1
+ world.nextArchetypeId = id
- local columns = {} :: { any }
+ local length = #types
+ local columns = table.create(length) :: {any}
- for _ in types do
- table.insert(columns, {})
+ for index in types do
+ columns[index] = {}
end
local archetype = {
- id = id,
- types = types,
- type = ty,
- columns = columns,
- entities = {},
- edges = {},
- records = {},
+ columns = columns;
+ edges = {};
+ entities = {};
+ id = id;
+ records = {};
+ type = ty;
+ types = types;
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
- if #types > 0 then
+ if length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev)
end
@@ -171,42 +176,42 @@ end
local World = {}
World.__index = World
-function World.new()
+function World.new()
local self = setmetatable({
- entityIndex = {},
- componentIndex = {},
- archetypes = {},
- archetypeIndex = {},
- ROOT_ARCHETYPE = (nil :: any) :: Archetype,
- nextEntityId = 0,
- nextComponentId = 0,
- nextArchetypeId = 0,
+ archetypeIndex = {};
+ archetypes = {};
+ componentIndex = {};
+ entityIndex = {};
hooks = {
- [ON_ADD] = {}
- }
+ [ON_ADD] = {};
+ };
+ nextArchetypeId = 0;
+ nextComponentId = 0;
+ nextEntityId = 0;
+ ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World)
- return self
+ return self
end
-local function emit(world, eventDescription)
+local function emit(world, eventDescription)
local event = eventDescription.event
table.insert(world.hooks[event], {
- ids = eventDescription.ids,
- archetype = eventDescription.archetype,
- otherArchetype = eventDescription.otherArchetype,
- offset = eventDescription.offset
+ archetype = eventDescription.archetype;
+ ids = eventDescription.ids;
+ offset = eventDescription.offset;
+ otherArchetype = eventDescription.otherArchetype;
})
end
-local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
- if #added > 0 then
+local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
+ if #added > 0 then
emit(world, {
- event = ON_ADD,
- ids = added,
- archetype = archetype,
- otherArchetype = otherArchetype,
- offset = row,
+ archetype = archetype;
+ event = ON_ADD;
+ ids = added;
+ offset = row;
+ otherArchetype = otherArchetype;
})
end
end
@@ -217,7 +222,7 @@ local function ensureArchetype(world: World, types, prev)
if #types < 1 then
return world.ROOT_ARCHETYPE
end
-
+
local ty = hash(types)
local archetype = world.archetypeIndex[ty]
if archetype then
@@ -227,10 +232,8 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev)
end
-local function findInsert(types: { i53 }, toAdd: i53)
- local count = #types
- for i = 1, count do
- local id = types[i]
+local function findInsert(types: {i53}, toAdd: i53)
+ for i, id in types do
if id == toAdd then
return -1
end
@@ -238,13 +241,13 @@ local function findInsert(types: { i53 }, toAdd: i53)
return i
end
end
- return count + 1
+ return #types + 1
end
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
local types = node.types
-- Component IDs are added incrementally, so inserting and sorting
- -- them each time would be expensive. Instead this insertion sort can find the insertion
+ -- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
local at = findInsert(types, componentId)
if at == -1 then
@@ -259,48 +262,57 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
end
local function ensureEdge(archetype: Archetype, componentId: i53)
- if not archetype.edges[componentId] then
- archetype.edges[componentId] = {} :: any
+ local edges = archetype.edges
+ local edge = edges[componentId]
+ if not edge then
+ edge = {} :: any
+ edges[componentId] = edge
end
- return archetype.edges[componentId]
+ return edge
end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
- if not from then
+ if not from then
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
- if not world.ROOT_ARCHETYPE then
- local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
- world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
- end
- from = world.ROOT_ARCHETYPE
+ local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
+ if not ROOT_ARCHETYPE then
+ ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
+ world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
+ end
+ from = ROOT_ARCHETYPE
end
+
local edge = ensureEdge(from, componentId)
-
- if not edge.add then
- -- Save an edge using the component ID to the archetype to allow
+ local add = edge.add
+ if not add then
+ -- Save an edge using the component ID to the archetype to allow
-- faster traversals to adjacent archetypes.
- edge.add = findArchetypeWith(world, from, componentId)
+ add = findArchetypeWith(world, from, componentId)
+ edge.add = add :: never
end
- return edge.add
+ return add
end
local function ensureRecord(entityIndex, entityId: i53): Record
- local id = entityId
- if not entityIndex[id] then
- entityIndex[id] = {}
+ local record = entityIndex[entityId]
+
+ if not record then
+ record = {}
+ entityIndex[entityId] = record
end
- return entityIndex[id] :: Record
+
+ return record :: Record
end
-function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
+function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = ensureRecord(world.entityIndex, entityId)
local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from)
- if from == to then
- -- If the archetypes are the same it can avoid moving the entity
- -- and just set the data directly.
+ if from == to then
+ -- If the archetypes are the same it can avoid moving the entity
+ -- and just set the data directly.
local archetypeRecord = to.records[componentId]
from.columns[archetypeRecord][record.row] = data
-- Should fire an OnSet event here.
@@ -308,13 +320,13 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
end
if from then
- -- If there was a previous archetype, then the entity needs to move the archetype
+ -- If there was a previous archetype, then the entity needs to move the archetype
moveEntity(world.entityIndex, entityId, record, to)
else
if #to.types > 0 then
-- When there is no previous archetype it should create the archetype
newEntity(entityId, record, to)
- onNotifyAdd(world, to, from, record.row, { componentId })
+ onNotifyAdd(world, to, from, record.row, {componentId})
end
end
@@ -326,28 +338,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
local edge = ensureEdge(from, componentId)
-
- if not edge.remove then
- local to = table.clone(from.types)
+ local remove = edge.remove
+ if not remove then
+ local to = table.clone(from.types)
table.remove(to, table.find(to, componentId))
- edge.remove = ensureArchetype(world, to, from)
+ remove = ensureArchetype(world, to, from)
+ edge.remove = remove :: never
end
- return edge.remove
+ return remove
end
-function World.remove(world: World, entityId: i53, componentId: i53)
- local record = ensureRecord(world.entityIndex, entityId)
+function World.remove(world: World, entityId: i53, componentId: i53)
+ local entityIndex = world.entityIndex
+ local record = ensureRecord(entityIndex, entityId)
local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
- if sourceArchetype and not (sourceArchetype == destinationArchetype) then
- moveEntity(world.entityIndex, entityId, record, destinationArchetype)
+ if sourceArchetype and not (sourceArchetype == destinationArchetype) then
+ moveEntity(entityIndex, entityId, record, destinationArchetype)
end
end
-- Keeping the function as small as possible to enable inlining
-local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
+local function get(record: Record, componentId: i24)
local archetype = record.archetype
local archetypeRecord = archetype.records[componentId]
@@ -360,35 +374,35 @@ end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId
- local componentIndex = world.componentIndex
local record = world.entityIndex[id]
if not record then
return nil
end
- local va = get(componentIndex, record, a)
+ local va = get(record, a)
if b == nil then
return va
elseif c == nil then
- return va, get(componentIndex, record, b)
+ return va, get(record, b)
elseif d == nil then
- return va, get(componentIndex, record, b), get(componentIndex, record, c)
+ return va, get(record, b), get(record, c)
elseif e == nil then
- return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
+ return va, get(record, b), get(record, c), get(record, d)
else
error("args exceeded")
end
end
-local function noop(self: Query, ...: i53): () -> (number, ...any)
- return function()
- end :: any
+-- the less creation the better
+local function actualNoOperation() end
+local function noop(_self: Query, ...: i53): () -> (number, ...any)
+ return actualNoOperation :: any
end
local EmptyQuery = {
- __iter = noop,
- without = noop
+ __iter = noop;
+ without = noop;
}
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
@@ -396,25 +410,28 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
- local compatibleArchetypes = {}
- local components = { ... }
- local archetypes = world.archetypes
- local queryLength = #components
-
- if queryLength == 0 then
+ -- breaking?
+ if (...) == nil then
error("Missing components")
end
+ local compatibleArchetypes = {}
+ local length = 0
+
+ local components = {...}
+ local archetypes = world.archetypes
+ local queryLength = #components
+
local firstArchetypeMap
local componentIndex = world.componentIndex
- for i, componentId in components do
+ for _, componentId in components do
local map = componentIndex[componentId]
if not map then
return EmptyQuery
end
- if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
+ if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map
end
end
@@ -422,110 +439,107 @@ function World.query(world: World, ...: i53): Query
for id in firstArchetypeMap.sparse do
local archetype = archetypes[id]
local archetypeRecords = archetype.records
- local indices = {}
+ local indices = {}
local skip = false
-
- for i, componentId in components do
+
+ for i, componentId in components do
local index = archetypeRecords[componentId]
- if not index then
+ if not index then
skip = true
break
end
- indices[i] = archetypeRecords[componentId]
+ indices[i] = index
end
- if skip then
+ if skip then
continue
end
- table.insert(compatibleArchetypes, { archetype, indices })
+
+ length += 1
+ compatibleArchetypes[length] = {archetype, indices}
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
- if not lastArchetype then
+ if not lastArchetype then
return EmptyQuery
end
-
+
local preparedQuery = {}
preparedQuery.__index = preparedQuery
- function preparedQuery:without(...)
- local components = { ... }
- for i = #compatibleArchetypes, 1, -1 do
+ function preparedQuery:without(...)
+ local withoutComponents = {...}
+ for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i][1]
+ local records = archetype.records
local shouldRemove = false
- for _, componentId in components do
- if archetype.records[componentId] then
+
+ for _, componentId in withoutComponents do
+ if records[componentId] then
shouldRemove = true
break
end
end
- if shouldRemove then
+
+ if shouldRemove then
table.remove(compatibleArchetypes, i)
end
- end
+ end
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
- if not lastArchetype then
+ if not lastArchetype then
return EmptyQuery
end
-
+
return self
end
local lastRow
local queryOutput = {}
-
- function preparedQuery:__iter()
- return function()
+ function preparedQuery:__iter()
+ return function()
local archetype = compatibleArchetype[1]
local row = next(archetype.entities, lastRow)
- while row == nil do
+ while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
- if lastArchetype == nil then
- return
+ if lastArchetype == nil then
+ return
end
archetype = compatibleArchetype[1]
row = next(archetype.entities, row)
end
lastRow = row
-
+
local entityId = archetype.entities[row :: number]
local columns = archetype.columns
local tr = compatibleArchetype[2]
-
- if queryLength == 1 then
+
+ if queryLength == 1 then
return entityId, columns[tr[1]][row]
- elseif queryLength == 2 then
+ elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row]
- elseif queryLength == 3 then
- return entityId,
- columns[tr[1]][row],
- columns[tr[2]][row],
- columns[tr[3]][row]
- elseif queryLength == 4 then
- return entityId,
- columns[tr[1]][row],
- columns[tr[2]][row],
- columns[tr[3]][row],
- columns[tr[4]][row]
- elseif queryLength == 5 then
- return entityId,
+ elseif queryLength == 3 then
+ return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
+ elseif queryLength == 4 then
+ return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
+ elseif queryLength == 5 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row]
- elseif queryLength == 6 then
- return entityId,
+ elseif queryLength == 6 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row]
- elseif queryLength == 7 then
- return entityId,
+ elseif queryLength == 7 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
@@ -533,8 +547,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
- elseif queryLength == 8 then
- return entityId,
+ elseif queryLength == 8 then
+ return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
@@ -545,8 +559,8 @@ function World.query(world: World, ...: i53): Query
columns[tr[8]][row]
end
- for i in components do
- queryOutput[i] = tr[i][row]
+ for i in components do
+ queryOutput[i] = columns[tr[i]][row]
end
return entityId, unpack(queryOutput, 1, queryLength)
@@ -556,23 +570,24 @@ function World.query(world: World, ...: i53): Query
return setmetatable({}, preparedQuery) :: any
end
-function World.component(world: World)
- local componentId = world.nextComponentId + 1
- if componentId > HI_COMPONENT_ID then
- -- IDs are partitioned into ranges because component IDs are not nominal,
+function World.component(world: World)
+ local componentId = world.nextComponentId + 1
+ if componentId > HI_COMPONENT_ID then
+ -- IDs are partitioned into ranges because component IDs are not nominal,
-- so it needs to error when IDs intersect into the entity range.
- error("Too many components, consider using world:entity() instead to create components.")
+ error("Too many components, consider using world:entity() instead to create components.")
end
world.nextComponentId = componentId
return componentId
end
function World.entity(world: World)
- world.nextEntityId += 1
- return world.nextEntityId + REST
+ local nextEntityId = world.nextEntityId + 1
+ world.nextEntityId = nextEntityId
+ return nextEntityId + REST
end
-function World.delete(world: World, entityId: i53)
+function World.delete(world: World, entityId: i53)
local entityIndex = world.entityIndex
local record = entityIndex[entityId]
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
@@ -584,57 +599,61 @@ function World.delete(world: World, entityId: i53)
end
function World.observer(world: World, ...)
- local componentIds = { ... }
-
+ local componentIds = {...}
+ local idsCount = #componentIds
+ local hooks = world.hooks
+
return {
- event = function(event)
- local hook = world.hooks[event]
- world.hooks[event] = nil
+ event = function(event)
+ local hook = hooks[event]
+ hooks[event] = nil
local last, change
- return function()
+ return function()
last, change = next(hook, last)
- if not last then
+ if not last then
return
end
local matched = false
-
- while not matched do
+ local ids = change.ids
+
+ while not matched do
local skip = false
- for _, id in change.ids do
- if not table.find(componentIds, id) then
+ for _, id in ids do
+ if not table.find(componentIds, id) then
skip = true
break
end
end
-
- if skip then
+
+ if skip then
last, change = next(hook, last)
+ ids = change.ids
continue
end
matched = true
end
-
- local queryOutput = {}
+
+ local queryOutput = table.create(idsCount)
local row = change.offset
local archetype = change.archetype
local columns = archetype.columns
local archetypeRecords = archetype.records
- for _, id in componentIds do
- table.insert(queryOutput, columns[archetypeRecords[id]][row])
+ for index, id in componentIds do
+ queryOutput[index] = columns[archetypeRecords[id]][row]
end
- return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
+ return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end
- end
+ end;
}
end
return table.freeze({
- World = World,
- ON_ADD = ON_ADD,
- ON_REMOVE = ON_REMOVE,
- ON_SET = ON_SET
+ World = World;
+ ON_ADD = ON_ADD;
+ ON_REMOVE = ON_REMOVE;
+ ON_SET = ON_SET;
})
diff --git a/test.project.json b/test.project.json
index b931a84..bdcbd0b 100644
--- a/test.project.json
+++ b/test.project.json
@@ -11,9 +11,6 @@
},
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
- "DevPackages": {
- "$path": "DevPackages"
- },
"Lib": {
"$path": "lib"
},
@@ -25,6 +22,9 @@
},
"mirror": {
"$path": "mirror"
+ },
+ "DevPackages": {
+ "$path": "DevPackages"
}
},
"TestService": {
diff --git a/tests/test1.lua b/tests/test1.lua
deleted file mode 100644
index 0b031d3..0000000
--- a/tests/test1.lua
+++ /dev/null
@@ -1,115 +0,0 @@
-local testkit = require("../testkit")
-local jecs = require("../lib/init")
-
-local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
-
-local N = 10
-
-TEST("world:query", function()
- do CASE "should query all matching entities"
-
- local world = jecs.World.new()
- local A = world:component()
- local B = world:component()
-
- local entities = {}
- for i = 1, N do
- local id = world:entity()
-
-
- world:set(id, A, true)
- if i > 5 then world:set(id, B, true) end
- entities[i] = id
- end
-
- for id in world:query(A) do
- table.remove(entities, CHECK(table.find(entities, id)))
- end
-
- CHECK(#entities == 0)
-
- end
-
- do CASE "should query all matching entities when irrelevant component is removed"
-
- local world = jecs.World.new()
- local A = world:component()
- local B = world:component()
-
- local entities = {}
- for i = 1, N do
- local id = world:entity()
-
- world:set(id, A, true)
- world:set(id, B, true)
- if i > 5 then world:remove(id, B, true) end
- entities[i] = id
- end
-
- local added = 0
- for id in world:query(A) do
- added += 1
- table.remove(entities, CHECK(table.find(entities, id)))
- end
-
- CHECK(added == N)
- end
-
- do CASE "should query all entities without B"
-
- local world = jecs.World.new()
- local A = world:component()
- local B = world:component()
-
- local entities = {}
- for i = 1, N do
- local id = world:entity()
-
- world:set(id, A, true)
- if i < 5 then
- entities[i] = id
- else
- world:set(id, B, true)
- end
-
- end
-
- for id in world:query(A):without(B) do
- table.remove(entities, CHECK(table.find(entities, id)))
- end
-
- CHECK(#entities == 0)
-
- end
-
- do CASE "should allow setting components in arbitrary order"
- local world = jecs.World.new()
-
- local Health = world:entity()
- local Poison = world:component()
-
- local id = world:entity()
- world:set(id, Poison, 5)
- world:set(id, Health, 50)
-
- CHECK(world:get(id, Poison) == 5)
- end
-
- do CASE "Should allow deleting components"
- local world = jecs.World.new()
-
- local Health = world:entity()
- local Poison = world:component()
-
- local id = world:entity()
- world:set(id, Poison, 5)
- world:set(id, Health, 50)
- world:delete(id)
-
- CHECK(world:get(id, Poison) == nil)
- CHECK(world:get(id, Health) == nil)
- end
-
-end)
-
-FINISH()
\ No newline at end of file
diff --git a/tests/world.lua b/tests/world.lua
new file mode 100644
index 0000000..1aff493
--- /dev/null
+++ b/tests/world.lua
@@ -0,0 +1,261 @@
+local testkit = require("../testkit")
+local jecs = require("../lib/init")
+local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
+local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
+local IS_PAIR = jecs.IS_PAIR
+local ECS_PAIR = jecs.ECS_PAIR
+local getAlive = jecs.getAlive
+local ecs_get_source = jecs.ecs_get_source
+local ecs_get_target = jecs.ecs_get_target
+
+local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
+
+local N = 10
+
+TEST("world", function()
+ do CASE "should be iterable"
+ local world = jecs.World.new()
+ local A = world:component()
+ local B = world:component()
+ local eA = world:entity()
+ world:set(eA, A, true)
+ local eB = world:entity()
+ world:set(eB, B, true)
+ local eAB = world:entity()
+ world:set(eAB, A, true)
+ world:set(eAB, B, true)
+
+ local count = 0
+ for id, data in world do
+ count += 1
+ if id == eA then
+ CHECK(data[A] == true)
+ CHECK(data[B] == nil)
+ elseif id == eB then
+ CHECK(data[A] == nil)
+ CHECK(data[B] == true)
+ elseif id == eAB then
+ CHECK(data[A] == true)
+ CHECK(data[B] == true)
+ end
+ end
+
+ -- components are registered in the entity index as well
+ -- so this test has to add 2 to account for them
+ CHECK(count == 3 + 2)
+ end
+
+ do CASE "should query all matching entities"
+ local world = jecs.World.new()
+ local A = world:component()
+ local B = world:component()
+
+ local entities = {}
+ for i = 1, N do
+ local id = world:entity()
+
+ world:set(id, A, true)
+ if i > 5 then world:set(id, B, true) end
+ entities[i] = id
+ end
+
+ for id in world:query(A) do
+ table.remove(entities, CHECK(table.find(entities, id)))
+ end
+
+ CHECK(#entities == 0)
+
+ end
+
+ do CASE "should query all matching entities when irrelevant component is removed"
+ local world = jecs.World.new()
+ local A = world:component()
+ local B = world:component()
+ local C = world:component()
+
+ local entities = {}
+ for i = 1, N do
+ local id = world:entity()
+
+ -- specifically put them in disorder to track regression
+ -- https://github.com/Ukendio/jecs/pull/15
+ world:set(id, B, true)
+ world:set(id, A, true)
+ if i > 5 then world:remove(id, B) end
+ entities[i] = id
+ end
+
+ local added = 0
+ for id in world:query(A) do
+ added += 1
+ table.remove(entities, CHECK(table.find(entities, id)))
+ end
+
+ CHECK(added == N)
+ end
+
+ do CASE "should query all entities without B"
+ local world = jecs.World.new()
+ local A = world:component()
+ local B = world:component()
+
+ local entities = {}
+ for i = 1, N do
+ local id = world:entity()
+
+ world:set(id, A, true)
+ if i < 5 then
+ entities[i] = id
+ else
+ world:set(id, B, true)
+ end
+
+ end
+
+ for id in world:query(A):without(B) do
+ table.remove(entities, CHECK(table.find(entities, id)))
+ end
+
+ CHECK(#entities == 0)
+
+ end
+
+ do CASE "should allow setting components in arbitrary order"
+ local world = jecs.World.new()
+
+ local Health = world:entity()
+ local Poison = world:component()
+
+ local id = world:entity()
+ world:set(id, Poison, 5)
+ world:set(id, Health, 50)
+
+ CHECK(world:get(id, Poison) == 5)
+ end
+
+ do CASE "should allow deleting components"
+ local world = jecs.World.new()
+
+ local Health = world:entity()
+ local Poison = world:component()
+
+ local id = world:entity()
+ world:set(id, Poison, 5)
+ world:set(id, Health, 50)
+ local id1 = world:entity()
+ world:set(id1, Poison, 500)
+ world:set(id1, Health, 50)
+
+ world:delete(id)
+
+ CHECK(world:get(id, Poison) == nil)
+ CHECK(world:get(id, Health) == nil)
+ CHECK(world:get(id1, Poison) == 500)
+ CHECK(world:get(id1, Health) == 50)
+
+ end
+
+ do CASE "should allow remove that doesn't exist on entity"
+ local world = jecs.World.new()
+
+ local Health = world:entity()
+ local Poison = world:component()
+
+ local id = world:entity()
+ world:set(id, Health, 50)
+ world:remove(id, Poison)
+
+ CHECK(world:get(id, Poison) == nil)
+ CHECK(world:get(id, Health) == 50)
+ end
+
+ do CASE "should increment generation"
+ local world = jecs.World.new()
+ local e = world:entity()
+ CHECK(ECS_ID(e) == 1 + jecs.Rest)
+ CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
+ CHECK(ECS_GENERATION(e) == 0) -- 0
+ e = ECS_GENERATION_INC(e)
+ CHECK(ECS_GENERATION(e) == 1) -- 1
+ end
+
+ do CASE "should get alive from index in the dense array"
+ local world = jecs.World.new()
+ local _e = world:entity()
+ local e2 = world:entity()
+ local e3 = world:entity()
+
+ CHECK(IS_PAIR(world:entity()) == false)
+
+ local pair = ECS_PAIR(e2, e3)
+ CHECK(IS_PAIR(pair) == true)
+ CHECK(ecs_get_source(world.entityIndex, pair) == e2)
+ CHECK(ecs_get_target(world.entityIndex, pair) == e3)
+ end
+
+ do CASE "should allow querying for relations"
+ local world = jecs.World.new()
+ local Eats = world:entity()
+ local Apples = world:entity()
+ local bob = world:entity()
+
+ world:set(bob, ECS_PAIR(Eats, Apples), true)
+ for e, bool in world:query(ECS_PAIR(Eats, Apples)) do
+ CHECK(e == bob)
+ CHECK(bool)
+ end
+ end
+
+ do CASE "should allow wildcards in queries"
+ local world = jecs.World.new()
+ local Eats = world:entity()
+ local Apples = world:entity()
+ local bob = world:entity()
+
+ world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
+
+ local w = jecs.Wildcard
+ for e, data in world:query(ECS_PAIR(Eats, w)) do
+ CHECK(e == bob)
+ CHECK(data == "bob eats apples")
+ end
+ for e, data in world:query(ECS_PAIR(w, Apples)) do
+ CHECK(e == bob)
+ CHECK(data == "bob eats apples")
+ end
+ end
+
+ do CASE "should match against multiple pairs"
+ local world = jecs.World.new()
+ local Eats = world:entity()
+ local Apples = world:entity()
+ local Oranges =world:entity()
+ local bob = world:entity()
+ local alice = world:entity()
+
+ world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples")
+ world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges")
+
+ local w = jecs.Wildcard
+ local count = 0
+ for e, data in world:query(ECS_PAIR(Eats, w)) do
+ count += 1
+ if e == bob then
+ CHECK(data == "bob eats apples")
+ else
+ CHECK(data == "alice eats oranges")
+ end
+ end
+
+ CHECK(count == 2)
+ count = 0
+
+ for e, data in world:query(ECS_PAIR(w, Apples)) do
+ count += 1
+ CHECK(data == "bob eats apples")
+ end
+ CHECK(count == 1)
+ end
+end)
+
+FINISH()
\ No newline at end of file
diff --git a/wally.toml b/wally.toml
index 5885799..41797b9 100644
--- a/wally.toml
+++ b/wally.toml
@@ -1,12 +1,10 @@
[package]
name = "ukendio/jecs"
-version = "0.0.0-prototype.rc.3"
+version = "0.1.0-rc.6"
registry = "https://github.com/UpliftGames/wally-index"
realm = "shared"
+include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
exclude = ["**"]
-include = ["default.project.json", "lib", "wally.toml", "README.md"]
[dev-dependencies]
-TestEZ = "roblox/testez@0.4.1"
-Matter = "matter-ecs/matter@0.8.0"
-ecr = "centau/ecr@0.8.0"
+TestEZ = "roblox/testez@0.4.1"
\ No newline at end of file