Move only once during removal of invalidated pair
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run

This commit is contained in:
Ukendio 2025-07-05 21:26:05 +02:00
parent 29305cac5d
commit 6053038cc1
2 changed files with 151 additions and 109 deletions

View file

@ -169,7 +169,7 @@ local function observers_add(world: jecs.World): PatchedWorld
local idr = world.component_index[component] local idr = world.component_index[component]
if idr then if idr then
idr.hooks.on_add = on_add idr.on_add = on_add
else else
world:set(component, jecs.OnAdd, on_add) world:set(component, jecs.OnAdd, on_add)
end end
@ -203,7 +203,7 @@ local function observers_add(world: jecs.World): PatchedWorld
end end
local idr = world.component_index[component] local idr = world.component_index[component]
if idr then if idr then
idr.hooks.on_change = on_change idr.on_change = on_change
else else
world:set(component, jecs.OnChange, on_change) world:set(component, jecs.OnChange, on_change)
end end
@ -238,7 +238,7 @@ local function observers_add(world: jecs.World): PatchedWorld
local idr = world.component_index[component] local idr = world.component_index[component]
if idr then if idr then
idr.hooks.on_remove = on_remove idr.on_remove = on_remove
else else
world:set(component, jecs.OnRemove, on_remove) world:set(component, jecs.OnRemove, on_remove)
end end

238
jecs.luau
View file

@ -170,11 +170,10 @@ export type ComponentRecord = {
counts: { [Id]: number }, counts: { [Id]: number },
flags: number, flags: number,
size: number, size: number,
hooks: {
on_add: (<T>(entity: Entity, id: Entity<T>, value: T?) -> ())?, on_add: (<T>(entity: Entity, id: Entity<T>, value: T?) -> ())?,
on_change: (<T>(entity: Entity, id: Entity<T>, value: T) -> ())?, on_change: (<T>(entity: Entity, id: Entity<T>, value: T) -> ())?,
on_remove: ((entity: Entity, id: Entity) -> ())?, on_remove: ((entity: Entity, id: Entity) -> ())?,
},
} }
export type ComponentIndex = Map<Id, ComponentRecord> export type ComponentIndex = Map<Id, ComponentRecord>
export type Archetypes = { [Id]: Archetype } export type Archetypes = { [Id]: Archetype }
@ -746,11 +745,10 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
records = {}, records = {},
counts = {}, counts = {},
flags = flags, flags = flags,
hooks = {
on_add = on_add, on_add = on_add,
on_change = on_change, on_change = on_change,
on_remove = on_remove, on_remove = on_remove,
},
} :: ComponentRecord } :: ComponentRecord
component_index[id] = idr component_index[id] = idr
@ -1029,7 +1027,7 @@ local function archetype_delete(world: World, archetype: Archetype, row: number)
for _, id in id_types do for _, id in id_types do
local idr = component_index[id] local idr = component_index[id]
local on_remove = idr.hooks.on_remove local on_remove = idr.on_remove
if on_remove then if on_remove then
on_remove(delete, id) on_remove(delete, id)
end end
@ -2025,7 +2023,7 @@ local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, va
local value = values[i] local value = values[i]
local cdr = component_index[id] local cdr = component_index[id]
local on_add = cdr.hooks.on_add local on_add = cdr.on_add
if value then if value then
columns_map[id][row] = value columns_map[id][row] = value
if on_add then if on_add then
@ -2070,11 +2068,11 @@ local function ecs_bulk_insert(world: World, entity: Entity, ids: { Entity }, va
local value = values[i] :: any local value = values[i] :: any
local on_add = idr.hooks.on_add local on_add = idr.on_add
if value ~= nil then if value ~= nil then
columns_map[id][row] = value columns_map[id][row] = value
local on_change = idr.hooks.on_change local on_change = idr.on_change
local hook = if set then on_change else on_add local hook = if set then on_change else on_add
if hook then if hook then
hook(entity, id, value :: any) hook(entity, id, value :: any)
@ -2109,7 +2107,7 @@ local function ecs_bulk_remove(world: World, entity: Entity, ids: { Entity })
remove[id] = true remove[id] = true
local idr = component_index[id] local idr = component_index[id]
local on_remove = idr.hooks.on_remove local on_remove = idr.on_remove
if on_remove then if on_remove then
on_remove(entity, id) on_remove(entity, id)
end end
@ -2183,6 +2181,76 @@ local function world_new()
return r return r
end end
local function inner_archetype_move(
entity: Entity,
to: Archetype,
dst_row: i24,
from: Archetype,
src_row: i24
)
local src_columns = from.columns
local dst_entities = to.entities
local src_entities = from.entities
local last = #src_entities
local id_types = from.types
local columns_map = to.columns_map
if src_row ~= last then
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
local dst_column = columns_map[id_types[i]]
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[src_row] = column[last]
column[last] = nil
end
local e2 = src_entities[last]
src_entities[src_row] = e2
local record2 = eindex_sparse_array[ECS_ENTITY_T_LO(e2 :: number)]
record2.row = src_row
else
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[last] = nil
end
end
src_entities[last] = nil :: any
dst_entities[dst_row] = entity
end
local function inner_entity_move(
entity_index: EntityIndex,
entity: Entity,
record: Record,
to: Archetype
)
local sourceRow = record.row
local from = record.archetype
local dst_row = archetype_append(entity, to)
inner_archetype_move(entity, to, dst_row, from, sourceRow)
record.archetype = to
record.row = dst_row
end
-- local function inner_entity_index_try_get(entity: number): Record? -- local function inner_entity_index_try_get(entity: number): Record?
-- local r = inner_entity_index_try_get_any(entity) -- local r = inner_entity_index_try_get_any(entity)
-- if r then -- if r then
@ -2235,7 +2303,7 @@ local function world_new()
if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local cr = idr.records[src.id] local cr = idr.records[src.id]
if cr then if cr then
local on_remove = idr.hooks.on_remove local on_remove = idr.on_remove
local id_types = src.types local id_types = src.types
if on_remove then if on_remove then
on_remove(entity, id_types[cr]) on_remove(entity, id_types[cr])
@ -2262,14 +2330,14 @@ local function world_new()
return return
end end
if from then if from then
entity_move(entity_index, entity, record, to) inner_entity_move(entity_index, entity, record, to)
else else
if #to.types > 0 then if #to.types > 0 then
new_entity(entity, record, to) new_entity(entity, record, to)
end end
end end
local on_add = idr.hooks.on_add local on_add = idr.on_add
if on_add then if on_add then
on_add(entity, id) on_add(entity, id)
@ -2281,7 +2349,7 @@ local function world_new()
return return
end end
if from then if from then
entity_move(entity_index, entity, record, to) inner_entity_move(entity_index, entity, record, to)
else else
if #to.types > 0 then if #to.types > 0 then
new_entity(entity, record, to) new_entity(entity, record, to)
@ -2289,7 +2357,7 @@ local function world_new()
end end
local idr = component_index[id] local idr = component_index[id]
local on_add = idr.hooks.on_add local on_add = idr.on_add
if on_add then if on_add then
on_add(entity, id) on_add(entity, id)
@ -2421,19 +2489,17 @@ local function world_new()
local column = src.columns_map[id] local column = src.columns_map[id]
if column then if column then
local idr = component_index[id] local idr = component_index[id]
local idr_hooks = idr.hooks
column[record.row] = data column[record.row] = data
-- 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 local on_change = idr.on_change
if on_change then if on_change then
on_change(entity, id, data) on_change(entity, id, data)
end end
else else
local to: Archetype local to: Archetype
local idr: ComponentRecord local idr: ComponentRecord
local idr_hooks
if ECS_IS_PAIR(id::number) then if ECS_IS_PAIR(id::number) then
local edge = archetype_edges[src.id] local edge = archetype_edges[src.id]
to = edge[id] to = edge[id]
@ -2444,7 +2510,7 @@ local function world_new()
if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then if idr and bit32.btest(idr.flags, ECS_ID_IS_EXCLUSIVE) then
local cr = idr.records[src.id] local cr = idr.records[src.id]
if cr then if cr then
local on_remove = idr.hooks.on_remove local on_remove = idr.on_remove
local id_types = src.types local id_types = src.types
if on_remove then if on_remove then
on_remove(entity, id_types[cr]) on_remove(entity, id_types[cr])
@ -2467,7 +2533,6 @@ local function world_new()
else else
idr = component_index[id] idr = component_index[id]
end end
idr_hooks = idr.hooks
else else
to = inner_archetype_traverse_add(id, from) to = inner_archetype_traverse_add(id, from)
idr = component_index[id] idr = component_index[id]
@ -2475,16 +2540,15 @@ local function world_new()
if from then if from then
-- If there was a previous archetype, then the entity needs to move the archetype -- If there was a previous archetype, then the entity needs to move the archetype
entity_move(entity_index, entity, record, to) inner_entity_move(entity_index, entity, record, to)
else else
new_entity(entity, record, to) new_entity(entity, record, to)
end end
idr_hooks = idr.hooks
column = to.columns_map[id] column = to.columns_map[id]
column[record.row] = data column[record.row] = data
local on_add = idr_hooks.on_add local on_add = idr.on_add
if on_add then if on_add then
on_add(entity, id, data) on_add(entity, id, data)
end end
@ -2565,14 +2629,14 @@ local function world_new()
if from.columns_map[id] then if from.columns_map[id] then
local idr = world.component_index[id] local idr = world.component_index[id]
local on_remove = idr.hooks.on_remove local on_remove = idr.on_remove
if on_remove then if on_remove then
on_remove(entity, id) on_remove(entity, id)
end end
local to = archetype_traverse_remove(world, id, record.archetype) local to = archetype_traverse_remove(world, id, record.archetype)
entity_move(entity_index, entity, record, to) inner_entity_move(entity_index, entity, record, to)
end end
end end
@ -2599,16 +2663,13 @@ local function world_new()
end end
if idr_t then if idr_t then
local queue: { i53 }
local ids: Map<i53, boolean>
local count = 0
local archetype_ids = idr_t.records local archetype_ids = idr_t.records
for archetype_id in archetype_ids do for archetype_id in archetype_ids do
local idr_t_archetype = archetypes[archetype_id] local idr_t_archetype = archetypes[archetype_id]
local idr_t_types = idr_t_archetype.types local idr_t_types = idr_t_archetype.types
local entities = idr_t_archetype.entities local entities = idr_t_archetype.entities
local removal_queued = false
local node = idr_t_archetype
for _, id in idr_t_types do for _, id in idr_t_types do
if not ECS_IS_PAIR(id::number) then if not ECS_IS_PAIR(id::number) then
@ -2619,57 +2680,48 @@ local function world_new()
if object ~= entity then if object ~= entity then
continue continue
end end
if not ids then node = archetype_traverse_remove(world, id, node)
ids = {} :: { [i53]: boolean } local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
end end
ids[id] = true
removal_queued = true
end end
if not removal_queued then for i = #entities, 1, -1 do
continue local e = entities[i]
end local r = inner_entity_index_try_get_unsafe(e::number) :: Record
inner_entity_move(entity_index, e, r, node)
if not queue then
queue = {} :: { i53 }
end
local n = #entities
table.move(entities, 1, n, count + 1, queue)
count += n
end
for id in ids do
for _, child in queue do
inner_world_remove(world, child, id)
end end
end end
end end
if idr_r then if idr_r then
local count = 0
local archetype_ids = idr_r.records local archetype_ids = idr_r.records
local ids = {}
local queue = {}
local records = idr_r.records local records = idr_r.records
local counts = idr_r.counts local counts = idr_r.counts
for archetype_id in archetype_ids do for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id] local idr_r_archetype = archetypes[archetype_id]
local node = idr_r_archetype
local entities = idr_r_archetype.entities local entities = idr_r_archetype.entities
local tr = records[archetype_id] local tr = records[archetype_id]
local tr_count = counts[archetype_id] local tr_count = counts[archetype_id]
local types = idr_r_archetype.types local types = idr_r_archetype.types
for i = tr, tr + tr_count - 1 do for i = tr, tr + tr_count - 1 do
ids[types[i]] = true local id = types[i]
node = archetype_traverse_remove(world, id, idr_r_archetype)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end end
local n = #entities
table.move(entities, 1, n, count + 1, queue)
count += n
end end
end
for _, e in queue do for i = #entities, 1, -1 do
for id in ids do local e = entities[i]
inner_world_remove(world, e, id) local r = inner_entity_index_try_get_unsafe(e::number) :: Record
inner_entity_move(entity_index, e, r, node)
end end
end end
end end
@ -2715,7 +2767,7 @@ local function world_new()
archetype_destroy(world, idr_archetype) archetype_destroy(world, idr_archetype)
end end
else else
local on_remove = idr.hooks.on_remove local on_remove = idr.on_remove
if on_remove then if on_remove then
for archetype_id in idr.records do for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id] local idr_archetype = archetypes[archetype_id]
@ -2732,7 +2784,7 @@ local function world_new()
-- this is hypothetically not that expensive of an operation anyways -- this is hypothetically not that expensive of an operation anyways
to = archetype_traverse_remove(world, entity, from) to = archetype_traverse_remove(world, entity, from)
end end
entity_move(entity_index, e, r, to) inner_entity_move(entity_index, e, r, to)
end end
archetype_destroy(world, idr_archetype) archetype_destroy(world, idr_archetype)
@ -2753,19 +2805,15 @@ local function world_new()
end end
end end
end end
if idr_t then if idr_t then
local children: { i53 }
local ids: Map<i53, boolean>
local count = 0
local archetype_ids = idr_t.records local archetype_ids = idr_t.records
for archetype_id in archetype_ids do for archetype_id in archetype_ids do
local idr_t_archetype = archetypes[archetype_id] local idr_t_archetype = archetypes[archetype_id]
local node = idr_t_archetype
local idr_t_types = idr_t_archetype.types local idr_t_types = idr_t_archetype.types
local entities = idr_t_archetype.entities local entities = idr_t_archetype.entities
local removal_queued = false
local deleted = false
for _, id in idr_t_types do for _, id in idr_t_types do
if not ECS_IS_PAIR(id::number) then if not ECS_IS_PAIR(id::number) then
continue continue
@ -2783,31 +2831,24 @@ local function world_new()
local child = entities[i] local child = entities[i]
inner_world_delete(world, child) inner_world_delete(world, child)
end end
deleted = true
break break
else else
if not ids then node = archetype_traverse_remove(world, id, node)
ids = {} :: { [i53]: boolean } local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end
end end
ids[id] = true
removal_queued = true
end end
end end
if not removal_queued then if not deleted then
continue for i = #entities, 1, -1 do
end local e = entities[i]
if not children then local r = inner_entity_index_try_get_unsafe(e::number) :: Record
children = {} :: { i53 } inner_entity_move(entity_index, e, r, node)
end
local n = #entities
table.move(entities, 1, n, count + 1, children)
count += n
end
if ids then
for _, child in children do
for id in ids do
inner_world_remove(world, child, id)
end end
end end
end end
@ -2831,28 +2872,29 @@ local function world_new()
archetype_destroy(world, idr_r_archetype) archetype_destroy(world, idr_r_archetype)
end end
else else
local children = {}
local count = 0
local ids = {}
local counts = idr_r.counts local counts = idr_r.counts
local records = idr_r.records local records = idr_r.records
for archetype_id in archetype_ids do for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id] local idr_r_archetype = archetypes[archetype_id]
local node = idr_r_archetype
local entities = idr_r_archetype.entities local entities = idr_r_archetype.entities
local tr = records[archetype_id] local tr = records[archetype_id]
local tr_count = counts[archetype_id] local tr_count = counts[archetype_id]
local types = idr_r_archetype.types local types = idr_r_archetype.types
for i = tr, tr + tr_count - 1 do for i = tr, tr + tr_count - 1 do
ids[types[i]] = true local id = types[i]
node = archetype_traverse_remove(world, id, node)
local on_remove = component_index[id].on_remove
if on_remove then
for _, entity in entities do
on_remove(entity, id)
end end
local n = #entities
table.move(entities, 1, n, count + 1, children)
count += n
end end
for _, child in children do end
for id in ids do for i = #entities, 1, -1 do
inner_world_remove(world, child, id) local e = entities[i]
local r = inner_entity_index_try_get_unsafe(e::number) :: Record
inner_entity_move(entity_index, e, r, node)
end end
end end