mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Add nth parameter to world:target
This commit is contained in:
parent
411138e1f7
commit
54b67ebdf3
3 changed files with 214 additions and 89 deletions
4
demo/src/ReplicatedStorage/ecs_init.luau
Normal file
4
demo/src/ReplicatedStorage/ecs_init.luau
Normal file
|
@ -0,0 +1,4 @@
|
|||
_G.JECS_DEBUG = true
|
||||
_G.JECS_HI_COMPONENT_ID = 32
|
||||
require(game:GetService("ReplicatedStorage").ecs)
|
||||
return
|
111
src/init.luau
111
src/init.luau
|
@ -382,11 +382,8 @@ local function world_has(world: World, entity: number, ...: i53): boolean
|
|||
return true
|
||||
end
|
||||
|
||||
-- TODO:
|
||||
-- should have an additional `nth` parameter which selects the nth target
|
||||
-- this is important when an entity can have multiple relationships with the same target
|
||||
local function world_target(world: World, entity: i53,
|
||||
relation: i24--[[, nth: number]]): i24?
|
||||
relation: i24, index: number): i24?
|
||||
|
||||
local record = world.entityIndex.sparse[entity]
|
||||
local archetype = record.archetype
|
||||
|
@ -404,7 +401,24 @@ local function world_target(world: World, entity: i53,
|
|||
return nil
|
||||
end
|
||||
|
||||
return ecs_pair_second(world, archetype.types[tr.column])
|
||||
local count = tr.count
|
||||
if index >= count then
|
||||
index = index + count + 1
|
||||
end
|
||||
|
||||
local nth = archetype.types[index + tr.column]
|
||||
|
||||
if not nth then
|
||||
return nil
|
||||
end
|
||||
|
||||
return ecs_pair_second(world, nth)
|
||||
end
|
||||
|
||||
local function ECS_ID_IS_WILDCARD(e: i53): boolean
|
||||
local first = ECS_ENTITY_T_HI(e)
|
||||
local second = ECS_ENTITY_T_LO(e)
|
||||
return first == EcsWildcard or second == EcsWildcard
|
||||
end
|
||||
|
||||
local function id_record_ensure(world: World, id: number): ArchetypeMap
|
||||
|
@ -415,8 +429,8 @@ local function id_record_ensure(world: World, id: number): ArchetypeMap
|
|||
local flags = ECS_ID_MASK
|
||||
local relation = ECS_ENTITY_T_HI(id)
|
||||
|
||||
local cleanup_policy = world_target(world, relation, EcsOnDelete)
|
||||
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget)
|
||||
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
|
||||
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0)
|
||||
|
||||
local has_delete = false
|
||||
|
||||
|
@ -426,14 +440,14 @@ local function id_record_ensure(world: World, id: number): ArchetypeMap
|
|||
|
||||
local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove)
|
||||
|
||||
local has_tag = world_has_one_inline(world, id, EcsTag)
|
||||
local is_tag = not world_has_one_inline(world, relation, EcsComponent)
|
||||
|
||||
flags = bit32.bor(flags,
|
||||
if on_add then ECS_ID_HAS_ON_ADD else 0,
|
||||
if on_remove then ECS_ID_HAS_ON_REMOVE else 0,
|
||||
if on_set then ECS_ID_HAS_ON_SET else 0,
|
||||
if has_delete then ECS_ID_DELETE else 0,
|
||||
if has_tag then ECS_ID_IS_TAG else 0
|
||||
if is_tag then ECS_ID_IS_TAG else 0
|
||||
)
|
||||
|
||||
idr = {
|
||||
|
@ -447,13 +461,6 @@ local function id_record_ensure(world: World, id: number): ArchetypeMap
|
|||
return idr
|
||||
end
|
||||
|
||||
local function ECS_ID_IS_WILDCARD(e: i53): boolean
|
||||
assert(ECS_IS_PAIR(e))
|
||||
local first = ECS_ENTITY_T_HI(e)
|
||||
local second = ECS_ENTITY_T_LO(e)
|
||||
return first == EcsWildcard or second == EcsWildcard
|
||||
end
|
||||
|
||||
local function archetype_create(world: World, types: { i24 }, prev: Archetype?): Archetype
|
||||
local ty = hash(types)
|
||||
|
||||
|
@ -465,29 +472,39 @@ local function archetype_create(world: World, types: { i24 }, prev: Archetype?):
|
|||
|
||||
local records: { ArchetypeRecord } = {}
|
||||
for i, componentId in types do
|
||||
local tr = { column = i, count = 1 }
|
||||
local idr = id_record_ensure(world, componentId)
|
||||
idr.cache[id] = tr
|
||||
idr.size += 1
|
||||
records[componentId] = tr
|
||||
local tr = { column = i, count = 1 }
|
||||
idr.cache[id] = tr
|
||||
idr.size += 1
|
||||
records[componentId] = tr
|
||||
|
||||
if ECS_IS_PAIR(componentId) then
|
||||
local relation = ecs_pair_first(world, componentId)
|
||||
local object = ecs_pair_second(world, componentId)
|
||||
|
||||
local r = ECS_PAIR(relation, EcsWildcard)
|
||||
local idr_r = id_record_ensure(world, r)
|
||||
local r_tr = idr_r.cache[id]
|
||||
if not r_tr then
|
||||
r_tr = { column = i, count = 1 }
|
||||
idr_r.cache[id] = r_tr
|
||||
idr_r.size += 1
|
||||
records[r] = r_tr
|
||||
else
|
||||
r_tr.count += 1
|
||||
end
|
||||
|
||||
local o = ECS_PAIR(EcsWildcard, object)
|
||||
local idr_o = id_record_ensure(world, o)
|
||||
|
||||
records[r] = tr
|
||||
records[o] = tr
|
||||
|
||||
idr_r.cache[id] = tr
|
||||
idr_o.cache[id] = tr
|
||||
|
||||
idr_r.size += 1
|
||||
idr_o.size += 1
|
||||
local t = ECS_PAIR(EcsWildcard, object)
|
||||
local idr_t = id_record_ensure(world, t)
|
||||
local t_tr = idr_t.cache[id]
|
||||
if not t_tr then
|
||||
t_tr = { column = i, count = 1 }
|
||||
idr_t.cache[id] = t_tr
|
||||
idr_t.size += 1
|
||||
records[t] = t_tr
|
||||
else
|
||||
t_tr.count += 1
|
||||
end
|
||||
end
|
||||
if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then
|
||||
columns[i] = {}
|
||||
|
@ -519,7 +536,7 @@ local function world_entity(world: World): i53
|
|||
end
|
||||
|
||||
local function world_parent(world: World, entity: i53)
|
||||
return world_target(world, entity, EcsChildOf)
|
||||
return world_target(world, entity, EcsChildOf, 0)
|
||||
end
|
||||
|
||||
local function archetype_ensure(world: World, types, prev): Archetype
|
||||
|
@ -747,7 +764,9 @@ do
|
|||
column_count: number, types: { i53 }, entity: i53)
|
||||
|
||||
for i, column in columns do
|
||||
column[column_count] = nil
|
||||
if column ~= NULL_ARRAY then
|
||||
column[column_count] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -755,8 +774,10 @@ do
|
|||
column_count: number, row, types, entity)
|
||||
|
||||
for i, column in columns do
|
||||
column[row] = column[column_count]
|
||||
column[column_count] = nil
|
||||
if column ~= NULL_ARRAY then
|
||||
column[row] = column[column_count]
|
||||
column[column_count] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local function archetype_delete(world: World,
|
||||
|
@ -1449,9 +1470,7 @@ if _G.__JECS_DEBUG == true then
|
|||
return world_query(world, ...)
|
||||
end
|
||||
|
||||
World.set = function(world: World, entity: i53,
|
||||
id: i53, value: any): ()
|
||||
|
||||
World.set = function(world: World, entity: i53, id: i53, value: any): ()
|
||||
local idr = world.componentIndex[id]
|
||||
local flags = idr.flags
|
||||
local id_is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
|
||||
|
@ -1475,7 +1494,7 @@ if _G.__JECS_DEBUG == true then
|
|||
world_add(world, entity, id)
|
||||
end
|
||||
|
||||
World.get = function(world: World, entity: i53, id: i53, ...: i53)
|
||||
World.get = function(world: World, entity: i53, ...)
|
||||
local length = select("#", ...)
|
||||
ASSERT(length > 4, "world:get does not support more than 4 components")
|
||||
for i = 1, length do
|
||||
|
@ -1484,15 +1503,12 @@ if _G.__JECS_DEBUG == true then
|
|||
local flags = idr.flags
|
||||
local id_is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0
|
||||
if id_is_tag then
|
||||
local name = world_get_one_inline(world, id, EcsName) or `${id}`
|
||||
throw(`cannot get component ({name}) value because it is a tag. If this was intentional, use "world:has(entity, {name})"`)
|
||||
end
|
||||
end
|
||||
if value ~= nil then
|
||||
local name = world_get_one_inline(world, id, EcsName) or `${id}`
|
||||
throw(`You provided a value when none was expected. Did you mean to use "world:add(entity, {name})"`)
|
||||
end
|
||||
|
||||
return world_get(world, entity, id)
|
||||
return world_get(world, entity, ...)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1518,6 +1534,11 @@ function World.new()
|
|||
entity_index_new_id(self.entityIndex, i)
|
||||
end
|
||||
|
||||
world_add(self, EcsOnSet, EcsComponent)
|
||||
world_add(self, EcsOnAdd, EcsComponent)
|
||||
world_add(self, EcsOnRemove, EcsComponent)
|
||||
world_add(self, EcsRest, EcsComponent)
|
||||
world_add(self, EcsName, EcsComponent)
|
||||
world_add(self, EcsChildOf, ECS_PAIR(EcsOnDeleteTarget, EcsDelete))
|
||||
|
||||
return self
|
||||
|
@ -1563,7 +1584,7 @@ export type World = {
|
|||
component: <T>(self: World) -> Entity<T>,
|
||||
--- Gets the target of an relationship. For example, when a user calls
|
||||
--- `world:target(id, ChildOf(parent))`, you will obtain the parent entity.
|
||||
target: (self: World, id: Entity, relation: Entity) -> Entity?,
|
||||
target: (self: World, id: Entity, relation: Entity, nth: number) -> Entity?,
|
||||
--- Deletes an entity and all it's related components and relationships.
|
||||
delete: (self: World, id: Entity) -> (),
|
||||
|
||||
|
|
188
test/tests.luau
188
test/tests.luau
|
@ -1,3 +1,4 @@
|
|||
|
||||
local jecs = require("@jecs")
|
||||
local testkit = require("@testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
|
@ -9,6 +10,7 @@ local pair = jecs.pair
|
|||
local getAlive = jecs.entity_index_get_alive
|
||||
local ecs_pair_first = jecs.pair_first
|
||||
local ecs_pair_second = jecs.pair_second
|
||||
local world_new = jecs.World.new
|
||||
|
||||
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
|
||||
local function CHECK_NO_ERR<T...>(s: string, fn: (T...) -> (), ...: T...)
|
||||
|
@ -138,24 +140,50 @@ TEST("world:set()", function()
|
|||
end
|
||||
|
||||
do CASE "arbitrary order"
|
||||
local world = jecs.World.new()
|
||||
local world = jecs.World.new()
|
||||
|
||||
local Health = world:entity()
|
||||
local Poison = world:component()
|
||||
local Health = world:entity()
|
||||
local Poison = world:component()
|
||||
|
||||
local id = world:entity()
|
||||
world:set(id, Poison, 5)
|
||||
world:set(id, Health, 50)
|
||||
local id = world:entity()
|
||||
world:set(id, Poison, 5)
|
||||
world:set(id, Health, 50)
|
||||
|
||||
CHECK(world:get(id, Poison) == 5)
|
||||
end
|
||||
CHECK(world:get(id, Poison) == 5)
|
||||
end
|
||||
|
||||
do CASE "pairs"
|
||||
local world = jecs.World.new()
|
||||
|
||||
local C1 = world:component()
|
||||
local C2 = world:component()
|
||||
local T1 = world:entity()
|
||||
local T2 = world:entity()
|
||||
|
||||
local e = world:entity()
|
||||
|
||||
world:set(e, pair(C1, C2), true)
|
||||
world:set(e, pair(C1, T1), true)
|
||||
world:set(e, pair(T1, C1), true)
|
||||
world:set(e, pair(T1, T2), true)
|
||||
|
||||
CHECK(world:get(e, pair(C1, C2)))
|
||||
CHECK(world:get(e, pair(C1, T1)))
|
||||
CHECK(not world:get(e, pair(T1, C1)))
|
||||
CHECK(not world:get(e, pair(T1, T2)))
|
||||
|
||||
local e2 = world:entity()
|
||||
|
||||
world:set(e2, pair(jecs.ChildOf, e), true)
|
||||
CHECK(not world:get(e2, pair(jecs.ChildOf, e)))
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("world:remove()", function()
|
||||
do CASE "should allow remove a component that doesn't exist on entity"
|
||||
local world = jecs.World.new()
|
||||
|
||||
local Health = world:entity()
|
||||
local Health = world:component()
|
||||
local Poison = world:component()
|
||||
|
||||
local id = world:entity()
|
||||
|
@ -213,14 +241,35 @@ end)
|
|||
TEST("world:query()", function()
|
||||
do CASE "tag"
|
||||
local world = jecs.World.new()
|
||||
local A = world:component()
|
||||
world:add(A, jecs.Tag)
|
||||
local A = world:entity()
|
||||
local e = world:entity()
|
||||
world:set(e, A, "test")
|
||||
for id, a in world:query(A) do
|
||||
CHECK(a == nil)
|
||||
end
|
||||
end
|
||||
do CASE "pairs"
|
||||
local world = jecs.World.new()
|
||||
|
||||
local C1 = world:component()
|
||||
local C2 = world:component()
|
||||
local T1 = world:entity()
|
||||
local T2 = world:entity()
|
||||
|
||||
local e = world:entity()
|
||||
|
||||
world:set(e, pair(C1, C2), true)
|
||||
world:set(e, pair(C1, T1), true)
|
||||
world:set(e, pair(T1, C1), true)
|
||||
world:set(e, pair(T1, T2), true)
|
||||
|
||||
for id, a, b, c, d in world:query(pair(C1, C2), pair(C1, T1), pair(T1, C1), pair(T1, T2)) do
|
||||
CHECK(a == true)
|
||||
CHECK(b == true)
|
||||
CHECK(c == nil)
|
||||
CHECK(d == nil)
|
||||
end
|
||||
end
|
||||
do CASE "query single component"
|
||||
do
|
||||
local world = jecs.World.new()
|
||||
|
@ -405,8 +454,8 @@ TEST("world:query()", function()
|
|||
|
||||
do CASE("should allow querying for relations")
|
||||
local world = jecs.World.new()
|
||||
local Eats = world:entity()
|
||||
local Apples = world:entity()
|
||||
local Eats = world:component()
|
||||
local Apples = world:component()
|
||||
local bob = world:entity()
|
||||
|
||||
world:set(bob, pair(Eats, Apples), true)
|
||||
|
@ -418,7 +467,7 @@ TEST("world:query()", function()
|
|||
|
||||
do CASE("should allow wildcards in queries")
|
||||
local world = jecs.World.new()
|
||||
local Eats = world:entity()
|
||||
local Eats = world:component()
|
||||
local Apples = world:entity()
|
||||
local bob = world:entity()
|
||||
|
||||
|
@ -437,7 +486,7 @@ TEST("world:query()", function()
|
|||
|
||||
do CASE("should match against multiple pairs")
|
||||
local world = jecs.World.new()
|
||||
local Eats = world:entity()
|
||||
local Eats = world:component()
|
||||
local Apples = world:entity()
|
||||
local Oranges = world:entity()
|
||||
local bob = world:entity()
|
||||
|
@ -697,32 +746,32 @@ TEST("world:clear()", function()
|
|||
end)
|
||||
|
||||
TEST("world:has()", function()
|
||||
do CASE "should find Tag on entity"
|
||||
local world = jecs.World.new()
|
||||
do CASE "should find Tag on entity"
|
||||
local world = jecs.World.new()
|
||||
|
||||
local Tag = world:component()
|
||||
local Tag = world:entity()
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, Tag)
|
||||
local e = world:entity()
|
||||
world:add(e, Tag)
|
||||
|
||||
CHECK(world:has(e, Tag))
|
||||
end
|
||||
CHECK(world:has(e, Tag))
|
||||
end
|
||||
|
||||
do CASE "should return false when missing one tag"
|
||||
local world = jecs.World.new()
|
||||
do CASE "should return false when missing one tag"
|
||||
local world = jecs.World.new()
|
||||
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
local A = world:entity()
|
||||
local B = world:entity()
|
||||
local C = world:entity()
|
||||
local D = world:entity()
|
||||
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
world:add(e, C)
|
||||
world:add(e, D)
|
||||
local e = world:entity()
|
||||
world:add(e, A)
|
||||
world:add(e, C)
|
||||
world:add(e, D)
|
||||
|
||||
CHECK(world:has(e, A, B, C, D) == false)
|
||||
end
|
||||
CHECK(world:has(e, A, B, C, D) == false)
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("world:component()", function()
|
||||
|
@ -738,10 +787,8 @@ TEST("world:component()", function()
|
|||
do CASE "tag"
|
||||
local world = jecs.World.new() :: World
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
world:add(B, jecs.Tag)
|
||||
world:add(C, jecs.Tag)
|
||||
local B = world:entity()
|
||||
local C = world:entity()
|
||||
local e = world:entity()
|
||||
world:set(e, A, "test")
|
||||
world:add(e, B, "test")
|
||||
|
@ -758,7 +805,7 @@ TEST("world:delete", function()
|
|||
do CASE("should allow deleting components")
|
||||
local world = jecs.World.new()
|
||||
|
||||
local Health = world:entity()
|
||||
local Health = world:component()
|
||||
local Poison = world:component()
|
||||
|
||||
local id = world:entity()
|
||||
|
@ -900,13 +947,66 @@ TEST("world:delete", function()
|
|||
end
|
||||
end)
|
||||
|
||||
TEST("world:target", function()
|
||||
do CASE "nth index"
|
||||
local world = world_new()
|
||||
local A = world:component()
|
||||
local B = world:component()
|
||||
local C = world:component()
|
||||
local D = world:component()
|
||||
local e = world:entity()
|
||||
|
||||
world:add(e, pair(A, B))
|
||||
world:add(e, pair(A, C))
|
||||
world:add(e, pair(B, C))
|
||||
world:add(e, pair(B, D))
|
||||
world:add(e, pair(C, D))
|
||||
|
||||
CHECK(pair(A, B) < pair(A, C))
|
||||
|
||||
CHECK(world:target(e, A, 0) == B)
|
||||
CHECK(world:target(e, A, 1) == C)
|
||||
CHECK(world:target(e, B, 0) == C)
|
||||
CHECK(world:target(e, B, 1) == D)
|
||||
CHECK(world:target(e, C, 0) == D)
|
||||
CHECK(world:target(e, C, 1) == nil)
|
||||
end
|
||||
|
||||
do CASE "loop until no target"
|
||||
local world = world_new()
|
||||
|
||||
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
|
||||
local target = world:target(e1, ROOT, 0)
|
||||
while target do
|
||||
i+=1
|
||||
CHECK(targets[i] == target)
|
||||
target = world:target(e1, ROOT, i)
|
||||
end
|
||||
|
||||
CHECK(i == 10)
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
TEST("world:contains", function()
|
||||
local world = jecs.World.new()
|
||||
|
||||
local id = world:entity()
|
||||
CHECK(world:contains(id))
|
||||
world:delete(id)
|
||||
CHECK(not world:contains(id))
|
||||
|
||||
do CASE "should not exist after delete"
|
||||
world:delete(id)
|
||||
CHECK(not world:contains(id))
|
||||
end
|
||||
end)
|
||||
type Tracker<T> = { track: (world: World, fn: (changes: {
|
||||
added: () -> () -> (number, T),
|
||||
|
@ -1434,7 +1534,7 @@ TEST("scheduler", function()
|
|||
local DependsOn = components.DependsOn
|
||||
|
||||
local Physics = scheduler.phase(Heartbeat)
|
||||
CHECK(world:target(Physics, DependsOn) == Heartbeat)
|
||||
CHECK(world:target(Physics, DependsOn, 0) == Heartbeat)
|
||||
end
|
||||
|
||||
do CASE "user-defined sub phases"
|
||||
|
@ -1447,7 +1547,7 @@ TEST("scheduler", function()
|
|||
local A = scheduler.phase(phases.Heartbeat)
|
||||
local B = scheduler.phase(A)
|
||||
|
||||
CHECK(world:target(B, DependsOn) == A)
|
||||
CHECK(world:target(B, DependsOn, 0) == A)
|
||||
end
|
||||
|
||||
do CASE "phase order"
|
||||
|
|
Loading…
Reference in a new issue