mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Relationships (#31)
* Sparse set for entity records * Swap dense indexes * Improve inlining * Add benchmarks * Add tests for relations * Add REST * Merge upstream changes * Add back symmetric and non idempotent add function * Only swap when not last row * Assert that the entity is alive * Update example with relations
This commit is contained in:
parent
6775601e21
commit
d63de48546
5 changed files with 414 additions and 208 deletions
22
README.md
22
README.md
|
@ -19,15 +19,18 @@ jecs is a stupidly fast Entity Component System (ECS).
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
local world = Jecs.World.new()
|
local world = World.new()
|
||||||
|
|
||||||
local Health = world:component()
|
|
||||||
local Damage = world:component()
|
|
||||||
local Position = world:component()
|
|
||||||
|
|
||||||
local player = world:entity()
|
local player = world:entity()
|
||||||
local opponent = world:entity()
|
local opponent = world:entity()
|
||||||
|
|
||||||
|
local Health = world:component()
|
||||||
|
local Position = world:component()
|
||||||
|
-- Notice how components can just be entities as well?
|
||||||
|
-- It allows you to model relationships easily!
|
||||||
|
local Damage = world:entity()
|
||||||
|
local DamagedBy = world:entity()
|
||||||
|
|
||||||
world:set(player, Health, 100)
|
world:set(player, Health, 100)
|
||||||
world:set(player, Damage, 8)
|
world:set(player, Damage, 8)
|
||||||
world:set(player, Position, Vector3.new(0, 5, 0))
|
world:set(player, Position, Vector3.new(0, 5, 0))
|
||||||
|
@ -38,17 +41,20 @@ world:set(opponent, Position, Vector3.new(0, 5, 3))
|
||||||
|
|
||||||
for playerId, playerPosition, health in world:query(Position, Health) do
|
for playerId, playerPosition, health in world:query(Position, Health) do
|
||||||
local totalDamage = 0
|
local totalDamage = 0
|
||||||
for _, opponentPosition, damage in world:query(Position, Damage) do
|
for opponentId, opponentPosition, damage in world:query(Position, Damage) do
|
||||||
if (playerPosition - opponentPosition).Magnitude < 5 then
|
if (playerPosition - opponentPosition).Magnitude < 5 then
|
||||||
totalDamage += damage
|
totalDamage += damage
|
||||||
end
|
end
|
||||||
|
world:set(playerId, ECS_PAIR(DamagedBy, opponentId), totalDamage)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
world:set(playerId, Health, health - totalDamage)
|
-- Gets the damage inflicted by our specific opponent!
|
||||||
|
for playerId, health, inflicted in world:query(Health, ECS_PAIR(DamagedBy, opponent)) do
|
||||||
|
world:set(playerId, health - inflicted)
|
||||||
end
|
end
|
||||||
|
|
||||||
assert(world:get(playerId, Health) == 79)
|
assert(world:get(playerId, Health) == 79)
|
||||||
assert(world:get(opponentId, Health) == 92)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
125 archetypes, 4 random components queried.
|
125 archetypes, 4 random components queried.
|
||||||
|
|
|
@ -39,6 +39,29 @@ do
|
||||||
for _ in world:query(A, B, C, D, E, F, G, H) do
|
for _ in world:query(A, B, C, D, E, F, G, H) do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local e = world:entity()
|
||||||
|
world:set(e, A, true)
|
||||||
|
world:set(e, B, true)
|
||||||
|
world:set(e, C, true)
|
||||||
|
world:set(e, D, true)
|
||||||
|
world:set(e, E, true)
|
||||||
|
world:set(e, F, true)
|
||||||
|
world:set(e, G, true)
|
||||||
|
world:set(e, H, true)
|
||||||
|
|
||||||
|
BENCH("Update Data", function()
|
||||||
|
for _ = 1, 100 do
|
||||||
|
world:set(e, A, false)
|
||||||
|
world:set(e, B, false)
|
||||||
|
world:set(e, C, false)
|
||||||
|
world:set(e, D, false)
|
||||||
|
world:set(e, E, false)
|
||||||
|
world:set(e, F, false)
|
||||||
|
world:set(e, G, false)
|
||||||
|
world:set(e, H, false)
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local D1 = ecs:component()
|
local D1 = ecs:component()
|
||||||
|
@ -132,6 +155,29 @@ do
|
||||||
for _ in world:query(A, B, C, D, E, F, G, H) do
|
for _ in world:query(A, B, C, D, E, F, G, H) do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
local e = world:entity()
|
||||||
|
world:set(e, A, true)
|
||||||
|
world:set(e, B, true)
|
||||||
|
world:set(e, C, true)
|
||||||
|
world:set(e, D, true)
|
||||||
|
world:set(e, E, true)
|
||||||
|
world:set(e, F, true)
|
||||||
|
world:set(e, G, true)
|
||||||
|
world:set(e, H, true)
|
||||||
|
|
||||||
|
BENCH("Update Data", function()
|
||||||
|
for _ = 1, 100 do
|
||||||
|
world:set(e, A, false)
|
||||||
|
world:set(e, B, false)
|
||||||
|
world:set(e, C, false)
|
||||||
|
world:set(e, D, false)
|
||||||
|
world:set(e, E, false)
|
||||||
|
world:set(e, F, false)
|
||||||
|
world:set(e, G, false)
|
||||||
|
world:set(e, H, false)
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local D1 = ecs:component()
|
local D1 = ecs:component()
|
||||||
|
|
|
@ -8,6 +8,8 @@ local jecs = require(ReplicatedStorage.Lib)
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||||
local newWorld = Matter.World.new()
|
local newWorld = Matter.World.new()
|
||||||
local ecs = jecs.World.new()
|
local ecs = jecs.World.new()
|
||||||
|
local mirror = require(ReplicatedStorage.mirror)
|
||||||
|
local mcs = mirror.World.new()
|
||||||
|
|
||||||
local A1 = Matter.component()
|
local A1 = Matter.component()
|
||||||
local A2 = Matter.component()
|
local A2 = Matter.component()
|
||||||
|
@ -35,6 +37,15 @@ local C5 = ecs:entity()
|
||||||
local C6 = ecs:entity()
|
local C6 = ecs:entity()
|
||||||
local C7 = ecs:entity()
|
local C7 = ecs:entity()
|
||||||
local C8 = ecs:entity()
|
local C8 = ecs:entity()
|
||||||
|
local E1 = mcs:entity()
|
||||||
|
local E2 = mcs:entity()
|
||||||
|
local E3 = mcs:entity()
|
||||||
|
local E4 = mcs:entity()
|
||||||
|
local E5 = mcs:entity()
|
||||||
|
local E6 = mcs:entity()
|
||||||
|
local E7 = mcs:entity()
|
||||||
|
local E8 = mcs:entity()
|
||||||
|
|
||||||
|
|
||||||
local registry2 = ecr.registry()
|
local registry2 = ecr.registry()
|
||||||
return {
|
return {
|
||||||
|
@ -44,7 +55,7 @@ return {
|
||||||
|
|
||||||
Functions = {
|
Functions = {
|
||||||
Matter = function()
|
Matter = function()
|
||||||
for i = 1, 50 do
|
for i = 1, 500 do
|
||||||
newWorld:spawn(
|
newWorld:spawn(
|
||||||
A1({ value = true }),
|
A1({ value = true }),
|
||||||
A2({ value = true }),
|
A2({ value = true }),
|
||||||
|
@ -60,8 +71,8 @@ return {
|
||||||
|
|
||||||
|
|
||||||
ECR = function()
|
ECR = function()
|
||||||
for i = 1, 50 do
|
local e = registry2.create()
|
||||||
local e = registry2.create()
|
for i = 1, 500 do
|
||||||
registry2:set(e, B1, {value = false})
|
registry2:set(e, B1, {value = false})
|
||||||
registry2:set(e, B2, {value = false})
|
registry2:set(e, B2, {value = false})
|
||||||
registry2:set(e, B3, {value = false})
|
registry2:set(e, B3, {value = false})
|
||||||
|
@ -78,7 +89,7 @@ return {
|
||||||
|
|
||||||
local e = ecs:entity()
|
local e = ecs:entity()
|
||||||
|
|
||||||
for i = 1, 50 do
|
for i = 1, 500 do
|
||||||
|
|
||||||
ecs:set(e, C1, {value = false})
|
ecs:set(e, C1, {value = false})
|
||||||
ecs:set(e, C2, {value = false})
|
ecs:set(e, C2, {value = false})
|
||||||
|
@ -89,6 +100,23 @@ return {
|
||||||
ecs:set(e, C7, {value = false})
|
ecs:set(e, C7, {value = false})
|
||||||
ecs:set(e, C8, {value = false})
|
ecs:set(e, C8, {value = false})
|
||||||
|
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Mirror = function()
|
||||||
|
|
||||||
|
local e = ecs:entity()
|
||||||
|
|
||||||
|
for i = 1, 500 do
|
||||||
|
|
||||||
|
mcs:set(e, E1, {value = false})
|
||||||
|
mcs:set(e, E2, {value = false})
|
||||||
|
mcs:set(e, E3, {value = false})
|
||||||
|
mcs:set(e, E4, {value = false})
|
||||||
|
mcs:set(e, E5, {value = false})
|
||||||
|
mcs:set(e, E6, {value = false})
|
||||||
|
mcs:set(e, E7, {value = false})
|
||||||
|
mcs:set(e, E8, {value = false})
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
411
lib/init.lua
411
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,21 +82,27 @@ local function transitionArchetype(
|
||||||
column[last] = nil
|
column[last] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Move the entity from the source to the destination archetype.
|
local sparse = entityIndex.sparse
|
||||||
local atSourceRow = sourceEntities[sourceRow]
|
local movedAway = #sourceEntities
|
||||||
destinationEntities[destinationRow] = atSourceRow
|
|
||||||
entityIndex[atSourceRow].row = destinationRow
|
|
||||||
|
|
||||||
|
-- Move the entity from the source to the destination archetype.
|
||||||
-- 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.
|
||||||
local movedAway = #sourceEntities
|
local e1 = sourceEntities[sourceRow]
|
||||||
if sourceRow ~= movedAway then
|
local e2 = sourceEntities[movedAway]
|
||||||
local atMovedAway = sourceEntities[movedAway]
|
|
||||||
sourceEntities[sourceRow] = atMovedAway
|
if sourceRow ~= movedAway then
|
||||||
entityIndex[atMovedAway].row = sourceRow
|
sourceEntities[sourceRow] = e2
|
||||||
end
|
end
|
||||||
|
|
||||||
sourceEntities[movedAway] = nil
|
sourceEntities[movedAway] = nil
|
||||||
|
destinationEntities[destinationRow] = e1
|
||||||
|
|
||||||
|
local record1 = sparse[e1]
|
||||||
|
local record2 = sparse[e2]
|
||||||
|
|
||||||
|
record1.row = destinationRow
|
||||||
|
record2.row = sourceRow
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeAppend(entity: number, archetype: Archetype): number
|
local function archetypeAppend(entity: number, archetype: Archetype): number
|
||||||
|
@ -143,14 +150,14 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype
|
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
|
||||||
local ty = hash(types)
|
local ty = hash(types)
|
||||||
|
|
||||||
local id = world.nextArchetypeId + 1
|
local id = world.nextArchetypeId + 1
|
||||||
world.nextArchetypeId = id
|
world.nextArchetypeId = id
|
||||||
|
|
||||||
local length = #types
|
local length = #types
|
||||||
local columns = table.create(length)
|
local columns = table.create(length) :: {any}
|
||||||
|
|
||||||
for index in types do
|
for index in types do
|
||||||
columns[index] = {}
|
columns[index] = {}
|
||||||
|
@ -174,6 +181,194 @@ local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetyp
|
||||||
return archetype
|
return archetype
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local World = {}
|
||||||
|
World.__index = World
|
||||||
|
function World.new()
|
||||||
|
local self = setmetatable({
|
||||||
|
archetypeIndex = {};
|
||||||
|
archetypes = {};
|
||||||
|
componentIndex = {};
|
||||||
|
entityIndex = {
|
||||||
|
dense = {},
|
||||||
|
sparse = {}
|
||||||
|
} :: EntityIndex;
|
||||||
|
hooks = {
|
||||||
|
[ON_ADD] = {};
|
||||||
|
};
|
||||||
|
nextArchetypeId = 0;
|
||||||
|
nextComponentId = 0;
|
||||||
|
nextEntityId = 0;
|
||||||
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
||||||
|
}, World)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
local FLAGS_PAIR = 0x8
|
||||||
|
|
||||||
|
local function addFlags(flags)
|
||||||
|
local typeFlags = 0x0
|
||||||
|
if flags.isPair then
|
||||||
|
typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID.
|
||||||
|
end
|
||||||
|
if false then
|
||||||
|
typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true
|
||||||
|
end
|
||||||
|
if false then
|
||||||
|
typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true
|
||||||
|
end
|
||||||
|
if false then
|
||||||
|
typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID.
|
||||||
|
end
|
||||||
|
|
||||||
|
return typeFlags
|
||||||
|
end
|
||||||
|
|
||||||
|
local ECS_ID_FLAGS_MASK = 0x10
|
||||||
|
|
||||||
|
-- ECS_ENTITY_MASK (0xFFFFFFFFull << 28)
|
||||||
|
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
|
||||||
|
|
||||||
|
-- ECS_GENERATION_MASK (0xFFFFull << 24)
|
||||||
|
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
|
||||||
|
|
||||||
|
local function newId(source: number, target: number)
|
||||||
|
local e = source * 2^28 + target * ECS_ID_FLAGS_MASK
|
||||||
|
return e
|
||||||
|
end
|
||||||
|
|
||||||
|
local function isPair(e: number)
|
||||||
|
return (e % 2^4) // FLAGS_PAIR ~= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function separate(entity: number)
|
||||||
|
local _typeFlags = entity % 0x10
|
||||||
|
entity //= ECS_ID_FLAGS_MASK
|
||||||
|
return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags
|
||||||
|
end
|
||||||
|
|
||||||
|
-- HIGH 24 bits LOW 24 bits
|
||||||
|
local function ECS_GENERATION(e: i53)
|
||||||
|
e //= 0x10
|
||||||
|
return e % ECS_GENERATION_MASK
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ECS_ID(e: i53)
|
||||||
|
e //= 0x10
|
||||||
|
return e // ECS_ENTITY_MASK
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ECS_GENERATION_INC(e: i53)
|
||||||
|
local id, generation, flags = separate(e)
|
||||||
|
|
||||||
|
return newId(id, generation + 1) + flags
|
||||||
|
end
|
||||||
|
|
||||||
|
-- gets the high ID
|
||||||
|
local function ECS_PAIR_FIRST(entity: i53): i24
|
||||||
|
entity //= 0x10
|
||||||
|
local first = entity % ECS_ENTITY_MASK
|
||||||
|
return first
|
||||||
|
end
|
||||||
|
|
||||||
|
-- gets the low ID
|
||||||
|
local ECS_PAIR_SECOND = ECS_ID
|
||||||
|
|
||||||
|
local function ECS_PAIR(source: number, target: number)
|
||||||
|
local id = newId(ECS_PAIR_SECOND(target), ECS_PAIR_SECOND(source)) + addFlags({ isPair = true })
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
|
||||||
|
local function getAlive(entityIndex: EntityIndex, id: i53)
|
||||||
|
return assert(entityIndex.dense[id], id .. "is not alive")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function ecs_get_source(entityIndex, e)
|
||||||
|
assert(isPair(e))
|
||||||
|
return getAlive(entityIndex, ECS_PAIR_FIRST(e))
|
||||||
|
end
|
||||||
|
local function ecs_get_target(entityIndex, e)
|
||||||
|
assert(isPair(e))
|
||||||
|
return getAlive(entityIndex, ECS_PAIR_SECOND(e))
|
||||||
|
end
|
||||||
|
|
||||||
|
function World.component(world: World)
|
||||||
|
local componentId = world.nextComponentId + 1
|
||||||
|
if componentId > HI_COMPONENT_ID then
|
||||||
|
-- IDs are partitioned into ranges because component IDs are not nominal,
|
||||||
|
-- so it needs to error when IDs intersect into the entity range.
|
||||||
|
error("Too many components, consider using world:entity() instead to create components.")
|
||||||
|
end
|
||||||
|
world.nextComponentId = componentId
|
||||||
|
return componentId
|
||||||
|
end
|
||||||
|
|
||||||
|
function World.entity(world: World)
|
||||||
|
local nextEntityId = world.nextEntityId + 1
|
||||||
|
world.nextEntityId = nextEntityId
|
||||||
|
local index = nextEntityId + REST
|
||||||
|
local id = newId(index, 0)
|
||||||
|
local entityIndex = world.entityIndex
|
||||||
|
entityIndex.sparse[id] = {
|
||||||
|
dense = index
|
||||||
|
} :: Record
|
||||||
|
entityIndex.dense[index] = id
|
||||||
|
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 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, record: Record, entityId: i53, destruct: boolean)
|
||||||
|
local sparse, dense = entityIndex.sparse, entityIndex.dense
|
||||||
|
local archetype = record.archetype
|
||||||
|
local row = record.row
|
||||||
|
local entities = archetype.entities
|
||||||
|
local last = #entities
|
||||||
|
|
||||||
|
local entityToMove = entities[last]
|
||||||
|
|
||||||
|
if row ~= last then
|
||||||
|
dense[record.dense] = entityToMove
|
||||||
|
sparse[entityToMove] = record
|
||||||
|
end
|
||||||
|
|
||||||
|
sparse[entityId] = nil
|
||||||
|
dense[#dense] = nil
|
||||||
|
|
||||||
|
entities[row], entities[last] = entities[last], 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 record = entityIndex.sparse[entityId]
|
||||||
|
if not record then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
archetypeDelete(entityIndex, record, entityId, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
export type World = typeof(World.new())
|
||||||
|
|
||||||
local function ensureArchetype(world: World, types, prev)
|
local function ensureArchetype(world: World, types, prev)
|
||||||
if #types < 1 then
|
if #types < 1 then
|
||||||
return world.ROOT_ARCHETYPE
|
return world.ROOT_ARCHETYPE
|
||||||
|
@ -228,7 +423,15 @@ local function ensureEdge(archetype: Archetype, componentId: i53)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
||||||
from = from or world.ROOT_ARCHETYPE
|
if not from then
|
||||||
|
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
|
||||||
|
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
|
||||||
|
if not ROOT_ARCHETYPE then
|
||||||
|
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
||||||
|
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
|
||||||
|
end
|
||||||
|
from = ROOT_ARCHETYPE
|
||||||
|
end
|
||||||
|
|
||||||
local edge = ensureEdge(from, componentId)
|
local edge = ensureEdge(from, componentId)
|
||||||
local add = edge.add
|
local add = edge.add
|
||||||
|
@ -242,101 +445,35 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
|
||||||
return add
|
return add
|
||||||
end
|
end
|
||||||
|
|
||||||
local function ensureRecord(world, entityId: i53): Record
|
|
||||||
local entityIndex = world.entityIndex
|
|
||||||
local record = entityIndex[entityId]
|
|
||||||
|
|
||||||
if record then
|
|
||||||
return record
|
|
||||||
end
|
|
||||||
|
|
||||||
local ROOT = world.ROOT_ARCHETYPE
|
|
||||||
local row = #ROOT.entities + 1
|
|
||||||
ROOT.entities[row] = entityId
|
|
||||||
record = {
|
|
||||||
archetype = ROOT,
|
|
||||||
row = row
|
|
||||||
}
|
|
||||||
entityIndex[entityId] = record
|
|
||||||
return record
|
|
||||||
end
|
|
||||||
|
|
||||||
local World = {}
|
|
||||||
World.__index = World
|
|
||||||
function World.new()
|
|
||||||
local self = setmetatable({
|
|
||||||
archetypeIndex = {};
|
|
||||||
archetypes = {};
|
|
||||||
componentIndex = {};
|
|
||||||
entityIndex = {};
|
|
||||||
hooks = {
|
|
||||||
[ON_ADD] = {};
|
|
||||||
};
|
|
||||||
nextArchetypeId = 0;
|
|
||||||
nextComponentId = 0;
|
|
||||||
nextEntityId = 0;
|
|
||||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
|
|
||||||
}, World)
|
|
||||||
self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
local function emit(world, eventDescription)
|
|
||||||
local event = eventDescription.event
|
|
||||||
|
|
||||||
table.insert(world.hooks[event], {
|
|
||||||
archetype = eventDescription.archetype;
|
|
||||||
ids = eventDescription.ids;
|
|
||||||
offset = eventDescription.offset;
|
|
||||||
otherArchetype = eventDescription.otherArchetype;
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
|
||||||
if #added > 0 then
|
|
||||||
emit(world, {
|
|
||||||
archetype = archetype;
|
|
||||||
event = ON_ADD;
|
|
||||||
ids = added;
|
|
||||||
offset = row;
|
|
||||||
otherArchetype = otherArchetype;
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
export type World = typeof(World.new())
|
|
||||||
|
|
||||||
|
|
||||||
function World.add(world: World, entityId: i53, componentId: i53)
|
function World.add(world: World, entityId: i53, componentId: i53)
|
||||||
local record = ensureRecord(world, entityId)
|
local entityIndex = world.entityIndex
|
||||||
|
local record = entityIndex.sparse[entityId]
|
||||||
local from = record.archetype
|
local from = record.archetype
|
||||||
local to = archetypeTraverseAdd(world, componentId, from)
|
local to = archetypeTraverseAdd(world, componentId, from)
|
||||||
if from and not (from == world.ROOT_ARCHETYPE) then
|
if from and not (from == world.ROOT_ARCHETYPE) then
|
||||||
moveEntity(world.entityIndex, entityId, record, to)
|
moveEntity(entityIndex, entityId, record, to)
|
||||||
else
|
else
|
||||||
if #to.types > 0 then
|
if #to.types > 0 then
|
||||||
newEntity(entityId, record, to)
|
newEntity(entityId, record, to)
|
||||||
onNotifyAdd(world, to, from, record.row, { componentId })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Symmetric like `World.add` but idempotent
|
-- Symmetric like `World.add` but idempotent
|
||||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
local record = ensureRecord(world, entityId)
|
local record = world.entityIndex.sparse[entityId]
|
||||||
local from = record.archetype
|
local from = record.archetype
|
||||||
|
local to = archetypeTraverseAdd(world, componentId, from)
|
||||||
|
|
||||||
local archetypeRecord = from.records[componentId]
|
if from == to then
|
||||||
if archetypeRecord then
|
|
||||||
-- If the archetypes are the same it can avoid moving the entity
|
-- If the archetypes are the same it can avoid moving the entity
|
||||||
-- and just set the data directly.
|
-- and just set the data directly.
|
||||||
|
local archetypeRecord = to.records[componentId]
|
||||||
from.columns[archetypeRecord][record.row] = data
|
from.columns[archetypeRecord][record.row] = data
|
||||||
-- Should fire an OnSet event here.
|
-- Should fire an OnSet event here.
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local to = archetypeTraverseAdd(world, componentId, from)
|
|
||||||
|
|
||||||
if from then
|
if from then
|
||||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
-- If there was a previous archetype, then the entity needs to move the archetype
|
||||||
moveEntity(world.entityIndex, entityId, record, to)
|
moveEntity(world.entityIndex, entityId, record, to)
|
||||||
|
@ -344,11 +481,10 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
if #to.types > 0 then
|
if #to.types > 0 then
|
||||||
-- When there is no previous archetype it should create the archetype
|
-- When there is no previous archetype it should create the archetype
|
||||||
newEntity(entityId, record, to)
|
newEntity(entityId, record, to)
|
||||||
--onNotifyAdd(world, to, from, record.row, {componentId})
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
archetypeRecord = to.records[componentId]
|
local archetypeRecord = to.records[componentId]
|
||||||
to.columns[archetypeRecord][record.row] = data
|
to.columns[archetypeRecord][record.row] = data
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -371,12 +507,13 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||||
local record = ensureRecord(world, entityId)
|
local entityIndex = world.entityIndex
|
||||||
|
local record = entityIndex.sparse[entityId]
|
||||||
local sourceArchetype = record.archetype
|
local sourceArchetype = record.archetype
|
||||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
||||||
|
|
||||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
||||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
moveEntity(entityIndex, entityId, record, destinationArchetype)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -394,7 +531,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
|
||||||
|
@ -590,86 +727,24 @@ function World.query(world: World, ...: i53): Query
|
||||||
return setmetatable({}, preparedQuery) :: any
|
return setmetatable({}, preparedQuery) :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.component(world: World)
|
|
||||||
local componentId = world.nextComponentId + 1
|
|
||||||
if componentId > HI_COMPONENT_ID then
|
|
||||||
-- IDs are partitioned into ranges because component IDs are not nominal,
|
|
||||||
-- so it needs to error when IDs intersect into the entity range.
|
|
||||||
error("Too many components, consider using world:entity() instead to create components.")
|
|
||||||
end
|
|
||||||
world.nextComponentId = componentId
|
|
||||||
return componentId
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.entity(world: World)
|
|
||||||
local nextEntityId = world.nextEntityId + 1
|
|
||||||
world.nextEntityId = nextEntityId
|
|
||||||
return nextEntityId + REST
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 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, archetype: Archetype, row: i24, destruct: boolean)
|
|
||||||
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 = entityIndex[entityToMove]
|
|
||||||
if recordToMove then
|
|
||||||
recordToMove.row = row
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
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 record = entityIndex[entityId]
|
|
||||||
local archetype = record.archetype
|
|
||||||
archetypeDelete(entityIndex, archetype, record.row, true)
|
|
||||||
entityIndex[entityId] = nil
|
|
||||||
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
|
||||||
|
@ -681,7 +756,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
|
||||||
|
|
||||||
|
@ -690,4 +765,12 @@ return table.freeze({
|
||||||
ON_ADD = ON_ADD;
|
ON_ADD = ON_ADD;
|
||||||
ON_REMOVE = ON_REMOVE;
|
ON_REMOVE = ON_REMOVE;
|
||||||
ON_SET = ON_SET;
|
ON_SET = ON_SET;
|
||||||
})
|
ECS_ID = ECS_ID,
|
||||||
|
IS_PAIR = isPair,
|
||||||
|
ECS_PAIR = ECS_PAIR,
|
||||||
|
ECS_GENERATION = ECS_GENERATION,
|
||||||
|
ECS_GENERATION_INC = ECS_GENERATION_INC,
|
||||||
|
getAlive = getAlive,
|
||||||
|
ecs_get_target = ecs_get_target,
|
||||||
|
ecs_get_source = ecs_get_source
|
||||||
|
})
|
||||||
|
|
|
@ -1,11 +1,52 @@
|
||||||
local testkit = require("../testkit")
|
local testkit = require("../testkit")
|
||||||
local jecs = require("../lib/init")
|
local jecs = require("../lib/init")
|
||||||
|
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
|
||||||
|
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
|
||||||
|
local IS_PAIR = jecs.IS_PAIR
|
||||||
|
local ECS_PAIR = jecs.ECS_PAIR
|
||||||
|
local getAlive = jecs.getAlive
|
||||||
|
local ecs_pair_first = jecs.ecs_pair_first
|
||||||
|
local ecs_pair_second = jecs.ecs_pair_second
|
||||||
|
local REST = 256 + 4
|
||||||
|
|
||||||
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
|
local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
|
||||||
|
|
||||||
local N = 10
|
local N = 10
|
||||||
|
|
||||||
TEST("world:query", function()
|
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"
|
do CASE "should query all matching entities"
|
||||||
|
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
@ -16,7 +57,6 @@ TEST("world:query", function()
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
local id = world:entity()
|
local id = world:entity()
|
||||||
|
|
||||||
|
|
||||||
world:set(id, A, true)
|
world:set(id, A, true)
|
||||||
if i > 5 then world:set(id, B, true) end
|
if i > 5 then world:set(id, B, true) end
|
||||||
entities[i] = id
|
entities[i] = id
|
||||||
|
@ -98,7 +138,7 @@ TEST("world:query", function()
|
||||||
CHECK(world:get(id, Poison) == 5)
|
CHECK(world:get(id, Poison) == 5)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "Should allow deleting components"
|
do CASE "should allow deleting components"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local Health = world:entity()
|
local Health = world:entity()
|
||||||
|
@ -107,13 +147,20 @@ TEST("world:query", function()
|
||||||
local id = world:entity()
|
local id = world:entity()
|
||||||
world:set(id, Poison, 5)
|
world:set(id, Poison, 5)
|
||||||
world:set(id, Health, 50)
|
world:set(id, Health, 50)
|
||||||
|
local id1 = world:entity()
|
||||||
|
world:set(id1, Poison, 500)
|
||||||
|
world:set(id1, Health, 50)
|
||||||
|
|
||||||
world:delete(id)
|
world:delete(id)
|
||||||
|
|
||||||
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, Health) == 50)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "show allow remove that doesn't exist on entity"
|
do CASE "should allow remove that doesn't exist on entity"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local Health = world:entity()
|
local Health = world:entity()
|
||||||
|
@ -124,40 +171,36 @@ TEST("world:query", function()
|
||||||
world:remove(id, Poison)
|
world:remove(id, Poison)
|
||||||
|
|
||||||
CHECK(world:get(id, Poison) == nil)
|
CHECK(world:get(id, Poison) == nil)
|
||||||
|
print(world:get(id, Health))
|
||||||
CHECK(world:get(id, Health) == 50)
|
CHECK(world:get(id, Health) == 50)
|
||||||
end
|
end
|
||||||
|
|
||||||
do CASE "Should allow iterating the whole world"
|
do CASE "should increment generation"
|
||||||
local world = jecs.World.new()
|
local world = jecs.World.new()
|
||||||
|
local e = world:entity()
|
||||||
|
CHECK(ECS_ID(e) == 1 + REST)
|
||||||
|
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
|
||||||
|
CHECK(ECS_GENERATION(e) == 0) -- 0
|
||||||
|
e = ECS_GENERATION_INC(e)
|
||||||
|
CHECK(ECS_GENERATION(e) == 1) -- 1
|
||||||
|
end
|
||||||
|
|
||||||
local A, B = world:entity(), world:entity()
|
do CASE "relations"
|
||||||
|
local world = jecs.World.new()
|
||||||
|
local _e = world:entity()
|
||||||
|
local e2 = world:entity()
|
||||||
|
local e3 = world:entity()
|
||||||
|
CHECK(ECS_ID(e2) == 2 + REST)
|
||||||
|
CHECK(ECS_ID(e3) == 3 + REST)
|
||||||
|
CHECK(ECS_GENERATION(e2) == 0)
|
||||||
|
CHECK(ECS_GENERATION(e3) == 0)
|
||||||
|
|
||||||
local eA = world:entity()
|
CHECK(IS_PAIR(world:entity()) == false)
|
||||||
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
|
local pair = ECS_PAIR(e2, e3)
|
||||||
for id, data in world do
|
CHECK(IS_PAIR(pair) == true)
|
||||||
count += 1
|
CHECK(ecs_pair_first(world.entityIndex, pair) == e2)
|
||||||
if id == eA then
|
CHECK(ecs_pair_second(world.entityIndex, pair) == e3)
|
||||||
CHECK(data[A] == true)
|
|
||||||
CHECK(data[B] == nil)
|
|
||||||
elseif id == eB then
|
|
||||||
CHECK(data[B] == true)
|
|
||||||
CHECK(data[A] == nil)
|
|
||||||
elseif id == eAB then
|
|
||||||
CHECK(data[A] == true)
|
|
||||||
CHECK(data[B] == true)
|
|
||||||
else
|
|
||||||
error("unknown entity", id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
CHECK(count == 3)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end)
|
end)
|
Loading…
Reference in a new issue