diff --git a/CHANGELOG.md b/CHANGELOG.md index 52f87cc..b3180c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - `[world]`: - 16% faster `world:get` - - `world:has` no longer typechecks components after the 8th one. - `[typescript]` - Fixed Entity type to default to `undefined | unknown` instead of just `undefined` @@ -166,7 +165,7 @@ The format is based on [Keep a Changelog][kac], and this project adheres to - Separates ranges for components and entity IDs. - - IDs created with `world:component()` will promote array lookups rather than map lookups in the `component_index` which is a significant boost + - IDs created with `world:component()` will promote array lookups rather than map lookups in the `componentIndex` which is a significant boost - No longer caches the column pointers directly and instead the column indices which stay persistent even when data is reallocated during swap-removals - This was an issue with the iterator being invalidated when you move an entity to a different archetype. diff --git a/init.luau b/init.luau index e53d366..d80cd00 100644 --- a/init.luau +++ b/init.luau @@ -35,8 +35,7 @@ export type Archetype = { type: string, entities: { number }, columns: { Column }, - records: { number }, - counts: { number }, + records: { ArchetypeRecord }, } & GraphNode export type Record = { @@ -45,9 +44,13 @@ export type Record = { dense: i24, } +type ArchetypeRecord = { + count: number, + column: number, +} + type IdRecord = { - columns: { number }, - counts: { number }, + cache: { ArchetypeRecord }, flags: number, size: number, hooks: { @@ -272,7 +275,7 @@ local function query_match(query, archetype: Archetype) end local function find_observers(world: World, event, component): { Observer }? - local cache = world.observable[event] + local cache = world.observerable[event] if not cache then return nil end @@ -299,7 +302,7 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: -- Sometimes target column may not exist, e.g. when you remove a component. if tr then - dst_columns[tr][dst_row] = column[src_row] + dst_columns[tr.column][dst_row] = column[src_row] end -- If the entity is the last row in the archetype then swapping it would be meaningless. @@ -340,17 +343,17 @@ local function archetype_append(entity: number, archetype: Archetype): number return length end -local function new_entity(entity: i53, record: Record, archetype: Archetype): Record - local row = archetype_append(entity, archetype) +local function new_entity(entityId: i53, record: Record, archetype: Archetype): Record + local row = archetype_append(entityId, archetype) record.archetype = archetype record.row = row return record end -local function entity_move(entity_index: EntityIndex, entity: i53, record: Record, to: Archetype) +local function entity_move(entity_index: EntityIndex, entityId: i53, record: Record, to: Archetype) local sourceRow = record.row local from = record.archetype - local dst_row = archetype_append(entity, to) + local dst_row = archetype_append(entityId, to) archetype_move(entity_index, to, dst_row, from, sourceRow) record.archetype = to record.row = dst_row @@ -360,14 +363,14 @@ local function hash(arr: { number }): string return table.concat(arr, "_") end -local function fetch(id, records: { number }, columns: { Column }, row: number): any +local function fetch(id, records: { ArchetypeRecord }, columns: { Column }, row: number): any local tr = records[id] if not tr then return nil end - return columns[tr][row] + return columns[tr.column][row] end local function world_get(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any @@ -415,7 +418,7 @@ local function world_get_one_inline(world: World, entity: i53, id: i53): any if not tr then return nil end - return archetype.columns[tr][record.row] + return archetype.columns[tr.column][record.row] end local function world_has_one_inline(world: World, entity: number, id: i53): boolean @@ -468,24 +471,22 @@ local function world_target(world: World, entity: i53, relation: i24, index: num return nil end - local idr = world.component_index[ECS_PAIR(relation, EcsWildcard)] + local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] if not idr then return nil end - local archetype_id = archetype.id - local count = idr.counts[archetype.id] - if not count then + local tr = idr.cache[archetype.id] + if not tr then return nil end + local count = tr.count if nth >= count then nth = nth + count + 1 end - local tr = idr.columns[archetype_id] - - nth = archetype.types[nth + tr] + nth = archetype.types[nth + tr.column] if not nth then return nil @@ -501,8 +502,8 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean end local function id_record_ensure(world: World, id: number): IdRecord - local component_index = world.component_index - local idr: IdRecord = component_index[id] + local componentIndex = world.componentIndex + local idr = componentIndex[id] if not idr then local flags = ECS_ID_MASK @@ -540,17 +541,15 @@ local function id_record_ensure(world: World, id: number): IdRecord idr = { size = 0, - columns = {}, - counts = {}, + cache = {}, flags = flags, hooks = { on_add = on_add, on_set = on_set, on_remove = on_remove, }, - } - - component_index[id] = idr + } :: IdRecord + componentIndex[id] = idr end return idr @@ -558,45 +557,36 @@ end local function archetype_append_to_records( idr: IdRecord, - archetype: Archetype, + archetype_id: number, + records: Map, id: number, index: number ) - local archetype_id = archetype.id - local archetype_records = archetype.records - local archetype_counts = archetype.counts - local idr_columns = idr.columns - local idr_counts = idr.counts - local tr = idr_columns[archetype_id] + local tr = idr.cache[archetype_id] if not tr then - idr_columns[archetype_id] = index - idr_counts[archetype_id] = 1 - - archetype_records[id] = index - archetype_counts[id] = 1 + tr = { column = index, count = 1 } + idr.cache[archetype_id] = tr + idr.size += 1 + records[id] = tr else - local max_count = idr_counts[archetype_id] + 1 - idr_counts[archetype_id] = max_count - archetype_counts[id] = max_count + tr.count += 1 end end local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype - local archetype_id = (world.max_archetype_id :: number) + 1 - world.max_archetype_id = archetype_id + local archetype_id = (world.nextArchetypeId :: number) + 1 + world.nextArchetypeId = archetype_id local length = #id_types local columns = (table.create(length) :: any) :: { Column } - local records: { number } = {} - local counts: {number} = {} + local records: { ArchetypeRecord } = {} local archetype: Archetype = { columns = columns, entities = {}, id = archetype_id, records = records, - counts = counts, type = ty, types = id_types, @@ -607,7 +597,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) for i, componentId in id_types do local idr = id_record_ensure(world, componentId) - archetype_append_to_records(idr, archetype, componentId, i) + archetype_append_to_records(idr, archetype_id, records, componentId, i) if ECS_IS_PAIR(componentId) then local relation = ecs_pair_first(world, componentId) @@ -615,11 +605,11 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) local r = ECS_PAIR(relation, EcsWildcard) local idr_r = id_record_ensure(world, r) - archetype_append_to_records(idr_r, archetype, r, i) + archetype_append_to_records(idr_r, archetype_id, records, r, i) local t = ECS_PAIR(EcsWildcard, object) local idr_t = id_record_ensure(world, t) - archetype_append_to_records(idr_t, archetype, t, i) + archetype_append_to_records(idr_t, archetype_id, records, t, i) end if bit32.band(idr.flags, ECS_ID_IS_TAG) == 0 then @@ -641,7 +631,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) end end - world.archetype_index[ty] = archetype + world.archetypeIndex[ty] = archetype world.archetypes[archetype_id] = archetype return archetype @@ -661,7 +651,7 @@ local function archetype_ensure(world: World, id_types): Archetype end local ty = hash(id_types) - local archetype = world.archetype_index[ty] + local archetype = world.archetypeIndex[ty] if archetype then return archetype end @@ -799,6 +789,10 @@ local function archetype_traverse_remove(world: World, id: i53, from: Archetype) return to :: Archetype end +local function invoke_hook(action, entity, data) + action(entity, data) +end + local function world_add(world: World, entity: i53, id: i53): () local entity_index = world.entity_index local record = entity_index_try_get_fast(entity_index, entity) @@ -819,7 +813,7 @@ local function world_add(world: World, entity: i53, id: i53): () end end - local idr = world.component_index[id] + local idr = world.componentIndex[id] local on_add = idr.hooks.on_add if on_add then @@ -836,14 +830,19 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () local from: Archetype = record.archetype local to: Archetype = archetype_traverse_add(world, id, from) - local idr = world.component_index[id] + local idr = world.componentIndex[id] + local flags = idr.flags + local is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0 local idr_hooks = idr.hooks if from == to then + if is_tag then + return + end -- If the archetypes are the same it can avoid moving the entity -- and just set the data directly. local tr = to.records[id] - local column = from.columns[tr] + local column = from.columns[tr.column] column[record.row] = data local on_set = idr_hooks.on_set if on_set then @@ -868,27 +867,31 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () on_add(entity) end + if is_tag then + return + end + local tr = to.records[id] - local column = to.columns[tr] + local column = to.columns[tr.column] column[record.row] = data local on_set = idr_hooks.on_set if on_set then - on_set(entity, data) + invoke_hook(on_set, entity, data) end end local function world_component(world: World): i53 - local id = (world.max_component_id :: number) + 1 - if id > HI_COMPONENT_ID then + local componentId = (world.nextComponentId :: number) + 1 + if componentId > HI_COMPONENT_ID then -- IDs are partitioned into ranges because component IDs are not nominal, -- so it needs to error when IDs intersect into the entity range. error("Too many components, consider using world:entity() instead to create components.") end - world.max_component_id = id + world.nextComponentId = componentId - return id + return componentId end local function world_remove(world: World, entity: i53, id: i53) @@ -904,8 +907,8 @@ local function world_remove(world: World, entity: i53, id: i53) end local to = archetype_traverse_remove(world, id, from) - if from ~= to then - local idr = world.component_index[id] + if from and not (from == to) then + local idr = world.componentIndex[id] local on_remove = idr.hooks.on_remove if on_remove then on_remove(entity) @@ -933,27 +936,28 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) - local entity_index = world.entity_index - local component_index = world.component_index + local entityIndex = world.entity_index local columns = archetype.columns local id_types = archetype.types local entities = archetype.entities local column_count = #entities local last = #entities local move = entities[last] - -- We assume first that the entity is the last in the archetype - local delete = move + local delete = entities[row] + entities[row] = move + entities[last] = nil :: any if row ~= last then - local record_to_move = entity_index_try_get_any(entity_index, move) + -- TODO: should be "entity_index_sparse_get(entityIndex, move)" + local record_to_move = entity_index_try_get_any(entityIndex, move) if record_to_move then record_to_move.row = row end - - delete = entities[row] - entities[row] = move end + -- TODO: if last == 0 then deactivate table + + local component_index = world.componentIndex for _, id in id_types do local idr = component_index[id] local on_remove = idr.hooks.on_remove @@ -962,8 +966,6 @@ local function archetype_delete(world: World, archetype: Archetype, row: number, end end - entities[last] = nil :: any - if row == last then archetype_fast_delete_last(columns, column_count, id_types, delete) else @@ -1045,11 +1047,11 @@ local function archetype_destroy(world: World, archetype: Archetype) return end - local component_index = world.component_index + local component_index = world.componentIndex archetype_clear_edges(archetype) local archetype_id = archetype.id world.archetypes[archetype_id] = nil :: any - world.archetype_index[archetype.type] = nil :: any + world.archetypeIndex[archetype.type] = nil :: any local records = archetype.records for id in records do @@ -1066,8 +1068,7 @@ local function archetype_destroy(world: World, archetype: Archetype) for id in records do local idr = component_index[id] - idr.columns[archetype_id] = nil :: any - idr.counts[archetype_id] = nil + idr.cache[archetype_id] = nil :: any idr.size -= 1 records[id] = nil :: any if idr.size == 0 then @@ -1094,7 +1095,7 @@ local function world_cleanup(world: World) end world.archetypes = new_archetypes - world.archetype_index = new_archetype_map + world.archetypeIndex = new_archetype_map end local world_delete: (world: World, entity: i53, destruct: boolean?) -> () @@ -1116,7 +1117,7 @@ do end local delete = entity - local component_index = world.component_index + local component_index = world.componentIndex local archetypes: Archetypes = world.archetypes local tgt = ECS_PAIR(EcsWildcard, delete) local idr_t = component_index[tgt] @@ -1125,7 +1126,7 @@ do if idr then local flags = idr.flags if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for archetype_id in idr.columns do + for archetype_id in idr.cache do local idr_archetype = archetypes[archetype_id] local entities = idr_archetype.entities @@ -1133,28 +1134,26 @@ do for i = n, 1, -1 do world_delete(world, entities[i]) end - - archetype_destroy(world, idr_archetype) end else - for archetype_id in idr.columns do + for archetype_id in idr.cache do local idr_archetype = archetypes[archetype_id] local entities = idr_archetype.entities local n = #entities for i = n, 1, -1 do world_remove(world, entities[i], delete) end + end + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] archetype_destroy(world, idr_archetype) end end end - local sparse_array = entity_index.sparse_array - local dense_array = entity_index.dense_array - if idr_t then - for archetype_id in idr_t.columns do + for archetype_id in idr_t.cache do local children = {} local idr_t_archetype = archetypes[archetype_id] @@ -1164,8 +1163,6 @@ do table.insert(children, child) end - local n = #children - for _, id in idr_t_types do if not ECS_IS_PAIR(id) then continue @@ -1176,21 +1173,23 @@ do local flags = id_record.flags local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) if flags_delete_mask ~= 0 then - for i = n, 1, -1 do - world_delete(world, children[i]) + for _, child in children do + -- Cascade deletions of it has Delete as component trait + world_delete(world, child, destruct) end break else local on_remove = id_record.hooks.on_remove local to = archetype_traverse_remove(world, id, idr_t_archetype) - local empty = #to.types == 0 - for i = n, 1, -1 do - local child = children[i] - if on_remove then + if on_remove then + for _, child in children do on_remove(child) + local r = entity_index_try_get_fast(entity_index, child) :: Record + entity_move(entity_index, child, r, to) end - local r = sparse_array[ECS_ENTITY_T_LO(child)] - if not empty then + else + for _, child in children do + local r = entity_index_try_get_fast(entity_index, child) :: Record entity_move(entity_index, child, r, to) end end @@ -1202,6 +1201,7 @@ 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 @@ -1274,55 +1274,55 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) local e: Column, f: Column, g: Column, h: Column if not B then - a = columns[records[A]] + a = columns[records[A].column] elseif not C then - a = columns[records[A]] - b = columns[records[B]] + a = columns[records[A].column] + b = columns[records[B].column] elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] elseif not F then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] elseif not G then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] elseif not H then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] elseif not I then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] end if not B then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1334,21 +1334,21 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] + a = columns[records[A].column] end local row = i i -= 1 - return entity, a[row] + return entityId, a[row] end elseif not C then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1360,22 +1360,22 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] - b = columns[records[B]] + a = columns[records[A].column] + b = columns[records[B].column] end local row = i i -= 1 - return entity, a[row], b[row] + return entityId, a[row], b[row] end elseif not D then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1387,23 +1387,23 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] end local row = i i -= 1 - return entity, a[row], b[row], c[row] + return entityId, a[row], b[row], c[row] end elseif not E then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1415,151 +1415,25 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] end local row = i i -= 1 - return entity, a[row], b[row], c[row], d[row] - end - elseif not F then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row] - end - elseif not G then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - end - elseif not H then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - end - elseif not I then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + return entityId, a[row], b[row], c[row], d[row] end else - local output = {} + local queryOutput = {} function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1571,19 +1445,61 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records + + if not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end end local row = i i -= 1 - for j, id in ids do - output[j] = columns[records[id]][row] + if not F then + return entityId, a[row], b[row], c[row], d[row], e[row] + elseif not G then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row] + elseif not H then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + elseif not I then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] end - return entity, unpack(output) + for j, id in ids do + queryOutput[j] = columns[records[id].column][row] + end + + return entityId, unpack(queryOutput) end end @@ -1688,17 +1604,17 @@ local function query_cached(query: QueryInner) local entities: { number } local i: number local archetype: Archetype - local records: { number } + local records: { ArchetypeRecord } local archetypes = query.compatible_archetypes - local world = query.world :: { observable: Observable } + local world = query.world :: World -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively -- because the event will be emitted for all components of that Archetype. - local observable = world.observable :: Observable - local on_create_action = observable[EcsOnArchetypeCreate] + local observerable = world.observerable + local on_create_action = observerable[EcsOnArchetypeCreate] if not on_create_action then on_create_action = {} - observable[EcsOnArchetypeCreate] = on_create_action + observerable[EcsOnArchetypeCreate] = on_create_action end local query_cache_on_create = on_create_action[A] if not query_cache_on_create then @@ -1706,10 +1622,10 @@ local function query_cached(query: QueryInner) on_create_action[A] = query_cache_on_create end - local on_delete_action = observable[EcsOnArchetypeDelete] + local on_delete_action = observerable[EcsOnArchetypeDelete] if not on_delete_action then on_delete_action = {} - observable[EcsOnArchetypeDelete] = on_delete_action + observerable[EcsOnArchetypeDelete] = on_delete_action end local query_cache_on_delete = on_delete_action[A] if not query_cache_on_delete then @@ -1745,49 +1661,49 @@ local function query_cached(query: QueryInner) records = archetype.records columns = archetype.columns if not B then - a = columns[records[A]] + a = columns[records[A].column] elseif not C then - a = columns[records[A]] - b = columns[records[B]] + a = columns[records[A].column] + b = columns[records[B].column] elseif not D then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] elseif not E then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] elseif not F then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] elseif not G then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] elseif not H then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] elseif not I then - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] end return world_query_iter_next @@ -1795,8 +1711,8 @@ local function query_cached(query: QueryInner) if not B then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1808,21 +1724,21 @@ local function query_cached(query: QueryInner) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] + a = columns[records[A].column] end local row = i i -= 1 - return entity, a[row] + return entityId, a[row] end elseif not C then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1834,22 +1750,22 @@ local function query_cached(query: QueryInner) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] - b = columns[records[B]] + a = columns[records[A].column] + b = columns[records[B].column] end local row = i i -= 1 - return entity, a[row], b[row] + return entityId, a[row], b[row] end elseif not D then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1861,23 +1777,23 @@ local function query_cached(query: QueryInner) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] end local row = i i -= 1 - return entity, a[row], b[row], c[row] + return entityId, a[row], b[row], c[row] end elseif not E then function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -1889,151 +1805,25 @@ local function query_cached(query: QueryInner) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] end local row = i i -= 1 - return entity, a[row], b[row], c[row], d[row] - end - elseif not F then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row] - end - elseif not G then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row] - end - elseif not H then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] - end - elseif not I then - function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do - lastArchetype += 1 - archetype = compatible_archetypes[lastArchetype] - if not archetype then - return nil - end - - entities = archetype.entities - i = #entities - if i == 0 then - continue - end - entity = entities[i] - columns = archetype.columns - records = archetype.records - a = columns[records[A]] - b = columns[records[B]] - c = columns[records[C]] - d = columns[records[D]] - e = columns[records[E]] - f = columns[records[F]] - g = columns[records[G]] - h = columns[records[H]] - end - - local row = i - i -= 1 - - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + return entityId, a[row], b[row], c[row], d[row] end else local queryOutput = {} function world_query_iter_next(): any - local entity = entities[i] - while entity == nil do + local entityId = entities[i] + while entityId == nil do lastArchetype += 1 archetype = compatible_archetypes[lastArchetype] if not archetype then @@ -2045,29 +1835,61 @@ local function query_cached(query: QueryInner) if i == 0 then continue end - entity = entities[i] + entityId = entities[i] columns = archetype.columns records = archetype.records + + if not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end end local row = i i -= 1 if not F then - return entity, a[row], b[row], c[row], d[row], e[row] + return entityId, a[row], b[row], c[row], d[row], e[row] elseif not G then - return entity, a[row], b[row], c[row], d[row], e[row], f[row] + return entityId, a[row], b[row], c[row], d[row], e[row], f[row] elseif not H then - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] elseif not I then - return entity, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] end for j, id in ids do - queryOutput[j] = columns[records[id]][row] + queryOutput[j] = columns[records[id].column][row] end - return entity, unpack(queryOutput) + return entityId, unpack(queryOutput) end end @@ -2097,7 +1919,7 @@ local function world_query(world: World, ...) local archetypes = world.archetypes local idr: IdRecord? - local component_index = world.component_index + local componentIndex = world.componentIndex local q = setmetatable({ ids = ids, @@ -2106,7 +1928,7 @@ local function world_query(world: World, ...) }, Query) for _, id in ids do - local map = component_index[id] + local map = componentIndex[id] if not map then return q end @@ -2120,7 +1942,7 @@ local function world_query(world: World, ...) return q end - for archetype_id in idr.columns do + for archetype_id in idr.cache do local compatibleArchetype = archetypes[archetype_id] if #compatibleArchetype.entities == 0 then continue @@ -2149,14 +1971,14 @@ local function world_query(world: World, ...) end local function world_each(world: World, id): () -> () - local idr = world.component_index[id] + local idr = world.componentIndex[id] if not idr then return NOOP end - local idr_columns = idr.columns + local idr_cache = idr.cache local archetypes = world.archetypes - local archetype_id = next(idr_columns, nil) :: number + local archetype_id = next(idr_cache, nil) :: number local archetype = archetypes[archetype_id] if not archetype then return NOOP @@ -2168,7 +1990,7 @@ local function world_each(world: World, id): () -> () return function(): any local entity = entities[row] while not entity do - archetype_id = next(idr_columns, archetype_id) :: number + archetype_id = next(idr_cache, archetype_id) :: number if not archetype_id then return end @@ -2323,16 +2145,15 @@ function World.new() max_id = 0, } local self = setmetatable({ - archetype_index = {} :: { [string]: Archetype }, + archetypeIndex = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, - component_index = {} :: ComponentIndex, + componentIndex = {} :: ComponentIndex, entity_index = entity_index, + nextArchetypeId = 0 :: number, + nextComponentId = 0 :: number, + nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, - - max_archetype_id = 0, - max_component_id = 0, - - observable = {} :: Observable, + observerable = {}, }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}, "") @@ -2372,9 +2193,7 @@ function World.new() return self end -export type Entity = {__T: T} - -export type Id = +export type Id = | Entity | Pair, Entity> | Pair> @@ -2385,103 +2204,6 @@ export type Pair = number & { __O: O, } -type Item = (self: Query) -> (Entity, T...) - -type Iter = (query: Query) -> () -> (Entity, T...) - - -export type Query = typeof(setmetatable({}, { - __iter = (nil :: any) :: Iter, -})) & { - iter: Iter, - with: (self: Query, ...Id) -> Query, - without: (self: Query, ...Id) -> Query, - archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query, -} - -export type Observer = { - callback: (archetype: Archetype) -> (), - query: QueryInner, -} - -type Observable = { - [i53]: { - [i53]: { - { Observer } - } - } -} - -export type World = { - archetype_index: { [string]: Archetype }, - archetypes: Archetypes, - component_index: ComponentIndex, - entity_index: EntityIndex, - ROOT_ARCHETYPE: Archetype, - - max_component_id: number, - max_archetype_id: number, - - observable: any, - - --- Creates a new entity - entity: (self: World) -> Entity, - --- Creates a new entity located in the first 256 ids. - --- These should be used for static components for fast access. - component: (self: World) -> Entity, - --- Gets the target of an relationship. For example, when a user calls - --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. - target: (self: World, id: Entity, relation: Entity, index: number?) -> Entity?, - --- Deletes an entity and all it's related components and relationships. - delete: (self: World, id: Entity) -> (), - - --- Adds a component to the entity with no value - add: (self: World, id: Entity, component: Id) -> (), - --- Assigns a value to a component on the given entity - set: (self: World, id: Entity, component: Id, data: U) -> (), - - cleanup: (self: World) -> (), - -- Clears an entity from the world - clear: (self: World, id: Entity) -> (), - --- Removes a component from the given entity - remove: (self: World, id: Entity, component: Id) -> (), - --- Retrieves the value of up to 4 components. These values may be nil. - get: ((self: World, id: Entity, Id) -> A?) - & ((self: World, id: Entity, Id, Id) -> (A?, B?)) - & ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?)) - & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), - - --- Returns whether the entity has the ID. - has: ((self: World, entity: Entity, ...Id) -> boolean) - & ((self: World, entity: Entity, Id, Id) -> boolean) - & ((self: World, entity: Entity, Id, Id, Id) -> boolean) - & ((self: World, entity: Entity, Id, Id, Id, Id) -> boolean) - & ((self: World, entity: Entity, Id, Id, Id, Id, Id) -> boolean) - & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id) -> boolean) - & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id, Id) -> boolean) - & ((self: World, entity: Entity, Id, Id, Id, Id, Id, Id, Id, ...unknown) -> boolean), - - --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. - parent: (self: World, entity: Entity) -> Entity, - - --- Checks if the world contains the given entity - contains: (self: World, entity: Entity) -> boolean, - - each: (self: World, id: Id) -> () -> Entity, - - children: (self: World, id: Id) -> () -> Entity, - - --- Searches the world for entities that match a given query - query: ((World, Id) -> Query) - & ((World, Id, Id) -> Query) - & ((World, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) - & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) -} -- type function ecs_id_t(entity) -- local ty = entity:components()[2] -- local __T = ty:readproperty(types.singleton("__T")) @@ -2499,6 +2221,97 @@ export type World = { -- end -- end +type Item = (self: Query) -> (Entity, T...) + +export type Entity = number & { __T: T } + +type Iter = (query: Query) -> () -> (Entity, T...) + +export type Query = typeof(setmetatable({}, { + __iter = (nil :: any) :: Iter, +})) & { + iter: Iter, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, + archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query, +} + +type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, +} + +export type World = { + archetypeIndex: { [string]: Archetype }, + archetypes: Archetypes, + componentIndex: ComponentIndex, + entity_index: EntityIndex, + ROOT_ARCHETYPE: Archetype, + + nextComponentId: number, + nextEntityId: number, + nextArchetypeId: number, + + observerable: { + [i53]: { + [i53]: { + { query: QueryInner, callback: (Archetype) -> () } + } + } + }, +} & { + --- Creates a new entity + entity: (self: World) -> Entity, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (self: World) -> Entity, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent), 0)`, you will obtain the parent entity. + target: (self: World, id: Entity, relation: Entity, index: number?) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (self: World, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (self: World, id: Entity, component: Id) -> (), + --- Assigns a value to a component on the given entity + set: (self: World, id: Entity, component: Id, data: T) -> (), + + cleanup: (self: World) -> (), + -- Clears an entity from the world + clear: (self: World, id: Entity) -> (), + --- Removes a component from the given entity + remove: (self: World, id: Entity, component: Id) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: ((self: World, id: any, Id) -> A?) + & ((self: World, id: Entity, Id, Id) -> (A?, B?)) + & ((self: World, id: Entity, Id, Id, Id) -> (A?, B?, C?)) + & (self: World, id: Entity, Id, Id, Id, Id) -> (A?, B?, C?, D?), + + --- Returns whether the entity has the ID. + has: (self: World, entity: Entity, ...Id) -> boolean, + + --- Get parent (target of ChildOf relationship) for entity. If there is no ChildOf relationship pair, it will return nil. + parent: (self: World, entity: Entity) -> Entity, + + --- Checks if the world contains the given entity + contains: (self: World, entity: Entity) -> boolean, + + each: (self: World, id: Id) -> () -> Entity, + + children: (self: World, id: Id) -> () -> Entity, + + --- Searches the world for entities that match a given query + query: ((World, Id) -> Query) + & ((World, Id, Id) -> Query) + & ((World, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id) -> Query) + & ((World, Id, Id, Id, Id, Id, Id, Id, Id, ...Id) -> Query) +} + return { World = World :: { new: () -> World }, @@ -2545,8 +2358,6 @@ return { archetype_traverse_add = archetype_traverse_add, archetype_traverse_remove = archetype_traverse_remove, - entity_move = entity_move, - entity_index_try_get = entity_index_try_get, entity_index_try_get_any = entity_index_try_get_any, entity_index_try_get_fast = entity_index_try_get_fast, @@ -2558,7 +2369,4 @@ return { query_with = query_with, query_without = query_without, query_archetypes = query_archetypes, - query_match = query_match, - - find_observers = find_observers, } diff --git a/pesde.toml b/pesde.toml index 67a7bc4..26b82af 100644 --- a/pesde.toml +++ b/pesde.toml @@ -11,7 +11,7 @@ includes = [ license = "MIT" name = "marked/jecs" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.5.5" +version = "0.5.3" [indices] default = "https://github.com/pesde-pkg/index"