Initial commit

This commit is contained in:
Ukendio 2024-08-13 20:08:58 +02:00
parent 0b9d9530b9
commit 735eb01526
2 changed files with 292 additions and 71 deletions

View file

@ -64,7 +64,8 @@ local EcsOnSet = HI_COMPONENT_ID + 3
local EcsWildcard = HI_COMPONENT_ID + 4 local EcsWildcard = HI_COMPONENT_ID + 4
local EcsChildOf = HI_COMPONENT_ID + 5 local EcsChildOf = HI_COMPONENT_ID + 5
local EcsComponent = HI_COMPONENT_ID + 6 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_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10 local ECS_ID_FLAGS_MASK = 0x10
@ -640,73 +641,6 @@ local function world_remove(world: World, entity: i53, id: i53)
end end
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) local function world_clear(world: World, entity: i53)
--TODO: use sparse_get (stashed) --TODO: use sparse_get (stashed)
local record = world.entityIndex.sparse[entity] 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) entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE)
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_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 } } type CompatibleArchetype = { archetype: Archetype, indices: { number } }
local function noop() local function noop()
@ -1244,6 +1355,7 @@ World.get = world_get
World.has = world_has World.has = world_has
World.target = world_target World.target = world_target
World.parent = world_parent World.parent = world_parent
World.contains = world_contains
function World.new() function World.new()
local self = setmetatable({ local self = setmetatable({
@ -1267,6 +1379,8 @@ function World.new()
entity_index_new_id(self.entityIndex, i) entity_index_new_id(self.entityIndex, i)
end end
world_add(self :: any, EcsChildOf, EcsDelete)
return self return self
end end

View file

@ -1,5 +1,6 @@
local jecs = require("@jecs") local jecs = require("@jecs")
local testkit = require("@testkit") local testkit = require("@testkit")
local BENCH, START = testkit.benchmark()
local __ = jecs.Wildcard local __ = jecs.Wildcard
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
@ -570,15 +571,13 @@ TEST("world:query()", function()
world:add(e2, A) world:add(e2, A)
world:add(e2, B) world:add(e2, B)
local count = 0
for id in world:query(A) do for id in world:query(A) do
local e = world:entity() local e = world:entity()
world:add(e, A) world:add(e, A)
world:add(e, B) world:add(e, B)
count += 1
end end
CHECK(count == 3) CHECK(true)
end end
end end
@ -745,11 +744,119 @@ TEST("world:delete", function()
CHECK(world:get(id, Poison) == nil) CHECK(world:get(id, Poison) == nil)
CHECK(world:get(id, Health) == nil) CHECK(world:get(id, Health) == nil)
CHECK(world:get(id1, Poison) == 500) CHECK(world:get(id1, Poison) == 500)
CHECK(world:get(id1, Health) == 50) CHECK(world:get(id1, Health) == 50)
end 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) 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<T> = { track: (world: World, fn: (changes: { type Tracker<T> = { track: (world: World, fn: (changes: {
added: () -> () -> (number, T), added: () -> () -> (number, T),
removed: () -> () -> number, removed: () -> () -> number,