mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 09:30:03 +00:00
Sparse set for entity records
This commit is contained in:
parent
d5414f1bc4
commit
cbe0710c37
2 changed files with 241 additions and 27 deletions
114
lib/init.lua
114
lib/init.lua
|
@ -29,9 +29,10 @@ type Archetype = {
|
||||||
type Record = {
|
type Record = {
|
||||||
archetype: Archetype,
|
archetype: Archetype,
|
||||||
row: number,
|
row: number,
|
||||||
|
dense: i24,
|
||||||
}
|
}
|
||||||
|
|
||||||
type EntityIndex = {[i24]: Record}
|
type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}}
|
||||||
type ComponentIndex = {[i24]: ArchetypeMap}
|
type ComponentIndex = {[i24]: ArchetypeMap}
|
||||||
|
|
||||||
type ArchetypeRecord = number
|
type ArchetypeRecord = number
|
||||||
|
@ -81,10 +82,12 @@ local function transitionArchetype(
|
||||||
column[last] = nil
|
column[last] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local dense, sparse = entityIndex.dense, entityIndex.sparse
|
||||||
-- Move the entity from the source to the destination archetype.
|
-- Move the entity from the source to the destination archetype.
|
||||||
local atSourceRow = sourceEntities[sourceRow]
|
local atSourceRow = sourceEntities[sourceRow]
|
||||||
destinationEntities[destinationRow] = atSourceRow
|
destinationEntities[destinationRow] = atSourceRow
|
||||||
entityIndex[atSourceRow].row = destinationRow
|
local record = sparse[atSourceRow]
|
||||||
|
record.row = destinationRow
|
||||||
|
|
||||||
-- Because we have swapped columns we now have to update the records
|
-- Because we have swapped columns we now have to update the records
|
||||||
-- corresponding to the entities' rows that were swapped.
|
-- corresponding to the entities' rows that were swapped.
|
||||||
|
@ -92,7 +95,7 @@ local function transitionArchetype(
|
||||||
if sourceRow ~= movedAway then
|
if sourceRow ~= movedAway then
|
||||||
local atMovedAway = sourceEntities[movedAway]
|
local atMovedAway = sourceEntities[movedAway]
|
||||||
sourceEntities[sourceRow] = atMovedAway
|
sourceEntities[sourceRow] = atMovedAway
|
||||||
entityIndex[atMovedAway].row = sourceRow
|
sparse[atMovedAway].row = sourceRow
|
||||||
end
|
end
|
||||||
|
|
||||||
sourceEntities[movedAway] = nil
|
sourceEntities[movedAway] = nil
|
||||||
|
@ -178,10 +181,13 @@ local World = {}
|
||||||
World.__index = World
|
World.__index = World
|
||||||
function World.new()
|
function World.new()
|
||||||
local self = setmetatable({
|
local self = setmetatable({
|
||||||
archetypeIndex = {};
|
archetypeIndex = {};
|
||||||
archetypes = {};
|
archetypes = {};
|
||||||
componentIndex = {};
|
componentIndex = {};
|
||||||
entityIndex = {};
|
entityIndex = {
|
||||||
|
dense = {},
|
||||||
|
sparse = {}
|
||||||
|
} :: EntityIndex;
|
||||||
hooks = {
|
hooks = {
|
||||||
[ON_ADD] = {};
|
[ON_ADD] = {};
|
||||||
};
|
};
|
||||||
|
@ -294,15 +300,18 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
|
||||||
return add
|
return add
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensureRecord(entityIndex, entityId: i53): Record
|
local function ensureRecord(entityIndex: EntityIndex, entityId: i53): Record
|
||||||
local record = entityIndex[entityId]
|
local sparse = entityIndex.sparse
|
||||||
|
local dense = entityIndex.dense
|
||||||
if not record then
|
local page = sparse[entityId]
|
||||||
record = {}
|
if not page then
|
||||||
entityIndex[entityId] = record
|
local i = #dense + 1
|
||||||
|
page = { dense = i } :: Record
|
||||||
|
sparse[entityId] = page
|
||||||
|
dense[i] = entityId
|
||||||
end
|
end
|
||||||
|
|
||||||
return record :: Record
|
return page
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
|
@ -374,7 +383,7 @@ end
|
||||||
|
|
||||||
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
|
||||||
local id = entityId
|
local id = entityId
|
||||||
local record = world.entityIndex[id]
|
local record = world.entityIndex.sparse[id]
|
||||||
if not record then
|
if not record then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
@ -587,15 +596,64 @@ function World.entity(world: World)
|
||||||
return nextEntityId + REST
|
return nextEntityId + REST
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.delete(world: World, entityId: i53)
|
-- should reuse this logic in World.set instead of swap removing in transition archetype
|
||||||
|
local function destructColumns(columns, count, row)
|
||||||
|
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 archetypeDelete(entityIndex, entityId: i53, destruct: boolean)
|
||||||
|
local sparse = entityIndex.sparse
|
||||||
|
local dense = entityIndex.dense
|
||||||
|
local record = sparse[entityId]
|
||||||
|
local archetype = record.archetype
|
||||||
|
local row = record.row
|
||||||
|
local denseIndex = record.dense
|
||||||
|
|
||||||
|
local entities = archetype.entities
|
||||||
|
local last = #entities
|
||||||
|
|
||||||
|
local entityToMove = entities[last]
|
||||||
|
--local entityToDelete = entities[row]
|
||||||
|
entities[row] = entityToMove
|
||||||
|
entities[last] = nil
|
||||||
|
|
||||||
|
if row ~= last then
|
||||||
|
local recordToMove = sparse[entityToMove]
|
||||||
|
if recordToMove then
|
||||||
|
recordToMove.row = row
|
||||||
|
record.dense = denseIndex
|
||||||
|
dense[denseIndex] = entityToMove
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
record.archetype = nil
|
||||||
|
record.row = nil
|
||||||
|
entityIndex.sparse[entityId] = nil
|
||||||
|
|
||||||
|
local atDense = record.dense
|
||||||
|
entityIndex[atDense] = nil
|
||||||
|
local columns = archetype.columns
|
||||||
|
|
||||||
|
if not destruct then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
destructColumns(columns, last, row)
|
||||||
|
end
|
||||||
|
|
||||||
|
function World.delete(world: World, entityId: i53)
|
||||||
local entityIndex = world.entityIndex
|
local entityIndex = world.entityIndex
|
||||||
local record = entityIndex[entityId]
|
archetypeDelete(entityIndex, entityId, true)
|
||||||
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
|
|
||||||
-- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from
|
|
||||||
-- the entities array and delete the record. We know there won't be the hole since
|
|
||||||
-- we are always removing the last row.
|
|
||||||
--world.ROOT_ARCHETYPE.entities[record.row] = nil
|
|
||||||
--entityIndex[entityId] = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.observer(world: World, ...)
|
function World.observer(world: World, ...)
|
||||||
|
@ -652,21 +710,23 @@ function World.observer(world: World, ...)
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.__iter(world: World): () -> (number?, unknown?)
|
function World.__iter(world: World): () -> (number?, unknown?)
|
||||||
local entityIndex = world.entityIndex
|
local dense = world.entityIndex.dense
|
||||||
|
local sparse = world.entityIndex.sparse
|
||||||
local last
|
local last
|
||||||
|
|
||||||
return function()
|
return function()
|
||||||
local entity, record = next(entityIndex, last)
|
local lastEntity, entityId = next(dense, last)
|
||||||
if not entity then
|
if not lastEntity then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
last = entity
|
last = lastEntity
|
||||||
|
|
||||||
|
local record = sparse[entityId]
|
||||||
local archetype = record.archetype
|
local archetype = record.archetype
|
||||||
if not archetype then
|
if not archetype then
|
||||||
-- Returns only the entity id as an entity without data should not return
|
-- Returns only the entity id as an entity without data should not return
|
||||||
-- data and allow the user to get an error if they don't handle the case.
|
-- data and allow the user to get an error if they don't handle the case.
|
||||||
return entity
|
return entityId
|
||||||
end
|
end
|
||||||
|
|
||||||
local row = record.row
|
local row = record.row
|
||||||
|
@ -678,7 +738,7 @@ function World.__iter(world: World): () -> (number?, unknown?)
|
||||||
entityData[types[i]] = column[row]
|
entityData[types[i]] = column[row]
|
||||||
end
|
end
|
||||||
|
|
||||||
return entity, entityData
|
return entityId, entityData
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
154
tests/world.lua
Normal file
154
tests/world.lua
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
local testkit = require("../testkit")
|
||||||
|
local jecs = require("../lib/init")
|
||||||
|
|
||||||
|
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
|
||||||
|
|
||||||
|
local N = 10
|
||||||
|
|
||||||
|
TEST("world", function()
|
||||||
|
do CASE "should be iterable"
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
|
local eA = world:entity()
|
||||||
|
world:set(eA, A, true)
|
||||||
|
local eB = world:entity()
|
||||||
|
world:set(eB, B, true)
|
||||||
|
local eAB = world:entity()
|
||||||
|
world:set(eAB, A, true)
|
||||||
|
world:set(eAB, B, true)
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
for id, data in world do
|
||||||
|
count += 1
|
||||||
|
if id == eA then
|
||||||
|
CHECK(data[A] == true)
|
||||||
|
CHECK(data[B] == nil)
|
||||||
|
elseif id == eB then
|
||||||
|
CHECK(data[A] == nil)
|
||||||
|
CHECK(data[B] == true)
|
||||||
|
elseif id == eAB then
|
||||||
|
CHECK(data[A] == true)
|
||||||
|
CHECK(data[B] == true)
|
||||||
|
else
|
||||||
|
error("unknown entity", id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(count == 3)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "should query all matching entities"
|
||||||
|
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
|
local entities = {}
|
||||||
|
for i = 1, N do
|
||||||
|
local id = world:entity()
|
||||||
|
|
||||||
|
world:set(id, A, true)
|
||||||
|
if i > 5 then world:set(id, B, true) end
|
||||||
|
entities[i] = id
|
||||||
|
end
|
||||||
|
|
||||||
|
for id in world:query(A) do
|
||||||
|
table.remove(entities, CHECK(table.find(entities, id)))
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(#entities == 0)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "should query all matching entities when irrelevant component is removed"
|
||||||
|
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
|
local entities = {}
|
||||||
|
for i = 1, N do
|
||||||
|
local id = world:entity()
|
||||||
|
|
||||||
|
world:set(id, A, true)
|
||||||
|
world:set(id, B, true)
|
||||||
|
if i > 5 then world:remove(id, B, true) end
|
||||||
|
entities[i] = id
|
||||||
|
end
|
||||||
|
|
||||||
|
local added = 0
|
||||||
|
for id in world:query(A) do
|
||||||
|
added += 1
|
||||||
|
table.remove(entities, CHECK(table.find(entities, id)))
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(added == N)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "should query all entities without B"
|
||||||
|
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local A = world:component()
|
||||||
|
local B = world:component()
|
||||||
|
|
||||||
|
local entities = {}
|
||||||
|
for i = 1, N do
|
||||||
|
local id = world:entity()
|
||||||
|
|
||||||
|
world:set(id, A, true)
|
||||||
|
if i < 5 then
|
||||||
|
entities[i] = id
|
||||||
|
else
|
||||||
|
world:set(id, B, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
for id in world:query(A):without(B) do
|
||||||
|
table.remove(entities, CHECK(table.find(entities, id)))
|
||||||
|
end
|
||||||
|
|
||||||
|
CHECK(#entities == 0)
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "should allow setting components in arbitrary order"
|
||||||
|
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)
|
||||||
|
|
||||||
|
CHECK(world:get(id, Poison) == 5)
|
||||||
|
end
|
||||||
|
|
||||||
|
do CASE "Should allow deleting components"
|
||||||
|
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)
|
||||||
|
|
||||||
|
world:delete(id)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
end)
|
||||||
|
|
||||||
|
FINISH()
|
Loading…
Reference in a new issue