From 735eb0152664f550be613f8299b1bc5ca1252e76 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 13 Aug 2024 20:08:58 +0200 Subject: [PATCH] Initial commit --- src/init.luau | 250 +++++++++++++++++++++++++++++++++++------------- test/tests.luau | 113 +++++++++++++++++++++- 2 files changed, 292 insertions(+), 71 deletions(-) diff --git a/src/init.luau b/src/init.luau index 135c0a7..36858b1 100644 --- a/src/init.luau +++ b/src/init.luau @@ -64,7 +64,8 @@ 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 EcsRest = HI_COMPONENT_ID + 7 +local EcsDelete = HI_COMPONENT_ID + 7 +local EcsRest = HI_COMPONENT_ID + 8 local ECS_PAIR_FLAG = 0x8 local ECS_ID_FLAGS_MASK = 0x10 @@ -640,73 +641,6 @@ local function world_remove(world: World, entity: i53, id: i53) end end --- should reuse this logic in World.set instead of swap removing in transition archetype -local function columns_destruct(columns: { Column }, count: number, row: number) - if row == count then - for _, column in columns do - column[count] = nil - end - else - for _, column in columns do - column[row] = column[count] - column[count] = nil - end - end -end - -local function archetype_delete(world: World, id: i53) - local componentIndex = world.componentIndex - local idr = componentIndex[id] - local archetypes = world.archetypes - - if idr then - for archetypeId in idr.cache do - for _, entity in archetypes[archetypeId].entities do - world_remove(world, entity, id) - end - end - - componentIndex[id] = nil :: any - end -end - -local function world_delete(world: World, entityId: i53) - local record = world.entityIndex.sparse[entityId] - if not record then - return - end - local entityIndex = world.entityIndex - local sparse, dense = entityIndex.sparse, entityIndex.dense - local archetype = record.archetype - local row = record.row - - archetype_delete(world, entityId) - -- TODO: should traverse linked )component records to pairs including entityId - archetype_delete(world, ECS_PAIR(entityId, EcsWildcard)) - archetype_delete(world, ECS_PAIR(EcsWildcard, entityId)) - - if archetype then - local entities = archetype.entities - local last = #entities - - if row ~= last then - local entityToMove = entities[last] - dense[record.dense] = entityToMove - sparse[entityToMove] = record - end - - entities[row], entities[last] = entities[last], nil :: any - - local columns = archetype.columns - - columns_destruct(columns, last, row) - end - - sparse[entityId] = nil :: any - dense[#dense] = nil :: any - -end - local function world_clear(world: World, entity: i53) --TODO: use sparse_get (stashed) local record = world.entityIndex.sparse[entity] @@ -724,6 +658,183 @@ local function world_clear(world: World, entity: i53) entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE) end +-- should reuse this logic in World.set instead of swap removing in transition archetype +local function columns_destruct(columns: { Column }, count: number, row: number) + if row == count then + for _, column in columns do + column[count] = nil + end + else + for _, column in columns do + column[row] = column[count] + column[count] = nil + end + end +end + +local function archetype_fast_delete_last(world, columns, + column_count, types, entity) + + for i, column in columns do + invoke_hook(world, EcsOnRemove, types[i], entity) + + column[column_count] = nil + end +end + +local function archetype_fast_delete(world, columns, + column_count, row, types, entity) + for i, column in columns do + invoke_hook(world, EcsOnRemove, types[i], entity) + + column[row] = column[column_count] + column[column_count] = nil + end +end + + +local function archetype_delete(world: World, archetype, row) + 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 + + if row == last then + archetype_fast_delete_last(world, columns, + column_count, types, delete) + else + archetype_fast_delete(world, columns, column_count, + row, types, delete) + end + + + + local component_index = world.componentIndex + local archetypes = world.archetypes + + local idr = component_index[delete] + if idr then + component_index[delete] = nil + -- TODO: remove direct descendamt because + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + + local children = {} + + for i, child in idr_archetype.entities do + table.insert(children, child) + end + + for _, child in children do + if world_has_one_inline(world, child, EcsDelete) then + world_delete(world, child) + else + world_remove(world, child, delete) + end + end + end + end + + -- TODO: iterate each linked record. + -- local r = ECS_PAIR(delete, EcsWildcard) + -- local idr_r = component_index[r] + -- if idr_r then + -- -- Doesn't work for relations atm + -- for archetype_id in idr_o.cache do + -- local children = {} + -- local idr_r_archetype = archetypes[archetype_id] + -- local idr_r_types = idr_r_archetype.types + + -- for _, child in idr_r_archetype.entities do + -- table.insert(children, child) + -- end + + -- for _, id in idr_r_types do + -- local relation = ECS_ENTITY_T_HI(id) + -- if world_target(world, child, relation) == delete then + -- world_remove(world, child, ECS_PAIR(relation, delete)) + -- end + -- end + -- end + -- end + + local o = ECS_PAIR(EcsWildcard, delete) + local idr_o = component_index[o] + if idr_o then + for archetype_id in idr_o.cache do + local children = {} + local idr_o_archetype = archetypes[archetype_id] + local idr_o_types = idr_o_archetype.types + + for _, child in idr_o_archetype.entities do + table.insert(children, child) + end + + for _, child in children do + -- In the future, this needs to be optimized to only + -- look for linked records instead of doing this linearly + for _, id in idr_o_types do + if not ECS_IS_PAIR(id) then + continue + end + local relation = ECS_ENTITY_T_HI(id) + if world_target(world, child, relation) == delete then + if world_has_one_inline(world, relation, EcsDelete) then + -- Cascade deletions of it has Delete as component trait + world_delete(world, child) + else + local p = ECS_PAIR(relation, delete) + world_remove(world, child, p) + end + end + end + end + end + end +end + +function world_delete(world: World, entity: i53) + local entityIndex = world.entityIndex + + local record = entityIndex.sparse[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 + entityIndex.sparse[entity] = nil +end + +local function world_contains(world, entity) + + return world.entityIndex.sparse[entity] +end + type CompatibleArchetype = { archetype: Archetype, indices: { number } } local function noop() @@ -1244,6 +1355,7 @@ World.get = world_get World.has = world_has World.target = world_target World.parent = world_parent +World.contains = world_contains function World.new() local self = setmetatable({ @@ -1267,6 +1379,8 @@ function World.new() entity_index_new_id(self.entityIndex, i) end + world_add(self :: any, EcsChildOf, EcsDelete) + return self end diff --git a/test/tests.luau b/test/tests.luau index 40672af..4dceed6 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -1,5 +1,6 @@ local jecs = require("@jecs") local testkit = require("@testkit") +local BENCH, START = testkit.benchmark() local __ = jecs.Wildcard local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC @@ -570,15 +571,13 @@ TEST("world:query()", function() world:add(e2, A) world:add(e2, B) - local count = 0 for id in world:query(A) do local e = world:entity() world:add(e, A) world:add(e, B) - count += 1 end - CHECK(count == 3) + CHECK(true) end end @@ -745,11 +744,119 @@ TEST("world:delete", function() CHECK(world:get(id, Poison) == nil) CHECK(world:get(id, Health) == nil) + CHECK(world:get(id1, Poison) == 500) CHECK(world:get(id1, Health) == 50) end + + do CASE "delete entities using another Entity as component" + local world = jecs.World.new() + + local Health = world:entity() + local Poison = world:component() + + local id = world:entity() + world:set(id, Poison, 5) + world:set(id, Health, 50) + local id1 = world:entity() + world:set(id1, Poison, 500) + world:set(id1, Health, 50) + + CHECK(world:has(id, Poison, Health)) + CHECK(world:has(id1, Poison, Health)) + world:delete(Poison) + + CHECK(not world:has(id, Poison)) + CHECK(not world:has(id1, Poison)) + end + + do CASE "delete children" + local world = jecs.World.new() + + local Health = world:component() + local Poison = world:component() + local FriendsWith = world:component() + + local e = world:entity() + world:set(e, Poison, 5) + world:set(e, Health, 50) + + local children = {} + for i = 1, 10 do + local child = world:entity() + world:set(child, Poison, 9999) + world:set(child, Health, 100) + world:add(child, pair(jecs.ChildOf, e)) + table.insert(children, child) + end + + + BENCH("delete children of entity", function() + world:delete(e) + end) + + for i, child in children do + CHECK(not world:contains(child)) + CHECK(not world:has(child, pair(jecs.ChildOf, e))) + CHECK(not world:has(child, Health)) + end + + e = world:entity() + + local friends = {} + for i = 1, 10 do + local friend = world:entity() + world:set(friend, Poison, 9999) + world:set(friend, Health, 100) + world:add(friend, pair(FriendsWith, e)) + table.insert(friends, friend) + end + + BENCH("remove friends of entity", function() + world:delete(e) + end) + + for i, friend in friends do + CHECK(not world:has(friends, pair(jecs.ChildOf, e))) + CHECK(world:has(friend, Health)) + end + end + + do CASE "fast delete" + local world = jecs.World.new() + + local entities = {} + local Health = world:component() + local Poison = world:component() + + for i = 1, 10 do + local child = world:entity() + world:set(child, Poison, 9999) + world:set(child, Health, 100) + table.insert(entities, child) + end + + BENCH("simple deletion of entity", function() + for i = 1, START(10) do + local e = entities[i] + world:delete(e) + end + end) + + for _, entity in entities do + CHECK(not world:contains(entity)) + end + end end) +TEST("world:contains", function() + local world = jecs.World.new() + + local id = world:entity() + CHECK(world:contains(id)) + world:delete(id) + CHECK(not world:contains(id)) +end) type Tracker = { track: (world: World, fn: (changes: { added: () -> () -> (number, T), removed: () -> () -> number,