From 372845d96ed85131540d3c7843f0f1d7e3feffb4 Mon Sep 17 00:00:00 2001 From: marked <+marked@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:37:30 +0000 Subject: [PATCH] Sync to upstream Jecs 0.4.0-rc.0 --- .luaurc | 4 +- README.md | 132 ++++----- init.luau | 766 +++++++++++++++++++++++++++-------------------------- pesde.toml | 2 +- 4 files changed, 453 insertions(+), 451 deletions(-) diff --git a/.luaurc b/.luaurc index 2747dd7..73598d2 100644 --- a/.luaurc +++ b/.luaurc @@ -2,7 +2,7 @@ "aliases": { "jecs": "src", "testkit": "testkit", - "mirror": "mirror", + "mirror": "mirror" }, "languageMode": "strict" -} +} \ No newline at end of file diff --git a/README.md b/README.md index 0d86c4c..166a5db 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,68 @@ -An unofficial pesde package for [jecs](https://github.com/ukendio/jecs). +An automatically synced minimal copy of the [jecs](https://github.com/Ukendio/jecs) repo published to pesde. - -

- - -

- -[![License: Apache 2.0](https://img.shields.io/badge/License-Apache-blue.svg?style=for-the-badge)](LICENSE-APACHE) -[![Wally](https://img.shields.io/github/v/tag/ukendio/jecs?&style=for-the-badge)](https://wally.run/package/ukendio/jecs) - -jecs is Just a stupidly fast Entity Component System - -- Entity Relationships as first class citizens -- Iterate 800,000 entities at 60 frames per second -- Type-safe [Luau](https://luau-lang.org/) API -- Zero-dependency package -- Optimized for column-major operations -- Cache friendly archetype/SoA storage -- Unit tested for stability - -### Example - -```lua -local world = jecs.World.new() -local pair = jecs.pair - -local ChildOf = world:component() -local Name = world:component() - -local function parent(entity) - return world:target(entity, ChildOf) -end -local function getName(entity) - return world:get(entity, Name) -end - -local alice = world:entity() -world:set(alice, Name, "alice") - -local bob = world:entity() -world:add(bob, pair(ChildOf, alice)) -world:set(bob, Name, "bob") - -local sara = world:entity() -world:add(sara, pair(ChildOf, alice)) -world:set(sara, Name, "sara") - -print(getName(parent(sara))) - -for e in world:query(pair(ChildOf, alice)) do - print(getName(e), "is the child of alice") -end - --- Output --- "alice" --- bob is the child of alice --- sara is the child of alice -``` - -21,000 entities 125 archetypes 4 random components queried. -![Queries](image-3.png) -Can be found under /benches/visual/query.luau - -Inserting 8 components to an entity and updating them over 50 times. -![Insertions](image-4.png) -Can be found under /benches/visual/insertions.luau + +

+ +

+ +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE) [![Wally](https://img.shields.io/github/v/tag/ukendio/jecs?&style=for-the-badge)](https://wally.run/package/ukendio/jecs) + +Just a stupidly fast Entity Component System + +* [Entity Relationships](https://ajmmertens.medium.com/building-games-in-ecs-with-entity-relationships-657275ba2c6c) as first class citizens +* Iterate 800,000 entities at 60 frames per second +* Type-safe [Luau](https://luau-lang.org/) API +* Zero-dependency package +* Optimized for column-major operations +* Cache friendly [archetype/SoA](https://ajmmertens.medium.com/building-an-ecs-2-archetypes-and-vectorization-fe21690805f9) storage +* Rigorously [unit tested](https://github.com/Ukendio/jecs/actions/workflows/ci.yaml) for stability + +### Example + +```lua +local world = jecs.World.new() +local pair = jecs.pair + +-- These components and functions are actually already builtin +-- but have been illustrated for demonstration purposes +local ChildOf = world:component() +local Name = world:component() + +local function parent(entity) + return world:target(entity, ChildOf) +end +local function getName(entity) + return world:get(entity, Name) +end + +local alice = world:entity() +world:set(alice, Name, "alice") + +local bob = world:entity() +world:add(bob, pair(ChildOf, alice)) +world:set(bob, Name, "bob") + +local sara = world:entity() +world:add(sara, pair(ChildOf, alice)) +world:set(sara, Name, "sara") + +print(getName(parent(sara))) + +for e in world:query(pair(ChildOf, alice)) do + print(getName(e), "is the child of alice") +end + +-- Output +-- "alice" +-- bob is the child of alice +-- sara is the child of alice +``` + +21,000 entities 125 archetypes 4 random components queried. +![Queries](image-3.png) +Can be found under /benches/visual/query.luau + +Inserting 8 components to an entity and updating them over 50 times. +![Insertions](image-4.png) +Can be found under /benches/visual/insertions.luau diff --git a/init.luau b/init.luau index 4223eed..2b96b0a 100644 --- a/init.luau +++ b/init.luau @@ -26,7 +26,7 @@ type GraphEdges = Map type GraphNode = { add: GraphEdges, remove: GraphEdges, - refs: GraphEdge + refs: GraphEdge, } export type Archetype = { @@ -44,11 +44,6 @@ type Record = { dense: i24, } -type EntityIndex = { - dense: Map, - sparse: Map, -} - type ArchetypeRecord = { count: number, column: number, @@ -58,6 +53,11 @@ type IdRecord = { cache: { ArchetypeRecord }, flags: number, size: number, + hooks: { + on_add: ((entity: i53) -> ())?, + on_set: ((entity: i53, data: any) -> ())?, + on_remove: ((entity: i53) -> ())?, + }, } type ComponentIndex = Map @@ -69,34 +69,41 @@ type ArchetypeDiff = { removed: Ty, } -local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 +type EntityIndex = { + dense_array: Map, + sparse_array: Map, + alive_count: number, + max_id: number, +} -local EcsOnAdd = HI_COMPONENT_ID + 1 -local EcsOnRemove = HI_COMPONENT_ID + 2 -local EcsOnSet = HI_COMPONENT_ID + 3 -local EcsWildcard = HI_COMPONENT_ID + 4 -local EcsChildOf = HI_COMPONENT_ID + 5 -local EcsComponent = HI_COMPONENT_ID + 6 -local EcsOnDelete = HI_COMPONENT_ID + 7 -local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 -local EcsDelete = HI_COMPONENT_ID + 9 -local EcsRemove = HI_COMPONENT_ID + 10 -local EcsName = HI_COMPONENT_ID + 11 -local EcsRest = HI_COMPONENT_ID + 12 +local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 +-- stylua: ignore start +local EcsOnAdd = HI_COMPONENT_ID + 1 +local EcsOnRemove = HI_COMPONENT_ID + 2 +local EcsOnSet = HI_COMPONENT_ID + 3 +local EcsWildcard = HI_COMPONENT_ID + 4 +local EcsChildOf = HI_COMPONENT_ID + 5 +local EcsComponent = HI_COMPONENT_ID + 6 +local EcsOnDelete = HI_COMPONENT_ID + 7 +local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 +local EcsDelete = HI_COMPONENT_ID + 9 +local EcsRemove = HI_COMPONENT_ID + 10 +local EcsName = HI_COMPONENT_ID + 11 +local EcsRest = HI_COMPONENT_ID + 12 -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) +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) -local ECS_ID_DELETE = 0b0000_0001 -local ECS_ID_IS_TAG = 0b0000_0010 -local ECS_ID_HAS_ON_ADD = 0b0000_0100 -local ECS_ID_HAS_ON_SET = 0b0000_1000 -local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 -local ECS_ID_MASK = 0b0000_0000 - -local NULL_ARRAY = table.freeze({}) +local ECS_ID_DELETE = 0b0000_0001 +local ECS_ID_IS_TAG = 0b0000_0010 +local ECS_ID_HAS_ON_ADD = 0b0000_0100 +local ECS_ID_HAS_ON_SET = 0b0000_1000 +local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 +local ECS_ID_MASK = 0b0000_0000 +-- stylua: ignore end +local NULL_ARRAY = table.freeze({}) :: Column local function FLAGS_ADD(is_pair: boolean): number local flags = 0x0 @@ -136,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) end return ECS_COMBINE(e, 1) end @@ -159,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_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 + return 0 +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_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) @@ -234,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. @@ -250,9 +285,10 @@ 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 sparse_array = entity_index.sparse_array + local record1 = sparse_array[ECS_ENTITY_T_LO(e1)] + local record2 = sparse_array[ECS_ENTITY_T_LO(e2)] record1.row = dst_row record2.row = src_row end @@ -287,11 +323,11 @@ end local world_get: (world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) -> ...any do -- Keeping the function as small as possible to enable inlining - local records - local columns - local row + local records: { ArchetypeRecord } + local columns: { { any } } + local row: number - local function fetch(id) + local function fetch(id): any local tr = records[id] if not tr then @@ -302,7 +338,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 @@ -332,8 +368,8 @@ do end end -local function world_get_one_inline(world: World, entity: i53, id: i53) - local record = world.entityIndex.sparse[entity] +local function world_get_one_inline(world: World, entity: i53, id: i53): any + local record = entity_index_try_get(world.entity_index, entity) if not record then return nil end @@ -351,7 +387,7 @@ local function world_get_one_inline(world: World, entity: i53, id: i53) 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 @@ -367,7 +403,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 @@ -388,11 +424,13 @@ local function world_has(world: World, entity: number, ...: i53): boolean return true end -local function world_target(world: World, entity: i53, relation: i24, index): i24? - if index == nil then - index = 0 - end - local record = world.entityIndex.sparse[entity] +local function world_target(world: World, entity: i53, relation: i24, index: number?): i24? + local nth = index or 0 + 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 @@ -409,11 +447,11 @@ local function world_target(world: World, entity: i53, relation: i24, index): i2 end local count = tr.count - if index >= count then - index = index + count + 1 + if nth >= count then + nth = nth + count + 1 end - local nth = archetype.types[index + tr.column] + nth = archetype.types[nth + tr.column] if not nth then return nil @@ -462,6 +500,11 @@ local function id_record_ensure(world: World, id: number): IdRecord size = 0, cache = {}, flags = flags, + hooks = { + on_add = on_add, + on_set = on_set, + on_remove = on_remove, + }, } :: IdRecord componentIndex[id] = idr end @@ -535,9 +578,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) @@ -607,16 +648,10 @@ local function archetype_init_edge(archetype: Archetype, edge: GraphEdge, id: i5 edge.id = id end -local function archetype_ensure_edge(world, edges, id): GraphEdge +local function archetype_ensure_edge(world, edges: GraphEdges, id): GraphEdge local edge = edges[id] if not edge then - edge = { - from = nil :: any, - to = nil :: any, - id = id, - prev = nil, - next = nil, - } :: GraphEdge + edge = {} :: GraphEdge edges[id] = edge end @@ -627,33 +662,33 @@ local function init_edge_for_add(world, archetype, edge: GraphEdge, id, to) archetype_init_edge(archetype, edge, id, to) archetype_ensure_edge(world, archetype.node.add, id) if archetype ~= to then - local to_refs = to.node.refs - local next_edge = to_refs.next + local to_refs = to.node.refs + local next_edge = to_refs.next - to_refs.next = edge - edge.prev = to_refs - edge.next = next_edge + to_refs.next = edge + edge.prev = to_refs + edge.next = next_edge - if next_edge then - next_edge.prev = edge - end + if next_edge then + next_edge.prev = edge + end end end -local function init_edge_for_remove(world, archetype, edge, id, to) +local function init_edge_for_remove(world: World, archetype: Archetype, edge: GraphEdge, id: number, to: Archetype) archetype_init_edge(archetype, edge, id, to) archetype_ensure_edge(world, archetype.node.remove, id) if archetype ~= to then - local to_refs = to.node.refs - local prev_edge = to_refs.prev + local to_refs = to.node.refs + local prev_edge = to_refs.prev - to_refs.prev = edge - edge.next = to_refs - edge.prev = prev_edge + to_refs.prev = edge + edge.next = to_refs + edge.prev = prev_edge - if prev_edge then - prev_edge.next = edge - end + if prev_edge then + prev_edge.next = edge + end end end @@ -669,7 +704,7 @@ local function create_edge_for_remove(world: World, node: Archetype, edge: Graph return to end -local function archetype_traverse_add(world: World, id: i53, from: Archetype): Archetype +local function archetype_traverse_add(world: World, id: i53, from: Archetype?): Archetype from = from or world.ROOT_ARCHETYPE local edge = archetype_ensure_edge(world, from.node.add, id) @@ -694,23 +729,24 @@ local function archetype_traverse_remove(world: World, id: i53, from: Archetype) return to :: Archetype end -local function invoke_hook(world: World, hook_id: number, id: i53, entity: i53, data: any?) - local hook = world_get_one_inline(world, id, hook_id) - if hook then - hook(entity, data) - end +local function invoke_hook(action, entity, data) + 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) @@ -718,22 +754,26 @@ local function world_add(world: World, entity: i53, id: i53): () end local idr = world.componentIndex[id] - local has_on_add = bit32.band(idr.flags, ECS_ID_HAS_ON_ADD) ~= 0 + local on_add = idr.hooks.on_add - if has_on_add then - invoke_hook(world, EcsOnAdd, id, entity) + if on_add then + on_add(entity) end end local function world_set(world: World, entity: i53, id: i53, data: unknown): () - local entityIndex = world.entityIndex - local record = entityIndex.sparse[entity] - local from = record.archetype - local to = archetype_traverse_add(world, id, from) + 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] local flags = idr.flags local is_tag = bit32.band(flags, ECS_ID_IS_TAG) ~= 0 - local has_on_set = bit32.band(flags, ECS_ID_HAS_ON_SET) ~= 0 + local idr_hooks = idr.hooks if from == to then if is_tag then @@ -742,9 +782,11 @@ 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 - if has_on_set then - invoke_hook(world, EcsOnSet, id, entity, 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) end return @@ -752,7 +794,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 @@ -760,10 +802,9 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () end end - local has_on_add = bit32.band(flags, ECS_ID_HAS_ON_ADD) ~= 0 - - if has_on_add then - invoke_hook(world, EcsOnAdd, id, entity) + local on_add = idr_hooks.on_add + if on_add then + on_add(entity) end if is_tag then @@ -775,8 +816,9 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () column[record.row] = data - if has_on_set then - invoke_hook(world, EcsOnSet, id, entity, data) + local on_set = idr_hooks.on_set + if on_set then + invoke_hook(on_set, entity, data) end end @@ -788,15 +830,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 @@ -804,33 +849,15 @@ local function world_remove(world: World, entity: i53, id: i53) if from and not (from == to) then local idr = world.componentIndex[id] - local flags = idr.flags - local has_on_remove = bit32.band(flags, ECS_ID_HAS_ON_REMOVE) ~= 0 - if has_on_remove then - invoke_hook(world, EcsOnRemove, id, entity) + local on_remove = idr.hooks.on_remove + if on_remove then + on_remove(entity) end entity_move(entity_index, entity, record, to) end end -local function world_clear(world: World, entity: i53) - --TODO: use sparse_get (stashed) - local record = world.entityIndex.sparse[entity] - if not record then - return - end - - local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE - local archetype = record.archetype - - if archetype == nil or archetype == ROOT_ARCHETYPE then - return - end - - entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE) -end - local function archetype_fast_delete_last(columns: { Column }, column_count: number, types: { i53 }, entity: i53) for i, column in columns do if column ~= NULL_ARRAY then @@ -848,6 +875,62 @@ local function archetype_fast_delete(columns: { Column }, column_count: number, end end +local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) + local entityIndex = world.entity_index + local columns = archetype.columns + local types = archetype.types + local entities = archetype.entities + local column_count = #entities + local last = #entities + local move = entities[last] + local delete = entities[row] + entities[row] = move + entities[last] = nil :: any + + if row ~= last then + -- 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 + end + + -- TODO: if last == 0 then deactivate table + + for _, id in types do + local on_remove: (entity: i53) -> () = world_get_one_inline(world, id, EcsOnRemove) + if on_remove then + on_remove(delete) + end + end + + if row == last then + archetype_fast_delete_last(columns, column_count, types, delete) + else + archetype_fast_delete(columns, column_count, row, types, delete) + end +end + +local function world_clear(world: World, entity: i53) + --TODO: use sparse_get (stashed) + local record = entity_index_try_get(world.entity_index, entity) + if not record then + return + end + + local archetype = record.archetype + local row = record.row + + if archetype then + -- In the future should have a destruct mode for + -- deleting archetypes themselves. Maybe requires recycling + archetype_delete(world, archetype, row) + end + + record.archetype = nil :: any + record.row = nil :: any +end + local function archetype_disconnect_edge(edge: GraphEdge) local edge_next = edge.next local edge_prev = edge.prev @@ -861,69 +944,68 @@ end local function archetype_remove_edge(edges: Map, id: i53, edge: GraphEdge) archetype_disconnect_edge(edge) - edges[id] = nil + edges[id] = nil :: any end local function archetype_clear_edges(archetype: Archetype) - local node = archetype.node - local add = node.add - local remove = node.remove - local node_refs = node.refs + local node: GraphNode = archetype.node + local add: GraphEdges = node.add + local remove: GraphEdges = node.remove + local node_refs: GraphEdge = node.refs for id, edge in add do archetype_disconnect_edge(edge) - add[id] = nil + add[id] = nil :: any end for id, edge in remove do archetype_disconnect_edge(edge) - remove[id] = nil + remove[id] = nil :: any end local cur = node_refs.next - while cur do - local edge = cur - local next_edge = edge.next - archetype_remove_edge(edge.from.node.add, edge.id, edge) - cur = next_edge - end + while cur do + local edge: GraphEdge = cur + local next_edge = edge.next + archetype_remove_edge(edge.from.node.add, edge.id, edge) + cur = next_edge + end - cur = node_refs.prev - while cur do - local edge = cur - local next_edge = edge.prev - archetype_remove_edge(edge.from.node.remove, edge.id, edge) - cur = next_edge - end + cur = node_refs.prev + while cur do + local edge: GraphEdge = cur + local next_edge = edge.prev + archetype_remove_edge(edge.from.node.remove, edge.id, edge) + cur = next_edge + end node_refs.next = nil node_refs.prev = nil end local function archetype_destroy(world: World, archetype: Archetype) - - if archetype == world.ROOT_ARCHETYPE then - return - end + if archetype == world.ROOT_ARCHETYPE then + return + end local component_index = world.componentIndex archetype_clear_edges(archetype) local archetype_id = archetype.id - world.archetypes[archetype_id] = nil - world.archetypeIndex[archetype.type] = nil + world.archetypes[archetype_id] = nil :: any + world.archetypeIndex[archetype.type] = nil :: any local records = archetype.records for id in records do local idr = component_index[id] - idr.cache[archetype_id] = nil + idr.cache[archetype_id] = nil :: any idr.size -= 1 - records[id] = nil + records[id] = nil :: any if idr.size == 0 then - component_index[id] = nil + component_index[id] = nil :: any end end end -local function world_cleanup(world) - local archetypes = world.archetypes +local function world_cleanup(world: World) + local archetypes = world.archetypes for _, archetype in archetypes do if #archetype.entities == 0 then @@ -931,11 +1013,11 @@ local function world_cleanup(world) end end - local new_archetypes = table.create(#archetypes) + local new_archetypes = table.create(#archetypes) :: { Archetype } local new_archetype_map = {} for index, archetype in archetypes do - new_archetypes[index] = archetype + new_archetypes[index] = archetype new_archetype_map[archetype.type] = archetype end @@ -945,44 +1027,9 @@ end local world_delete: (world: World, entity: i53, destruct: boolean?) -> () do - local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?) - local entityIndex = world.entityIndex - local columns = archetype.columns - local types = archetype.types - local entities = archetype.entities - local column_count = #entities - local last = #entities - local move = entities[last] - local delete = entities[row] - entities[row] = move - entities[last] = nil - - if row ~= last then - -- TODO: should be "entity_index_sparse_get(entityIndex, move)" - local record_to_move = entityIndex.sparse[move] - if record_to_move then - record_to_move.row = row - end - end - - -- TODO: if last == 0 then deactivate table - - for _, id in types do - invoke_hook(world, EcsOnRemove, id, delete) - end - - if row == last then - archetype_fast_delete_last(columns, column_count, types, delete) - else - archetype_fast_delete(columns, column_count, row, types, delete) - end - end - 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 @@ -1040,37 +1087,48 @@ 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 id_record = component_index[id] local flags = id_record.flags - if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for _, child in children do - -- Cascade deletions of it has Delete as component trait - world_delete(world, child, destruct) - end - + local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) + if flags_delete_mask ~= 0 then + for _, child in children do + -- Cascade deletions of it has Delete as component trait + world_delete(world, child, destruct) + end break else - for _, child in children do - world_remove(world, child, id) - end + for _, child in children do + world_remove(world, child, id) + end end end end - archetype_destroy(world, idr_t_archetype) 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 + 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 @@ -1087,9 +1145,6 @@ local EMPTY_QUERY = { iter = function() return NOOP end, - drain = ARM, - next = NOOP, - replace = NOOP, with = ARM, without = ARM, archetypes = function() @@ -1099,21 +1154,14 @@ local EMPTY_QUERY = { setmetatable(EMPTY_QUERY, EMPTY_QUERY) -local function query_iter(query) +local function query_iter_init(query): () -> (number, ...any) local world_query_iter_next - if query.should_drain then - world_query_iter_next = query.iter_next - if world_query_iter_next then - return world_query_iter_next - end - end - local compatible_archetypes = query.compatible_archetypes local lastArchetype = 1 local archetype = compatible_archetypes[1] if not archetype then - return EMPTY_QUERY + return NOOP :: () -> (number, ...any) end local columns = archetype.columns local entities = archetype.entities @@ -1122,7 +1170,8 @@ local function query_iter(query) local ids = query.ids local A, B, C, D, E, F, G, H, I = unpack(ids) - local a, b, c, d, e, f, g, h + local a: Column, b: Column, c: Column, d: Column + local e: Column, f: Column, g: Column, h: Column if not B then a = columns[records[A].column] @@ -1182,9 +1231,6 @@ local function query_iter(query) entities = archetype.entities i = #entities - if i == 0 then - continue - end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1208,9 +1254,6 @@ local function query_iter(query) entities = archetype.entities i = #entities - if i == 0 then - continue - end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1235,9 +1278,6 @@ local function query_iter(query) entities = archetype.entities i = #entities - if i == 0 then - continue - end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1263,9 +1303,6 @@ local function query_iter(query) entities = archetype.entities i = #entities - if i == 0 then - continue - end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1293,9 +1330,6 @@ local function query_iter(query) entities = archetype.entities i = #entities - if i == 0 then - continue - end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1355,22 +1389,19 @@ local function query_iter(query) end end - query.iter_next = world_query_iter_next + query.next = world_query_iter_next return world_query_iter_next end -local function query_drain(query) - local query_iter_next = query_iter(query) - query.next = query_iter_next - query.should_drain = true - return query +local function query_iter(query): () -> (number, ...any) + local query_next = query.next + if not query_next then + query_next = query_iter_init(query) + end + return query_next end -local function query_next(query) - error("Did you forget to call drain?") -end - -local function query_without(query, ...) +local function query_without(query: { compatible_archetypes: { Archetype } }, ...) local compatible_archetypes = query.compatible_archetypes local N = select("#", ...) for i = #compatible_archetypes, 1, -1 do @@ -1391,7 +1422,7 @@ local function query_without(query, ...) if last ~= i then compatible_archetypes[i] = compatible_archetypes[last] end - compatible_archetypes[last] = nil + compatible_archetypes[last] = nil :: any end end @@ -1399,10 +1430,10 @@ local function query_without(query, ...) return EMPTY_QUERY end - return query + return query :: any end -local function query_with(query, ...) +local function query_with(query: { compatible_archetypes: { Archetype } }, ...) local compatible_archetypes = query.compatible_archetypes local N = select("#", ...) for i = #compatible_archetypes, 1, -1 do @@ -1423,62 +1454,13 @@ local function query_with(query, ...) if last ~= i then compatible_archetypes[i] = compatible_archetypes[last] end - compatible_archetypes[last] = nil + compatible_archetypes[last] = nil :: any end end if #compatible_archetypes == 0 then return EMPTY_QUERY end - return query -end - -local function columns_replace_values(row, columns, ...) - for i, column in columns do - column[row] = select(i, ...) - end -end - -local function query_replace(query, fn: (...any) -> ...any) - local compatible_archetypes = query.compatible_archetypes - local ids = query.ids - local A, B, C, D, E = unpack(ids, 1, 5) - local queryOutput = {} - for i, archetype in compatible_archetypes do - local columns = archetype.columns - local records = archetype.records - for row in archetype.entities do - if not B then - local va = columns[records[A].column] - local pa = fn(va[row]) - - va[row] = pa - elseif not C then - local va = columns[records[A].column] - local vb = columns[records[B].column] - - va[row], vb[row] = fn(va[row], vb[row]) - elseif not D then - local va = columns[records[A].column] - local vb = columns[records[B].column] - local vc = columns[records[C].column] - - va[row], vb[row], vc[row] = fn(va[row], vb[row], vc[row]) - elseif not E then - local va = columns[records[A].column] - local vb = columns[records[B].column] - local vc = columns[records[C].column] - local vd = columns[records[D].column] - - va[row], vb[row], vc[row], vd[row] = fn(va[row], vb[row], vc[row], vd[row]) - else - for j, id in ids do - local tr = records[id] - queryOutput[j] = columns[tr.column][row] - end - columns_replace_values(row, columns, fn(unpack(queryOutput))) - end - end - end + return query :: any end -- Meant for directly iterating over archetypes to minimize @@ -1491,13 +1473,10 @@ end local Query = {} Query.__index = Query Query.__iter = query_iter -Query.iter = query_iter +Query.iter = query_iter_init Query.without = query_without Query.with = query_with Query.archetypes = query_archetypes -Query.drain = query_drain -Query.next = query_next -Query.replace = query_replace local function world_query(world: World, ...) local compatible_archetypes = {} @@ -1507,7 +1486,7 @@ local function world_query(world: World, ...) local archetypes = world.archetypes - local idr: IdRecord + local idr: IdRecord? local componentIndex = world.componentIndex for _, id in ids do @@ -1521,6 +1500,10 @@ local function world_query(world: World, ...) end end + if not idr then + return EMPTY_QUERY + end + for archetype_id in idr.cache do local compatibleArchetype = archetypes[archetype_id] if #compatibleArchetype.entities == 0 then @@ -1619,14 +1602,6 @@ if _G.__JECS_DEBUG then return not world_has_one_inline(world, ECS_ENTITY_T_HI(id), EcsComponent) end - local original_invoke_hook = invoke_hook - local invoked_hook = false - invoke_hook = function(...) - invoked_hook = true - original_invoke_hook(...) - invoked_hook = false - end - World.query = function(world: World, ...) ASSERT((...), "Requires at least a single component") return world_query(world, ...) @@ -1651,17 +1626,6 @@ if _G.__JECS_DEBUG then return end - if world_has_one_inline(world, entity, id) then - if invoked_hook then - local file, line = debug.info(2, "sl") - local hook_fn = `{file}::{line}` - local why = `cannot call world:set inside {hook_fn} because it adds the component {get_name(world, id)}` - why ..= `\n[jecs note]: consider handling this logic inside of a system` - throw(why) - return - end - end - world_set(world, entity, id, value) end @@ -1673,10 +1637,6 @@ if _G.__JECS_DEBUG then return end - if invoked_hook then - local hook_fn = debug.info(2, "sl") - throw(`Cannot call world:add when the hook {hook_fn} is in process`) - end world_add(world, entity, id) end @@ -1704,14 +1664,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, @@ -1720,9 +1683,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) + 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) end world_add(self, EcsName, EcsComponent) @@ -1750,9 +1718,23 @@ function World.new() return self end -export type Id = Entity | Pair +export type Id = Entity | Pair, Entity> -export type Pair = number +export type Pair = number & { + __relation: First, +} + +-- type function _Pair(first, second) +-- local thing = first:components()[2] + +-- if thing:readproperty(types.singleton("__T")):is("nil") then +-- return second +-- else +-- return first +-- end +-- end + +-- type TestPair = _Pair, Entity> type Item = (self: Query) -> (Entity, T...) @@ -1764,19 +1746,16 @@ type Query = typeof(setmetatable({}, { __iter = (nil :: any) :: Iter, })) & { iter: Iter, - next: Item, - drain: (self: Query) -> Query, with: (self: Query, ...i53) -> Query, without: (self: Query, ...i53) -> Query, - replace: (self: Query, (T...) -> U...) -> (), - archetypes: () -> { Archetype }, + archetypes: (self: Query) -> { Archetype }, } export type World = { archetypeIndex: { [string]: Archetype }, archetypes: Archetypes, componentIndex: ComponentIndex, - entityIndex: EntityIndex, + entity_index: EntityIndex, ROOT_ARCHETYPE: Archetype, nextComponentId: number, @@ -1799,6 +1778,7 @@ export type World = { --- 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 @@ -1874,7 +1854,7 @@ return { Name = EcsName :: Entity, Rest = EcsRest :: Entity, - pair = ECS_PAIR, + pair = ECS_PAIR :: (first: P, second: O) -> Pair, -- Inwards facing API for testing ECS_ID = ECS_ENTITY_T_LO, @@ -1886,4 +1866,26 @@ return { pair_first = ecs_pair_first, pair_second = ecs_pair_second, entity_index_get_alive = entity_index_get_alive, + + archetype_append_to_records = archetype_append_to_records, + id_record_ensure = id_record_ensure, + archetype_create = archetype_create, + archetype_ensure = archetype_ensure, + find_insert = find_insert, + find_archetype_with = find_archetype_with, + find_archetype_without = find_archetype_without, + archetype_init_edge = archetype_init_edge, + archetype_ensure_edge = archetype_ensure_edge, + init_edge_for_add = init_edge_for_add, + init_edge_for_remove = init_edge_for_remove, + create_edge_for_add = create_edge_for_add, + 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, + entity_index_remove = entity_index_remove, + entity_index_new_id = entity_index_new_id, } diff --git a/pesde.toml b/pesde.toml index bd6fdee..0fd4bfd 100644 --- a/pesde.toml +++ b/pesde.toml @@ -10,7 +10,7 @@ includes = [ license = "MIT" name = "mark_marks/jecs_pesde" repository = "https://git.devmarked.win/marked/jecs-pesde" -version = "0.3.2" +version = "0.4.0-rc.0" [indices] default = "https://github.com/daimond113/pesde-index"