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