From 6d45af93f0099bc0bb679d4b3244237528226c6c Mon Sep 17 00:00:00 2001 From: Marcus Date: Fri, 16 Aug 2024 19:13:30 +0200 Subject: [PATCH] Add OnDeleteTarget (#102) --- demo/src/ReplicatedStorage/std/init.luau | 1 - demo/src/ReplicatedStorage/std/world.luau | 2 +- src/init.luau | 243 +++++++++++++--------- test/tests.luau | 2 +- 4 files changed, 143 insertions(+), 105 deletions(-) diff --git a/demo/src/ReplicatedStorage/std/init.luau b/demo/src/ReplicatedStorage/std/init.luau index 23f6cc37..6014b4e9 100644 --- a/demo/src/ReplicatedStorage/std/init.luau +++ b/demo/src/ReplicatedStorage/std/init.luau @@ -1,6 +1,5 @@ local jecs = require(game:GetService("ReplicatedStorage").ecs) local world = require(script.world) -export type World = world.World local std = { ChangeTracker = require(script.changetracker), diff --git a/demo/src/ReplicatedStorage/std/world.luau b/demo/src/ReplicatedStorage/std/world.luau index 84b90b31..67a1a564 100644 --- a/demo/src/ReplicatedStorage/std/world.luau +++ b/demo/src/ReplicatedStorage/std/world.luau @@ -1,5 +1,5 @@ local jecs = require(game:GetService("ReplicatedStorage").ecs) -export type World = jecs.WorldShim +export type World = jecs.t_world -- I like the idea of only having the world be a singleton. return jecs.World.new() diff --git a/src/init.luau b/src/init.luau index 64091eee..51a5f535 100644 --- a/src/init.luau +++ b/src/init.luau @@ -16,7 +16,7 @@ type ArchetypeEdge = { remove: Archetype, } -type Archetype = { +export type Archetype = { id: number, edges: { [i53]: ArchetypeEdge }, types: Ty, @@ -65,8 +65,9 @@ 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 EcsDelete = HI_COMPONENT_ID + 7 -local EcsRest = HI_COMPONENT_ID + 8 +local EcsOnDeleteTarget = HI_COMPONENT_ID + 7 +local EcsDelete = HI_COMPONENT_ID + 8 +local EcsRest = HI_COMPONENT_ID + 9 local ECS_PAIR_FLAG = 0x8 local ECS_ID_FLAGS_MASK = 0x10 @@ -328,20 +329,6 @@ local function world_get_one_inline(world: World, entity: i53, id: i53) return archetype.columns[tr.column][record.row] end -local function world_has_one_inline(world: World, entity: i53, id: i53): boolean - local record = world.entityIndex.sparse[entity] - if not record then - return false - end - - local archetype = record.archetype - if not archetype then - return false - end - - return archetype.records[id] ~= nil -end - local function world_has(world: World, entity: number, ...: i53): boolean local record = world.entityIndex.sparse[entity] if not record then @@ -386,9 +373,31 @@ local function world_has_any(world: World, entity: number, ...: i53): boolean return false end +-- TODO: +-- should have an additional `nth` parameter which selects the nth target +-- this is important when an entity can have multiple relationships with the same target +local function world_target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24? + local record = world.entityIndex.sparse[entity] + local archetype = record.archetype + if not archetype then + return nil + end + + local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] + if not idr then + return nil + end + + local tr = idr.cache[archetype.id] + if not tr then + return nil + end + + return ecs_pair_second(world, archetype.types[tr.column]) +end local function id_record_ensure( - world, + world: World, id: number ): ArchetypeMap local componentIndex = world.componentIndex @@ -397,8 +406,9 @@ local function id_record_ensure( if not idr then local flags = 0b0000 local relation = ECS_ENTITY_T_HI(id) - if world_has_one_inline(world, relation, EcsDelete) then - flags = bit32.bor(flags, ECS_ID_HAS_DELETE) + local cleanup_policy = world_target(world, relation, EcsOnDeleteTarget) + if cleanup_policy == EcsDelete then + flags = bit32.bor(flags, ECS_ID_HAS_DELETE) end if world_has_any(world, relation, @@ -429,7 +439,7 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean return first == EcsWildcard or second == EcsWildcard end -local function archetype_create(world: any, types: { i24 }, prev: Archetype?): Archetype +local function archetype_create(world: World, types: { i24 }, prev: Archetype?): Archetype local ty = hash(types) local id = world.nextArchetypeId + 1 @@ -489,29 +499,6 @@ local function world_entity(world: World): i53 return entity_index_new_id(world.entityIndex, entityId + EcsRest) end --- TODO: --- should have an additional `nth` parameter which selects the nth target --- this is important when an entity can have multiple relationships with the same target -local function world_target(world: World, entity: i53, relation: i24--[[, nth: number]]): i24? - local record = world.entityIndex.sparse[entity] - local archetype = record.archetype - if not archetype then - return nil - end - - local idr = world.componentIndex[ECS_PAIR(relation, EcsWildcard)] - if not idr then - return nil - end - - local tr = idr.cache[archetype.id] - if not tr then - return nil - end - - return ecs_pair_second(world, archetype.types[tr.column]) -end - local function world_parent(world: World, entity: i53) return world_target(world, entity, EcsChildOf) end @@ -717,25 +704,27 @@ local function world_clear(world: World, entity: i53) entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE) end -local function archetype_fast_delete_last(world, columns, - column_count, types, entity) +local function archetype_fast_delete_last(columns, column_count, + types, entity) for i, column in columns do column[column_count] = nil end end -local function archetype_fast_delete(world, columns, - column_count, row, types, entity) +local function archetype_fast_delete(columns, column_count, + row, types, entity) + for i, column in columns do column[row] = column[column_count] column[column_count] = nil end end +local ERROR_DELETE_PANIC = "Tried to delete entity that has (OnDelete, Panic)" -local function archetype_delete(world: World, archetype, - row, track) +local function archetype_delete(world: World, + archetype: Archetype, row: number) local entityIndex = world.entityIndex local columns = archetype.columns @@ -763,10 +752,10 @@ local function archetype_delete(world: World, archetype, end if row == last then - archetype_fast_delete_last(world, columns, + archetype_fast_delete_last(columns, column_count, types, delete) else - archetype_fast_delete(world, columns, column_count, + archetype_fast_delete(columns, column_count, row, types, delete) end @@ -852,12 +841,12 @@ local function archetype_delete(world: World, archetype, -- Cascade deletions of it has Delete as component trait world_delete(world, child) end - end - else - local object = ECS_ENTITY_T_LO(id) - if object == delete then - for _, child in children do - world_remove(world, child, id) + else + local object = ECS_ENTITY_T_LO(id) + if object == delete then + for _, child in children do + world_remove(world, child, id) + end end end end @@ -867,7 +856,7 @@ local function archetype_delete(world: World, archetype, end end -function world_delete(world: World, entity: i53, track) +function world_delete(world: World, entity: i53) local entityIndex = world.entityIndex local record = entityIndex.sparse[entity] @@ -881,14 +870,14 @@ function world_delete(world: World, entity: i53, track) if archetype then -- In the future should have a destruct mode for -- deleting archetypes themselves. Maybe requires recycling - archetype_delete(world, archetype, row, track) + archetype_delete(world, archetype, row) end record.archetype = nil :: any entityIndex.sparse[entity] = nil end -local function world_contains(world, entity) +local function world_contains(world: World, entity) return world.entityIndex.sparse[entity] end @@ -930,7 +919,7 @@ do end end - function world_query(world, ...) + function world_query(world: World, ...) -- breaking if (...) == nil then error("Missing components") @@ -1418,7 +1407,7 @@ World.target = world_target World.parent = world_parent World.contains = world_contains -function World.new() +function World.new(): t_world local self = setmetatable({ archetypeIndex = {} :: { [string]: Archetype }, archetypes = {} :: Archetypes, @@ -1427,11 +1416,11 @@ function World.new() dense = {} :: { [i24]: i53 }, sparse = {} :: { [i53]: Record }, } :: EntityIndex, - nextArchetypeId = 0, - nextComponentId = 0, - nextEntityId = 0, + nextArchetypeId = 0 :: number, + nextComponentId = 0 :: number, + nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, - }, World) + }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}) @@ -1440,7 +1429,8 @@ function World.new() entity_index_new_id(self.entityIndex, i) end - world_add(self :: any, EcsChildOf, EcsDelete) + world_add(self, EcsChildOf, + ECS_PAIR(EcsOnDeleteTarget, EcsDelete)) return self end @@ -1464,49 +1454,97 @@ type Query = typeof(setmetatable({}, { archetypes: () -> { Archetype }, } -export type World = { +type World = { archetypeIndex: { [string]: Archetype }, archetypes: Archetypes, componentIndex: ComponentIndex, entityIndex: EntityIndex, - nextArchetypeId: number, + ROOT_ARCHETYPE: Archetype, + nextComponentId: number, nextEntityId: number, - ROOT_ARCHETYPE: Archetype, -} & { - target: (World, entity: Entity, relation: Entity) -> Entity, - parent: (World, entity: Entity) -> Entity, - entity: (World) -> Entity, - clear: (World, entity: Entity) -> (), - delete: (World, entity: Entity) -> (), - component: (World) -> Entity, - get: ((World, entity: Entity, id: Entity) -> T) - & ((World, id: Entity, Entity, Entity) -> (A, B)) - & ((World, id: Entity, Entity, Entity, Entity) -> (A, B, C)) - & (World, id: Entity, Entity, Entity, Entity, Entity) -> (A, B, C, D), - has: (World, entity: Entity, ...Entity) -> boolean, - add: (World, entity: Entity, id: Entity) -> (), - set: (World, entity: Entity, - id: Entity, data: T) -> (), - remove: (World, entity: Entity, id: Entity) -> (), - query: - ((World, Entity) -> Query) - & ((World, Entity, Entity) -> Query) - & ((World, Entity, Entity, Entity) -> Query) - & ((World, Entity, Entity, Entity, - Entity) -> Query) - & ((World, Entity, Entity, Entity, - Entity, Entity) -> Query) - & ((World, Entity, Entity, Entity, - Entity, Entity, Entity) -> Query) - & ((World, Entity, Entity, Entity, - Entity, Entity, Entity, Entity) -> Query) - & ((World, Entity, Entity, Entity, - Entity, Entity, Entity, Entity, Entity) -> Query) + nextArchetypeId: number, } +export type t_world = typeof(setmetatable( + {} :: { + + --- Creates a new entity + entity: (t_world) -> Entity, + --- Creates a new entity located in the first 256 ids. + --- These should be used for static components for fast access. + component: (t_world) -> Entity, + --- Gets the target of an relationship. For example, when a user calls + --- `world:target(id, ChildOf(parent))`, you will obtain the parent entity. + target: (t_world, id: Entity, relation: Entity) -> Entity?, + --- Deletes an entity and all it's related components and relationships. + delete: (t_world, id: Entity) -> (), + + --- Adds a component to the entity with no value + add: (world: t_world, id: Entity, component: Entity) -> (), + --- Assigns a value to a component on the given entity + set: (world: World, id: Entity, component: Entity, data: T) -> (), + + -- Clears an entity from the world + clear: (t_world, id: Entity) -> (), + --- Removes a component from the given entity + remove: (t_world, id: Entity, component: Entity) -> (), + --- Retrieves the value of up to 4 components. These values may be nil. + get: ((t_world, id: any, Entity) -> A) + & ((t_world, id: Entity, Entity, Entity) -> (A, B)) + & ((t_world, id: Entity, Entity, Entity, Entity) -> (A, B, C)) + & (t_world, id: Entity, Entity, Entity, Entity, Entity) -> (A, B, C, D), + + --- Searches the world for entities that match a given query + query: ((t_world, Entity) -> Query) + & ((t_world, Entity, Entity) -> Query) + & ((t_world, Entity, Entity, Entity) -> Query) + & ((t_world, Entity, Entity, Entity, Entity) -> Query) + & (( + t_world, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> Query) + & (( + t_world, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> Query) + & (( + t_world, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity + ) -> Query) + & (( + t_world, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + Entity, + ...Entity + ) -> Query), + }, {} +)) + + return { - World = World :: { new: () -> World }, + World = World :: { new: () -> t_world }, OnAdd = EcsOnAdd :: Entity, OnRemove = EcsOnRemove :: Entity, @@ -1515,6 +1553,7 @@ return { Component = EcsComponent :: Entity, Wildcard = EcsWildcard :: Entity, w = EcsWildcard :: Entity, + OnDeleteTarget = EcsOnDeleteTarget :: Entity, Delete = EcsDelete :: Entity, Rest = EcsRest :: Entity, diff --git a/test/tests.luau b/test/tests.luau index f63f67db..23a6aa24 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -851,7 +851,7 @@ TEST("world:delete", function() do CASE "cycle" local world = jecs.World.new() local Likes = world:component() - world:add(Likes, jecs.Delete) + world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete)) local bob = world:entity() local alice = world:entity()