diff --git a/src/init.luau b/src/init.luau index 1eb67d29..a97d3696 100644 --- a/src/init.luau +++ b/src/init.luau @@ -148,7 +148,7 @@ local function ECS_GENERATION_INC(e: i53) return id end - return ECS_COMBINE(id, next_gen) + flags + return ECS_COMBINE(id, next_gen) end return ECS_COMBINE(e, 1) end @@ -1683,13 +1683,13 @@ 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) + local e = entity_index_new_id(entity_index) world_add(self, e, EcsComponent) end for i = HI_COMPONENT_ID + 1, EcsRest do -- Initialize built-in components - entity_index_new_id(entity_index, i) + entity_index_new_id(entity_index) end world_add(self, EcsName, EcsComponent) @@ -1885,4 +1885,6 @@ return { 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, + entity_index_remove = entity_index_remove, + entity_index_new_id = entity_index_new_id } diff --git a/test/gen.luau b/test/gen.luau new file mode 100644 index 00000000..19d1244f --- /dev/null +++ b/test/gen.luau @@ -0,0 +1,192 @@ +type i53 = number +type i24 = number + +type Ty = { i53 } +type ArchetypeId = number + +type Column = { any } + +type Map = { [K]: V } + +type GraphEdge = { + from: Archetype, + to: Archetype?, + prev: GraphEdge?, + next: GraphEdge?, + id: number, +} + +type GraphEdges = Map + +type GraphNode = { + add: GraphEdges, + remove: GraphEdges, + refs: GraphEdge, +} + +type ArchetypeRecord = { + count: number, + column: number, +} + +export type Archetype = { + id: number, + node: GraphNode, + types: Ty, + type: string, + entities: { number }, + columns: { Column }, + records: { ArchetypeRecord }, +} +type Record = { + archetype: Archetype, + row: number, + dense: i24, +} + +type EntityIndex = { + dense_array: Map, + sparse_array: Map, + sparse_count: number, + alive_count: number, + max_id: number +} + + +local ECS_PAIR_FLAG = 0x8 +local ECS_ID_FLAGS_MASK = 0x10 +local ECS_ENTITY_MASK = bit32.lshift(1, 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) + +-- HIGH 24 bits LOW 24 bits +local function ECS_GENERATION(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 +end + +local function ECS_COMBINE(source: number, target: number): i53 + return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) +end + +local function ECS_GENERATION_INC(e: i53) + if e > ECS_ENTITY_MASK then + local flags = e // ECS_ID_FLAGS_MASK + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK + + 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 +local function ECS_ENTITY_T_LO(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e +end + + +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 + + if not r or r.dense == 0 then + return nil + end + + 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 + +local function entity_index_get_alive(entity_index: EntityIndex, + entity: number): number + + local r = entity_index_try_get_any(entity_index, entity) + if r then + return entity_index.dense_array[r.dense] + end + return 0 +end + +local function entity_index_remove(entity_index: EntityIndex, entity: number) + local r = entity_index_try_get(entity_index, entity) + if not r then + return + end + local dense_array = entity_index.dense_array + local index_of_deleted_entity = r.dense + local last_entity_alive_at_index = entity_index.alive_count + entity_index.alive_count -= 1 + + local last_alive_entity = dense_array[last_entity_alive_at_index] + local r_swap = entity_index_try_get_any( + entity_index, last_alive_entity) :: Record + r_swap.dense = index_of_deleted_entity + r.archetype = nil :: any + r.row = nil :: any + r.dense = last_entity_alive_at_index + + dense_array[index_of_deleted_entity] = last_alive_entity + dense_array[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) +end + +local function entity_index_new_id(entity_index: EntityIndex, data): i53 + local dense_array = entity_index.dense_array + if entity_index.alive_count ~= #dense_array then + entity_index.alive_count += 1 + local id = dense_array[entity_index.alive_count] + return id + end + entity_index.max_id +=1 + local id = entity_index.max_id + entity_index.alive_count += 1 + + dense_array[entity_index.alive_count] = id + entity_index.sparse_array[id] = { + dense = entity_index.alive_count, + archetype = data + } :: Record + + entity_index.sparse_count += 1 + + return id +end + +local function entity_index_is_alive(entity_index: EntityIndex, entity: number) + return entity_index_try_get(entity_index, entity) ~= nil +end + +local eidx = { + alive_count = 0, + max_id = 0, + sparse_array = {} :: { Record }, + sparse_count = 0, + dense_array = {} :: { i53 } +} +local e1v0 = entity_index_new_id(eidx, "e1v0") +local e2v0 = entity_index_new_id(eidx, "e2v0") +local e3v0 = entity_index_new_id(eidx, "e3v0") +local e4v0 = entity_index_new_id(eidx, "e4v0") +local e5v0 = entity_index_new_id(eidx, "e5v0") +local t = require("@testkit") +local tprint = t.print +entity_index_remove(eidx, e5v0) +local e5v1 = entity_index_new_id(eidx, "e5v1") +entity_index_remove(eidx, e2v0) +tprint(eidx) diff --git a/test/lol.luau b/test/lol.luau new file mode 100644 index 00000000..cbf50dd2 --- /dev/null +++ b/test/lol.luau @@ -0,0 +1,157 @@ +local c = { + white_underline = function(s: any) + return `\27[1;4m{s}\27[0m` + end, + + white = function(s: any) + return `\27[37;1m{s}\27[0m` + end, + + green = function(s: any) + return `\27[32;1m{s}\27[0m` + end, + + red = function(s: any) + return `\27[31;1m{s}\27[0m` + end, + + yellow = function(s: any) + return `\27[33;1m{s}\27[0m` + end, + + red_highlight = function(s: any) + return `\27[41;1;30m{s}\27[0m` + end, + + green_highlight = function(s: any) + return `\27[42;1;30m{s}\27[0m` + end, + + gray = function(s: any) + return `\27[30;1m{s}\27[0m` + end, +} + + +local ECS_PAIR_FLAG = 0x8 +local ECS_ID_FLAGS_MASK = 0x10 +local ECS_ENTITY_MASK = bit32.lshift(1, 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) + +type i53 = number +type i24 = number + +local function ECS_ENTITY_T_LO(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e +end + +local function ECS_GENERATION(e: i53): i24 + return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0 +end + +local ECS_ID = ECS_ENTITY_T_LO + +local function ECS_COMBINE(source: number, target: number): i53 + return (source * 268435456) + (target * ECS_ID_FLAGS_MASK) +end + +local function ECS_GENERATION_INC(e: i53) + if e > ECS_ENTITY_MASK then + local flags = e // ECS_ID_FLAGS_MASK + local id = flags // ECS_ENTITY_MASK + local generation = flags % ECS_GENERATION_MASK + + 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 + +local function bl() + print("") +end + +local function pe(e) + local gen = ECS_GENERATION(e) + return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`) +end + +local function dprint(tbl: { [number]: number }) + bl() + print("--------") + for i, e in tbl do + print("| "..pe(e).." |") + print("--------") + end + bl() +end + +local max_id = 0 +local alive_count = 0 +local dense = {} +local sparse = {} +local function alloc() + if alive_count ~= #dense then + alive_count += 1 + print("*recycled", pe(dense[alive_count])) + return dense[alive_count] + end + max_id += 1 + local id = max_id + alive_count += 1 + dense[alive_count] = id + sparse[id] = { + dense = alive_count + } + print("*allocated", pe(id)) + return id +end + +local function remove(entity) + local id = ECS_ID(entity) + local r = sparse[id] + local index_of_deleted_entity = r.dense + local last_entity_alive_at_index = alive_count -- last entity alive + alive_count -= 1 + local last_alive_entity = dense[last_entity_alive_at_index] + local r_swap = sparse[ECS_ID(last_alive_entity)] + r_swap.dense = r.dense + r.dense = last_entity_alive_at_index + dense[index_of_deleted_entity] = last_alive_entity + dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity) +end + +local function alive(e) + local r = sparse[ECS_ID(e)] + + return dense[r.dense] == e +end + +local function pa(e) + print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`) +end + +local tprint = require("@testkit").print +local e1v0 = alloc() +local e2v0 = alloc() +local e3v0 = alloc() +local e4v0 = alloc() +local e5v0 = alloc() +pa(e1v0) +pa(e4v0) +remove(e5v0) +pa(e5v0) + +local e5v1 = alloc() +pa(e5v0) +pa(e5v1) +pa(e2v0) +print(ECS_ID(e2v0)) + +dprint(dense) +remove(e2v0) +dprint(dense) diff --git a/test/tests.luau b/test/tests.luau index ee02770a..efa86cd9 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -191,6 +191,36 @@ TEST("world:entity()", function() CHECK(ecs_pair_first(world, pair) == e2) CHECK(ecs_pair_second(world, pair) == e3) end + + do CASE "Recycling" + local world = world_new() + local e = world:entity() + world:delete(e) + local e1 = world:entity() + world:delete(e1) + local e2 = world:entity() + CHECK(ECS_ID(e2) == e) + CHECK(ECS_GENERATION(e2) == 2) + CHECK(world:contains(e2)) + CHECK(not world:contains(e1)) + CHECK(not world:contains(e)) + end + + do CASE "Recycling max generation" + local world = world_new() + local pin = jecs.Rest + 1 + for i = 1, 2^16-1 do + local e = world:entity() + world:delete(e) + end + local e = world:entity() + CHECK(ECS_ID(e) == pin) + CHECK(ECS_GENERATION(e) == 2^16-1) + world:delete(e) + e = world:entity() + CHECK(ECS_ID(e) == pin) + CHECK(ECS_GENERATION(e) == 0) + end end) TEST("world:set()", function()