diff --git a/.luaurc b/.luaurc index 73598d2..2747dd7 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 166a5db..0d86c4c 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,68 @@ -An automatically synced minimal copy of the [jecs](https://github.com/Ukendio/jecs) repo published to pesde. +An unofficial pesde package for [jecs](https://github.com/ukendio/jecs). - -

- -

- -[![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 + +

+ + +

+ +[![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 diff --git a/init.luau b/init.luau index 2b96b0a..4223eed 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,6 +44,11 @@ type Record = { dense: i24, } +type EntityIndex = { + dense: Map, + sparse: Map, +} + type ArchetypeRecord = { count: number, column: number, @@ -53,11 +58,6 @@ 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,41 +69,34 @@ type ArchetypeDiff = { removed: Ty, } -type EntityIndex = { - dense_array: Map, - sparse_array: Map, - alive_count: number, - max_id: number, -} +local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 -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 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 --- stylua: ignore end -local NULL_ARRAY = table.freeze({}) :: Column +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 function FLAGS_ADD(is_pair: boolean): number local flags = 0x0 @@ -143,12 +136,7 @@ local function ECS_GENERATION_INC(e: i53) local id = flags // ECS_ENTITY_MASK local generation = flags % ECS_GENERATION_MASK - local next_gen = generation + 1 - if next_gen > ECS_GENERATION_MASK then - return id - end - - return ECS_COMBINE(id, next_gen) + return ECS_COMBINE(id, generation + 1) + flags end return ECS_COMBINE(e, 1) end @@ -171,73 +159,49 @@ 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 function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record? - local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)] - if not r then - return nil - end - - if not r or r.dense == 0 then - return nil - end - - return r -end - -local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record? - local r = entity_index_try_get_any(entity_index, entity) - if r then - local r_dense = r.dense - if r_dense > entity_index.alive_count then - return nil - end - if entity_index.dense_array[r_dense] ~= entity then - return nil - end - end - return r -end +local 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 r = entity_index_try_get_any(index, e) - if r then - return index.dense_array[r.dense] - end - return 0 -end + local denseArray = index.dense + local id = denseArray[ECS_ENTITY_T_LO(e)] -local function entity_index_is_alive(entity_index: EntityIndex, entity: number) - return entity_index_try_get(entity_index, entity) ~= nil -end + if id then + local currentGeneration = ECS_GENERATION(id) + local gen = ECS_GENERATION(e) + if gen == currentGeneration then + return id + 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 + error(ERROR_GENERATION_INVALID) 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 + error(ERROR_ENTITY_NOT_ALIVE) +end - return id +local function _entity_index_sparse_get(entityIndex, id) + return entityIndex.sparse[entity_index_get_alive(entityIndex, 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.entity_index, ECS_ENTITY_T_HI(e)) + return entity_index_get_alive(world.entityIndex, 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.entity_index, ECS_ENTITY_T_LO(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 end local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) @@ -270,6 +234,7 @@ 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. @@ -285,10 +250,9 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: src_entities[moved] = nil :: any dst_entities[dst_row] = e1 - local sparse_array = entity_index.sparse_array + local record1 = sparse[e1] + local record2 = sparse[e2] - 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 @@ -323,11 +287,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: { ArchetypeRecord } - local columns: { { any } } - local row: number + local records + local columns + local row - local function fetch(id): any + local function fetch(id) local tr = records[id] if not tr then @@ -338,7 +302,7 @@ do end function world_get(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any - local record = entity_index_try_get(world.entity_index, entity) + local record = world.entityIndex.sparse[entity] if not record then return nil end @@ -368,8 +332,8 @@ do end end -local function world_get_one_inline(world: World, entity: i53, id: i53): any - local record = entity_index_try_get(world.entity_index, entity) +local function world_get_one_inline(world: World, entity: i53, id: i53) + local record = world.entityIndex.sparse[entity] if not record then return nil end @@ -387,7 +351,7 @@ local function world_get_one_inline(world: World, entity: i53, id: i53): any end local function world_has_one_inline(world: World, entity: number, id: i53): boolean - local record = entity_index_try_get(world.entity_index, entity) + local record = world.entityIndex.sparse[entity] if not record then return false end @@ -403,7 +367,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 = entity_index_try_get(world.entity_index, entity) + local record = world.entityIndex.sparse[entity] if not record then return false end @@ -424,13 +388,11 @@ local function world_has(world: World, entity: number, ...: i53): boolean return true end -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 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 archetype = record.archetype if not archetype then return nil @@ -447,11 +409,11 @@ local function world_target(world: World, entity: i53, relation: i24, index: num end local count = tr.count - if nth >= count then - nth = nth + count + 1 + if index >= count then + index = index + count + 1 end - nth = archetype.types[nth + tr.column] + local nth = archetype.types[index + tr.column] if not nth then return nil @@ -500,11 +462,6 @@ 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 @@ -578,7 +535,9 @@ local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): A end local function world_entity(world: World): i53 - return entity_index_new_id(world.entity_index) + local entityId = (world.nextEntityId :: number) + 1 + world.nextEntityId = entityId + return entity_index_new_id(world.entityIndex, entityId + EcsRest) end local function world_parent(world: World, entity: i53) @@ -648,10 +607,16 @@ local function archetype_init_edge(archetype: Archetype, edge: GraphEdge, id: i5 edge.id = id end -local function archetype_ensure_edge(world, edges: GraphEdges, id): GraphEdge +local function archetype_ensure_edge(world, edges, id): GraphEdge local edge = edges[id] if not edge then - edge = {} :: GraphEdge + edge = { + from = nil :: any, + to = nil :: any, + id = id, + prev = nil, + next = nil, + } :: GraphEdge edges[id] = edge end @@ -662,33 +627,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: World, archetype: Archetype, edge: GraphEdge, id: number, to: Archetype) +local function init_edge_for_remove(world, archetype, edge, id, to) 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 @@ -704,7 +669,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) @@ -729,24 +694,23 @@ 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) +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 end local function world_add(world: World, entity: i53, id: i53): () - local entity_index = world.entity_index - local record = entity_index_try_get(entity_index, entity) - if not record then - return - end - + local entityIndex = world.entityIndex + local record = entityIndex.sparse[entity] local from = record.archetype local to = archetype_traverse_add(world, id, from) if from == to then return end if from then - entity_move(entity_index, entity, record, to) + entity_move(entityIndex, entity, record, to) else if #to.types > 0 then new_entity(entity, record, to) @@ -754,26 +718,22 @@ local function world_add(world: World, entity: i53, id: i53): () end local idr = world.componentIndex[id] - local on_add = idr.hooks.on_add + local has_on_add = bit32.band(idr.flags, ECS_ID_HAS_ON_ADD) ~= 0 - if on_add then - on_add(entity) + if has_on_add then + invoke_hook(world, EcsOnAdd, id, entity) end end local function world_set(world: World, entity: i53, id: i53, data: unknown): () - 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 entityIndex = world.entityIndex + local record = entityIndex.sparse[entity] + local from = record.archetype + local to = 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 idr_hooks = idr.hooks + local has_on_set = bit32.band(flags, ECS_ID_HAS_ON_SET) ~= 0 if from == to then if is_tag then @@ -782,11 +742,9 @@ 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] - 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) + from.columns[tr.column][record.row] = data + if has_on_set then + invoke_hook(world, EcsOnSet, id, entity, data) end return @@ -794,7 +752,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(entity_index, entity, record, to) + entity_move(entityIndex, entity, record, to) else if #to.types > 0 then -- When there is no previous archetype it should create the archetype @@ -802,9 +760,10 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () end end - local on_add = idr_hooks.on_add - if on_add then - on_add(entity) + local has_on_add = bit32.band(flags, ECS_ID_HAS_ON_ADD) ~= 0 + + if has_on_add then + invoke_hook(world, EcsOnAdd, id, entity) end if is_tag then @@ -816,9 +775,8 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): () column[record.row] = data - local on_set = idr_hooks.on_set - if on_set then - invoke_hook(on_set, entity, data) + if has_on_set then + invoke_hook(world, EcsOnSet, id, entity, data) end end @@ -830,18 +788,15 @@ local function world_component(world: World): i53 error("Too many components, consider using world:entity() instead to create components.") end world.nextComponentId = componentId - - return componentId + local id = entity_index_new_id(world.entityIndex, componentId) + world_add(world, id, EcsComponent) + return id end local function world_remove(world: World, entity: i53, id: i53) - local entity_index = world.entity_index - local record = entity_index_try_get(entity_index, entity) - if not record then - return - end + local entity_index = world.entityIndex + local record = entity_index.sparse[entity] local from = record.archetype - if not from then return end @@ -849,15 +804,33 @@ local function world_remove(world: World, entity: i53, id: i53) 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) + 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) 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 @@ -875,62 +848,6 @@ 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 @@ -944,68 +861,69 @@ end local function archetype_remove_edge(edges: Map, id: i53, edge: GraphEdge) archetype_disconnect_edge(edge) - edges[id] = nil :: any + edges[id] = nil end local function archetype_clear_edges(archetype: Archetype) - local node: GraphNode = archetype.node - local add: GraphEdges = node.add - local remove: GraphEdges = node.remove - local node_refs: GraphEdge = node.refs + local node = archetype.node + local add = node.add + local remove = node.remove + local node_refs = node.refs for id, edge in add do archetype_disconnect_edge(edge) - add[id] = nil :: any + add[id] = nil end for id, edge in remove do archetype_disconnect_edge(edge) - remove[id] = nil :: any + remove[id] = nil end local cur = node_refs.next - 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 + 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 - 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 + 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 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 :: any - world.archetypeIndex[archetype.type] = nil :: any + world.archetypes[archetype_id] = nil + world.archetypeIndex[archetype.type] = nil local records = archetype.records for id in records do local idr = component_index[id] - idr.cache[archetype_id] = nil :: any + idr.cache[archetype_id] = nil idr.size -= 1 - records[id] = nil :: any + records[id] = nil if idr.size == 0 then - component_index[id] = nil :: any + component_index[id] = nil end end end -local function world_cleanup(world: World) - local archetypes = world.archetypes +local function world_cleanup(world) + local archetypes = world.archetypes for _, archetype in archetypes do if #archetype.entities == 0 then @@ -1013,11 +931,11 @@ local function world_cleanup(world: World) end end - local new_archetypes = table.create(#archetypes) :: { Archetype } + local new_archetypes = table.create(#archetypes) 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 @@ -1027,9 +945,44 @@ 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 entity_index = world.entity_index - local record = entity_index_try_get(entity_index, entity) + local entityIndex = world.entityIndex + local sparse_array = entityIndex.sparse + + local record = sparse_array[entity] if not record then return end @@ -1087,48 +1040,37 @@ do if not ECS_IS_PAIR(id) then continue end - local object = ecs_pair_second(world, id) + local object = ECS_ENTITY_T_LO(id) if object == delete then - local id_record = component_index[id] + local id_record = component_index[id] local flags = id_record.flags - 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 + 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 + 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 - 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) + sparse_array[entity] = nil end end local function world_contains(world: World, entity): boolean - return entity_index_is_alive(world.entity_index, entity) + return world.entityIndex.sparse[entity] ~= nil end local function NOOP() end @@ -1145,6 +1087,9 @@ local EMPTY_QUERY = { iter = function() return NOOP end, + drain = ARM, + next = NOOP, + replace = NOOP, with = ARM, without = ARM, archetypes = function() @@ -1154,14 +1099,21 @@ local EMPTY_QUERY = { setmetatable(EMPTY_QUERY, EMPTY_QUERY) -local function query_iter_init(query): () -> (number, ...any) +local function query_iter(query) 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 NOOP :: () -> (number, ...any) + return EMPTY_QUERY end local columns = archetype.columns local entities = archetype.entities @@ -1170,8 +1122,7 @@ local function query_iter_init(query): () -> (number, ...any) local ids = query.ids local A, B, C, D, E, F, G, H, I = unpack(ids) - local a: Column, b: Column, c: Column, d: Column - local e: Column, f: Column, g: Column, h: Column + local a, b, c, d, e, f, g, h if not B then a = columns[records[A].column] @@ -1231,6 +1182,9 @@ local function query_iter_init(query): () -> (number, ...any) entities = archetype.entities i = #entities + if i == 0 then + continue + end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1254,6 +1208,9 @@ local function query_iter_init(query): () -> (number, ...any) entities = archetype.entities i = #entities + if i == 0 then + continue + end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1278,6 +1235,9 @@ local function query_iter_init(query): () -> (number, ...any) entities = archetype.entities i = #entities + if i == 0 then + continue + end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1303,6 +1263,9 @@ local function query_iter_init(query): () -> (number, ...any) entities = archetype.entities i = #entities + if i == 0 then + continue + end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1330,6 +1293,9 @@ local function query_iter_init(query): () -> (number, ...any) entities = archetype.entities i = #entities + if i == 0 then + continue + end entityId = entities[i] columns = archetype.columns local records = archetype.records @@ -1389,19 +1355,22 @@ local function query_iter_init(query): () -> (number, ...any) end end - query.next = world_query_iter_next + query.iter_next = world_query_iter_next return world_query_iter_next end -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 +local function query_drain(query) + local query_iter_next = query_iter(query) + query.next = query_iter_next + query.should_drain = true + return query end -local function query_without(query: { compatible_archetypes: { Archetype } }, ...) +local function query_next(query) + error("Did you forget to call drain?") +end + +local function query_without(query, ...) local compatible_archetypes = query.compatible_archetypes local N = select("#", ...) for i = #compatible_archetypes, 1, -1 do @@ -1422,7 +1391,7 @@ local function query_without(query: { compatible_archetypes: { Archetype } }, .. if last ~= i then compatible_archetypes[i] = compatible_archetypes[last] end - compatible_archetypes[last] = nil :: any + compatible_archetypes[last] = nil end end @@ -1430,10 +1399,10 @@ local function query_without(query: { compatible_archetypes: { Archetype } }, .. return EMPTY_QUERY end - return query :: any + return query end -local function query_with(query: { compatible_archetypes: { Archetype } }, ...) +local function query_with(query, ...) local compatible_archetypes = query.compatible_archetypes local N = select("#", ...) for i = #compatible_archetypes, 1, -1 do @@ -1454,13 +1423,62 @@ local function query_with(query: { compatible_archetypes: { Archetype } }, ...) if last ~= i then compatible_archetypes[i] = compatible_archetypes[last] end - compatible_archetypes[last] = nil :: any + compatible_archetypes[last] = nil end end if #compatible_archetypes == 0 then return EMPTY_QUERY end - return query :: any + 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 end -- Meant for directly iterating over archetypes to minimize @@ -1473,10 +1491,13 @@ end local Query = {} Query.__index = Query Query.__iter = query_iter -Query.iter = query_iter_init +Query.iter = query_iter 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 = {} @@ -1486,7 +1507,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 @@ -1500,10 +1521,6 @@ 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 @@ -1602,6 +1619,14 @@ 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, ...) @@ -1626,6 +1651,17 @@ 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 @@ -1637,6 +1673,10 @@ 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 @@ -1664,17 +1704,14 @@ 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, - entity_index = entity_index, + entityIndex = { + dense = {} :: { [i24]: i53 }, + sparse = {} :: { [i53]: Record }, + } :: EntityIndex, nextArchetypeId = 0 :: number, nextComponentId = 0 :: number, nextEntityId = 0 :: number, @@ -1683,14 +1720,9 @@ 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(entity_index) + entity_index_new_id(self.entityIndex, i) end world_add(self, EcsName, EcsComponent) @@ -1718,23 +1750,9 @@ function World.new() return self end -export type Id = Entity | Pair, Entity> +export type Id = Entity | Pair -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> +export type Pair = number type Item = (self: Query) -> (Entity, T...) @@ -1746,16 +1764,19 @@ 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, - archetypes: (self: Query) -> { Archetype }, + replace: (self: Query, (T...) -> U...) -> (), + archetypes: () -> { Archetype }, } export type World = { archetypeIndex: { [string]: Archetype }, archetypes: Archetypes, componentIndex: ComponentIndex, - entity_index: EntityIndex, + entityIndex: EntityIndex, ROOT_ARCHETYPE: Archetype, nextComponentId: number, @@ -1778,7 +1799,6 @@ 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 @@ -1854,7 +1874,7 @@ return { Name = EcsName :: Entity, Rest = EcsRest :: Entity, - pair = ECS_PAIR :: (first: P, second: O) -> Pair, + pair = ECS_PAIR, -- Inwards facing API for testing ECS_ID = ECS_ENTITY_T_LO, @@ -1866,26 +1886,4 @@ 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 0fd4bfd..bd6fdee 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.4.0-rc.0" +version = "0.3.2" [indices] default = "https://github.com/daimond113/pesde-index"