Replace OnSet hook with OnChange

This commit is contained in:
Ukendio 2025-03-30 21:31:18 +02:00
parent a466ab151b
commit cf88c259f8
3 changed files with 209 additions and 25 deletions

View file

@ -61,8 +61,8 @@ type ecs_id_record_t = {
flags: number, flags: number,
size: number, size: number,
hooks: { hooks: {
on_add: ((entity: i53) -> ())?, on_add: ((entity: i53, data: any?) -> ())?,
on_set: ((entity: i53, data: any) -> ())?, on_change: ((entity: i53, data: any) -> ())?,
on_remove: ((entity: i53) -> ())?, on_remove: ((entity: i53) -> ())?,
}, },
} }
@ -111,7 +111,7 @@ local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
-- stylua: ignore start -- stylua: ignore start
local EcsOnAdd = HI_COMPONENT_ID + 1 local EcsOnAdd = HI_COMPONENT_ID + 1
local EcsOnRemove = HI_COMPONENT_ID + 2 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 EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5 local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6 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 has_delete = true
end 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 if is_tag and is_pair then
is_tag = not world_has_one_inline(world, target, EcsComponent) 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 = bit32.bor(
flags, 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_delete then ECS_ID_DELETE else 0,
if is_tag then ECS_ID_IS_TAG 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, flags = flags,
hooks = { hooks = {
on_add = on_add, on_add = on_add,
on_set = on_set, on_change = on_change,
on_remove = on_remove, 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 -- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array. -- point in the types array.
local dst = table.clone(node.types) :: { i53 }
local at = find_insert(id_types, id) local at = find_insert(id_types, id)
if at == -1 then if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it -- 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. -- directly instead of needing to hash types for a lookup to the archetype.
return node return node
end end
local dst = table.clone(id_types) :: { i53 }
table.insert(dst, at, id) table.insert(dst, at, id)
return archetype_ensure(world, dst) 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 from == to then
-- If the archetypes are the same it can avoid moving the entity -- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly. -- 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 tr = to.records[id]
local column = from.columns[tr] local column = from.columns[tr]
column[record.row] = data column[record.row] = data
local on_set = idr_hooks.on_set
if on_set then
on_set(entity, data)
end
return return
end 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 local on_add = idr_hooks.on_add
if on_add then if on_add then
on_add(entity) on_add(entity, data)
end end
local on_set = idr_hooks.on_set
if on_set then
on_set(entity, data)
end
end end
local function world_component(world: World): i53 local function world_component(world: World): i53
@ -2471,7 +2467,7 @@ local function world_new()
end end
world_add(self, EcsName, EcsComponent) world_add(self, EcsName, EcsComponent)
world_add(self, EcsOnSet, EcsComponent) world_add(self, EcsOnChange, EcsComponent)
world_add(self, EcsOnAdd, EcsComponent) world_add(self, EcsOnAdd, EcsComponent)
world_add(self, EcsOnRemove, EcsComponent) world_add(self, EcsOnRemove, EcsComponent)
world_add(self, EcsWildcard, EcsComponent) world_add(self, EcsWildcard, EcsComponent)
@ -2479,7 +2475,7 @@ local function world_new()
world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd") world_set(self, EcsOnAdd, EcsName, "jecs.OnAdd")
world_set(self, EcsOnRemove, EcsName, "jecs.OnRemove") 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, EcsWildcard, EcsName, "jecs.Wildcard")
world_set(self, EcsChildOf, EcsName, "jecs.ChildOf") world_set(self, EcsChildOf, EcsName, "jecs.ChildOf")
world_set(self, EcsComponent, EcsName, "jecs.Component") world_set(self, EcsComponent, EcsName, "jecs.Component")
@ -2612,7 +2608,7 @@ return {
OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>, OnAdd = EcsOnAdd :: Entity<(entity: Entity) -> ()>,
OnRemove = EcsOnRemove :: 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, ChildOf = EcsChildOf :: Entity,
Component = EcsComponent :: Entity, Component = EcsComponent :: Entity,
Wildcard = EcsWildcard :: Entity, Wildcard = EcsWildcard :: Entity,

View file

@ -1664,9 +1664,9 @@ TEST("Hooks", function()
local Number = world:component() local Number = world:component()
local e1 = world:entity() local e1 = world:entity()
world:set(Number, jecs.OnSet, function(entity, data) world:set(Number, jecs.OnChange, function(entity, data)
CHECK(e1 == entity) CHECK(e1 == entity)
CHECK(data == world:get(entity, Number)) CHECK(world:get(entity, Number) == nil)
CHECK(data == 1) CHECK(data == 1)
end) end)
world:set(e1, Number, 1) world:set(e1, Number, 1)

188
tools/observers.luau Normal file
View file

@ -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