From 303bd4ed165395d632c1155543e998c6913cdcbc Mon Sep 17 00:00:00 2001 From: Marcus Date: Wed, 13 Nov 2024 20:05:01 +0100 Subject: [PATCH] Entity recycling (#156) * Merge * Initial commit * Fix indentations * Remove sparse_count * Add whiteline * return 0 instead * Add check for friend existing * Force inlining * Manual inlining --- src/init.luau | 210 +++++++++++++++++++++++++++++++----------------- test/tests.luau | 15 ++-- 2 files changed, 147 insertions(+), 78 deletions(-) diff --git a/src/init.luau b/src/init.luau index 7cb467df..35c8d72b 100644 --- a/src/init.luau +++ b/src/init.luau @@ -44,11 +44,6 @@ type Record = { dense: i24, } -type EntityIndex = { - dense: Map, - sparse: Map, -} - type ArchetypeRecord = { count: number, column: number, @@ -74,6 +69,13 @@ type ArchetypeDiff = { removed: Ty, } +type EntityIndex = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, +} + local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 -- stylua: ignore start local EcsOnAdd = HI_COMPONENT_ID + 1 @@ -141,7 +143,12 @@ local function ECS_GENERATION_INC(e: i53) local id = flags // ECS_ENTITY_MASK local generation = flags % ECS_GENERATION_MASK - return ECS_COMBINE(id, generation + 1) + flags + local next_gen = generation + 1 + if next_gen > ECS_GENERATION_MASK then + return id + end + + return ECS_COMBINE(id, next_gen) + flags end return ECS_COMBINE(e, 1) end @@ -164,49 +171,73 @@ local function ECS_PAIR(pred: i53, obj: i53): i53 return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + FLAGS_ADD(--[[isPair]] true) :: i53 end -local ERROR_ENTITY_NOT_ALIVE = "Entity is not alive" -local ERROR_GENERATION_INVALID = "INVALID GENERATION" +local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record? + local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] + if not r then + return nil + end -local function entity_index_get_alive(index: EntityIndex, e: i24): i53 - local denseArray = index.dense - local id = denseArray[ECS_ENTITY_T_LO(e)] + if not r or r.dense == 0 then + return nil + end - if id then - local currentGeneration = ECS_GENERATION(id) - local gen = ECS_GENERATION(e) - if gen == currentGeneration then - return id + return r +end + +local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record? + local r = entity_index_try_get_any(entity_index, entity) + if r then + local r_dense = r.dense + if r_dense > entity_index.alive_count then + return nil + end + if entity_index.dense_array[r_dense] ~= entity then + return nil end + end + return r +end - error(ERROR_GENERATION_INVALID) +local function entity_index_get_alive(index: EntityIndex, e: i24): i53 + local r = entity_index_try_get_any(index, e) + if r then + return index.dense_array[r.dense] end + return 0 +end - error(ERROR_ENTITY_NOT_ALIVE) +local function entity_index_is_alive(entity_index: EntityIndex, entity: number) + return entity_index_try_get(entity_index, entity) ~= nil end -local function _entity_index_sparse_get(entityIndex, id) - return entityIndex.sparse[entity_index_get_alive(entityIndex, id)] +local function entity_index_new_id(entity_index: EntityIndex, data): i53 + local dense_array = entity_index.dense_array + local alive_count = entity_index.alive_count + if alive_count ~= #dense_array then + alive_count += 1 + entity_index.alive_count = alive_count + local id = dense_array[alive_count] + return id + end + + local id = entity_index.max_id + 1 + entity_index.max_id = id + alive_count += 1 + entity_index.alive_count = alive_count + dense_array[alive_count] = id + entity_index.sparse_array[id] = { dense = alive_count } :: Record + + return id end -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits local function ecs_pair_first(world, e) - return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_HI(e)) + return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e)) end -- ECS_PAIR_SECOND gets the relationship / pred / LOW bits local function ecs_pair_second(world, e) - return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_LO(e)) -end - -local function entity_index_new_id(entityIndex: EntityIndex, index: i24): i53 - --local id = ECS_COMBINE(index, 0) - local id = index - entityIndex.sparse[id] = { - dense = index, - } :: Record - entityIndex.dense[index] = id - - return id + return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) end local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) @@ -239,7 +270,6 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: column[last] = nil end - local sparse = entity_index.sparse local moved = #src_entities -- Move the entity from the source to the destination archetype. @@ -255,8 +285,8 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: src_entities[moved] = nil :: any dst_entities[dst_row] = e1 - local record1 = sparse[e1] - local record2 = sparse[e2] + local record1 = entity_index_try_get_any(entity_index, e1) + local record2 = entity_index_try_get_any(entity_index, e2) record1.row = dst_row record2.row = src_row @@ -307,7 +337,7 @@ do end function world_get(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any - local record = world.entityIndex.sparse[entity] + local record = entity_index_try_get(world.entity_index, entity) if not record then return nil end @@ -338,7 +368,7 @@ do end local function world_get_one_inline(world: World, entity: i53, id: i53): any - local record = world.entityIndex.sparse[entity] + local record = entity_index_try_get(world.entity_index, entity) if not record then return nil end @@ -356,7 +386,7 @@ local function world_get_one_inline(world: World, entity: i53, id: i53): any end local function world_has_one_inline(world: World, entity: number, id: i53): boolean - local record = world.entityIndex.sparse[entity] + local record = entity_index_try_get(world.entity_index, entity) if not record then return false end @@ -372,7 +402,7 @@ local function world_has_one_inline(world: World, entity: number, id: i53): bool end local function world_has(world: World, entity: number, ...: i53): boolean - local record = world.entityIndex.sparse[entity] + local record = entity_index_try_get(world.entity_index, entity) if not record then return false end @@ -395,7 +425,11 @@ end local function world_target(world: World, entity: i53, relation: i24, index: number?): i24? local nth = index or 0 - local record = world.entityIndex.sparse[entity] + local record = entity_index_try_get(world.entity_index, entity) + if not record then + return nil + end + local archetype = record.archetype if not archetype then return nil @@ -543,9 +577,7 @@ local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): A end local function world_entity(world: World): i53 - local entityId = (world.nextEntityId :: number) + 1 - world.nextEntityId = entityId - return entity_index_new_id(world.entityIndex, entityId + EcsRest) + return entity_index_new_id(world.entity_index) end local function world_parent(world: World, entity: i53) @@ -701,15 +733,19 @@ local function invoke_hook(action, entity, data) end local function world_add(world: World, entity: i53, id: i53): () - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entity] + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) + if not record then + return + end + local from = record.archetype local to = archetype_traverse_add(world, id, from) if from == to then return end if from then - entity_move(entityIndex, entity, record, to) + entity_move(entity_index, entity, record, to) else if #to.types > 0 then new_entity(entity, record, to) @@ -725,8 +761,12 @@ local function world_add(world: World, entity: i53, id: i53): () end local function world_set(world: World, entity: i53, id: i53, data: unknown): () - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entity] + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) + if not record then + return + end + local from: Archetype = record.archetype local to: Archetype = archetype_traverse_add(world, id, from) local idr = world.componentIndex[id] @@ -741,7 +781,8 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. local tr = to.records[id] - from.columns[tr.column][record.row] = data + local column = from.columns[tr.column] + column[record.row] = data local on_set = idr_hooks.on_set if on_set then on_set(entity, data) @@ -752,7 +793,7 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () if from then -- If there was a previous archetype, then the entity needs to move the archetype - entity_move(entityIndex, entity, record, to) + entity_move(entity_index, entity, record, to) else if #to.types > 0 then -- When there is no previous archetype it should create the archetype @@ -788,15 +829,18 @@ local function world_component(world: World): i53 error("Too many components, consider using world:entity() instead to create components.") end world.nextComponentId = componentId - local id = entity_index_new_id(world.entityIndex, componentId) - world_add(world, id, EcsComponent) - return id + + return componentId end local function world_remove(world: World, entity: i53, id: i53) - local entity_index = world.entityIndex - local record = entity_index.sparse[entity] + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) + if not record then + return + end local from = record.archetype + if not from then return end @@ -831,7 +875,7 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) - local entityIndex = world.entityIndex + local entityIndex = world.entity_index local columns = archetype.columns local types = archetype.types local entities = archetype.entities @@ -844,7 +888,7 @@ local function archetype_delete(world: World, archetype: Archetype, row: number, if row ~= last then -- TODO: should be "entity_index_sparse_get(entityIndex, move)" - local record_to_move = entityIndex.sparse[move] + local record_to_move = entity_index_try_get_any(entityIndex, move) if record_to_move then record_to_move.row = row end @@ -868,7 +912,7 @@ end local function world_clear(world: World, entity: i53) --TODO: use sparse_get (stashed) - local record = world.entityIndex.sparse[entity] + local record = entity_index_try_get(world.entity_index, entity) if not record then return end @@ -983,10 +1027,8 @@ end local world_delete: (world: World, entity: i53, destruct: boolean?) -> () do function world_delete(world: World, entity: i53, destruct: boolean?) - local entityIndex = world.entityIndex - local sparse_array = entityIndex.sparse - - local record = sparse_array[entity] + local entity_index = world.entity_index + local record = entity_index_try_get(entity_index, entity) if not record then return end @@ -1044,7 +1086,7 @@ do if not ECS_IS_PAIR(id) then continue end - local object = ECS_ENTITY_T_LO(id) + local object = ecs_pair_second(world, id) if object == delete then local id_record = component_index[id] local flags = id_record.flags @@ -1067,13 +1109,25 @@ do end end + local dense_array = entity_index.dense_array + local index_of_deleted_entity = record.dense + local index_of_last_alive_entity = entity_index.alive_count + entity_index.alive_count = index_of_last_alive_entity - 1 + + local last_alive_entity = dense_array[index_of_last_alive_entity] + local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: Record + r_swap.dense = index_of_deleted_entity record.archetype = nil :: any - sparse_array[entity] = nil :: any + record.row = nil :: any + record.dense = index_of_last_alive_entity + + dense_array[index_of_deleted_entity] = last_alive_entity + dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity) end end local function world_contains(world: World, entity): boolean - return world.entityIndex.sparse[entity] ~= nil + return entity_index_is_alive(world.entity_index, entity) end local function NOOP() end @@ -1609,14 +1663,17 @@ if _G.__JECS_DEBUG then end function World.new() + local entity_index: EntityIndex = { + dense_array = {} :: { [i24]: i53 }, + sparse_array = {} :: { [i53]: Record }, + alive_count = 0, + max_id = 0, + } local self = setmetatable({ archetypeIndex = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, componentIndex = {} :: ComponentIndex, - entityIndex = { - dense = {} :: { [i24]: i53 }, - sparse = {} :: { [i53]: Record }, - } :: EntityIndex, + entity_index = entity_index, nextArchetypeId = 0 :: number, nextComponentId = 0 :: number, nextEntityId = 0 :: number, @@ -1625,9 +1682,14 @@ function World.new() self.ROOT_ARCHETYPE = archetype_create(self, {}, "") + for i = 1, HI_COMPONENT_ID do + local e = entity_index_new_id(entity_index, i) + world_add(self, e, EcsComponent) + end + for i = HI_COMPONENT_ID + 1, EcsRest do -- Initialize built-in components - entity_index_new_id(self.entityIndex, i) + entity_index_new_id(entity_index, i) end world_add(self, EcsName, EcsComponent) @@ -1692,7 +1754,7 @@ export type World = { archetypeIndex: { [string]: Archetype }, archetypes: Archetypes, componentIndex: ComponentIndex, - entityIndex: EntityIndex, + entity_index: EntityIndex, ROOT_ARCHETYPE: Archetype, nextComponentId: number, @@ -1819,4 +1881,8 @@ return { create_edge_for_remove = create_edge_for_remove, archetype_traverse_add = archetype_traverse_add, archetype_traverse_remove = archetype_traverse_remove, + + entity_index_try_get = entity_index_try_get, + entity_index_try_get_any = entity_index_try_get_any, + entity_index_is_alive = entity_index_is_alive, } diff --git a/test/tests.luau b/test/tests.luau index b9375efe..ee02770a 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -7,9 +7,11 @@ local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local IS_PAIR = jecs.IS_PAIR 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 entity_index_try_get_any = jecs.entity_index_try_get_any +local entity_index_get_alive = jecs.entity_index_get_alive +local entity_index_is_alive = jecs.entity_index_is_alive local world_new = jecs.World.new local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test() @@ -29,7 +31,7 @@ type World = jecs.WorldShim local function debug_world_inspect(world) local function record(e) - return world.entityIndex.sparse[e] + return entity_index_try_get_any(world.entity_index, e) end local function tbl(e) return record(e).archetype @@ -168,7 +170,6 @@ TEST("world:entity()", function() local world = jecs.World.new() local e = world:entity() CHECK(ECS_ID(e) == 1 + jecs.Rest) - CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) CHECK(ECS_GENERATION(e) == 0) -- 0 e = ECS_GENERATION_INC(e) CHECK(ECS_GENERATION(e) == 1) -- 1 @@ -878,9 +879,10 @@ TEST("world:clear()", function() CHECK(archetype_entities[1] == _e) CHECK(archetype_entities[2] == _e1) - local sparse_array = world.entityIndex.sparse - local e_record = sparse_array[e] - local e1_record = sparse_array[e1] + local e_record = entity_index_try_get_any( + world.entity_index, e) + local e1_record = entity_index_try_get_any( + world.entity_index, e1) CHECK(e_record.archetype == archetype) CHECK(e1_record.archetype == archetype) CHECK(e1_record.row == 2) @@ -1085,6 +1087,7 @@ TEST("world:delete", function() for i, friend in friends do CHECK(not world:has(friend, pair(FriendsWith, e))) CHECK(world:has(friend, Health)) + CHECK(world:contains(friend)) end end