mirror of
https://github.com/Ukendio/jecs.git
synced 2026-05-13 14:24:14 +00:00
Implement world:targets() as a valid method (#311)
* Implement world:targets * Proper indexing in ECS_PAIR_SECOND * Unit tests * pull idr.records index out of iterator * move tests below world:target * style * better test names * change nth to use idr.records and pull out variables from iter * local NOOP fn * pull NOOP out of fn * redeclare component_index as ct_idx * black magic (inlined most of the function calls in iterator - yes it still passes tests) * remove redundant fn call (shoutout to nnullcolumn for spotting this) * add test for rapid add/remove calls * run secondary mixing step for all entities and not just the alive ones * redundant check * be a bit more explicit for checking if an entity is alive
This commit is contained in:
parent
19823453aa
commit
e2c56f5420
2 changed files with 255 additions and 1 deletions
|
|
@ -194,6 +194,7 @@ type world = {
|
||||||
entity: (self: world, id: i53?) -> i53,
|
entity: (self: world, id: i53?) -> i53,
|
||||||
component: (self: world) -> i53,
|
component: (self: world) -> i53,
|
||||||
target: (self: world, id: i53, relation: i53, index: number?) -> i53?,
|
target: (self: world, id: i53, relation: i53, index: number?) -> i53?,
|
||||||
|
targets: (self: world, id: i53, relation: i53) -> () -> i53?,
|
||||||
delete: (self: world, id: i53) -> (),
|
delete: (self: world, id: i53) -> (),
|
||||||
add: (self: world, id: i53, component: i53) -> (),
|
add: (self: world, id: i53, component: i53) -> (),
|
||||||
set: (self: world, id: i53, component: i53, data: any) -> (),
|
set: (self: world, id: i53, component: i53, data: any) -> (),
|
||||||
|
|
@ -253,6 +254,13 @@ export type World = {
|
||||||
index: number?
|
index: number?
|
||||||
) -> Entity<unknown>?,
|
) -> Entity<unknown>?,
|
||||||
|
|
||||||
|
-- Gets an iterator for all viable targets of a relationship
|
||||||
|
targets: <T>(
|
||||||
|
self: World,
|
||||||
|
id: Entity<T> | number,
|
||||||
|
relation: ecs_entity_t<Component>
|
||||||
|
) -> () -> Id,
|
||||||
|
|
||||||
--- Deletes an entity and all it's related components and relationships.
|
--- Deletes an entity and all it's related components and relationships.
|
||||||
delete: <T>(self: World, id: Entity<T>) -> (),
|
delete: <T>(self: World, id: Entity<T>) -> (),
|
||||||
|
|
||||||
|
|
@ -3302,6 +3310,50 @@ local function world_new(DEBUG: boolean?)
|
||||||
ECS_PAIR_SECOND(nth))
|
ECS_PAIR_SECOND(nth))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local NOOP = NOOP :: () -> i53
|
||||||
|
|
||||||
|
local function world_targets(world: world, entity: i53, relation: i53): () -> i53?
|
||||||
|
local record = entity_index_try_get_unsafe(entity)
|
||||||
|
if not record then
|
||||||
|
return NOOP
|
||||||
|
end
|
||||||
|
|
||||||
|
local archetype = record.archetype
|
||||||
|
if not archetype then
|
||||||
|
return NOOP
|
||||||
|
end
|
||||||
|
|
||||||
|
local r = ECS_PAIR(relation, EcsWildcard)
|
||||||
|
local ct_idx = world.component_index
|
||||||
|
local idr = ct_idx[r]
|
||||||
|
|
||||||
|
if not idr then
|
||||||
|
return NOOP
|
||||||
|
end
|
||||||
|
|
||||||
|
local archetype_id = archetype.id
|
||||||
|
local count = idr.counts[archetype_id]
|
||||||
|
if not count then
|
||||||
|
return NOOP
|
||||||
|
end
|
||||||
|
|
||||||
|
local nth = idr.records[archetype_id]
|
||||||
|
local end_count = nth + count
|
||||||
|
|
||||||
|
local archetype_types = archetype.types
|
||||||
|
local sparse_array = entity_index.sparse_array
|
||||||
|
local dense_array = entity_index.dense_array
|
||||||
|
|
||||||
|
return function(): i53?
|
||||||
|
if nth == end_count then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local target = dense_array[sparse_array[ECS_PAIR_SECOND(archetype_types[nth])].dense]
|
||||||
|
nth += 1
|
||||||
|
return target
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local function world_parent(world: world, entity: i53): i53?
|
local function world_parent(world: world, entity: i53): i53?
|
||||||
return world_target(world, entity, EcsChildOf, 0)
|
return world_target(world, entity, EcsChildOf, 0)
|
||||||
end
|
end
|
||||||
|
|
@ -3730,6 +3782,7 @@ local function world_new(DEBUG: boolean?)
|
||||||
world.get = world_get :: any
|
world.get = world_get :: any
|
||||||
world.has = world_has :: any
|
world.has = world_has :: any
|
||||||
world.target = world_target
|
world.target = world_target
|
||||||
|
world.targets = world_targets
|
||||||
world.parent = world_parent
|
world.parent = world_parent
|
||||||
world.contains = world_contains
|
world.contains = world_contains
|
||||||
world.exists = world_exists
|
world.exists = world_exists
|
||||||
|
|
|
||||||
203
test/tests.luau
203
test/tests.luau
|
|
@ -3077,7 +3077,7 @@ TEST("world:set()", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
TEST("world:target", function()
|
TEST("world:target()", function()
|
||||||
do CASE "nth index"
|
do CASE "nth index"
|
||||||
local world = jecs.world()
|
local world = jecs.world()
|
||||||
local A = world:component()
|
local A = world:component()
|
||||||
|
|
@ -3178,6 +3178,207 @@ TEST("world:target", function()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
TEST("world:targets()", function()
|
||||||
|
do CASE "should find single relation"
|
||||||
|
local world = jecs.world()
|
||||||
|
|
||||||
|
local Alice = world:entity()
|
||||||
|
local Bob = world:entity()
|
||||||
|
local Likes = world:entity()
|
||||||
|
|
||||||
|
world:add(Alice, jecs.pair(Likes, Bob))
|
||||||
|
|
||||||
|
local i = 0
|
||||||
|
for target in world:targets(Alice, Likes) do
|
||||||
|
i += 1
|
||||||
|
CHECK(target == Bob)
|
||||||
|
end
|
||||||
|
CHECK(i == 1)
|
||||||
|
end
|
||||||
|
do CASE "basic iteration"
|
||||||
|
local world = jecs.world()
|
||||||
|
|
||||||
|
local ROOT = world:entity()
|
||||||
|
local e1 = world:entity()
|
||||||
|
local targets = {}
|
||||||
|
|
||||||
|
for i = 1, 10 do
|
||||||
|
local target = world:entity()
|
||||||
|
targets[i] = target
|
||||||
|
world:add(e1, pair(ROOT, target))
|
||||||
|
end
|
||||||
|
|
||||||
|
local i = 0
|
||||||
|
for target in world:targets(e1, ROOT) do
|
||||||
|
i += 1
|
||||||
|
CHECK(targets[i] == target)
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(i == 10)
|
||||||
|
end
|
||||||
|
do CASE "multiple iterations"
|
||||||
|
local world = jecs.world()
|
||||||
|
|
||||||
|
local ROOT = world:entity()
|
||||||
|
local OTHER = world:entity()
|
||||||
|
|
||||||
|
local e = world:entity()
|
||||||
|
|
||||||
|
local root_targets = {}
|
||||||
|
local other_targets = {}
|
||||||
|
|
||||||
|
for i = 1, 5 do
|
||||||
|
local t = world:entity()
|
||||||
|
root_targets[i] = t
|
||||||
|
world:add(e, pair(ROOT, t))
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 3 do
|
||||||
|
local t = world:entity()
|
||||||
|
other_targets[i] = t
|
||||||
|
world:add(e, pair(OTHER, t))
|
||||||
|
end
|
||||||
|
|
||||||
|
local i = 0
|
||||||
|
for target in world:targets(e, ROOT) do
|
||||||
|
i += 1
|
||||||
|
CHECK(root_targets[i] == target)
|
||||||
|
end
|
||||||
|
CHECK(i == 5)
|
||||||
|
|
||||||
|
local j = 0
|
||||||
|
for target in world:targets(e, OTHER) do
|
||||||
|
j += 1
|
||||||
|
CHECK(other_targets[j] == target)
|
||||||
|
end
|
||||||
|
CHECK(j == 3)
|
||||||
|
end
|
||||||
|
do CASE "empty iterator"
|
||||||
|
local world = jecs.world()
|
||||||
|
|
||||||
|
local ROOT = world:entity()
|
||||||
|
local OTHER = world:entity()
|
||||||
|
|
||||||
|
local e = world:entity()
|
||||||
|
|
||||||
|
world:add(e, pair(ROOT, world:entity()))
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
for _ in world:targets(e, OTHER) do
|
||||||
|
count += 1
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(count == 0)
|
||||||
|
end
|
||||||
|
do CASE "should ignore deleted targets"
|
||||||
|
local world = jecs.world()
|
||||||
|
|
||||||
|
local ROOT = world:entity()
|
||||||
|
local e = world:entity()
|
||||||
|
|
||||||
|
local alive = {}
|
||||||
|
local dead = {}
|
||||||
|
|
||||||
|
for i = 1, 3 do
|
||||||
|
local t = world:entity()
|
||||||
|
alive[#alive + 1] = t
|
||||||
|
world:add(e, pair(ROOT, t))
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 2 do
|
||||||
|
local t = world:entity()
|
||||||
|
dead[#dead + 1] = t
|
||||||
|
world:add(e, pair(ROOT, t))
|
||||||
|
world:delete(t)
|
||||||
|
end
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
for t in world:targets(e, ROOT) do
|
||||||
|
count += 1
|
||||||
|
CHECK(t ~= nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(count == #alive)
|
||||||
|
end
|
||||||
|
do CASE "should properly handle rapid add/remove calls"
|
||||||
|
local world = jecs.world()
|
||||||
|
|
||||||
|
local ROOT = world:entity()
|
||||||
|
local e = world:entity()
|
||||||
|
|
||||||
|
local alive = {}
|
||||||
|
local all_targets = {} :: {Entity}
|
||||||
|
|
||||||
|
for i = 1, 100 do
|
||||||
|
local t = world:entity()
|
||||||
|
all_targets[#all_targets + 1] = t
|
||||||
|
|
||||||
|
world:add(e, jecs.pair(ROOT, t))
|
||||||
|
alive[t] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
for i = 1, 500 do
|
||||||
|
local t: Entity
|
||||||
|
while t == nil do
|
||||||
|
t = all_targets[math.random(#all_targets)]
|
||||||
|
end
|
||||||
|
|
||||||
|
if math.random() < 0.5 then
|
||||||
|
world:remove(e, jecs.pair(ROOT, t))
|
||||||
|
alive[t] = nil
|
||||||
|
else
|
||||||
|
world:add(e, jecs.pair(ROOT, t))
|
||||||
|
alive[t] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local entity_index = world.entity_index
|
||||||
|
local function entity_index_check_alive(entity)
|
||||||
|
local r = entity_index.sparse_array[ECS_ID(entity)]
|
||||||
|
|
||||||
|
if not r or r.dense == 0 then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local dense = r.dense
|
||||||
|
if dense > entity_index.alive_count then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return entity_index.dense_array[dense] ~= nil
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
for i=1, 10 do
|
||||||
|
local seen = {}
|
||||||
|
for t in world:targets(e, ROOT) do
|
||||||
|
CHECK(entity_index_check_alive(t))
|
||||||
|
CHECK(entity_index_check_alive(jecs.pair(ROOT, t)))
|
||||||
|
|
||||||
|
CHECK(alive[t] == true)
|
||||||
|
|
||||||
|
CHECK(seen[t] == nil)
|
||||||
|
seen[t] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
for t, _ in pairs(alive) do
|
||||||
|
CHECK(seen[t] == true)
|
||||||
|
end
|
||||||
|
|
||||||
|
for j = 1, #all_targets do
|
||||||
|
local t = all_targets[j]
|
||||||
|
|
||||||
|
if math.random() < 0.5 then
|
||||||
|
world:remove(e, jecs.pair(ROOT, t))
|
||||||
|
alive[t] = nil
|
||||||
|
else
|
||||||
|
world:add(e, jecs.pair(ROOT, t))
|
||||||
|
alive[t] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
TEST("#adding a recycled target", function()
|
TEST("#adding a recycled target", function()
|
||||||
local world = jecs.world()
|
local world = jecs.world()
|
||||||
local R = world:component()
|
local R = world:component()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue