mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 09:30:03 +00:00
Merge branch 'main' into add-delete-entity
This commit is contained in:
commit
60cfb70c1c
19 changed files with 1195 additions and 3781 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -50,3 +50,6 @@ WallyPatches
|
|||
roblox.toml
|
||||
sourcemap.json
|
||||
drafts/*.lua
|
||||
|
||||
*.code-workspace
|
||||
roblox.yml
|
||||
|
|
|
@ -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)
|
||||
|
|
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
|
|
@ -1,35 +1,33 @@
|
|||
--!optimize 2
|
||||
--!native
|
||||
|
||||
local testkit = require('../testkit')
|
||||
local testkit = require("../testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
local function TITLE(title: string)
|
||||
print()
|
||||
print(testkit.color.white(title))
|
||||
end
|
||||
|
||||
local jecs = require("../mirror/init")
|
||||
local jecs = require("../lib/init")
|
||||
local mirror = require("../mirror/init")
|
||||
|
||||
local oldMatter = require("../oldMatter")
|
||||
|
||||
local newMatter = require("../newMatter")
|
||||
type i53 = number
|
||||
|
||||
do TITLE (testkit.color.white_underline("Jecs query"))
|
||||
do
|
||||
TITLE(testkit.color.white_underline("Jecs query"))
|
||||
local ecs = jecs.World.new()
|
||||
do TITLE "one component in common"
|
||||
|
||||
local function view_bench(
|
||||
world: jecs.World,
|
||||
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
|
||||
)
|
||||
do
|
||||
TITLE("one component in common")
|
||||
|
||||
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
|
||||
BENCH("1 component", function()
|
||||
for _ in world:query(A) do end
|
||||
for _ in world:query(A) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("2 component", function()
|
||||
for _ in world:query(A, B) do end
|
||||
for _ in world:query(A, B) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("4 component", function()
|
||||
|
@ -38,7 +36,8 @@ do TITLE (testkit.color.white_underline("Jecs query"))
|
|||
end)
|
||||
|
||||
BENCH("8 component", function()
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do end
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -89,7 +88,6 @@ do TITLE (testkit.color.white_underline("Jecs query"))
|
|||
if flip() then
|
||||
combination ..= "H"
|
||||
ecs:set(entity, D8, {value = true})
|
||||
|
||||
end
|
||||
|
||||
if #combination == 7 then
|
||||
|
@ -100,29 +98,29 @@ do TITLE (testkit.color.white_underline("Jecs query"))
|
|||
end
|
||||
|
||||
local a = 0
|
||||
for _ in archetypes do a+= 1 end
|
||||
for _ in archetypes do
|
||||
a += 1
|
||||
end
|
||||
|
||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
||||
end
|
||||
end
|
||||
|
||||
do TITLE(testkit.color.white_underline("OldMatter query"))
|
||||
|
||||
local ecs = oldMatter.World.new()
|
||||
local component = oldMatter.component
|
||||
|
||||
do TITLE "one component in common"
|
||||
local function view_bench(
|
||||
world: jecs.World,
|
||||
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
|
||||
)
|
||||
do
|
||||
TITLE(testkit.color.white_underline("Mirror query"))
|
||||
local ecs = mirror.World.new()
|
||||
do
|
||||
TITLE("one component in common")
|
||||
|
||||
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
|
||||
BENCH("1 component", function()
|
||||
for _ in world:query(A) do end
|
||||
for _ in world:query(A) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("2 component", function()
|
||||
for _ in world:query(A, B) do end
|
||||
for _ in world:query(A, B) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("4 component", function()
|
||||
|
@ -131,18 +129,19 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
|
|||
end)
|
||||
|
||||
BENCH("8 component", function()
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do end
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local D1 = component()
|
||||
local D2 = component()
|
||||
local D3 = component()
|
||||
local D4 = component()
|
||||
local D5 = component()
|
||||
local D6 = component()
|
||||
local D7 = component()
|
||||
local D8 = component()
|
||||
local D1 = ecs:component()
|
||||
local D2 = ecs:component()
|
||||
local D3 = ecs:component()
|
||||
local D4 = ecs:component()
|
||||
local D5 = ecs:component()
|
||||
local D6 = ecs:component()
|
||||
local D7 = ecs:component()
|
||||
local D8 = ecs:component()
|
||||
|
||||
local function flip()
|
||||
return math.random() >= 0.15
|
||||
|
@ -151,149 +150,51 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
|
|||
local added = 0
|
||||
local archetypes = {}
|
||||
for i = 1, 2 ^ 16 - 2 do
|
||||
local entity = ecs:spawn()
|
||||
local entity = ecs:entity()
|
||||
|
||||
local combination = ""
|
||||
|
||||
if flip() then
|
||||
combination ..= "B"
|
||||
ecs:insert(entity, D2({value = true}))
|
||||
ecs:set(entity, D2, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "C"
|
||||
ecs:insert(entity, D3({value = true}))
|
||||
ecs:set(entity, D3, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "D"
|
||||
ecs:insert(entity, D4({value = true}))
|
||||
ecs:set(entity, D4, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "E"
|
||||
ecs:insert(entity, D5({value = true}))
|
||||
ecs:set(entity, D5, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "F"
|
||||
ecs:insert(entity, D6({value = true}))
|
||||
|
||||
ecs:set(entity, D6, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "G"
|
||||
ecs:insert(entity, D7({value = true}))
|
||||
|
||||
ecs:set(entity, D7, {value = true})
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "H"
|
||||
ecs:insert(entity, D8({value = true}))
|
||||
ecs:set(entity, D8, {value = true})
|
||||
end
|
||||
|
||||
if #combination == 7 then
|
||||
added += 1
|
||||
ecs:insert(entity, D1({value = true}))
|
||||
|
||||
ecs:set(entity, D1, {value = true})
|
||||
end
|
||||
archetypes[combination] = true
|
||||
end
|
||||
|
||||
local a = 0
|
||||
for _ in archetypes do a+= 1 end
|
||||
for _ in archetypes do
|
||||
a += 1
|
||||
end
|
||||
|
||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
do TITLE(testkit.color.white_underline("NewMatter query"))
|
||||
|
||||
local ecs = newMatter.World.new()
|
||||
local component = newMatter.component
|
||||
|
||||
do TITLE "one component in common"
|
||||
local function view_bench(
|
||||
world: jecs.World,
|
||||
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
|
||||
)
|
||||
|
||||
BENCH("1 component", function()
|
||||
for _ in world:query(A) do end
|
||||
end)
|
||||
|
||||
BENCH("2 component", function()
|
||||
for _ in world:query(A, B) do end
|
||||
end)
|
||||
|
||||
BENCH("4 component", function()
|
||||
for _ in world:query(A, B, C, D) do
|
||||
end
|
||||
end)
|
||||
|
||||
BENCH("8 component", function()
|
||||
for _ in world:query(A, B, C, D, E, F, G, H) do end
|
||||
end)
|
||||
end
|
||||
|
||||
local D1 = component()
|
||||
local D2 = component()
|
||||
local D3 = component()
|
||||
local D4 = component()
|
||||
local D5 = component()
|
||||
local D6 = component()
|
||||
local D7 = component()
|
||||
local D8 = component()
|
||||
|
||||
local function flip()
|
||||
return math.random() >= 0.15
|
||||
end
|
||||
|
||||
local added = 0
|
||||
local archetypes = {}
|
||||
for i = 1, 2^16-2 do
|
||||
local entity = ecs:spawn()
|
||||
|
||||
local combination = ""
|
||||
|
||||
if flip() then
|
||||
combination ..= "B"
|
||||
ecs:insert(entity, D2({value = true}))
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "C"
|
||||
ecs:insert(entity, D3({value = true}))
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "D"
|
||||
ecs:insert(entity, D4({value = true}))
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "E"
|
||||
ecs:insert(entity, D5({value = true}))
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "F"
|
||||
ecs:insert(entity, D6({value = true}))
|
||||
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "G"
|
||||
ecs:insert(entity, D7({value = true}))
|
||||
|
||||
end
|
||||
if flip() then
|
||||
combination ..= "H"
|
||||
ecs:insert(entity, D8({value = true}))
|
||||
end
|
||||
|
||||
if #combination == 7 then
|
||||
added += 1
|
||||
ecs:insert(entity, D1({value = true}))
|
||||
|
||||
end
|
||||
archetypes[combination] = true
|
||||
end
|
||||
|
||||
local a = 0
|
||||
for _ in archetypes do a+= 1 end
|
||||
|
||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
||||
end
|
||||
|
||||
end
|
|
@ -2,41 +2,37 @@
|
|||
--!native
|
||||
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
local rgb = require(ReplicatedStorage.rgb)
|
||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||
local jecs = require(ReplicatedStorage.Lib)
|
||||
local rgb = require(ReplicatedStorage.rgb)
|
||||
local newWorld = Matter.World.new()
|
||||
local ecs = jecs.World.new()
|
||||
|
||||
|
||||
return {
|
||||
ParameterGenerator = function()
|
||||
local registry2 = ecr.registry()
|
||||
|
||||
return registry2
|
||||
end,
|
||||
end;
|
||||
|
||||
Functions = {
|
||||
Matter = function()
|
||||
for i = 1, 1000 do
|
||||
newWorld:spawn()
|
||||
end
|
||||
end,
|
||||
|
||||
end;
|
||||
|
||||
ECR = function(_, registry2)
|
||||
for i = 1, 1000 do
|
||||
registry2.create()
|
||||
end
|
||||
end,
|
||||
|
||||
end;
|
||||
|
||||
Jecs = function()
|
||||
for i = 1, 1000 do
|
||||
ecs:entity()
|
||||
end
|
||||
end
|
||||
|
||||
},
|
||||
end;
|
||||
};
|
||||
}
|
||||
|
|
6
jecs_darkmode.svg
Normal file
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 |
307
lib/init.lua
307
lib/init.lua
|
@ -82,24 +82,27 @@ local function transitionArchetype(
|
|||
end
|
||||
|
||||
-- Move the entity from the source to the destination archetype.
|
||||
destinationEntities[destinationRow] = sourceEntities[sourceRow]
|
||||
entityIndex[sourceEntities[sourceRow]].row = destinationRow
|
||||
local atSourceRow = sourceEntities[sourceRow]
|
||||
destinationEntities[destinationRow] = atSourceRow
|
||||
entityIndex[atSourceRow].row = destinationRow
|
||||
|
||||
-- Because we have swapped columns we now have to update the records
|
||||
-- corresponding to the entities' rows that were swapped.
|
||||
local movedAway = #sourceEntities
|
||||
if sourceRow ~= movedAway then
|
||||
sourceEntities[sourceRow] = sourceEntities[movedAway]
|
||||
entityIndex[sourceEntities[movedAway]].row = sourceRow
|
||||
local atMovedAway = sourceEntities[movedAway]
|
||||
sourceEntities[sourceRow] = atMovedAway
|
||||
entityIndex[atMovedAway].row = sourceRow
|
||||
end
|
||||
|
||||
sourceEntities[movedAway] = nil
|
||||
end
|
||||
|
||||
local function archetypeAppend(entity: i53, archetype: Archetype): i24
|
||||
local function archetypeAppend(entity: number, archetype: Archetype): number
|
||||
local entities = archetype.entities
|
||||
table.insert(entities, entity)
|
||||
return #entities
|
||||
local length = #entities + 1
|
||||
entities[length] = entity
|
||||
return length
|
||||
end
|
||||
|
||||
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
|
||||
|
@ -122,47 +125,49 @@ local function hash(arr): string | number
|
|||
return table.concat(arr, "_")
|
||||
end
|
||||
|
||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
||||
local destinationCount = #to.types
|
||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
|
||||
local destinationIds = to.types
|
||||
local records = to.records
|
||||
local id = to.id
|
||||
|
||||
for i = 1, destinationCount do
|
||||
local destinationId = destinationIds[i]
|
||||
for i, destinationId in destinationIds do
|
||||
local archetypesMap = componentIndex[destinationId]
|
||||
|
||||
if not componentIndex[destinationId] then
|
||||
componentIndex[destinationId] = { size = 0, sparse = {} }
|
||||
if not archetypesMap then
|
||||
archetypesMap = {size = 0, sparse = {}}
|
||||
componentIndex[destinationId] = archetypesMap
|
||||
end
|
||||
|
||||
local archetypesMap = componentIndex[destinationId]
|
||||
archetypesMap.sparse[to.id] = i
|
||||
to.records[destinationId] = i
|
||||
archetypesMap.sparse[id] = i
|
||||
records[destinationId] = i
|
||||
end
|
||||
end
|
||||
|
||||
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
|
||||
local ty = hash(types)
|
||||
|
||||
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
|
||||
local id = world.nextArchetypeId
|
||||
local id = world.nextArchetypeId + 1
|
||||
world.nextArchetypeId = id
|
||||
|
||||
local columns = {} :: { any }
|
||||
local length = #types
|
||||
local columns = table.create(length) :: {any}
|
||||
|
||||
for _ in types do
|
||||
table.insert(columns, {})
|
||||
for index in types do
|
||||
columns[index] = {}
|
||||
end
|
||||
|
||||
local archetype = {
|
||||
id = id,
|
||||
types = types,
|
||||
type = ty,
|
||||
columns = columns,
|
||||
entities = {},
|
||||
edges = {},
|
||||
records = {},
|
||||
columns = columns;
|
||||
edges = {};
|
||||
entities = {};
|
||||
id = id;
|
||||
records = {};
|
||||
type = ty;
|
||||
types = types;
|
||||
}
|
||||
world.archetypeIndex[ty] = archetype
|
||||
world.archetypes[id] = archetype
|
||||
if #types > 0 then
|
||||
if length > 0 then
|
||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
||||
end
|
||||
|
||||
|
@ -173,18 +178,19 @@ local World = {}
|
|||
World.__index = World
|
||||
function World.new()
|
||||
local self = setmetatable({
|
||||
entityIndex = {},
|
||||
componentIndex = {},
|
||||
archetypes = {},
|
||||
archetypeIndex = {},
|
||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||
nextEntityId = 0,
|
||||
nextComponentId = 0,
|
||||
nextArchetypeId = 0,
|
||||
archetypeIndex = {};
|
||||
archetypes = {};
|
||||
componentIndex = {};
|
||||
entityIndex = {};
|
||||
hooks = {
|
||||
[ON_ADD] = {}
|
||||
}
|
||||
[ON_ADD] = {};
|
||||
};
|
||||
nextArchetypeId = 0;
|
||||
nextComponentId = 0;
|
||||
nextEntityId = 0;
|
||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
||||
}, World)
|
||||
self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
|
||||
return self
|
||||
end
|
||||
|
||||
|
@ -192,21 +198,21 @@ local function emit(world, eventDescription)
|
|||
local event = eventDescription.event
|
||||
|
||||
table.insert(world.hooks[event], {
|
||||
ids = eventDescription.ids,
|
||||
archetype = eventDescription.archetype,
|
||||
otherArchetype = eventDescription.otherArchetype,
|
||||
offset = eventDescription.offset
|
||||
archetype = eventDescription.archetype;
|
||||
ids = eventDescription.ids;
|
||||
offset = eventDescription.offset;
|
||||
otherArchetype = eventDescription.otherArchetype;
|
||||
})
|
||||
end
|
||||
|
||||
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
||||
if #added > 0 then
|
||||
emit(world, {
|
||||
event = ON_ADD,
|
||||
ids = added,
|
||||
archetype = archetype,
|
||||
otherArchetype = otherArchetype,
|
||||
offset = row,
|
||||
archetype = archetype;
|
||||
event = ON_ADD;
|
||||
ids = added;
|
||||
offset = row;
|
||||
otherArchetype = otherArchetype;
|
||||
})
|
||||
end
|
||||
end
|
||||
|
@ -228,9 +234,7 @@ local function ensureArchetype(world: World, types, prev)
|
|||
end
|
||||
|
||||
local function findInsert(types: {i53}, toAdd: i53)
|
||||
local count = #types
|
||||
for i = 1, count do
|
||||
local id = types[i]
|
||||
for i, id in types do
|
||||
if id == toAdd then
|
||||
return -1
|
||||
end
|
||||
|
@ -238,7 +242,7 @@ local function findInsert(types: { i53 }, toAdd: i53)
|
|||
return i
|
||||
end
|
||||
end
|
||||
return count + 1
|
||||
return #types + 1
|
||||
end
|
||||
|
||||
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
||||
|
@ -259,40 +263,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 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
|
||||
end
|
||||
local edge = ensureEdge(from, componentId)
|
||||
from = from or world.ROOT_ARCHETYPE
|
||||
|
||||
if not edge.add then
|
||||
local edge = ensureEdge(from, componentId)
|
||||
local add = edge.add
|
||||
if not add then
|
||||
-- Save an edge using the component ID to the archetype to allow
|
||||
-- faster traversals to adjacent archetypes.
|
||||
edge.add = findArchetypeWith(world, from, componentId)
|
||||
add = findArchetypeWith(world, from, componentId)
|
||||
edge.add = add :: never
|
||||
end
|
||||
|
||||
return edge.add
|
||||
return add
|
||||
end
|
||||
|
||||
local function ensureRecord(entityIndex, entityId: i53): Record
|
||||
local id = entityId
|
||||
if not entityIndex[id] then
|
||||
entityIndex[id] = {}
|
||||
end
|
||||
return entityIndex[id] :: Record
|
||||
local record = entityIndex[entityId]
|
||||
|
||||
if not record then
|
||||
record = {}
|
||||
entityIndex[entityId] = record
|
||||
end
|
||||
|
||||
return record :: Record
|
||||
end
|
||||
|
||||
|
||||
function World.add(world: World, entityId: i53, componentId: i53)
|
||||
local record = ensureRecord(world.entityIndex, entityId)
|
||||
local from = record.archetype
|
||||
local to = archetypeTraverseAdd(world, componentId, from)
|
||||
if from then
|
||||
moveEntity(world.entityIndex, entityId, record, to)
|
||||
else
|
||||
if #to.types > 0 then
|
||||
newEntity(entityId, record, to)
|
||||
onNotifyAdd(world, to, from, record.row, { componentId })
|
||||
end
|
||||
end
|
||||
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 from = record.archetype
|
||||
|
@ -326,28 +347,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
|
|||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
||||
local edge = ensureEdge(from, componentId)
|
||||
|
||||
|
||||
if not edge.remove then
|
||||
local remove = edge.remove
|
||||
if not remove then
|
||||
local to = table.clone(from.types)
|
||||
table.remove(to, table.find(to, componentId))
|
||||
edge.remove = ensureArchetype(world, to, from)
|
||||
remove = ensureArchetype(world, to, from)
|
||||
edge.remove = remove :: never
|
||||
end
|
||||
|
||||
return edge.remove
|
||||
return remove
|
||||
end
|
||||
|
||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||
local record = ensureRecord(world.entityIndex, entityId)
|
||||
local entityIndex = world.entityIndex
|
||||
local record = ensureRecord(entityIndex, entityId)
|
||||
local sourceArchetype = record.archetype
|
||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
||||
|
||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
||||
moveEntity(entityIndex, entityId, record, destinationArchetype)
|
||||
end
|
||||
end
|
||||
|
||||
-- Keeping the function as small as possible to enable inlining
|
||||
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
|
||||
local function get(record: Record, componentId: i24)
|
||||
local archetype = record.archetype
|
||||
local archetypeRecord = archetype.records[componentId]
|
||||
|
||||
|
@ -360,35 +383,35 @@ end
|
|||
|
||||
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
||||
local id = entityId
|
||||
local componentIndex = world.componentIndex
|
||||
local record = world.entityIndex[id]
|
||||
if not record then
|
||||
return nil
|
||||
end
|
||||
|
||||
local va = get(componentIndex, record, a)
|
||||
local va = get(record, a)
|
||||
|
||||
if b == nil then
|
||||
return va
|
||||
elseif c == nil then
|
||||
return va, get(componentIndex, record, b)
|
||||
return va, get(record, b)
|
||||
elseif d == nil then
|
||||
return va, get(componentIndex, record, b), get(componentIndex, record, c)
|
||||
return va, get(record, b), get(record, c)
|
||||
elseif e == nil then
|
||||
return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
|
||||
return va, get(record, b), get(record, c), get(record, d)
|
||||
else
|
||||
error("args exceeded")
|
||||
end
|
||||
end
|
||||
|
||||
local function noop(self: Query, ...: i53): () -> (number, ...any)
|
||||
return function()
|
||||
end :: any
|
||||
-- the less creation the better
|
||||
local function actualNoOperation() end
|
||||
local function noop(_self: Query, ...: i53): () -> (number, ...any)
|
||||
return actualNoOperation :: any
|
||||
end
|
||||
|
||||
local EmptyQuery = {
|
||||
__iter = noop,
|
||||
without = noop
|
||||
__iter = noop;
|
||||
without = noop;
|
||||
}
|
||||
EmptyQuery.__index = EmptyQuery
|
||||
setmetatable(EmptyQuery, EmptyQuery)
|
||||
|
@ -396,19 +419,22 @@ setmetatable(EmptyQuery, EmptyQuery)
|
|||
export type Query = typeof(EmptyQuery)
|
||||
|
||||
function World.query(world: World, ...: i53): Query
|
||||
-- breaking?
|
||||
if (...) == nil then
|
||||
error("Missing components")
|
||||
end
|
||||
|
||||
local compatibleArchetypes = {}
|
||||
local length = 0
|
||||
|
||||
local components = {...}
|
||||
local archetypes = world.archetypes
|
||||
local queryLength = #components
|
||||
|
||||
if queryLength == 0 then
|
||||
error("Missing components")
|
||||
end
|
||||
|
||||
local firstArchetypeMap
|
||||
local componentIndex = world.componentIndex
|
||||
|
||||
for i, componentId in components do
|
||||
for _, componentId in components do
|
||||
local map = componentIndex[componentId]
|
||||
if not map then
|
||||
return EmptyQuery
|
||||
|
@ -431,13 +457,15 @@ function World.query(world: World, ...: i53): Query
|
|||
skip = true
|
||||
break
|
||||
end
|
||||
indices[i] = archetypeRecords[componentId]
|
||||
indices[i] = index
|
||||
end
|
||||
|
||||
if skip then
|
||||
continue
|
||||
end
|
||||
table.insert(compatibleArchetypes, { archetype, indices })
|
||||
|
||||
length += 1
|
||||
compatibleArchetypes[length] = {archetype, indices}
|
||||
end
|
||||
|
||||
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
||||
|
@ -449,16 +477,19 @@ function World.query(world: World, ...: i53): Query
|
|||
preparedQuery.__index = preparedQuery
|
||||
|
||||
function preparedQuery:without(...)
|
||||
local components = { ... }
|
||||
local withoutComponents = {...}
|
||||
for i = #compatibleArchetypes, 1, -1 do
|
||||
local archetype = compatibleArchetypes[i][1]
|
||||
local records = archetype.records
|
||||
local shouldRemove = false
|
||||
for _, componentId in components do
|
||||
if archetype.records[componentId] then
|
||||
|
||||
for _, componentId in withoutComponents do
|
||||
if records[componentId] then
|
||||
shouldRemove = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if shouldRemove then
|
||||
table.remove(compatibleArchetypes, i)
|
||||
end
|
||||
|
@ -475,7 +506,6 @@ function World.query(world: World, ...: i53): Query
|
|||
local lastRow
|
||||
local queryOutput = {}
|
||||
|
||||
|
||||
function preparedQuery:__iter()
|
||||
return function()
|
||||
local archetype = compatibleArchetype[1]
|
||||
|
@ -499,16 +529,9 @@ function World.query(world: World, ...: i53): Query
|
|||
elseif queryLength == 2 then
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row]
|
||||
elseif queryLength == 3 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row]
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
|
||||
elseif queryLength == 4 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row],
|
||||
columns[tr[4]][row]
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
|
||||
elseif queryLength == 5 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
|
@ -546,7 +569,7 @@ function World.query(world: World, ...: i53): Query
|
|||
end
|
||||
|
||||
for i in components do
|
||||
queryOutput[i] = tr[i][row]
|
||||
queryOutput[i] = columns[tr[i]][row]
|
||||
end
|
||||
|
||||
return entityId, unpack(queryOutput, 1, queryLength)
|
||||
|
@ -568,8 +591,9 @@ function World.component(world: World)
|
|||
end
|
||||
|
||||
function World.entity(world: World)
|
||||
world.nextEntityId += 1
|
||||
return world.nextEntityId + REST
|
||||
local nextEntityId = world.nextEntityId + 1
|
||||
world.nextEntityId = nextEntityId
|
||||
return nextEntityId + REST
|
||||
end
|
||||
|
||||
-- should reuse this logic in World.set instead of swap removing in transition archetype
|
||||
|
@ -621,11 +645,13 @@ end
|
|||
|
||||
function World.observer(world: World, ...)
|
||||
local componentIds = {...}
|
||||
local idsCount = #componentIds
|
||||
local hooks = world.hooks
|
||||
|
||||
return {
|
||||
event = function(event)
|
||||
local hook = world.hooks[event]
|
||||
world.hooks[event] = nil
|
||||
local hook = hooks[event]
|
||||
hooks[event] = nil
|
||||
|
||||
local last, change
|
||||
return function()
|
||||
|
@ -635,10 +661,11 @@ function World.observer(world: World, ...)
|
|||
end
|
||||
|
||||
local matched = false
|
||||
local ids = change.ids
|
||||
|
||||
while not matched do
|
||||
local skip = false
|
||||
for _, id in change.ids do
|
||||
for _, id in ids do
|
||||
if not table.find(componentIds, id) then
|
||||
skip = true
|
||||
break
|
||||
|
@ -647,30 +674,62 @@ function World.observer(world: World, ...)
|
|||
|
||||
if skip then
|
||||
last, change = next(hook, last)
|
||||
ids = change.ids
|
||||
continue
|
||||
end
|
||||
|
||||
matched = true
|
||||
end
|
||||
|
||||
local queryOutput = {}
|
||||
local queryOutput = table.create(idsCount)
|
||||
local row = change.offset
|
||||
local archetype = change.archetype
|
||||
local columns = archetype.columns
|
||||
local archetypeRecords = archetype.records
|
||||
for _, id in componentIds do
|
||||
table.insert(queryOutput, columns[archetypeRecords[id]][row])
|
||||
for index, id in componentIds do
|
||||
queryOutput[index] = columns[archetypeRecords[id]][row]
|
||||
end
|
||||
|
||||
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
|
||||
end
|
||||
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
|
||||
end
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
function World.__iter(world: World): () -> (number?, unknown?)
|
||||
local entityIndex = world.entityIndex
|
||||
local last
|
||||
|
||||
return function()
|
||||
local entity, record = next(entityIndex, last)
|
||||
if not entity then
|
||||
return
|
||||
end
|
||||
last = entity
|
||||
|
||||
local archetype = record.archetype
|
||||
if not archetype then
|
||||
-- Returns only the entity id as an entity without data should not return
|
||||
-- data and allow the user to get an error if they don't handle the case.
|
||||
return entity
|
||||
end
|
||||
|
||||
local row = record.row
|
||||
local types = archetype.types
|
||||
local columns = archetype.columns
|
||||
local entityData = {}
|
||||
for i, column in columns do
|
||||
-- We use types because the key should be the component ID not the column index
|
||||
entityData[types[i]] = column[row]
|
||||
end
|
||||
|
||||
return entity, entityData
|
||||
end
|
||||
end
|
||||
|
||||
return table.freeze({
|
||||
World = World,
|
||||
ON_ADD = ON_ADD,
|
||||
ON_REMOVE = ON_REMOVE,
|
||||
ON_SET = ON_SET
|
||||
World = World;
|
||||
ON_ADD = ON_ADD;
|
||||
ON_REMOVE = ON_REMOVE;
|
||||
ON_SET = ON_SET;
|
||||
})
|
||||
|
|
|
@ -299,5 +299,38 @@ return function()
|
|||
expect(world:get(id, Poison)).to.never.be.ok()
|
||||
expect(world:get(id, Health)).to.never.be.ok()
|
||||
end)
|
||||
|
||||
it("should allow iterating the whole world", function()
|
||||
local world = jecs.World.new()
|
||||
|
||||
local A, B = world:entity(), world:entity()
|
||||
|
||||
local eA = world:entity()
|
||||
world:set(eA, A, true)
|
||||
local eB = world:entity()
|
||||
world:set(eB, B, true)
|
||||
local eAB = world:entity()
|
||||
world:set(eAB, A, true)
|
||||
world:set(eAB, B, true)
|
||||
|
||||
local count = 0
|
||||
for id, data in world do
|
||||
count += 1
|
||||
if id == eA then
|
||||
expect(data[A]).to.be.ok()
|
||||
expect(data[B]).to.never.be.ok()
|
||||
elseif id == eB then
|
||||
expect(data[B]).to.be.ok()
|
||||
expect(data[A]).to.never.be.ok()
|
||||
elseif id == eAB then
|
||||
expect(data[A]).to.be.ok()
|
||||
expect(data[B]).to.be.ok()
|
||||
else
|
||||
error("unknown entity", id)
|
||||
end
|
||||
end
|
||||
|
||||
expect(count).to.equal(3)
|
||||
end)
|
||||
end)
|
||||
end
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
357
mirror/init.lua
357
mirror/init.lua
|
@ -51,38 +51,58 @@ local REST = HI_COMPONENT_ID + 4
|
|||
|
||||
local function transitionArchetype(
|
||||
entityIndex: EntityIndex,
|
||||
destinationArchetype: Archetype,
|
||||
to: Archetype,
|
||||
destinationRow: i24,
|
||||
sourceArchetype: Archetype,
|
||||
from: Archetype,
|
||||
sourceRow: i24
|
||||
)
|
||||
local columns = sourceArchetype.columns
|
||||
local sourceEntities = sourceArchetype.entities
|
||||
local destinationEntities = destinationArchetype.entities
|
||||
local destinationColumns = destinationArchetype.columns
|
||||
local columns = from.columns
|
||||
local sourceEntities = from.entities
|
||||
local destinationEntities = to.entities
|
||||
local destinationColumns = to.columns
|
||||
local tr = to.records
|
||||
local types = from.types
|
||||
|
||||
for componentId, column in columns do
|
||||
local targetColumn = destinationColumns[componentId]
|
||||
for i, column in columns do
|
||||
-- Retrieves the new column index from the source archetype's record from each component
|
||||
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
|
||||
local targetColumn = destinationColumns[tr[types[i]]]
|
||||
|
||||
-- Sometimes target column may not exist, e.g. when you remove a component.
|
||||
if targetColumn then
|
||||
targetColumn[destinationRow] = column[sourceRow]
|
||||
end
|
||||
column[sourceRow] = column[#column]
|
||||
column[#column] = nil
|
||||
-- If the entity is the last row in the archetype then swapping it would be meaningless.
|
||||
local last = #column
|
||||
if sourceRow ~= last then
|
||||
-- Swap rempves columns to ensure there are no holes in the archetype.
|
||||
column[sourceRow] = column[last]
|
||||
end
|
||||
column[last] = nil
|
||||
end
|
||||
|
||||
destinationEntities[destinationRow] = sourceEntities[sourceRow]
|
||||
entityIndex[sourceEntities[sourceRow]].row = destinationRow
|
||||
-- Move the entity from the source to the destination archetype.
|
||||
local atSourceRow = sourceEntities[sourceRow]
|
||||
destinationEntities[destinationRow] = atSourceRow
|
||||
entityIndex[atSourceRow].row = destinationRow
|
||||
|
||||
-- Because we have swapped columns we now have to update the records
|
||||
-- corresponding to the entities' rows that were swapped.
|
||||
local movedAway = #sourceEntities
|
||||
sourceEntities[sourceRow] = sourceEntities[movedAway]
|
||||
entityIndex[sourceEntities[movedAway]].row = sourceRow
|
||||
if sourceRow ~= movedAway then
|
||||
local atMovedAway = sourceEntities[movedAway]
|
||||
sourceEntities[sourceRow] = atMovedAway
|
||||
entityIndex[atMovedAway].row = sourceRow
|
||||
end
|
||||
|
||||
sourceEntities[movedAway] = nil
|
||||
end
|
||||
|
||||
local function archetypeAppend(entity: i53, archetype: Archetype): i24
|
||||
local function archetypeAppend(entity: number, archetype: Archetype): number
|
||||
local entities = archetype.entities
|
||||
table.insert(entities, entity)
|
||||
return #entities
|
||||
local length = #entities + 1
|
||||
entities[length] = entity
|
||||
return length
|
||||
end
|
||||
|
||||
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
|
||||
|
@ -105,47 +125,51 @@ local function hash(arr): string | number
|
|||
return table.concat(arr, "_")
|
||||
end
|
||||
|
||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
||||
local destinationCount = #to.types
|
||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
|
||||
local destinationIds = to.types
|
||||
local records = to.records
|
||||
local id = to.id
|
||||
|
||||
for i = 1, destinationCount do
|
||||
local destinationId = destinationIds[i]
|
||||
for i, destinationId in destinationIds do
|
||||
local archetypesMap = componentIndex[destinationId]
|
||||
|
||||
if not componentIndex[destinationId] then
|
||||
componentIndex[destinationId] = { size = 0, sparse = {} }
|
||||
if not archetypesMap then
|
||||
archetypesMap = {size = 0, sparse = {}}
|
||||
componentIndex[destinationId] = archetypesMap
|
||||
end
|
||||
|
||||
local archetypesMap = componentIndex[destinationId]
|
||||
archetypesMap.sparse[to.id] = i
|
||||
to.records[destinationId] = i
|
||||
archetypesMap.sparse[id] = i
|
||||
records[destinationId] = i
|
||||
end
|
||||
end
|
||||
|
||||
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
|
||||
local ty = hash(types)
|
||||
|
||||
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
|
||||
local id = world.nextArchetypeId
|
||||
local id = world.nextArchetypeId + 1
|
||||
world.nextArchetypeId = id
|
||||
|
||||
local columns = {} :: { any }
|
||||
local length = #types
|
||||
local columns = table.create(length) :: {any}
|
||||
|
||||
for _ in types do
|
||||
table.insert(columns, {})
|
||||
for index in types do
|
||||
columns[index] = {}
|
||||
end
|
||||
|
||||
local archetype = {
|
||||
id = id,
|
||||
types = types,
|
||||
type = ty,
|
||||
columns = columns,
|
||||
entities = {},
|
||||
edges = {},
|
||||
records = {},
|
||||
columns = columns;
|
||||
edges = {};
|
||||
entities = {};
|
||||
id = id;
|
||||
records = {};
|
||||
type = ty;
|
||||
types = types;
|
||||
}
|
||||
world.archetypeIndex[ty] = archetype
|
||||
world.archetypes[id] = archetype
|
||||
if length > 0 then
|
||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
||||
end
|
||||
|
||||
return archetype
|
||||
end
|
||||
|
@ -154,17 +178,17 @@ local World = {}
|
|||
World.__index = World
|
||||
function World.new()
|
||||
local self = setmetatable({
|
||||
entityIndex = {},
|
||||
componentIndex = {},
|
||||
archetypes = {},
|
||||
archetypeIndex = {},
|
||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||
nextEntityId = 0,
|
||||
nextComponentId = 0,
|
||||
nextArchetypeId = 0,
|
||||
archetypeIndex = {};
|
||||
archetypes = {};
|
||||
componentIndex = {};
|
||||
entityIndex = {};
|
||||
hooks = {
|
||||
[ON_ADD] = {}
|
||||
}
|
||||
[ON_ADD] = {};
|
||||
};
|
||||
nextArchetypeId = 0;
|
||||
nextComponentId = 0;
|
||||
nextEntityId = 0;
|
||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
||||
}, World)
|
||||
return self
|
||||
end
|
||||
|
@ -173,34 +197,32 @@ local function emit(world, eventDescription)
|
|||
local event = eventDescription.event
|
||||
|
||||
table.insert(world.hooks[event], {
|
||||
ids = eventDescription.ids,
|
||||
archetype = eventDescription.archetype,
|
||||
otherArchetype = eventDescription.otherArchetype,
|
||||
offset = eventDescription.offset
|
||||
archetype = eventDescription.archetype;
|
||||
ids = eventDescription.ids;
|
||||
offset = eventDescription.offset;
|
||||
otherArchetype = eventDescription.otherArchetype;
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
||||
if #added > 0 then
|
||||
emit(world, {
|
||||
event = ON_ADD,
|
||||
ids = added,
|
||||
archetype = archetype,
|
||||
otherArchetype = otherArchetype,
|
||||
offset = row,
|
||||
archetype = archetype;
|
||||
event = ON_ADD;
|
||||
ids = added;
|
||||
offset = row;
|
||||
otherArchetype = otherArchetype;
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
export type World = typeof(World.new())
|
||||
|
||||
local function ensureArchetype(world: World, types, prev)
|
||||
if #types < 1 then
|
||||
return world.ROOT_ARCHETYPE
|
||||
end
|
||||
|
||||
local ty = hash(types)
|
||||
local archetype = world.archetypeIndex[ty]
|
||||
if archetype then
|
||||
|
@ -211,9 +233,7 @@ local function ensureArchetype(world: World, types, prev)
|
|||
end
|
||||
|
||||
local function findInsert(types: {i53}, toAdd: i53)
|
||||
local count = #types
|
||||
for i = 1, count do
|
||||
local id = types[i]
|
||||
for i, id in types do
|
||||
if id == toAdd then
|
||||
return -1
|
||||
end
|
||||
|
@ -221,13 +241,18 @@ local function findInsert(types: { i53 }, toAdd: i53)
|
|||
return i
|
||||
end
|
||||
end
|
||||
return count + 1
|
||||
return #types + 1
|
||||
end
|
||||
|
||||
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
||||
local types = node.types
|
||||
-- Component IDs are added incrementally, so inserting and sorting
|
||||
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
||||
-- point in the types array.
|
||||
local at = findInsert(types, componentId)
|
||||
if at == -1 then
|
||||
-- If it finds a duplicate, it just means it is the same archetype so it can return it
|
||||
-- directly instead of needing to hash types for a lookup to the archetype.
|
||||
return node
|
||||
end
|
||||
|
||||
|
@ -237,88 +262,108 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
|
|||
end
|
||||
|
||||
local function ensureEdge(archetype: Archetype, componentId: i53)
|
||||
if not archetype.edges[componentId] then
|
||||
archetype.edges[componentId] = {} :: any
|
||||
local edges = archetype.edges
|
||||
local edge = edges[componentId]
|
||||
if not edge then
|
||||
edge = {} :: any
|
||||
edges[componentId] = edge
|
||||
end
|
||||
return archetype.edges[componentId]
|
||||
return edge
|
||||
end
|
||||
|
||||
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
||||
if not from then
|
||||
if not world.ROOT_ARCHETYPE then
|
||||
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
|
||||
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
|
||||
if not ROOT_ARCHETYPE then
|
||||
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
|
||||
end
|
||||
from = world.ROOT_ARCHETYPE
|
||||
from = ROOT_ARCHETYPE
|
||||
end
|
||||
|
||||
local edge = ensureEdge(from, componentId)
|
||||
|
||||
if not edge.add then
|
||||
edge.add = findArchetypeWith(world, from, componentId)
|
||||
local add = edge.add
|
||||
if not add then
|
||||
-- Save an edge using the component ID to the archetype to allow
|
||||
-- faster traversals to adjacent archetypes.
|
||||
add = findArchetypeWith(world, from, componentId)
|
||||
edge.add = add :: never
|
||||
end
|
||||
|
||||
return edge.add
|
||||
return add
|
||||
end
|
||||
|
||||
local function ensureRecord(entityIndex, entityId: i53): Record
|
||||
local id = entityId
|
||||
if not entityIndex[id] then
|
||||
entityIndex[id] = {}
|
||||
local record = entityIndex[entityId]
|
||||
|
||||
if not record then
|
||||
record = {}
|
||||
entityIndex[entityId] = record
|
||||
end
|
||||
return entityIndex[id] :: Record
|
||||
|
||||
return record :: Record
|
||||
end
|
||||
|
||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||
local record = ensureRecord(world.entityIndex, entityId)
|
||||
local sourceArchetype = record.archetype
|
||||
local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
|
||||
local from = record.archetype
|
||||
local to = archetypeTraverseAdd(world, componentId, from)
|
||||
|
||||
if sourceArchetype == destinationArchetype then
|
||||
local archetypeRecord = destinationArchetype.records[componentId]
|
||||
destinationArchetype.columns[archetypeRecord][record.row] = data
|
||||
if from == to then
|
||||
-- If the archetypes are the same it can avoid moving the entity
|
||||
-- and just set the data directly.
|
||||
local archetypeRecord = to.records[componentId]
|
||||
from.columns[archetypeRecord][record.row] = data
|
||||
-- Should fire an OnSet event here.
|
||||
return
|
||||
end
|
||||
|
||||
if sourceArchetype then
|
||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
||||
if from then
|
||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
||||
moveEntity(world.entityIndex, entityId, record, to)
|
||||
else
|
||||
if #destinationArchetype.types > 0 then
|
||||
newEntity(entityId, record, destinationArchetype)
|
||||
onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId })
|
||||
if #to.types > 0 then
|
||||
-- When there is no previous archetype it should create the archetype
|
||||
newEntity(entityId, record, to)
|
||||
onNotifyAdd(world, to, from, record.row, {componentId})
|
||||
end
|
||||
end
|
||||
|
||||
local archetypeRecord = destinationArchetype.records[componentId]
|
||||
destinationArchetype.columns[archetypeRecord][record.row] = data
|
||||
local archetypeRecord = to.records[componentId]
|
||||
to.columns[archetypeRecord][record.row] = data
|
||||
end
|
||||
|
||||
local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
|
||||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
||||
local edge = ensureEdge(from, componentId)
|
||||
|
||||
|
||||
if not edge.remove then
|
||||
local remove = edge.remove
|
||||
if not remove then
|
||||
local to = table.clone(from.types)
|
||||
table.remove(to, table.find(to, componentId))
|
||||
edge.remove = ensureArchetype(world, to, from)
|
||||
remove = ensureArchetype(world, to, from)
|
||||
edge.remove = remove :: never
|
||||
end
|
||||
|
||||
return edge.remove
|
||||
return remove
|
||||
end
|
||||
|
||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||
local record = ensureRecord(world.entityIndex, entityId)
|
||||
local entityIndex = world.entityIndex
|
||||
local record = ensureRecord(entityIndex, entityId)
|
||||
local sourceArchetype = record.archetype
|
||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
||||
|
||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
||||
moveEntity(entityIndex, entityId, record, destinationArchetype)
|
||||
end
|
||||
end
|
||||
|
||||
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
|
||||
-- Keeping the function as small as possible to enable inlining
|
||||
local function get(record: Record, componentId: i24)
|
||||
local archetype = record.archetype
|
||||
local archetypeRecord = componentIndex[componentId].sparse[archetype.id]
|
||||
local archetypeRecord = archetype.records[componentId]
|
||||
|
||||
if not archetypeRecord then
|
||||
return nil
|
||||
|
@ -329,35 +374,35 @@ end
|
|||
|
||||
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
||||
local id = entityId
|
||||
local componentIndex = world.componentIndex
|
||||
local record = world.entityIndex[id]
|
||||
if not record then
|
||||
return nil
|
||||
end
|
||||
|
||||
local va = get(componentIndex, record, a)
|
||||
local va = get(record, a)
|
||||
|
||||
if b == nil then
|
||||
return va
|
||||
elseif c == nil then
|
||||
return va, get(componentIndex, record, b)
|
||||
return va, get(record, b)
|
||||
elseif d == nil then
|
||||
return va, get(componentIndex, record, b), get(componentIndex, record, c)
|
||||
return va, get(record, b), get(record, c)
|
||||
elseif e == nil then
|
||||
return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
|
||||
return va, get(record, b), get(record, c), get(record, d)
|
||||
else
|
||||
error("args exceeded")
|
||||
end
|
||||
end
|
||||
|
||||
local function noop(self: Query, ...: i53): () -> (number, ...any)
|
||||
return function()
|
||||
end :: any
|
||||
-- the less creation the better
|
||||
local function actualNoOperation() end
|
||||
local function noop(_self: Query, ...: i53): () -> (number, ...any)
|
||||
return actualNoOperation :: any
|
||||
end
|
||||
|
||||
local EmptyQuery = {
|
||||
__iter = noop,
|
||||
without = noop
|
||||
__iter = noop;
|
||||
without = noop;
|
||||
}
|
||||
EmptyQuery.__index = EmptyQuery
|
||||
setmetatable(EmptyQuery, EmptyQuery)
|
||||
|
@ -365,19 +410,22 @@ setmetatable(EmptyQuery, EmptyQuery)
|
|||
export type Query = typeof(EmptyQuery)
|
||||
|
||||
function World.query(world: World, ...: i53): Query
|
||||
-- breaking?
|
||||
if (...) == nil then
|
||||
error("Missing components")
|
||||
end
|
||||
|
||||
local compatibleArchetypes = {}
|
||||
local length = 0
|
||||
|
||||
local components = {...}
|
||||
local archetypes = world.archetypes
|
||||
local queryLength = #components
|
||||
|
||||
if queryLength == 0 then
|
||||
error("Missing components")
|
||||
end
|
||||
|
||||
local firstArchetypeMap
|
||||
local componentIndex = world.componentIndex
|
||||
|
||||
for i, componentId in components do
|
||||
for _, componentId in components do
|
||||
local map = componentIndex[componentId]
|
||||
if not map then
|
||||
return EmptyQuery
|
||||
|
@ -388,27 +436,27 @@ function World.query(world: World, ...: i53): Query
|
|||
end
|
||||
end
|
||||
|
||||
local i = 0
|
||||
for id in firstArchetypeMap.sparse do
|
||||
local archetype = archetypes[id]
|
||||
local archetypeRecords = archetype.records
|
||||
local indices = {}
|
||||
local skip = false
|
||||
|
||||
for j, componentId in components do
|
||||
for i, componentId in components do
|
||||
local index = archetypeRecords[componentId]
|
||||
if not index then
|
||||
skip = true
|
||||
break
|
||||
end
|
||||
indices[j] = archetypeRecords[componentId]
|
||||
indices[i] = index
|
||||
end
|
||||
|
||||
if skip then
|
||||
continue
|
||||
end
|
||||
i += 1
|
||||
table.insert(compatibleArchetypes, { archetype, indices })
|
||||
|
||||
length += 1
|
||||
compatibleArchetypes[length] = {archetype, indices}
|
||||
end
|
||||
|
||||
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
||||
|
@ -420,16 +468,19 @@ function World.query(world: World, ...: i53): Query
|
|||
preparedQuery.__index = preparedQuery
|
||||
|
||||
function preparedQuery:without(...)
|
||||
local components = { ... }
|
||||
local withoutComponents = {...}
|
||||
for i = #compatibleArchetypes, 1, -1 do
|
||||
local archetype = compatibleArchetypes[i][1]
|
||||
local records = archetype.records
|
||||
local shouldRemove = false
|
||||
for _, componentId in components do
|
||||
if archetype.records[componentId] then
|
||||
|
||||
for _, componentId in withoutComponents do
|
||||
if records[componentId] then
|
||||
shouldRemove = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if shouldRemove then
|
||||
table.remove(compatibleArchetypes, i)
|
||||
end
|
||||
|
@ -446,7 +497,6 @@ function World.query(world: World, ...: i53): Query
|
|||
local lastRow
|
||||
local queryOutput = {}
|
||||
|
||||
|
||||
function preparedQuery:__iter()
|
||||
return function()
|
||||
local archetype = compatibleArchetype[1]
|
||||
|
@ -470,16 +520,9 @@ function World.query(world: World, ...: i53): Query
|
|||
elseif queryLength == 2 then
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row]
|
||||
elseif queryLength == 3 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row]
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
|
||||
elseif queryLength == 4 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
columns[tr[2]][row],
|
||||
columns[tr[3]][row],
|
||||
columns[tr[4]][row]
|
||||
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
|
||||
elseif queryLength == 5 then
|
||||
return entityId,
|
||||
columns[tr[1]][row],
|
||||
|
@ -517,7 +560,7 @@ function World.query(world: World, ...: i53): Query
|
|||
end
|
||||
|
||||
for i in components do
|
||||
queryOutput[i] = tr[i][row]
|
||||
queryOutput[i] = columns[tr[i]][row]
|
||||
end
|
||||
|
||||
return entityId, unpack(queryOutput, 1, queryLength)
|
||||
|
@ -530,24 +573,40 @@ end
|
|||
function World.component(world: World)
|
||||
local componentId = world.nextComponentId + 1
|
||||
if componentId > HI_COMPONENT_ID then
|
||||
error("Too many components")
|
||||
-- IDs are partitioned into ranges because component IDs are not nominal,
|
||||
-- so it needs to error when IDs intersect into the entity range.
|
||||
error("Too many components, consider using world:entity() instead to create components.")
|
||||
end
|
||||
world.nextComponentId = componentId
|
||||
return componentId
|
||||
end
|
||||
|
||||
function World.entity(world: World)
|
||||
world.nextEntityId += 1
|
||||
return world.nextEntityId + REST
|
||||
local nextEntityId = world.nextEntityId + 1
|
||||
world.nextEntityId = nextEntityId
|
||||
return nextEntityId + REST
|
||||
end
|
||||
|
||||
function World.delete(world: World, entityId: i53)
|
||||
local entityIndex = world.entityIndex
|
||||
local record = entityIndex[entityId]
|
||||
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
|
||||
-- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from
|
||||
-- the entities array and delete the record. We know there won't be the hole since
|
||||
-- we are always removing the last row.
|
||||
--world.ROOT_ARCHETYPE.entities[record.row] = nil
|
||||
--entityIndex[entityId] = nil
|
||||
end
|
||||
|
||||
function World.observer(world: World, ...)
|
||||
local componentIds = {...}
|
||||
local idsCount = #componentIds
|
||||
local hooks = world.hooks
|
||||
|
||||
return {
|
||||
event = function(event)
|
||||
local hook = world.hooks[event]
|
||||
world.hooks[event] = nil
|
||||
local hook = hooks[event]
|
||||
hooks[event] = nil
|
||||
|
||||
local last, change
|
||||
return function()
|
||||
|
@ -557,10 +616,11 @@ function World.observer(world: World, ...)
|
|||
end
|
||||
|
||||
local matched = false
|
||||
local ids = change.ids
|
||||
|
||||
while not matched do
|
||||
local skip = false
|
||||
for _, id in change.ids do
|
||||
for _, id in ids do
|
||||
if not table.find(componentIds, id) then
|
||||
skip = true
|
||||
break
|
||||
|
@ -569,30 +629,31 @@ function World.observer(world: World, ...)
|
|||
|
||||
if skip then
|
||||
last, change = next(hook, last)
|
||||
ids = change.ids
|
||||
continue
|
||||
end
|
||||
|
||||
matched = true
|
||||
end
|
||||
|
||||
local queryOutput = {}
|
||||
local queryOutput = table.create(idsCount)
|
||||
local row = change.offset
|
||||
local archetype = change.archetype
|
||||
local columns = archetype.columns
|
||||
local archetypeRecords = archetype.records
|
||||
for _, id in componentIds do
|
||||
table.insert(queryOutput, columns[archetypeRecords[id]][row])
|
||||
for index, id in componentIds do
|
||||
queryOutput[index] = columns[archetypeRecords[id]][row]
|
||||
end
|
||||
|
||||
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
|
||||
end
|
||||
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
|
||||
end
|
||||
end;
|
||||
}
|
||||
end
|
||||
|
||||
return table.freeze({
|
||||
World = World,
|
||||
ON_ADD = ON_ADD,
|
||||
ON_REMOVE = ON_REMOVE,
|
||||
ON_SET = ON_SET
|
||||
World = World;
|
||||
ON_ADD = ON_ADD;
|
||||
ON_REMOVE = ON_REMOVE;
|
||||
ON_SET = ON_SET;
|
||||
})
|
||||
|
|
1499
newMatter.lua
1499
newMatter.lua
File diff suppressed because it is too large
Load diff
1567
oldMatter.lua
1567
oldMatter.lua
File diff suppressed because it is too large
Load diff
4
selene.toml
Normal file
4
selene.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
std = "roblox"
|
||||
|
||||
[lints]
|
||||
global_usage = "allow"
|
5
stylua.toml
Normal file
5
stylua.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
column_width = 120
|
||||
quote_style = "ForceDouble"
|
||||
|
||||
[sort_requires]
|
||||
enabled = true
|
3
testez-companion.toml
Normal file
3
testez-companion.toml
Normal file
|
@ -0,0 +1,3 @@
|
|||
roots = ["ServerStorage"]
|
||||
|
||||
[extraOptions]
|
|
@ -110,6 +110,39 @@ TEST("world:query", function()
|
|||
CHECK(world:get(id, Health) == nil)
|
||||
end
|
||||
|
||||
do CASE "Should allow iterating the whole world"
|
||||
local world = jecs.World.new()
|
||||
|
||||
local A, B = world:entity(), world:entity()
|
||||
|
||||
local eA = world:entity()
|
||||
world:set(eA, A, true)
|
||||
local eB = world:entity()
|
||||
world:set(eB, B, true)
|
||||
local eAB = world:entity()
|
||||
world:set(eAB, A, true)
|
||||
world:set(eAB, B, true)
|
||||
|
||||
local count = 0
|
||||
for id, data in world do
|
||||
count += 1
|
||||
if id == eA then
|
||||
CHECK(data[A] == true)
|
||||
CHECK(data[B] == nil)
|
||||
elseif id == eB then
|
||||
CHECK(data[B] == true)
|
||||
CHECK(data[A] == nil)
|
||||
elseif id == eAB then
|
||||
CHECK(data[A] == true)
|
||||
CHECK(data[B] == true)
|
||||
else
|
||||
error("unknown entity", id)
|
||||
end
|
||||
end
|
||||
|
||||
CHECK(count == 3)
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
FINISH()
|
|
@ -10,6 +10,3 @@ include = ["default.project.json", "lib", "wally.toml", "README.md"]
|
|||
TestEZ = "roblox/testez@0.4.1"
|
||||
Matter = "matter-ecs/matter@0.8.0"
|
||||
ecr = "centau/ecr@0.8.0"
|
||||
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue