This commit is contained in:
Ukendio 2024-05-21 17:46:36 +02:00
parent 09ae7794ea
commit 4375150683
4 changed files with 136 additions and 147 deletions

View file

@ -31,7 +31,7 @@ local Name = world:component()
local function parent(entity) local function parent(entity)
return world:target(entity, ChildOf) return world:target(entity, ChildOf)
end end
local function name(entity) local function getName(entity)
return world:get(entity, Name) return world:get(entity, Name)
end end

0
docs/api/index.md Normal file
View file

View file

@ -1,6 +0,0 @@
# Installing Jecs
To use Jecs, you will need to add the library to your project's source folder.
## Installing with Wally
Navigate over to the Wally

View file

@ -6,10 +6,10 @@
type i53 = number type i53 = number
type i24 = number type i24 = number
type Ty = {i53} type Ty = { i53 }
type ArchetypeId = number type ArchetypeId = number
type Column = {any} type Column = { any }
type Archetype = { type Archetype = {
id: number, id: number,
@ -21,20 +21,19 @@ type Archetype = {
}, },
types: Ty, types: Ty,
type: string | number, type: string | number,
entities: {number}, entities: { number },
columns: {Column}, columns: { Column },
records: {}, records: {},
} }
type Record = { type Record = {
archetype: Archetype, archetype: Archetype,
row: number, row: number,
dense: i24, dense: i24,
componentRecord: ArchetypeMap componentRecord: ArchetypeMap,
} }
type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}} type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } }
type ArchetypeRecord = number type ArchetypeRecord = number
--[[ --[[
@ -48,16 +47,16 @@ TODO:
]] ]]
type ArchetypeMap = { type ArchetypeMap = {
cache: {[number]: ArchetypeRecord}, cache: { [number]: ArchetypeRecord },
first: ArchetypeMap, first: ArchetypeMap,
second: ArchetypeMap, second: ArchetypeMap,
parent: ArchetypeMap, parent: ArchetypeMap,
size: number size: number,
} }
type ComponentIndex = {[i24]: ArchetypeMap} type ComponentIndex = { [i24]: ArchetypeMap }
type Archetypes = {[ArchetypeId]: Archetype} type Archetypes = { [ArchetypeId]: Archetype }
type ArchetypeDiff = { type ArchetypeDiff = {
added: Ty, added: Ty,
@ -70,114 +69,108 @@ local ON_ADD = HI_COMPONENT_ID + 1
local ON_REMOVE = HI_COMPONENT_ID + 2 local ON_REMOVE = HI_COMPONENT_ID + 2
local ON_SET = HI_COMPONENT_ID + 3 local ON_SET = HI_COMPONENT_ID + 3
local WILDCARD = HI_COMPONENT_ID + 4 local WILDCARD = HI_COMPONENT_ID + 4
local REST = HI_COMPONENT_ID + 5 local REST = HI_COMPONENT_ID + 5
local ECS_ID_FLAGS_MASK = 0x10 local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24) local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local function addFlags(isPair: boolean) local function addFlags(isPair: boolean)
local typeFlags = 0x0 local typeFlags = 0x0
if isPair then if isPair then
typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID.
end end
if false then if false then
typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true
end end
if false then if false then
typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true
end end
if false then if false then
typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID.
end end
return typeFlags return typeFlags
end end
local function ECS_COMBINE(source: number, target: number): i53 local function ECS_COMBINE(source: number, target: number): i53
local e = source * 2^28 + target * ECS_ID_FLAGS_MASK local e = source * 268435456 + target * ECS_ID_FLAGS_MASK
return e return e
end end
local function ECS_IS_PAIR(e: number) local function ECS_IS_PAIR(e: number)
return (e % 2^4) // FLAGS_PAIR ~= 0 return (e % 2 ^ 4) // FLAGS_PAIR ~= 0
end end
function separate(entity: number) function separate(e: number)
local _typeFlags = entity % 0x10 local _typeFlags = e % 0x10
entity //= ECS_ID_FLAGS_MASK -- Revert to //= after highligting gets fixed
return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags --
e = e // ECS_ID_FLAGS_MASK
return e // ECS_ENTITY_MASK, e % ECS_GENERATION_MASK, _typeFlags
end end
-- HIGH 24 bits LOW 24 bits -- HIGH 24 bits LOW 24 bits
local function ECS_GENERATION(e: i53) local function ECS_GENERATION(e: i53)
e //= 0x10 e = e // 0x10
return e % ECS_GENERATION_MASK return e % ECS_GENERATION_MASK
end
-- SECOND
local function ECS_ENTITY_T_LO(e: i53)
e //= 0x10
return e // ECS_ENTITY_MASK
end end
local function ECS_GENERATION_INC(e: i53) local function ECS_GENERATION_INC(e: i53)
local id, generation, flags = separate(e) local id, generation, flags = separate(e)
return ECS_COMBINE(id, generation + 1) + flags return ECS_COMBINE(id, generation + 1) + flags
end end
-- FIRST gets the high ID -- FIRST gets the high ID
local function ECS_ENTITY_T_HI(entity: i53): i24 local function ECS_ENTITY_T_HI(e: i53): i24
entity //= 0x10 e = e // 0x10
local first = entity % ECS_ENTITY_MASK return e % ECS_ENTITY_MASK
return first
end end
local function ECS_PAIR(pred: number, obj: number) -- SECOND
local first local function ECS_ENTITY_T_LO(e: i53)
e = e // 0x10
return e // ECS_ENTITY_MASK
end
local function ECS_PAIR(pred: i53, obj: i53): i53
local first
local second: number = WILDCARD local second: number = WILDCARD
if pred == WILDCARD then if pred == WILDCARD then
first = obj first = obj
elseif obj == WILDCARD then elseif obj == WILDCARD then
first = pred first = pred
else else
first = obj first = obj
second = ECS_ENTITY_T_LO(pred) second = ECS_ENTITY_T_LO(pred)
end end
return ECS_COMBINE( return ECS_COMBINE(ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true)
ECS_ENTITY_T_LO(first), second) + addFlags(--[[isPair]] true) end
end
local function getAlive(entityIndex: EntityIndex, id: i24) local function getAlive(entityIndex: EntityIndex, id: i24)
local entityId = entityIndex.dense[id] local entityId = entityIndex.dense[id]
local record = entityIndex.sparse[entityIndex.dense[id]] return entityId
if not record then
error(id.." is not alive")
end
return entityId
end end
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
local function ECS_PAIR_RELATION(entityIndex, e) local function ECS_PAIR_RELATION(entityIndex, e)
assert(ECS_IS_PAIR(e)) return getAlive(entityIndex, ECS_ENTITY_T_HI(e))
return getAlive(entityIndex, ECS_ENTITY_T_HI(e))
end end
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
local function ECS_PAIR_OBJECT(entityIndex, e) local function ECS_PAIR_OBJECT(entityIndex, e)
assert(ECS_IS_PAIR(e)) return getAlive(entityIndex, ECS_ENTITY_T_LO(e))
return getAlive(entityIndex, ECS_ENTITY_T_LO(e))
end end
local function nextEntityId(entityIndex, index: i24): i53 local function nextEntityId(entityIndex, index: i24): i53
local id = ECS_COMBINE(index, 0) local id = ECS_COMBINE(index, 0)
entityIndex.sparse[id] = { entityIndex.sparse[id] = {
dense = index dense = index,
} :: Record } :: Record
entityIndex.dense[index] = id entityIndex.dense[index] = id
return id return id
@ -224,7 +217,7 @@ local function transitionArchetype(
local e1 = sourceEntities[sourceRow] local e1 = sourceEntities[sourceRow]
local e2 = sourceEntities[movedAway] local e2 = sourceEntities[movedAway]
if sourceRow ~= movedAway then if sourceRow ~= movedAway then
sourceEntities[sourceRow] = e2 sourceEntities[sourceRow] = e2
end end
@ -252,7 +245,7 @@ local function newEntity(entityId: i53, record: Record, archetype: Archetype)
return record return record
end end
local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype) local function moveEntity(entityIndex: EntityIndex, entityId: i53, record: Record, to: Archetype)
local sourceRow = record.row local sourceRow = record.row
local from = record.archetype local from = record.archetype
local destinationRow = archetypeAppend(entityId, to) local destinationRow = archetypeAppend(entityId, to)
@ -265,11 +258,16 @@ local function hash(arr): string | number
return table.concat(arr, "_") return table.concat(arr, "_")
end end
local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId, componentId, i): ArchetypeMap local function ensureComponentRecord(
componentIndex: ComponentIndex,
archetypeId: number,
componentId: number,
i: number
): ArchetypeMap
local archetypesMap = componentIndex[componentId] local archetypesMap = componentIndex[componentId]
if not archetypesMap then if not archetypesMap then
archetypesMap = {size = 0, cache = {}, first = {}, second = {}} :: ArchetypeMap archetypesMap = { size = 0, cache = {}, first = {}, second = {} } :: ArchetypeMap
componentIndex[componentId] = archetypesMap componentIndex[componentId] = archetypesMap
end end
@ -279,15 +277,14 @@ local function ensureComponentRecord(componentIndex: ComponentIndex, archetypeId
return archetypesMap return archetypesMap
end end
local function ECS_ID_IS_WILDCARD(e) local function ECS_ID_IS_WILDCARD(e)
assert(ECS_IS_PAIR(e)) assert(ECS_IS_PAIR(e))
local first = ECS_ENTITY_T_HI(e) local first = ECS_ENTITY_T_HI(e)
local second = ECS_ENTITY_T_LO(e) local second = ECS_ENTITY_T_LO(e)
return first == WILDCARD or second == WILDCARD return first == WILDCARD or second == WILDCARD
end end
local function archetypeOf(world: any, types: { i24 }, prev: Archetype?): Archetype
local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype
local ty = hash(types) local ty = hash(types)
local id = world.nextArchetypeId + 1 local id = world.nextArchetypeId + 1
@ -301,31 +298,29 @@ local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetyp
for i, componentId in types do for i, componentId in types do
ensureComponentRecord(componentIndex, id, componentId, i) ensureComponentRecord(componentIndex, id, componentId, i)
records[componentId] = i records[componentId] = i
if ECS_IS_PAIR(componentId) then if ECS_IS_PAIR(componentId) then
local relation = ECS_PAIR_RELATION(world.entityIndex, componentId) local relation = ECS_PAIR_RELATION(world.entityIndex, componentId)
local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) local object = ECS_PAIR_OBJECT(world.entityIndex, componentId)
local idr_r = ECS_PAIR(relation, WILDCARD) local idr_r = ECS_PAIR(relation, WILDCARD)
ensureComponentRecord( ensureComponentRecord(componentIndex, id, idr_r, i)
componentIndex, id, idr_r, i)
records[idr_r] = i records[idr_r] = i
local idr_t = ECS_PAIR(WILDCARD, object) local idr_t = ECS_PAIR(WILDCARD, object)
ensureComponentRecord( ensureComponentRecord(componentIndex, id, idr_t, i)
componentIndex, id, idr_t, i)
records[idr_t] = i records[idr_t] = i
end end
columns[i] = {} columns[i] = {}
end end
local archetype = { local archetype = {
columns = columns; columns = columns,
edges = {}; edges = {},
entities = {}; entities = {},
id = id; id = id,
records = records; records = records,
type = ty; type = ty,
types = types; types = types,
} }
world.archetypeIndex[ty] = archetype world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype world.archetypes[id] = archetype
@ -337,20 +332,20 @@ local World = {}
World.__index = World World.__index = World
function World.new() function World.new()
local self = setmetatable({ local self = setmetatable({
archetypeIndex = {}; archetypeIndex = {},
archetypes = {} :: Archetypes; archetypes = {} :: Archetypes,
componentIndex = {} :: ComponentIndex; componentIndex = {} :: ComponentIndex,
entityIndex = { entityIndex = {
dense = {}, dense = {},
sparse = {} sparse = {},
} :: EntityIndex; } :: EntityIndex,
hooks = { hooks = {
[ON_ADD] = {}; [ON_ADD] = {},
}; },
nextArchetypeId = 0; nextArchetypeId = 0,
nextComponentId = 0; nextComponentId = 0,
nextEntityId = 0; nextEntityId = 0,
ROOT_ARCHETYPE = (nil :: any) :: Archetype; ROOT_ARCHETYPE = (nil :: any) :: Archetype,
}, World) }, World)
self.ROOT_ARCHETYPE = archetypeOf(self, {}) self.ROOT_ARCHETYPE = archetypeOf(self, {})
return self return self
@ -380,16 +375,16 @@ function World.target(world: World, entity: i53, relation: i24): i24?
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
local record = entityIndex.sparse[entity] local record = entityIndex.sparse[entity]
local archetype = record.archetype local archetype = record.archetype
if not archetype then if not archetype then
return nil return nil
end end
local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)] local componentRecord = world.componentIndex[ECS_PAIR(relation, WILDCARD)]
if not componentRecord then if not componentRecord then
return nil return nil
end end
local archetypeRecord = componentRecord.cache[archetype.id] local archetypeRecord = componentRecord.cache[archetype.id]
if not archetypeRecord then if not archetypeRecord then
return nil return nil
end end
@ -397,37 +392,37 @@ function World.target(world: World, entity: i53, relation: i24): i24?
end end
-- should reuse this logic in World.set instead of swap removing in transition archetype -- should reuse this logic in World.set instead of swap removing in transition archetype
local function destructColumns(columns, count, row) local function destructColumns(columns, count, row)
if row == count then if row == count then
for _, column in columns do for _, column in columns do
column[count] = nil column[count] = nil
end end
else else
for _, column in columns do for _, column in columns do
column[row] = column[count] column[row] = column[count]
column[count] = nil column[count] = nil
end end
end end
end end
local function archetypeDelete(world: World, id: i53) local function archetypeDelete(world: World, id: i53)
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
local archetypesMap = componentIndex[id] local archetypesMap = componentIndex[id]
local archetypes = world.archetypes local archetypes = world.archetypes
if archetypesMap then if archetypesMap then
for archetypeId in archetypesMap.cache do for archetypeId in archetypesMap.cache do
for _, entity in archetypes[archetypeId].entities do for _, entity in archetypes[archetypeId].entities do
world:remove(entity, id) world:remove(entity, id)
end end
end end
componentIndex[id] = nil componentIndex[id] = nil
end end
end end
function World.delete(world: World, entityId: i53) function World.delete(world: World, entityId: i53)
local record = world.entityIndex.sparse[entityId] local record = world.entityIndex.sparse[entityId]
if not record then if not record then
return return
end end
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
@ -439,12 +434,12 @@ function World.delete(world: World, entityId: i53)
-- TODO: should traverse linked )component records to pairs including entityId -- TODO: should traverse linked )component records to pairs including entityId
archetypeDelete(world, ECS_PAIR(entityId, WILDCARD)) archetypeDelete(world, ECS_PAIR(entityId, WILDCARD))
archetypeDelete(world, ECS_PAIR(WILDCARD, entityId)) archetypeDelete(world, ECS_PAIR(WILDCARD, entityId))
if archetype then if archetype then
local entities = archetype.entities local entities = archetype.entities
local last = #entities local last = #entities
if row ~= last then if row ~= last then
local entityToMove = entities[last] local entityToMove = entities[last]
dense[record.dense] = entityToMove dense[record.dense] = entityToMove
sparse[entityToMove] = record sparse[entityToMove] = record
@ -477,7 +472,7 @@ local function ensureArchetype(world: World, types, prev)
return archetypeOf(world, types, prev) return archetypeOf(world, types, prev)
end end
local function findInsert(types: {i53}, toAdd: i53) local function findInsert(types: { i53 }, toAdd: i53)
for i, id in types do for i, id in types do
if id == toAdd then if id == toAdd then
return -1 return -1
@ -494,7 +489,7 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
-- Component IDs are added incrementally, so inserting and sorting -- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion -- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array. -- point in the types array.
local destinationType = table.clone(node.types) local destinationType = table.clone(node.types)
local at = findInsert(types, componentId) local at = findInsert(types, componentId)
if at == -1 then if at == -1 then
@ -532,7 +527,7 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
return add return add
end end
function World.add(world: World, entityId: i53, componentId: i53) function World.add(world: World, entityId: i53, componentId: i53)
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
local record = entityIndex.sparse[entityId] local record = entityIndex.sparse[entityId]
local from = record.archetype local from = record.archetype
@ -582,7 +577,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, from: Arc
if not remove then if not remove then
local to = table.clone(from.types) local to = table.clone(from.types)
local at = table.find(to, componentId) local at = table.find(to, componentId)
if not at then if not at then
return from return from
end end
table.remove(to, at) table.remove(to, at)
@ -607,7 +602,7 @@ end
-- Keeping the function as small as possible to enable inlining -- Keeping the function as small as possible to enable inlining
local function get(record: Record, componentId: i24) local function get(record: Record, componentId: i24)
local archetype = record.archetype local archetype = record.archetype
if not archetype then if not archetype then
return nil return nil
end end
@ -644,20 +639,20 @@ end
-- the less creation the better -- the less creation the better
local function actualNoOperation() end local function actualNoOperation() end
local function noop(_self: Query, ...: i53): () -> (number, ...any) local function noop(_self: Query, ...): () -> ()
return actualNoOperation :: any return actualNoOperation :: any
end end
local EmptyQuery = { local EmptyQuery = {
__iter = noop; __iter = noop,
without = noop; without = noop,
} }
EmptyQuery.__index = EmptyQuery EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery) setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery) export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query function World.query(world: World, ...): Query
-- breaking? -- breaking?
if (...) == nil then if (...) == nil then
error("Missing components") error("Missing components")
@ -666,7 +661,7 @@ function World.query(world: World, ...: i53): Query
local compatibleArchetypes = {} local compatibleArchetypes = {}
local length = 0 local length = 0
local components = {...} local components = { ... }
local archetypes = world.archetypes local archetypes = world.archetypes
local queryLength = #components local queryLength = #components
@ -707,8 +702,8 @@ function World.query(world: World, ...: i53): Query
length += 1 length += 1
compatibleArchetypes[length] = { compatibleArchetypes[length] = {
archetype = archetype, archetype = archetype,
indices = indices indices = indices,
} }
end end
@ -721,7 +716,7 @@ function World.query(world: World, ...: i53): Query
preparedQuery.__index = preparedQuery preparedQuery.__index = preparedQuery
function preparedQuery:without(...) function preparedQuery:without(...)
local withoutComponents = {...} local withoutComponents = { ... }
for i = #compatibleArchetypes, 1, -1 do for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i].archetype local archetype = compatibleArchetypes[i].archetype
local records = archetype.records local records = archetype.records
@ -828,16 +823,16 @@ function World.__iter(world: World): () -> (number?, unknown?)
local sparse = world.entityIndex.sparse local sparse = world.entityIndex.sparse
local last local last
return function() return function()
local lastEntity, entityId = next(dense, last) local lastEntity, entityId = next(dense, last)
if not lastEntity then if not lastEntity then
return return
end end
last = lastEntity last = lastEntity
local record = sparse[entityId] 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 entityId return entityId
@ -851,17 +846,17 @@ function World.__iter(world: World): () -> (number?, unknown?)
-- We use types because the key should be the component ID not the column index -- We use types because the key should be the component ID not the column index
entityData[types[i]] = column[row] entityData[types[i]] = column[row]
end end
return entityId, entityData return entityId, entityData
end end
end end
return table.freeze({ return table.freeze({
World = World; World = World,
OnAdd = ON_ADD; OnAdd = ON_ADD,
OnRemove = ON_REMOVE; OnRemove = ON_REMOVE,
OnSet = ON_SET; OnSet = ON_SET,
Wildcard = WILDCARD, Wildcard = WILDCARD,
w = WILDCARD, w = WILDCARD,
Rest = REST, Rest = REST,