mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Merge branch 'main' of https://github.com/Ukendio/jecs into mirror-matter
This commit is contained in:
commit
99ce25a5d6
15 changed files with 1478 additions and 535 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -50,6 +50,3 @@ WallyPatches
|
||||||
roblox.toml
|
roblox.toml
|
||||||
sourcemap.json
|
sourcemap.json
|
||||||
drafts/*.lua
|
drafts/*.lua
|
||||||
|
|
||||||
*.code-workspace
|
|
||||||
roblox.yml
|
|
||||||
|
|
39
README.md
39
README.md
|
@ -1,6 +1,7 @@
|
||||||
|
|
||||||
<p align="center">
|
<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>
|
</p>
|
||||||
|
|
||||||
[](LICENSE-APACHE)
|
[](LICENSE-APACHE)
|
||||||
|
@ -10,23 +11,29 @@ Just an ECS
|
||||||
|
|
||||||
jecs is a stupidly fast Entity Component System (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
|
- 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
|
- Optimized for column-major operations
|
||||||
- Cache friendly archetype/SoA storage
|
- Cache friendly archetype/SoA storage
|
||||||
|
- Unit tested for stability
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local world = Jecs.World.new()
|
local world = World.new()
|
||||||
|
|
||||||
local Health = world:component()
|
|
||||||
local Damage = world:component()
|
|
||||||
local Position = world:component()
|
|
||||||
|
|
||||||
local player = world:entity()
|
local player = world:entity()
|
||||||
local opponent = 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, Health, 100)
|
||||||
world:set(player, Damage, 8)
|
world:set(player, Damage, 8)
|
||||||
world:set(player, Position, Vector3.new(0, 5, 0))
|
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
|
for playerId, playerPosition, health in world:query(Position, Health) do
|
||||||
local totalDamage = 0
|
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
|
if (playerPosition - opponentPosition).Magnitude < 5 then
|
||||||
totalDamage += damage
|
totalDamage += damage
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
world:set(playerId, Health, health - totalDamage)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
assert(world:get(playerId, Health) == 79)
|
-- Gets the damage inflicted by our specific opponent!
|
||||||
assert(world:get(opponentId, Health) == 92)
|
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.
|
125 archetypes, 4 random components queried.
|
||||||
|
|
372
benches/exhaustive.lua
Normal file
372
benches/exhaustive.lua
Normal 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
|
|
@ -8,7 +8,8 @@ local function TITLE(title: string)
|
||||||
print(testkit.color.white(title))
|
print(testkit.color.white(title))
|
||||||
end
|
end
|
||||||
|
|
||||||
local jecs = require("../mirror/init")
|
local jecs = require("../lib/init")
|
||||||
|
local mirror = require("../mirror/init")
|
||||||
|
|
||||||
local oldMatter = require("../oldMatter")
|
local oldMatter = require("../oldMatter")
|
||||||
|
|
||||||
|
@ -41,6 +42,145 @@ do
|
||||||
for _ in world:query(A, B, C, D, E, F, G, H) do
|
for _ in world:query(A, B, C, D, E, F, G, H) do
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
local D1 = ecs:component()
|
local D1 = ecs:component()
|
||||||
|
|
|
@ -8,6 +8,8 @@ local jecs = require(ReplicatedStorage.Lib)
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||||
local newWorld = Matter.World.new()
|
local newWorld = Matter.World.new()
|
||||||
local ecs = jecs.World.new()
|
local ecs = jecs.World.new()
|
||||||
|
local mirror = require(ReplicatedStorage.mirror)
|
||||||
|
local mcs = mirror.World.new()
|
||||||
|
|
||||||
local A1 = Matter.component()
|
local A1 = Matter.component()
|
||||||
local A2 = Matter.component()
|
local A2 = Matter.component()
|
||||||
|
@ -35,6 +37,15 @@ local C5 = ecs:entity()
|
||||||
local C6 = ecs:entity()
|
local C6 = ecs:entity()
|
||||||
local C7 = ecs:entity()
|
local C7 = ecs:entity()
|
||||||
local C8 = 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()
|
local registry2 = ecr.registry()
|
||||||
return {
|
return {
|
||||||
|
@ -44,7 +55,7 @@ return {
|
||||||
|
|
||||||
Functions = {
|
Functions = {
|
||||||
Matter = function()
|
Matter = function()
|
||||||
for i = 1, 50 do
|
for i = 1, 500 do
|
||||||
newWorld:spawn(
|
newWorld:spawn(
|
||||||
A1({ value = true }),
|
A1({ value = true }),
|
||||||
A2({ value = true }),
|
A2({ value = true }),
|
||||||
|
@ -60,8 +71,8 @@ return {
|
||||||
|
|
||||||
|
|
||||||
ECR = function()
|
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, B1, {value = false})
|
||||||
registry2:set(e, B2, {value = false})
|
registry2:set(e, B2, {value = false})
|
||||||
registry2:set(e, B3, {value = false})
|
registry2:set(e, B3, {value = false})
|
||||||
|
@ -78,7 +89,7 @@ return {
|
||||||
|
|
||||||
local e = ecs:entity()
|
local e = ecs:entity()
|
||||||
|
|
||||||
for i = 1, 50 do
|
for i = 1, 500 do
|
||||||
|
|
||||||
ecs:set(e, C1, {value = false})
|
ecs:set(e, C1, {value = false})
|
||||||
ecs:set(e, C2, {value = false})
|
ecs:set(e, C2, {value = false})
|
||||||
|
@ -89,6 +100,23 @@ return {
|
||||||
ecs:set(e, C7, {value = false})
|
ecs:set(e, C7, {value = false})
|
||||||
ecs:set(e, C8, {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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
6
jecs_darkmode.svg
Normal file
6
jecs_darkmode.svg
Normal 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
6
jecs_lightmode.svg
Normal 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 |
473
lib/init.lua
473
lib/init.lua
|
@ -29,9 +29,10 @@ type Archetype = {
|
||||||
type Record = {
|
type Record = {
|
||||||
archetype: Archetype,
|
archetype: Archetype,
|
||||||
row: number,
|
row: number,
|
||||||
|
dense: i24,
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntityIndex = {[i24]: Record}
|
type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}}
|
||||||
type ComponentIndex = {[i24]: ArchetypeMap}
|
type ComponentIndex = {[i24]: ArchetypeMap}
|
||||||
|
|
||||||
type ArchetypeRecord = number
|
type ArchetypeRecord = number
|
||||||
|
@ -43,11 +44,118 @@ type ArchetypeDiff = {
|
||||||
removed: Ty,
|
removed: Ty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local FLAGS_PAIR = 0x8
|
||||||
local HI_COMPONENT_ID = 256
|
local HI_COMPONENT_ID = 256
|
||||||
local ON_ADD = HI_COMPONENT_ID + 1
|
local ON_ADD = HI_COMPONENT_ID + 1
|
||||||
local ON_REMOVE = HI_COMPONENT_ID + 2
|
local ON_REMOVE = HI_COMPONENT_ID + 2
|
||||||
local ON_SET = HI_COMPONENT_ID + 3
|
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(
|
local function transitionArchetype(
|
||||||
entityIndex: EntityIndex,
|
entityIndex: EntityIndex,
|
||||||
|
@ -81,21 +189,27 @@ local function transitionArchetype(
|
||||||
column[last] = nil
|
column[last] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Move the entity from the source to the destination archetype.
|
local sparse = entityIndex.sparse
|
||||||
local atSourceRow = sourceEntities[sourceRow]
|
local movedAway = #sourceEntities
|
||||||
destinationEntities[destinationRow] = atSourceRow
|
|
||||||
entityIndex[atSourceRow].row = destinationRow
|
|
||||||
|
|
||||||
|
-- Move the entity from the source to the destination archetype.
|
||||||
-- Because we have swapped columns we now have to update the records
|
-- Because we have swapped columns we now have to update the records
|
||||||
-- corresponding to the entities' rows that were swapped.
|
-- corresponding to the entities' rows that were swapped.
|
||||||
local movedAway = #sourceEntities
|
local e1 = sourceEntities[sourceRow]
|
||||||
if sourceRow ~= movedAway then
|
local e2 = sourceEntities[movedAway]
|
||||||
local atMovedAway = sourceEntities[movedAway]
|
|
||||||
sourceEntities[sourceRow] = atMovedAway
|
if sourceRow ~= movedAway then
|
||||||
entityIndex[atMovedAway].row = sourceRow
|
sourceEntities[sourceRow] = e2
|
||||||
end
|
end
|
||||||
|
|
||||||
sourceEntities[movedAway] = nil
|
sourceEntities[movedAway] = nil
|
||||||
|
destinationEntities[destinationRow] = e1
|
||||||
|
|
||||||
|
local record1 = sparse[e1]
|
||||||
|
local record2 = sparse[e2]
|
||||||
|
|
||||||
|
record1.row = destinationRow
|
||||||
|
record2.row = sourceRow
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeAppend(entity: number, archetype: Archetype): number
|
local function archetypeAppend(entity: number, archetype: Archetype): number
|
||||||
|
@ -125,22 +239,14 @@ local function hash(arr): string | number
|
||||||
return table.concat(arr, "_")
|
return table.concat(arr, "_")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
|
local function createArchetypeRecord(componentIndex, id, componentId, i)
|
||||||
local destinationIds = to.types
|
local archetypesMap = componentIndex[componentId]
|
||||||
local records = to.records
|
|
||||||
local id = to.id
|
|
||||||
|
|
||||||
for i, destinationId in destinationIds do
|
if not archetypesMap then
|
||||||
local archetypesMap = componentIndex[destinationId]
|
archetypesMap = {size = 0, sparse = {}}
|
||||||
|
componentIndex[componentId] = archetypesMap
|
||||||
if not archetypesMap then
|
|
||||||
archetypesMap = {size = 0, sparse = {}}
|
|
||||||
componentIndex[destinationId] = archetypesMap
|
|
||||||
end
|
|
||||||
|
|
||||||
archetypesMap.sparse[id] = i
|
|
||||||
records[destinationId] = i
|
|
||||||
end
|
end
|
||||||
|
archetypesMap.sparse[id] = i
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
|
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
|
world.nextArchetypeId = id
|
||||||
|
|
||||||
local length = #types
|
local length = #types
|
||||||
local columns = table.create(length) :: {any}
|
local columns = table.create(length)
|
||||||
|
|
||||||
for index in types do
|
local records = {}
|
||||||
columns[index] = {}
|
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
|
end
|
||||||
|
|
||||||
local archetype = {
|
local archetype = {
|
||||||
|
@ -161,15 +283,12 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet
|
||||||
edges = {};
|
edges = {};
|
||||||
entities = {};
|
entities = {};
|
||||||
id = id;
|
id = id;
|
||||||
records = {};
|
records = records;
|
||||||
type = ty;
|
type = ty;
|
||||||
types = types;
|
types = types;
|
||||||
}
|
}
|
||||||
world.archetypeIndex[ty] = archetype
|
world.archetypeIndex[ty] = archetype
|
||||||
world.archetypes[id] = archetype
|
world.archetypes[id] = archetype
|
||||||
if length > 0 then
|
|
||||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
|
||||||
end
|
|
||||||
|
|
||||||
return archetype
|
return archetype
|
||||||
end
|
end
|
||||||
|
@ -179,9 +298,12 @@ World.__index = World
|
||||||
function World.new()
|
function World.new()
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
archetypeIndex = {};
|
archetypeIndex = {};
|
||||||
archetypes = {};
|
archetypes = {} :: Archetypes;
|
||||||
componentIndex = {};
|
componentIndex = {} :: ComponentIndex;
|
||||||
entityIndex = {};
|
entityIndex = {
|
||||||
|
dense = {},
|
||||||
|
sparse = {}
|
||||||
|
} :: EntityIndex;
|
||||||
hooks = {
|
hooks = {
|
||||||
[ON_ADD] = {};
|
[ON_ADD] = {};
|
||||||
};
|
};
|
||||||
|
@ -190,32 +312,78 @@ function World.new()
|
||||||
nextEntityId = 0;
|
nextEntityId = 0;
|
||||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
||||||
}, World)
|
}, World)
|
||||||
|
self.ROOT_ARCHETYPE = archetypeOf(self, {})
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
local function emit(world, eventDescription)
|
function World.component(world: World)
|
||||||
local event = eventDescription.event
|
local componentId = world.nextComponentId + 1
|
||||||
|
if componentId > HI_COMPONENT_ID then
|
||||||
table.insert(world.hooks[event], {
|
-- IDs are partitioned into ranges because component IDs are not nominal,
|
||||||
archetype = eventDescription.archetype;
|
-- so it needs to error when IDs intersect into the entity range.
|
||||||
ids = eventDescription.ids;
|
error("Too many components, consider using world:entity() instead to create components.")
|
||||||
offset = eventDescription.offset;
|
end
|
||||||
otherArchetype = eventDescription.otherArchetype;
|
world.nextComponentId = componentId
|
||||||
})
|
return nextEntityId(world.entityIndex, componentId)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
function World.entity(world: World)
|
||||||
if #added > 0 then
|
local entityId = world.nextEntityId + 1
|
||||||
emit(world, {
|
world.nextEntityId = entityId
|
||||||
archetype = archetype;
|
return nextEntityId(world.entityIndex, entityId + REST)
|
||||||
event = ON_ADD;
|
end
|
||||||
ids = added;
|
|
||||||
offset = row;
|
-- should reuse this logic in World.set instead of swap removing in transition archetype
|
||||||
otherArchetype = otherArchetype;
|
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
|
||||||
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())
|
export type World = typeof(World.new())
|
||||||
|
|
||||||
local function ensureArchetype(world: World, types, prev)
|
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
|
-- Component IDs are added incrementally, so inserting and sorting
|
||||||
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
||||||
-- point in the types array.
|
-- point in the types array.
|
||||||
|
|
||||||
|
local destinationType = table.clone(node.types)
|
||||||
local at = findInsert(types, componentId)
|
local at = findInsert(types, componentId)
|
||||||
if at == -1 then
|
if at == -1 then
|
||||||
-- If it finds a duplicate, it just means it is the same archetype so it can return it
|
-- 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.
|
-- directly instead of needing to hash types for a lookup to the archetype.
|
||||||
return node
|
return node
|
||||||
end
|
end
|
||||||
|
|
||||||
local destinationType = table.clone(node.types)
|
|
||||||
table.insert(destinationType, at, componentId)
|
table.insert(destinationType, at, componentId)
|
||||||
|
|
||||||
return ensureArchetype(world, destinationType, node)
|
return ensureArchetype(world, destinationType, node)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -272,15 +441,7 @@ local function ensureEdge(archetype: Archetype, componentId: i53)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
||||||
if not from then
|
from = from or world.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 = ROOT_ARCHETYPE
|
|
||||||
end
|
|
||||||
|
|
||||||
local edge = ensureEdge(from, componentId)
|
local edge = ensureEdge(from, componentId)
|
||||||
local add = edge.add
|
local add = edge.add
|
||||||
|
@ -294,19 +455,23 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
|
||||||
return add
|
return add
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensureRecord(entityIndex, entityId: i53): Record
|
function World.add(world: World, entityId: i53, componentId: i53)
|
||||||
local record = entityIndex[entityId]
|
local entityIndex = world.entityIndex
|
||||||
|
local record = entityIndex.sparse[entityId]
|
||||||
if not record then
|
local from = record.archetype
|
||||||
record = {}
|
local to = archetypeTraverseAdd(world, componentId, from)
|
||||||
entityIndex[entityId] = record
|
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
|
end
|
||||||
|
|
||||||
return record :: Record
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Symmetric like `World.add` but idempotent
|
||||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
local record = ensureRecord(world.entityIndex, entityId)
|
local record = world.entityIndex.sparse[entityId]
|
||||||
local from = record.archetype
|
local from = record.archetype
|
||||||
local to = archetypeTraverseAdd(world, componentId, from)
|
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
|
if #to.types > 0 then
|
||||||
-- When there is no previous archetype it should create the archetype
|
-- When there is no previous archetype it should create the archetype
|
||||||
newEntity(entityId, record, to)
|
newEntity(entityId, record, to)
|
||||||
onNotifyAdd(world, to, from, record.row, {componentId})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -334,14 +498,17 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
to.columns[archetypeRecord][record.row] = data
|
to.columns[archetypeRecord][record.row] = data
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
|
local function archetypeTraverseRemove(world: World, componentId: i53, from: Archetype): Archetype
|
||||||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
|
||||||
local edge = ensureEdge(from, componentId)
|
local edge = ensureEdge(from, componentId)
|
||||||
|
|
||||||
local remove = edge.remove
|
local remove = edge.remove
|
||||||
if not remove then
|
if not remove then
|
||||||
local to = table.clone(from.types)
|
local to = table.clone(from.types)
|
||||||
table.remove(to, table.find(to, componentId))
|
local at = table.find(to, componentId)
|
||||||
|
if not at then
|
||||||
|
return from
|
||||||
|
end
|
||||||
|
table.remove(to, at)
|
||||||
remove = ensureArchetype(world, to, from)
|
remove = ensureArchetype(world, to, from)
|
||||||
edge.remove = remove :: never
|
edge.remove = remove :: never
|
||||||
end
|
end
|
||||||
|
@ -351,7 +518,7 @@ end
|
||||||
|
|
||||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||||
local entityIndex = world.entityIndex
|
local entityIndex = world.entityIndex
|
||||||
local record = ensureRecord(entityIndex, entityId)
|
local record = entityIndex.sparse[entityId]
|
||||||
local sourceArchetype = record.archetype
|
local sourceArchetype = record.archetype
|
||||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
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?)
|
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
||||||
local id = entityId
|
local id = entityId
|
||||||
local record = world.entityIndex[id]
|
local record = world.entityIndex.sparse[id]
|
||||||
if not record then
|
if not record then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
@ -456,7 +623,10 @@ function World.query(world: World, ...: i53): Query
|
||||||
end
|
end
|
||||||
|
|
||||||
length += 1
|
length += 1
|
||||||
compatibleArchetypes[length] = {archetype, indices}
|
compatibleArchetypes[length] = {
|
||||||
|
archetype = archetype,
|
||||||
|
indices = indices
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
||||||
|
@ -470,7 +640,7 @@ function World.query(world: World, ...: i53): Query
|
||||||
function preparedQuery:without(...)
|
function preparedQuery:without(...)
|
||||||
local withoutComponents = {...}
|
local withoutComponents = {...}
|
||||||
for i = #compatibleArchetypes, 1, -1 do
|
for i = #compatibleArchetypes, 1, -1 do
|
||||||
local archetype = compatibleArchetypes[i][1]
|
local archetype = compatibleArchetypes[i].archetype
|
||||||
local records = archetype.records
|
local records = archetype.records
|
||||||
local shouldRemove = false
|
local shouldRemove = false
|
||||||
|
|
||||||
|
@ -499,21 +669,21 @@ function World.query(world: World, ...: i53): Query
|
||||||
|
|
||||||
function preparedQuery:__iter()
|
function preparedQuery:__iter()
|
||||||
return function()
|
return function()
|
||||||
local archetype = compatibleArchetype[1]
|
local archetype = compatibleArchetype.archetype
|
||||||
local row = next(archetype.entities, lastRow)
|
local row: number = next(archetype.entities, lastRow) :: number
|
||||||
while row == nil do
|
while row == nil do
|
||||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
||||||
if lastArchetype == nil then
|
if lastArchetype == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
archetype = compatibleArchetype[1]
|
archetype = compatibleArchetype.archetype
|
||||||
row = next(archetype.entities, row)
|
row = next(archetype.entities, row) :: number
|
||||||
end
|
end
|
||||||
lastRow = row
|
lastRow = row
|
||||||
|
|
||||||
local entityId = archetype.entities[row :: number]
|
local entityId = archetype.entities[row :: number]
|
||||||
local columns = archetype.columns
|
local columns = archetype.columns
|
||||||
local tr = compatibleArchetype[2]
|
local tr = compatibleArchetype.indices
|
||||||
|
|
||||||
if queryLength == 1 then
|
if queryLength == 1 then
|
||||||
return entityId, columns[tr[1]][row]
|
return entityId, columns[tr[1]][row]
|
||||||
|
@ -560,7 +730,7 @@ function World.query(world: World, ...: i53): Query
|
||||||
end
|
end
|
||||||
|
|
||||||
for i in components do
|
for i in components do
|
||||||
queryOutput[i] = tr[i][row]
|
queryOutput[i] = columns[tr[i]][row]
|
||||||
end
|
end
|
||||||
|
|
||||||
return entityId, unpack(queryOutput, 1, queryLength)
|
return entityId, unpack(queryOutput, 1, queryLength)
|
||||||
|
@ -570,90 +740,57 @@ function World.query(world: World, ...: i53): Query
|
||||||
return setmetatable({}, preparedQuery) :: any
|
return setmetatable({}, preparedQuery) :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.component(world: World)
|
function World.__iter(world: World): () -> (number?, unknown?)
|
||||||
local componentId = world.nextComponentId + 1
|
local dense = world.entityIndex.dense
|
||||||
if componentId > HI_COMPONENT_ID then
|
local sparse = world.entityIndex.sparse
|
||||||
-- IDs are partitioned into ranges because component IDs are not nominal,
|
local last
|
||||||
-- so it needs to error when IDs intersect into the entity range.
|
|
||||||
error("Too many components, consider using world:entity() instead to create components.")
|
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
|
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
|
end
|
||||||
|
|
||||||
return table.freeze({
|
return table.freeze({
|
||||||
World = World;
|
World = World;
|
||||||
ON_ADD = ON_ADD;
|
|
||||||
ON_REMOVE = ON_REMOVE;
|
OnAdd = ON_ADD;
|
||||||
ON_SET = ON_SET;
|
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,
|
||||||
})
|
})
|
||||||
|
|
|
@ -176,22 +176,6 @@ return function()
|
||||||
expect(added).to.equal(0)
|
expect(added).to.equal(0)
|
||||||
end)
|
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()
|
it("should query all matching entities", function()
|
||||||
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
@ -299,5 +283,100 @@ return function()
|
||||||
expect(world:get(id, Poison)).to.never.be.ok()
|
expect(world:get(id, Poison)).to.never.be.ok()
|
||||||
expect(world:get(id, Health)).to.never.be.ok()
|
expect(world:get(id, Health)).to.never.be.ok()
|
||||||
end)
|
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)
|
||||||
end
|
end
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
435
mirror/init.lua
435
mirror/init.lua
|
@ -6,10 +6,10 @@
|
||||||
type i53 = number
|
type i53 = number
|
||||||
type i24 = number
|
type i24 = number
|
||||||
|
|
||||||
type Ty = { i53 }
|
type Ty = {i53}
|
||||||
type ArchetypeId = number
|
type ArchetypeId = number
|
||||||
|
|
||||||
type Column = { any }
|
type Column = {any}
|
||||||
|
|
||||||
type Archetype = {
|
type Archetype = {
|
||||||
id: number,
|
id: number,
|
||||||
|
@ -20,9 +20,9 @@ type Archetype = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
types: Ty,
|
types: Ty,
|
||||||
type: string | number,
|
type: string | number,
|
||||||
entities: { number },
|
entities: {number},
|
||||||
columns: { Column },
|
columns: {Column},
|
||||||
records: {},
|
records: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,13 +31,13 @@ type Record = {
|
||||||
row: number,
|
row: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntityIndex = { [i24]: Record }
|
type EntityIndex = {[i24]: Record}
|
||||||
type ComponentIndex = { [i24]: ArchetypeMap}
|
type ComponentIndex = {[i24]: ArchetypeMap}
|
||||||
|
|
||||||
type ArchetypeRecord = number
|
type ArchetypeRecord = number
|
||||||
type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
|
type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
|
||||||
type Archetypes = { [ArchetypeId]: Archetype }
|
type Archetypes = {[ArchetypeId]: Archetype}
|
||||||
|
|
||||||
type ArchetypeDiff = {
|
type ArchetypeDiff = {
|
||||||
added: Ty,
|
added: Ty,
|
||||||
removed: Ty,
|
removed: Ty,
|
||||||
|
@ -64,17 +64,17 @@ local function transitionArchetype(
|
||||||
local types = from.types
|
local types = from.types
|
||||||
|
|
||||||
for i, column in columns do
|
for i, column in columns do
|
||||||
-- Retrieves the new column index from the source archetype's record from each component
|
-- Retrieves the new column index from the source archetype's record from each component
|
||||||
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
|
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
|
||||||
local targetColumn = destinationColumns[tr[types[i]]]
|
local targetColumn = destinationColumns[tr[types[i]]]
|
||||||
|
|
||||||
-- Sometimes target column may not exist, e.g. when you remove a component.
|
-- Sometimes target column may not exist, e.g. when you remove a component.
|
||||||
if targetColumn then
|
if targetColumn then
|
||||||
targetColumn[destinationRow] = column[sourceRow]
|
targetColumn[destinationRow] = column[sourceRow]
|
||||||
end
|
end
|
||||||
-- If the entity is the last row in the archetype then swapping it would be meaningless.
|
-- If the entity is the last row in the archetype then swapping it would be meaningless.
|
||||||
local last = #column
|
local last = #column
|
||||||
if sourceRow ~= last then
|
if sourceRow ~= last then
|
||||||
-- Swap rempves columns to ensure there are no holes in the archetype.
|
-- Swap rempves columns to ensure there are no holes in the archetype.
|
||||||
column[sourceRow] = column[last]
|
column[sourceRow] = column[last]
|
||||||
end
|
end
|
||||||
|
@ -82,24 +82,27 @@ local function transitionArchetype(
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Move the entity from the source to the destination archetype.
|
-- Move the entity from the source to the destination archetype.
|
||||||
destinationEntities[destinationRow] = sourceEntities[sourceRow]
|
local atSourceRow = sourceEntities[sourceRow]
|
||||||
entityIndex[sourceEntities[sourceRow]].row = destinationRow
|
destinationEntities[destinationRow] = atSourceRow
|
||||||
|
entityIndex[atSourceRow].row = destinationRow
|
||||||
|
|
||||||
-- Because we have swapped columns we now have to update the records
|
-- Because we have swapped columns we now have to update the records
|
||||||
-- corresponding to the entities' rows that were swapped.
|
-- corresponding to the entities' rows that were swapped.
|
||||||
local movedAway = #sourceEntities
|
local movedAway = #sourceEntities
|
||||||
if sourceRow ~= movedAway then
|
if sourceRow ~= movedAway then
|
||||||
sourceEntities[sourceRow] = sourceEntities[movedAway]
|
local atMovedAway = sourceEntities[movedAway]
|
||||||
entityIndex[sourceEntities[movedAway]].row = sourceRow
|
sourceEntities[sourceRow] = atMovedAway
|
||||||
|
entityIndex[atMovedAway].row = sourceRow
|
||||||
end
|
end
|
||||||
|
|
||||||
sourceEntities[movedAway] = nil
|
sourceEntities[movedAway] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeAppend(entity: i53, archetype: Archetype): i24
|
local function archetypeAppend(entity: number, archetype: Archetype): number
|
||||||
local entities = archetype.entities
|
local entities = archetype.entities
|
||||||
table.insert(entities, entity)
|
local length = #entities + 1
|
||||||
return #entities
|
entities[length] = entity
|
||||||
|
return length
|
||||||
end
|
end
|
||||||
|
|
||||||
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
|
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
|
||||||
|
@ -122,47 +125,49 @@ local function hash(arr): string | number
|
||||||
return table.concat(arr, "_")
|
return table.concat(arr, "_")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
|
||||||
local destinationCount = #to.types
|
|
||||||
local destinationIds = to.types
|
local destinationIds = to.types
|
||||||
|
local records = to.records
|
||||||
|
local id = to.id
|
||||||
|
|
||||||
for i = 1, destinationCount do
|
for i, destinationId in destinationIds do
|
||||||
local destinationId = destinationIds[i]
|
local archetypesMap = componentIndex[destinationId]
|
||||||
|
|
||||||
if not componentIndex[destinationId] then
|
if not archetypesMap then
|
||||||
componentIndex[destinationId] = { size = 0, sparse = {} }
|
archetypesMap = {size = 0, sparse = {}}
|
||||||
|
componentIndex[destinationId] = archetypesMap
|
||||||
end
|
end
|
||||||
|
|
||||||
local archetypesMap = componentIndex[destinationId]
|
archetypesMap.sparse[id] = i
|
||||||
archetypesMap.sparse[to.id] = i
|
records[destinationId] = i
|
||||||
to.records[destinationId] = i
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
|
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
|
||||||
local ty = hash(types)
|
local ty = hash(types)
|
||||||
|
|
||||||
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
|
local id = world.nextArchetypeId + 1
|
||||||
local id = world.nextArchetypeId
|
world.nextArchetypeId = id
|
||||||
|
|
||||||
local columns = {} :: { any }
|
local length = #types
|
||||||
|
local columns = table.create(length) :: {any}
|
||||||
|
|
||||||
for _ in types do
|
for index in types do
|
||||||
table.insert(columns, {})
|
columns[index] = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local archetype = {
|
local archetype = {
|
||||||
id = id,
|
columns = columns;
|
||||||
types = types,
|
edges = {};
|
||||||
type = ty,
|
entities = {};
|
||||||
columns = columns,
|
id = id;
|
||||||
entities = {},
|
records = {};
|
||||||
edges = {},
|
type = ty;
|
||||||
records = {},
|
types = types;
|
||||||
}
|
}
|
||||||
world.archetypeIndex[ty] = archetype
|
world.archetypeIndex[ty] = archetype
|
||||||
world.archetypes[id] = archetype
|
world.archetypes[id] = archetype
|
||||||
if #types > 0 then
|
if length > 0 then
|
||||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
createArchetypeRecords(world.componentIndex, archetype, prev)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -171,42 +176,42 @@ end
|
||||||
|
|
||||||
local World = {}
|
local World = {}
|
||||||
World.__index = World
|
World.__index = World
|
||||||
function World.new()
|
function World.new()
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
entityIndex = {},
|
archetypeIndex = {};
|
||||||
componentIndex = {},
|
archetypes = {};
|
||||||
archetypes = {},
|
componentIndex = {};
|
||||||
archetypeIndex = {},
|
entityIndex = {};
|
||||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
|
||||||
nextEntityId = 0,
|
|
||||||
nextComponentId = 0,
|
|
||||||
nextArchetypeId = 0,
|
|
||||||
hooks = {
|
hooks = {
|
||||||
[ON_ADD] = {}
|
[ON_ADD] = {};
|
||||||
}
|
};
|
||||||
|
nextArchetypeId = 0;
|
||||||
|
nextComponentId = 0;
|
||||||
|
nextEntityId = 0;
|
||||||
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
||||||
}, World)
|
}, World)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
local function emit(world, eventDescription)
|
local function emit(world, eventDescription)
|
||||||
local event = eventDescription.event
|
local event = eventDescription.event
|
||||||
|
|
||||||
table.insert(world.hooks[event], {
|
table.insert(world.hooks[event], {
|
||||||
ids = eventDescription.ids,
|
archetype = eventDescription.archetype;
|
||||||
archetype = eventDescription.archetype,
|
ids = eventDescription.ids;
|
||||||
otherArchetype = eventDescription.otherArchetype,
|
offset = eventDescription.offset;
|
||||||
offset = eventDescription.offset
|
otherArchetype = eventDescription.otherArchetype;
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
||||||
if #added > 0 then
|
if #added > 0 then
|
||||||
emit(world, {
|
emit(world, {
|
||||||
event = ON_ADD,
|
archetype = archetype;
|
||||||
ids = added,
|
event = ON_ADD;
|
||||||
archetype = archetype,
|
ids = added;
|
||||||
otherArchetype = otherArchetype,
|
offset = row;
|
||||||
offset = row,
|
otherArchetype = otherArchetype;
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -217,7 +222,7 @@ local function ensureArchetype(world: World, types, prev)
|
||||||
if #types < 1 then
|
if #types < 1 then
|
||||||
return world.ROOT_ARCHETYPE
|
return world.ROOT_ARCHETYPE
|
||||||
end
|
end
|
||||||
|
|
||||||
local ty = hash(types)
|
local ty = hash(types)
|
||||||
local archetype = world.archetypeIndex[ty]
|
local archetype = world.archetypeIndex[ty]
|
||||||
if archetype then
|
if archetype then
|
||||||
|
@ -227,10 +232,8 @@ local function ensureArchetype(world: World, types, prev)
|
||||||
return archetypeOf(world, types, prev)
|
return archetypeOf(world, types, prev)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function findInsert(types: { i53 }, toAdd: i53)
|
local function findInsert(types: {i53}, toAdd: i53)
|
||||||
local count = #types
|
for i, id in types do
|
||||||
for i = 1, count do
|
|
||||||
local id = types[i]
|
|
||||||
if id == toAdd then
|
if id == toAdd then
|
||||||
return -1
|
return -1
|
||||||
end
|
end
|
||||||
|
@ -238,13 +241,13 @@ local function findInsert(types: { i53 }, toAdd: i53)
|
||||||
return i
|
return i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return count + 1
|
return #types + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
||||||
local types = node.types
|
local types = node.types
|
||||||
-- Component IDs are added incrementally, so inserting and sorting
|
-- Component IDs are added incrementally, so inserting and sorting
|
||||||
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
||||||
-- point in the types array.
|
-- point in the types array.
|
||||||
local at = findInsert(types, componentId)
|
local at = findInsert(types, componentId)
|
||||||
if at == -1 then
|
if at == -1 then
|
||||||
|
@ -259,48 +262,57 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensureEdge(archetype: Archetype, componentId: i53)
|
local function ensureEdge(archetype: Archetype, componentId: i53)
|
||||||
if not archetype.edges[componentId] then
|
local edges = archetype.edges
|
||||||
archetype.edges[componentId] = {} :: any
|
local edge = edges[componentId]
|
||||||
|
if not edge then
|
||||||
|
edge = {} :: any
|
||||||
|
edges[componentId] = edge
|
||||||
end
|
end
|
||||||
return archetype.edges[componentId]
|
return edge
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
||||||
if not from then
|
if not from then
|
||||||
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
|
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
|
||||||
if not world.ROOT_ARCHETYPE then
|
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
|
||||||
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
if not ROOT_ARCHETYPE then
|
||||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
||||||
end
|
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
|
||||||
from = world.ROOT_ARCHETYPE
|
end
|
||||||
|
from = ROOT_ARCHETYPE
|
||||||
end
|
end
|
||||||
|
|
||||||
local edge = ensureEdge(from, componentId)
|
local edge = ensureEdge(from, componentId)
|
||||||
|
local add = edge.add
|
||||||
if not edge.add then
|
if not add then
|
||||||
-- Save an edge using the component ID to the archetype to allow
|
-- Save an edge using the component ID to the archetype to allow
|
||||||
-- faster traversals to adjacent archetypes.
|
-- faster traversals to adjacent archetypes.
|
||||||
edge.add = findArchetypeWith(world, from, componentId)
|
add = findArchetypeWith(world, from, componentId)
|
||||||
|
edge.add = add :: never
|
||||||
end
|
end
|
||||||
|
|
||||||
return edge.add
|
return add
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensureRecord(entityIndex, entityId: i53): Record
|
local function ensureRecord(entityIndex, entityId: i53): Record
|
||||||
local id = entityId
|
local record = entityIndex[entityId]
|
||||||
if not entityIndex[id] then
|
|
||||||
entityIndex[id] = {}
|
if not record then
|
||||||
|
record = {}
|
||||||
|
entityIndex[entityId] = record
|
||||||
end
|
end
|
||||||
return entityIndex[id] :: Record
|
|
||||||
|
return record :: Record
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
local record = ensureRecord(world.entityIndex, entityId)
|
local record = ensureRecord(world.entityIndex, entityId)
|
||||||
local from = record.archetype
|
local from = record.archetype
|
||||||
local to = archetypeTraverseAdd(world, componentId, from)
|
local to = archetypeTraverseAdd(world, componentId, from)
|
||||||
|
|
||||||
if from == to then
|
if from == to then
|
||||||
-- If the archetypes are the same it can avoid moving the entity
|
-- If the archetypes are the same it can avoid moving the entity
|
||||||
-- and just set the data directly.
|
-- and just set the data directly.
|
||||||
local archetypeRecord = to.records[componentId]
|
local archetypeRecord = to.records[componentId]
|
||||||
from.columns[archetypeRecord][record.row] = data
|
from.columns[archetypeRecord][record.row] = data
|
||||||
-- Should fire an OnSet event here.
|
-- Should fire an OnSet event here.
|
||||||
|
@ -308,13 +320,13 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
end
|
end
|
||||||
|
|
||||||
if from then
|
if from then
|
||||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
-- If there was a previous archetype, then the entity needs to move the archetype
|
||||||
moveEntity(world.entityIndex, entityId, record, to)
|
moveEntity(world.entityIndex, entityId, record, to)
|
||||||
else
|
else
|
||||||
if #to.types > 0 then
|
if #to.types > 0 then
|
||||||
-- When there is no previous archetype it should create the archetype
|
-- When there is no previous archetype it should create the archetype
|
||||||
newEntity(entityId, record, to)
|
newEntity(entityId, record, to)
|
||||||
onNotifyAdd(world, to, from, record.row, { componentId })
|
onNotifyAdd(world, to, from, record.row, {componentId})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -326,28 +338,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
|
||||||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
||||||
local edge = ensureEdge(from, componentId)
|
local edge = ensureEdge(from, componentId)
|
||||||
|
|
||||||
|
local remove = edge.remove
|
||||||
if not edge.remove then
|
if not remove then
|
||||||
local to = table.clone(from.types)
|
local to = table.clone(from.types)
|
||||||
table.remove(to, table.find(to, componentId))
|
table.remove(to, table.find(to, componentId))
|
||||||
edge.remove = ensureArchetype(world, to, from)
|
remove = ensureArchetype(world, to, from)
|
||||||
|
edge.remove = remove :: never
|
||||||
end
|
end
|
||||||
|
|
||||||
return edge.remove
|
return remove
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||||
local record = ensureRecord(world.entityIndex, entityId)
|
local entityIndex = world.entityIndex
|
||||||
|
local record = ensureRecord(entityIndex, entityId)
|
||||||
local sourceArchetype = record.archetype
|
local sourceArchetype = record.archetype
|
||||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
||||||
|
|
||||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
||||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
moveEntity(entityIndex, entityId, record, destinationArchetype)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Keeping the function as small as possible to enable inlining
|
-- Keeping the function as small as possible to enable inlining
|
||||||
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
|
local function get(record: Record, componentId: i24)
|
||||||
local archetype = record.archetype
|
local archetype = record.archetype
|
||||||
local archetypeRecord = archetype.records[componentId]
|
local archetypeRecord = archetype.records[componentId]
|
||||||
|
|
||||||
|
@ -360,35 +374,35 @@ end
|
||||||
|
|
||||||
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
||||||
local id = entityId
|
local id = entityId
|
||||||
local componentIndex = world.componentIndex
|
|
||||||
local record = world.entityIndex[id]
|
local record = world.entityIndex[id]
|
||||||
if not record then
|
if not record then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
local va = get(componentIndex, record, a)
|
local va = get(record, a)
|
||||||
|
|
||||||
if b == nil then
|
if b == nil then
|
||||||
return va
|
return va
|
||||||
elseif c == nil then
|
elseif c == nil then
|
||||||
return va, get(componentIndex, record, b)
|
return va, get(record, b)
|
||||||
elseif d == nil then
|
elseif d == nil then
|
||||||
return va, get(componentIndex, record, b), get(componentIndex, record, c)
|
return va, get(record, b), get(record, c)
|
||||||
elseif e == nil then
|
elseif e == nil then
|
||||||
return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
|
return va, get(record, b), get(record, c), get(record, d)
|
||||||
else
|
else
|
||||||
error("args exceeded")
|
error("args exceeded")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function noop(self: Query, ...: i53): () -> (number, ...any)
|
-- the less creation the better
|
||||||
return function()
|
local function actualNoOperation() end
|
||||||
end :: any
|
local function noop(_self: Query, ...: i53): () -> (number, ...any)
|
||||||
|
return actualNoOperation :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
local EmptyQuery = {
|
local EmptyQuery = {
|
||||||
__iter = noop,
|
__iter = noop;
|
||||||
without = noop
|
without = noop;
|
||||||
}
|
}
|
||||||
EmptyQuery.__index = EmptyQuery
|
EmptyQuery.__index = EmptyQuery
|
||||||
setmetatable(EmptyQuery, EmptyQuery)
|
setmetatable(EmptyQuery, EmptyQuery)
|
||||||
|
@ -396,25 +410,28 @@ setmetatable(EmptyQuery, EmptyQuery)
|
||||||
export type Query = typeof(EmptyQuery)
|
export type Query = typeof(EmptyQuery)
|
||||||
|
|
||||||
function World.query(world: World, ...: i53): Query
|
function World.query(world: World, ...: i53): Query
|
||||||
local compatibleArchetypes = {}
|
-- breaking?
|
||||||
local components = { ... }
|
if (...) == nil then
|
||||||
local archetypes = world.archetypes
|
|
||||||
local queryLength = #components
|
|
||||||
|
|
||||||
if queryLength == 0 then
|
|
||||||
error("Missing components")
|
error("Missing components")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local compatibleArchetypes = {}
|
||||||
|
local length = 0
|
||||||
|
|
||||||
|
local components = {...}
|
||||||
|
local archetypes = world.archetypes
|
||||||
|
local queryLength = #components
|
||||||
|
|
||||||
local firstArchetypeMap
|
local firstArchetypeMap
|
||||||
local componentIndex = world.componentIndex
|
local componentIndex = world.componentIndex
|
||||||
|
|
||||||
for i, componentId in components do
|
for _, componentId in components do
|
||||||
local map = componentIndex[componentId]
|
local map = componentIndex[componentId]
|
||||||
if not map then
|
if not map then
|
||||||
return EmptyQuery
|
return EmptyQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
|
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
|
||||||
firstArchetypeMap = map
|
firstArchetypeMap = map
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -422,110 +439,107 @@ function World.query(world: World, ...: i53): Query
|
||||||
for id in firstArchetypeMap.sparse do
|
for id in firstArchetypeMap.sparse do
|
||||||
local archetype = archetypes[id]
|
local archetype = archetypes[id]
|
||||||
local archetypeRecords = archetype.records
|
local archetypeRecords = archetype.records
|
||||||
local indices = {}
|
local indices = {}
|
||||||
local skip = false
|
local skip = false
|
||||||
|
|
||||||
for i, componentId in components do
|
for i, componentId in components do
|
||||||
local index = archetypeRecords[componentId]
|
local index = archetypeRecords[componentId]
|
||||||
if not index then
|
if not index then
|
||||||
skip = true
|
skip = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
indices[i] = archetypeRecords[componentId]
|
indices[i] = index
|
||||||
end
|
end
|
||||||
|
|
||||||
if skip then
|
if skip then
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
table.insert(compatibleArchetypes, { archetype, indices })
|
|
||||||
|
length += 1
|
||||||
|
compatibleArchetypes[length] = {archetype, indices}
|
||||||
end
|
end
|
||||||
|
|
||||||
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
||||||
if not lastArchetype then
|
if not lastArchetype then
|
||||||
return EmptyQuery
|
return EmptyQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
local preparedQuery = {}
|
local preparedQuery = {}
|
||||||
preparedQuery.__index = preparedQuery
|
preparedQuery.__index = preparedQuery
|
||||||
|
|
||||||
function preparedQuery:without(...)
|
function preparedQuery:without(...)
|
||||||
local components = { ... }
|
local withoutComponents = {...}
|
||||||
for i = #compatibleArchetypes, 1, -1 do
|
for i = #compatibleArchetypes, 1, -1 do
|
||||||
local archetype = compatibleArchetypes[i][1]
|
local archetype = compatibleArchetypes[i][1]
|
||||||
|
local records = archetype.records
|
||||||
local shouldRemove = false
|
local shouldRemove = false
|
||||||
for _, componentId in components do
|
|
||||||
if archetype.records[componentId] then
|
for _, componentId in withoutComponents do
|
||||||
|
if records[componentId] then
|
||||||
shouldRemove = true
|
shouldRemove = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if shouldRemove then
|
|
||||||
|
if shouldRemove then
|
||||||
table.remove(compatibleArchetypes, i)
|
table.remove(compatibleArchetypes, i)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
||||||
if not lastArchetype then
|
if not lastArchetype then
|
||||||
return EmptyQuery
|
return EmptyQuery
|
||||||
end
|
end
|
||||||
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
local lastRow
|
local lastRow
|
||||||
local queryOutput = {}
|
local queryOutput = {}
|
||||||
|
|
||||||
|
function preparedQuery:__iter()
|
||||||
function preparedQuery:__iter()
|
return function()
|
||||||
return function()
|
|
||||||
local archetype = compatibleArchetype[1]
|
local archetype = compatibleArchetype[1]
|
||||||
local row = next(archetype.entities, lastRow)
|
local row = next(archetype.entities, lastRow)
|
||||||
while row == nil do
|
while row == nil do
|
||||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
||||||
if lastArchetype == nil then
|
if lastArchetype == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
archetype = compatibleArchetype[1]
|
archetype = compatibleArchetype[1]
|
||||||
row = next(archetype.entities, row)
|
row = next(archetype.entities, row)
|
||||||
end
|
end
|
||||||
lastRow = row
|
lastRow = row
|
||||||
|
|
||||||
local entityId = archetype.entities[row :: number]
|
local entityId = archetype.entities[row :: number]
|
||||||
local columns = archetype.columns
|
local columns = archetype.columns
|
||||||
local tr = compatibleArchetype[2]
|
local tr = compatibleArchetype[2]
|
||||||
|
|
||||||
if queryLength == 1 then
|
if queryLength == 1 then
|
||||||
return entityId, columns[tr[1]][row]
|
return entityId, columns[tr[1]][row]
|
||||||
elseif queryLength == 2 then
|
elseif queryLength == 2 then
|
||||||
return entityId, columns[tr[1]][row], columns[tr[2]][row]
|
return entityId, columns[tr[1]][row], columns[tr[2]][row]
|
||||||
elseif queryLength == 3 then
|
elseif queryLength == 3 then
|
||||||
return entityId,
|
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
|
||||||
columns[tr[1]][row],
|
elseif queryLength == 4 then
|
||||||
columns[tr[2]][row],
|
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
|
||||||
columns[tr[3]][row]
|
elseif queryLength == 5 then
|
||||||
elseif queryLength == 4 then
|
return entityId,
|
||||||
return entityId,
|
|
||||||
columns[tr[1]][row],
|
|
||||||
columns[tr[2]][row],
|
|
||||||
columns[tr[3]][row],
|
|
||||||
columns[tr[4]][row]
|
|
||||||
elseif queryLength == 5 then
|
|
||||||
return entityId,
|
|
||||||
columns[tr[1]][row],
|
columns[tr[1]][row],
|
||||||
columns[tr[2]][row],
|
columns[tr[2]][row],
|
||||||
columns[tr[3]][row],
|
columns[tr[3]][row],
|
||||||
columns[tr[4]][row],
|
columns[tr[4]][row],
|
||||||
columns[tr[5]][row]
|
columns[tr[5]][row]
|
||||||
elseif queryLength == 6 then
|
elseif queryLength == 6 then
|
||||||
return entityId,
|
return entityId,
|
||||||
columns[tr[1]][row],
|
columns[tr[1]][row],
|
||||||
columns[tr[2]][row],
|
columns[tr[2]][row],
|
||||||
columns[tr[3]][row],
|
columns[tr[3]][row],
|
||||||
columns[tr[4]][row],
|
columns[tr[4]][row],
|
||||||
columns[tr[5]][row],
|
columns[tr[5]][row],
|
||||||
columns[tr[6]][row]
|
columns[tr[6]][row]
|
||||||
elseif queryLength == 7 then
|
elseif queryLength == 7 then
|
||||||
return entityId,
|
return entityId,
|
||||||
columns[tr[1]][row],
|
columns[tr[1]][row],
|
||||||
columns[tr[2]][row],
|
columns[tr[2]][row],
|
||||||
columns[tr[3]][row],
|
columns[tr[3]][row],
|
||||||
|
@ -533,8 +547,8 @@ function World.query(world: World, ...: i53): Query
|
||||||
columns[tr[5]][row],
|
columns[tr[5]][row],
|
||||||
columns[tr[6]][row],
|
columns[tr[6]][row],
|
||||||
columns[tr[7]][row]
|
columns[tr[7]][row]
|
||||||
elseif queryLength == 8 then
|
elseif queryLength == 8 then
|
||||||
return entityId,
|
return entityId,
|
||||||
columns[tr[1]][row],
|
columns[tr[1]][row],
|
||||||
columns[tr[2]][row],
|
columns[tr[2]][row],
|
||||||
columns[tr[3]][row],
|
columns[tr[3]][row],
|
||||||
|
@ -545,8 +559,8 @@ function World.query(world: World, ...: i53): Query
|
||||||
columns[tr[8]][row]
|
columns[tr[8]][row]
|
||||||
end
|
end
|
||||||
|
|
||||||
for i in components do
|
for i in components do
|
||||||
queryOutput[i] = tr[i][row]
|
queryOutput[i] = columns[tr[i]][row]
|
||||||
end
|
end
|
||||||
|
|
||||||
return entityId, unpack(queryOutput, 1, queryLength)
|
return entityId, unpack(queryOutput, 1, queryLength)
|
||||||
|
@ -556,23 +570,24 @@ function World.query(world: World, ...: i53): Query
|
||||||
return setmetatable({}, preparedQuery) :: any
|
return setmetatable({}, preparedQuery) :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.component(world: World)
|
function World.component(world: World)
|
||||||
local componentId = world.nextComponentId + 1
|
local componentId = world.nextComponentId + 1
|
||||||
if componentId > HI_COMPONENT_ID then
|
if componentId > HI_COMPONENT_ID then
|
||||||
-- IDs are partitioned into ranges because component IDs are not nominal,
|
-- IDs are partitioned into ranges because component IDs are not nominal,
|
||||||
-- so it needs to error when IDs intersect into the entity range.
|
-- so it needs to error when IDs intersect into the entity range.
|
||||||
error("Too many components, consider using world:entity() instead to create components.")
|
error("Too many components, consider using world:entity() instead to create components.")
|
||||||
end
|
end
|
||||||
world.nextComponentId = componentId
|
world.nextComponentId = componentId
|
||||||
return componentId
|
return componentId
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.entity(world: World)
|
function World.entity(world: World)
|
||||||
world.nextEntityId += 1
|
local nextEntityId = world.nextEntityId + 1
|
||||||
return world.nextEntityId + REST
|
world.nextEntityId = nextEntityId
|
||||||
|
return nextEntityId + REST
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.delete(world: World, entityId: i53)
|
function World.delete(world: World, entityId: i53)
|
||||||
local entityIndex = world.entityIndex
|
local entityIndex = world.entityIndex
|
||||||
local record = entityIndex[entityId]
|
local record = entityIndex[entityId]
|
||||||
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
|
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
|
||||||
|
@ -584,57 +599,61 @@ function World.delete(world: World, entityId: i53)
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.observer(world: World, ...)
|
function World.observer(world: World, ...)
|
||||||
local componentIds = { ... }
|
local componentIds = {...}
|
||||||
|
local idsCount = #componentIds
|
||||||
|
local hooks = world.hooks
|
||||||
|
|
||||||
return {
|
return {
|
||||||
event = function(event)
|
event = function(event)
|
||||||
local hook = world.hooks[event]
|
local hook = hooks[event]
|
||||||
world.hooks[event] = nil
|
hooks[event] = nil
|
||||||
|
|
||||||
local last, change
|
local last, change
|
||||||
return function()
|
return function()
|
||||||
last, change = next(hook, last)
|
last, change = next(hook, last)
|
||||||
if not last then
|
if not last then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local matched = false
|
local matched = false
|
||||||
|
local ids = change.ids
|
||||||
while not matched do
|
|
||||||
|
while not matched do
|
||||||
local skip = false
|
local skip = false
|
||||||
for _, id in change.ids do
|
for _, id in ids do
|
||||||
if not table.find(componentIds, id) then
|
if not table.find(componentIds, id) then
|
||||||
skip = true
|
skip = true
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if skip then
|
if skip then
|
||||||
last, change = next(hook, last)
|
last, change = next(hook, last)
|
||||||
|
ids = change.ids
|
||||||
continue
|
continue
|
||||||
end
|
end
|
||||||
|
|
||||||
matched = true
|
matched = true
|
||||||
end
|
end
|
||||||
|
|
||||||
local queryOutput = {}
|
local queryOutput = table.create(idsCount)
|
||||||
local row = change.offset
|
local row = change.offset
|
||||||
local archetype = change.archetype
|
local archetype = change.archetype
|
||||||
local columns = archetype.columns
|
local columns = archetype.columns
|
||||||
local archetypeRecords = archetype.records
|
local archetypeRecords = archetype.records
|
||||||
for _, id in componentIds do
|
for index, id in componentIds do
|
||||||
table.insert(queryOutput, columns[archetypeRecords[id]][row])
|
queryOutput[index] = columns[archetypeRecords[id]][row]
|
||||||
end
|
end
|
||||||
|
|
||||||
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
|
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
|
||||||
end
|
end
|
||||||
end
|
end;
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
return table.freeze({
|
return table.freeze({
|
||||||
World = World,
|
World = World;
|
||||||
ON_ADD = ON_ADD,
|
ON_ADD = ON_ADD;
|
||||||
ON_REMOVE = ON_REMOVE,
|
ON_REMOVE = ON_REMOVE;
|
||||||
ON_SET = ON_SET
|
ON_SET = ON_SET;
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,9 +11,6 @@
|
||||||
},
|
},
|
||||||
"ReplicatedStorage": {
|
"ReplicatedStorage": {
|
||||||
"$className": "ReplicatedStorage",
|
"$className": "ReplicatedStorage",
|
||||||
"DevPackages": {
|
|
||||||
"$path": "DevPackages"
|
|
||||||
},
|
|
||||||
"Lib": {
|
"Lib": {
|
||||||
"$path": "lib"
|
"$path": "lib"
|
||||||
},
|
},
|
||||||
|
@ -25,6 +22,9 @@
|
||||||
},
|
},
|
||||||
"mirror": {
|
"mirror": {
|
||||||
"$path": "mirror"
|
"$path": "mirror"
|
||||||
|
},
|
||||||
|
"DevPackages": {
|
||||||
|
"$path": "DevPackages"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"TestService": {
|
"TestService": {
|
||||||
|
|
115
tests/test1.lua
115
tests/test1.lua
|
@ -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()
|
|
261
tests/world.lua
Normal file
261
tests/world.lua
Normal file
|
@ -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()
|
|
@ -1,12 +1,10 @@
|
||||||
[package]
|
[package]
|
||||||
name = "ukendio/jecs"
|
name = "ukendio/jecs"
|
||||||
version = "0.0.0-prototype.rc.3"
|
version = "0.1.0-rc.6"
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
realm = "shared"
|
realm = "shared"
|
||||||
|
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
|
||||||
exclude = ["**"]
|
exclude = ["**"]
|
||||||
include = ["default.project.json", "lib", "wally.toml", "README.md"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
TestEZ = "roblox/testez@0.4.1"
|
TestEZ = "roblox/testez@0.4.1"
|
||||||
Matter = "matter-ecs/matter@0.8.0"
|
|
||||||
ecr = "centau/ecr@0.8.0"
|
|
Loading…
Reference in a new issue