Initial commit

This commit is contained in:
Ukendio 2024-11-13 00:44:07 +01:00
parent c3874486a0
commit 7d09aca97d
2 changed files with 167 additions and 77 deletions

View file

@ -44,11 +44,6 @@ type Record = {
dense: i24,
}
type EntityIndex = {
dense: Map<i24, i53>,
sparse: Map<i53, Record>,
}
type ArchetypeRecord = {
count: number,
column: number,
@ -74,6 +69,14 @@ type ArchetypeDiff = {
removed: Ty,
}
type EntityIndex = {
dense_array: Map<i24, i53>,
sparse_array: Map<i53, Record>,
sparse_count: number,
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 +144,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
@ -167,46 +175,104 @@ end
local ERROR_ENTITY_NOT_ALIVE = "Entity is not alive"
local ERROR_GENERATION_INVALID = "INVALID GENERATION"
local function entity_index_get_alive(index: EntityIndex, e: i24): i53
local denseArray = index.dense
local id = denseArray[ECS_ENTITY_T_LO(e)]
if id then
local currentGeneration = ECS_GENERATION(id)
local gen = ECS_GENERATION(e)
if gen == currentGeneration then
return id
end
error(ERROR_GENERATION_INVALID)
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
error(ERROR_ENTITY_NOT_ALIVE)
if not r or r.dense == 0 then
return nil
end
return r
end
local function _entity_index_sparse_get(entityIndex, id)
return entityIndex.sparse[entity_index_get_alive(entityIndex, id)]
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(index: EntityIndex, e: i24): i53
local r = entity_index_try_get_any(index, e)
if r then
return index.dense_array[r.dense]
end
error("Invalid entity")
end
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_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 record_create(entity_index: EntityIndex, id: number, count: number )
entity_index.sparse_array[id] = { dense = count } :: Record
entity_index.sparse_count += 1
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,
} :: Record
entity_index.sparse_count += 1
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 +305,7 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row:
column[last] = nil
end
local sparse = entity_index.sparse
local sparse_array = entity_index.sparse_array
local moved = #src_entities
-- Move the entity from the source to the destination archetype.
@ -255,8 +321,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 +373,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 +404,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
@ -355,8 +421,9 @@ local function world_get_one_inline(world: World, entity: i53, id: i53): any
return archetype.columns[tr.column][record.row]
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 +439,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 +462,10 @@ 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 +613,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 +769,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 +797,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 +817,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 +829,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
@ -793,8 +870,11 @@ local function world_component(world: World): i53
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
@ -831,7 +911,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 +924,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 +948,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
@ -959,6 +1039,7 @@ local function archetype_destroy(world: World, archetype: Archetype)
end
end
local function world_cleanup(world: World)
local archetypes = world.archetypes
@ -983,10 +1064,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 +1123,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,15 +1146,17 @@ do
end
end
record.archetype = nil :: any
sparse_array[entity] = nil :: any
entity_index_remove(entity_index, delete)
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
local function ARM(query, ...)
@ -1610,14 +1691,17 @@ end
function World.new()
local entity_index: EntityIndex = {
dense = {} :: { [i24]: i53 },
sparse = {} :: { [i53]: Record },
dense_array = {} :: { [i24]: i53 },
sparse_array = {} :: { [i53]: Record },
sparse_count = 0,
alive_count = 0,
max_id = 0,
}
local self = setmetatable({
archetypeIndex = {} :: { [string]: Archetype },
archetypes = {} :: Archetypes,
componentIndex = {} :: ComponentIndex,
entityIndex = entity_index,
entity_index = entity_index,
nextArchetypeId = 0 :: number,
nextComponentId = 0 :: number,
nextEntityId = 0 :: number,
@ -1627,13 +1711,13 @@ function World.new()
self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
for i = 1, HI_COMPONENT_ID do
local e = entity_index_new_id(self.entityIndex, i)
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)
@ -1698,7 +1782,7 @@ export type World = {
archetypeIndex: { [string]: Archetype },
archetypes: Archetypes,
componentIndex: ComponentIndex,
entityIndex: EntityIndex,
entity_index: EntityIndex,
ROOT_ARCHETYPE: Archetype,
nextComponentId: number,
@ -1825,4 +1909,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
}

View file

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