From cf88c259f8766ae706f5f07749d90b3dd2eeca91 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Sun, 30 Mar 2025 21:31:18 +0200 Subject: [PATCH] Replace OnSet hook with OnChange --- jecs.luau | 42 +++++----- test/tests.luau | 4 +- tools/observers.luau | 188 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 25 deletions(-) create mode 100644 tools/observers.luau diff --git a/jecs.luau b/jecs.luau index 9fca297..4815c9a 100644 --- a/jecs.luau +++ b/jecs.luau @@ -61,8 +61,8 @@ type ecs_id_record_t = { flags: number, size: number, hooks: { - on_add: ((entity: i53) -> ())?, - on_set: ((entity: i53, data: any) -> ())?, + on_add: ((entity: i53, data: any?) -> ())?, + on_change: ((entity: i53, data: any) -> ())?, on_remove: ((entity: i53) -> ())?, }, } @@ -111,7 +111,7 @@ local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 -- stylua: ignore start local EcsOnAdd = HI_COMPONENT_ID + 1 local EcsOnRemove = HI_COMPONENT_ID + 2 -local EcsOnSet = HI_COMPONENT_ID + 3 +local EcsOnChange = HI_COMPONENT_ID + 3 local EcsWildcard = HI_COMPONENT_ID + 4 local EcsChildOf = HI_COMPONENT_ID + 5 local EcsComponent = HI_COMPONENT_ID + 6 @@ -572,9 +572,11 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t has_delete = true end - local on_add, on_set, on_remove = world_get(world, relation, EcsOnAdd, EcsOnSet, EcsOnRemove) + local on_add, on_change, on_remove = world_get(world, + relation, EcsOnAdd, EcsOnChange, EcsOnRemove) - local is_tag = not world_has_one_inline(world, relation, EcsComponent) + local is_tag = not world_has_one_inline(world, + relation, EcsComponent) if is_tag and is_pair then is_tag = not world_has_one_inline(world, target, EcsComponent) @@ -582,9 +584,6 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t 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 is_tag then ECS_ID_IS_TAG else 0 ) @@ -596,7 +595,7 @@ local function id_record_ensure(world: ecs_world_t, id: number): ecs_id_record_t flags = flags, hooks = { on_add = on_add, - on_set = on_set, + on_change = on_change, on_remove = on_remove, }, } @@ -737,13 +736,13 @@ local function find_archetype_with(world: ecs_world_t, node: ecs_archetype_t, id -- them each time would be expensive. Instead this insertion sort can find the insertion -- point in the types array. + local dst = table.clone(node.types) :: { i53 } local at = find_insert(id_types, id) if at == -1 then -- If it finds a duplicate, it just means it is the same archetype so it can return it -- directly instead of needing to hash types for a lookup to the archetype. return node end - local dst = table.clone(id_types) :: { i53 } table.insert(dst, at, id) return archetype_ensure(world, dst) @@ -933,14 +932,14 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown if from == to then -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. + local on_change = idr_hooks.on_change + if on_change then + on_change(entity, data) + end + local tr = to.records[id] local column = from.columns[tr] column[record.row] = data - local on_set = idr_hooks.on_set - if on_set then - on_set(entity, data) - end - return end @@ -961,13 +960,10 @@ local function world_set(world: ecs_world_t, entity: i53, id: i53, data: unknown local on_add = idr_hooks.on_add if on_add then - on_add(entity) + on_add(entity, data) end - local on_set = idr_hooks.on_set - if on_set then - on_set(entity, data) - end + end local function world_component(world: World): i53 @@ -2471,7 +2467,7 @@ local function world_new() end world_add(self, EcsName, EcsComponent) - world_add(self, EcsOnSet, EcsComponent) + world_add(self, EcsOnChange, EcsComponent) world_add(self, EcsOnAdd, EcsComponent) world_add(self, EcsOnRemove, EcsComponent) world_add(self, EcsWildcard, EcsComponent) @@ -2479,7 +2475,7 @@ local function world_new() world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") - world_set(self, EcsOnSet, EcsName, "jecs.OnSet") + world_set(self, EcsOnChange, EcsName, "jecs.OnChange") world_set(self, EcsWildcard, EcsName, "jecs.Wildcard") world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") world_set(self, EcsComponent, EcsName, "jecs.Component") @@ -2612,7 +2608,7 @@ return { OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnRemove = EcsOnRemove :: Entity<(entity: Entity) -> ()>, - OnSet = EcsOnSet :: Entity<(entity: Entity, data: any) -> ()>, + OnChange = EcsOnChange :: Entity<(entity: Entity, data: any) -> ()>, ChildOf = EcsChildOf :: Entity, Component = EcsComponent :: Entity, Wildcard = EcsWildcard :: Entity, diff --git a/test/tests.luau b/test/tests.luau index 9fe76af..002b4af 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1664,9 +1664,9 @@ TEST("Hooks", function() local Number = world:component() local e1 = world:entity() - world:set(Number, jecs.OnSet, function(entity, data) + world:set(Number, jecs.OnChange, function(entity, data) CHECK(e1 == entity) - CHECK(data == world:get(entity, Number)) + CHECK(world:get(entity, Number) == nil) CHECK(data == 1) end) world:set(e1, Number, 1) diff --git a/tools/observers.luau b/tools/observers.luau new file mode 100644 index 0000000..6090adf --- /dev/null +++ b/tools/observers.luau @@ -0,0 +1,188 @@ +local jecs = require("@jecs") +local testkit = require("@testkit") + +local function observers_new(description) + local event = description.event + local query = description.query + local callback = description.callback + local world = query.world + local terms = query.filter_with + if not terms then + local ids = query.ids + query.filter_with = ids + terms = ids + end + + local entity_index = world.entity_index + local function emplaced(entity) + local r = jecs.entity_index_try_get_fast( + entity_index, entity) + + local archetype = r.archetype + + if jecs.query_match(query, archetype) then + callback(entity) + end + end + + for _, term in terms do + if event == jecs.OnAdd then + world:added(term, emplaced) + elseif event == jecs.OnSet then + world:emplaced(term, emplaced) + end + end +end + +local function world_track(world, ...) + local entity_index = world.entity_index + local terms = { ... } + local q_shim = { filter_with = terms } + + local n = 0 + local dense_array = {} + local sparse_array = {} + + local function emplaced(entity) + local r = jecs.entity_index_try_get_fast( + entity_index, entity) + + local archetype = r.archetype + + if jecs.query_match(q_shim, archetype) then + n += 1 + dense_array[n] = entity + sparse_array[entity] = n + end + end + + local function removed(entity) + local i = sparse_array[entity] + if i ~= n then + dense_array[i] = dense_array[n] + end + + dense_array[n] = nil + end + + for _, term in terms do + world:added(term, emplaced) + world:emplaced(term, emplaced) + end + + local function iter() + local i = n + print(i, "i") + return function() + local row = i + if row == 0 then + return nil + end + i -= 1 + return dense_array[row] + end + end + + local it = { + __iter = iter + } + return setmetatable(it, it) +end + +local function observers_init(world) + local signals = { + added = {}, + emplaced = {}, + removed = {} + } + world.added = function(_, component, fn) + local listeners = signals.added[component] + if not listeners then + listeners = {} + signals.added[component] = listeners + local idr = jecs.id_record_ensure(world, component) + idr.hooks.on_add = function(entity) + for _, listener in listeners do + listener(entity) + end + end + end + table.insert(listeners, fn) + end + + world.emplaced = function(_, component, fn) + local listeners = signals.emplaced[component] + if not listeners then + listeners = {} + signals.emplaced[component] = listeners + local idr = jecs.id_record_ensure(world, component) + idr.hooks.on_set = function(entity, value) + for _, listener in listeners do + listener(entity, value) + end + end + end + table.insert(listeners, fn) + end + + world.removed = function(_, component, fn) + local listeners = signals.removed[component] + if not listeners then + listeners = {} + signals.removed[component] = listeners + local idr = jecs.id_record_ensure(world, component) + idr.hooks.on_remove = function(entity) + for _, listener in listeners do + listener(entity) + end + end + end + table.insert(listeners, fn) + end + + world.signals = signals + + world.track = world_track + return +end + +local world = jecs.world() +observers_init(world) + +local A = world:component() +local B = world:component() + +world:added(A, print) +world:added(A, function(entity) + print(entity, 2) +end) + +local observer = observers_new({ + event = jecs.OnAdd, + query = world:query(A, B), + callback = function(entity) + print(entity, 3) + end +}) + +local e = world:entity() +local Test = world:track(A, B) +for a in Test do + print(a) + assert(false) +end +world:add(e, A) + +-- Output: +-- 271 +-- 271, 2 +for _ in Test do + assert(false) +end +world:add(e, B) +for a in Test do + print("lol") + assert(true) +end +-- Output: +-- 271, 3