mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 09:30:03 +00:00
Initial commit
This commit is contained in:
parent
0b9d9530b9
commit
735eb01526
2 changed files with 292 additions and 71 deletions
250
src/init.luau
250
src/init.luau
|
@ -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
|
||||||
|
|
||||||
|
|
113
test/tests.luau
113
test/tests.luau
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue