From 54b67ebdf37b672e53cd7a0d02569d2887896fd7 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 10 Sep 2024 02:59:27 +0200 Subject: [PATCH] Add nth parameter to world:target --- demo/src/ReplicatedStorage/ecs_init.luau | 4 + src/init.luau | 111 +++++++------ test/tests.luau | 188 +++++++++++++++++------ 3 files changed, 214 insertions(+), 89 deletions(-) create mode 100644 demo/src/ReplicatedStorage/ecs_init.luau diff --git a/demo/src/ReplicatedStorage/ecs_init.luau b/demo/src/ReplicatedStorage/ecs_init.luau new file mode 100644 index 0000000..455fb9b --- /dev/null +++ b/demo/src/ReplicatedStorage/ecs_init.luau @@ -0,0 +1,4 @@ +_G.JECS_DEBUG = true +_G.JECS_HI_COMPONENT_ID = 32 +require(game:GetService("ReplicatedStorage").ecs) +return diff --git a/src/init.luau b/src/init.luau index 74931ca..dd39c5d 100644 --- a/src/init.luau +++ b/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: (self: World) -> Entity, --- 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) -> (), diff --git a/test/tests.luau b/test/tests.luau index e69e1eb..d95ff1f 100644 --- a/test/tests.luau +++ b/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(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 = { 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"