jecs/lib/init.lua

693 lines
17 KiB
Lua
Raw Normal View History

2024-04-23 15:10:49 +00:00
--!optimize 2
--!native
--!strict
--draft 4
type i53 = number
type i24 = number
type Ty = {i53}
2024-04-23 15:10:49 +00:00
type ArchetypeId = number
type Column = {any}
2024-04-23 15:10:49 +00:00
type Archetype = {
id: number,
edges: {
[i24]: {
add: Archetype,
remove: Archetype,
},
},
types: Ty,
type: string | number,
entities: {number},
columns: {Column},
2024-04-23 15:10:49 +00:00
records: {},
}
type Record = {
archetype: Archetype,
row: number,
}
type EntityIndex = {[i24]: Record}
type ComponentIndex = {[i24]: ArchetypeMap}
2024-04-23 15:10:49 +00:00
type ArchetypeRecord = number
type ArchetypeMap = {sparse: {[ArchetypeId]: ArchetypeRecord}, size: number}
type Archetypes = {[ArchetypeId]: Archetype}
2024-04-28 14:46:40 +00:00
type ArchetypeDiff = {
added: Ty,
removed: Ty,
}
local HI_COMPONENT_ID = 256
local ON_ADD = HI_COMPONENT_ID + 1
local ON_REMOVE = HI_COMPONENT_ID + 2
local ON_SET = HI_COMPONENT_ID + 3
local REST = HI_COMPONENT_ID + 4
2024-04-23 15:10:49 +00:00
local function transitionArchetype(
entityIndex: EntityIndex,
to: Archetype,
2024-04-23 15:10:49 +00:00
destinationRow: i24,
from: Archetype,
2024-04-23 15:10:49 +00:00
sourceRow: i24
)
local columns = from.columns
local sourceEntities = from.entities
local destinationEntities = to.entities
local destinationColumns = to.columns
local tr = to.records
local types = from.types
for i, column in columns do
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local targetColumn = destinationColumns[tr[types[i]]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if targetColumn then
2024-04-23 15:10:49 +00:00
targetColumn[destinationRow] = column[sourceRow]
end
-- If the entity is the last row in the archetype then swapping it would be meaningless.
local last = #column
if sourceRow ~= last then
-- Swap rempves columns to ensure there are no holes in the archetype.
column[sourceRow] = column[last]
end
column[last] = nil
2024-04-23 15:10:49 +00:00
end
-- Move the entity from the source to the destination archetype.
local atSourceRow = sourceEntities[sourceRow]
destinationEntities[destinationRow] = atSourceRow
entityIndex[atSourceRow].row = destinationRow
2024-04-30 14:05:31 +00:00
-- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped.
2024-04-30 14:05:31 +00:00
local movedAway = #sourceEntities
if sourceRow ~= movedAway then
local atMovedAway = sourceEntities[movedAway]
sourceEntities[sourceRow] = atMovedAway
entityIndex[atMovedAway].row = sourceRow
end
2024-04-30 14:05:31 +00:00
sourceEntities[movedAway] = nil
2024-04-23 15:10:49 +00:00
end
local function archetypeAppend(entity: number, archetype: Archetype): number
2024-04-23 15:10:49 +00:00
local entities = archetype.entities
local length = #entities + 1
entities[length] = entity
return length
2024-04-23 15:10:49 +00:00
end
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
local row = archetypeAppend(entityId, archetype)
record.archetype = archetype
record.row = row
return record
end
local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype)
local sourceRow = record.row
local from = record.archetype
local destinationRow = archetypeAppend(entityId, to)
transitionArchetype(entityIndex, to, destinationRow, from, sourceRow)
record.archetype = to
record.row = destinationRow
end
local function hash(arr): string | number
2024-04-28 14:46:40 +00:00
return table.concat(arr, "_")
2024-04-23 15:10:49 +00:00
end
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
2024-04-23 15:10:49 +00:00
local destinationIds = to.types
local records = to.records
local id = to.id
2024-04-23 15:10:49 +00:00
for i, destinationId in destinationIds do
local archetypesMap = componentIndex[destinationId]
2024-04-23 15:10:49 +00:00
if not archetypesMap then
archetypesMap = {size = 0, sparse = {}}
componentIndex[destinationId] = archetypesMap
2024-04-23 15:10:49 +00:00
end
2024-04-25 03:45:33 +00:00
archetypesMap.sparse[id] = i
records[destinationId] = i
2024-04-23 15:10:49 +00:00
end
end
2024-05-09 00:20:54 +00:00
local function archetypeOf(world: any, types: {i24}, prev: Archetype?): Archetype
2024-04-23 15:10:49 +00:00
local ty = hash(types)
local id = world.nextArchetypeId + 1
world.nextArchetypeId = id
2024-04-23 15:10:49 +00:00
local length = #types
2024-05-09 00:20:54 +00:00
local columns = table.create(length)
2024-04-23 15:10:49 +00:00
for index in types do
columns[index] = {}
2024-04-23 15:10:49 +00:00
end
local archetype = {
columns = columns;
edges = {};
entities = {};
id = id;
records = {};
type = ty;
types = types;
2024-04-23 15:10:49 +00:00
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
if length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev)
end
2024-04-23 15:10:49 +00:00
return archetype
end
local function ensureArchetype(world: World, types, prev)
if #types < 1 then
return world.ROOT_ARCHETYPE
2024-04-23 15:10:49 +00:00
end
2024-04-23 15:10:49 +00:00
local ty = hash(types)
local archetype = world.archetypeIndex[ty]
if archetype then
return archetype
end
return archetypeOf(world, types, prev)
end
local function findInsert(types: {i53}, toAdd: i53)
for i, id in types do
2024-04-23 15:10:49 +00:00
if id == toAdd then
return -1
end
if id > toAdd then
return i
end
end
return #types + 1
2024-04-23 15:10:49 +00:00
end
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
local types = node.types
-- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array.
2024-04-23 15:10:49 +00:00
local at = findInsert(types, componentId)
if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it
-- directly instead of needing to hash types for a lookup to the archetype.
2024-04-23 15:10:49 +00:00
return node
end
local destinationType = table.clone(node.types)
table.insert(destinationType, at, componentId)
return ensureArchetype(world, destinationType, node)
end
local function ensureEdge(archetype: Archetype, componentId: i53)
local edges = archetype.edges
local edge = edges[componentId]
if not edge then
edge = {} :: any
edges[componentId] = edge
2024-04-23 15:10:49 +00:00
end
return edge
2024-04-23 15:10:49 +00:00
end
2024-04-28 14:46:40 +00:00
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
2024-05-07 19:30:36 +00:00
from = from or world.ROOT_ARCHETYPE
2024-04-23 15:10:49 +00:00
local edge = ensureEdge(from, componentId)
local add = edge.add
if not add then
-- Save an edge using the component ID to the archetype to allow
-- faster traversals to adjacent archetypes.
add = findArchetypeWith(world, from, componentId)
edge.add = add :: never
2024-04-23 15:10:49 +00:00
end
return add
2024-04-23 15:10:49 +00:00
end
local function ensureRecord(world, entityId: i53): Record
local entityIndex = world.entityIndex
local record = entityIndex[entityId]
if record then
return record
2024-04-23 15:10:49 +00:00
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
2024-04-23 15:10:49 +00:00
end
2024-05-09 00:20:54 +00:00
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)
local record = ensureRecord(world, entityId)
local from = record.archetype
local to = archetypeTraverseAdd(world, componentId, from)
if from and not (from == world.ROOT_ARCHETYPE) then
moveEntity(world.entityIndex, entityId, record, to)
else
if #to.types > 0 then
newEntity(entityId, record, to)
onNotifyAdd(world, to, from, record.row, { componentId })
end
end
end
-- Symmetric like `World.add` but idempotent
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = ensureRecord(world, entityId)
local from = record.archetype
local archetypeRecord = from.records[componentId]
if archetypeRecord then
-- If the archetypes are the same it can avoid moving the entity
-- and just set the data directly.
from.columns[archetypeRecord][record.row] = data
-- Should fire an OnSet event here.
2024-04-30 15:52:44 +00:00
return
end
local to = archetypeTraverseAdd(world, componentId, from)
if from then
-- If there was a previous archetype, then the entity needs to move the archetype
moveEntity(world.entityIndex, entityId, record, to)
2024-04-23 15:10:49 +00:00
else
if #to.types > 0 then
-- When there is no previous archetype it should create the archetype
newEntity(entityId, record, to)
--onNotifyAdd(world, to, from, record.row, {componentId})
2024-04-23 15:10:49 +00:00
end
end
archetypeRecord = to.records[componentId]
to.columns[archetypeRecord][record.row] = data
2024-04-23 15:10:49 +00:00
end
local function archetypeTraverseRemove(world: World, componentId: i53, from: Archetype): Archetype
2024-04-23 15:10:49 +00:00
local edge = ensureEdge(from, componentId)
local remove = edge.remove
if not remove then
local to = table.clone(from.types)
local at = table.find(to, componentId)
if not at then
return from
end
table.remove(to, at)
remove = ensureArchetype(world, to, from)
edge.remove = remove :: never
2024-04-23 15:10:49 +00:00
end
return remove
2024-04-23 15:10:49 +00:00
end
function World.remove(world: World, entityId: i53, componentId: i53)
local record = ensureRecord(world, entityId)
2024-04-23 15:10:49 +00:00
local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
2024-04-23 15:10:49 +00:00
end
end
-- Keeping the function as small as possible to enable inlining
local function get(record: Record, componentId: i24)
2024-04-23 15:10:49 +00:00
local archetype = record.archetype
local archetypeRecord = archetype.records[componentId]
2024-04-23 15:10:49 +00:00
if not archetypeRecord then
return nil
end
return archetype.columns[archetypeRecord][record.row]
end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId
local record = world.entityIndex[id]
if not record then
return nil
end
local va = get(record, a)
2024-04-23 15:10:49 +00:00
if b == nil then
return va
elseif c == nil then
return va, get(record, b)
2024-04-23 15:10:49 +00:00
elseif d == nil then
return va, get(record, b), get(record, c)
2024-04-23 15:10:49 +00:00
elseif e == nil then
return va, get(record, b), get(record, c), get(record, d)
2024-04-23 15:10:49 +00:00
else
error("args exceeded")
end
end
-- the less creation the better
local function actualNoOperation() end
local function noop(_self: Query, ...: i53): () -> (number, ...any)
return actualNoOperation :: any
2024-04-30 23:45:42 +00:00
end
2024-04-28 14:46:40 +00:00
2024-04-30 23:45:42 +00:00
local EmptyQuery = {
__iter = noop;
without = noop;
2024-04-30 23:45:42 +00:00
}
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
2024-04-23 15:10:49 +00:00
export type Query = typeof(EmptyQuery)
2024-04-23 15:10:49 +00:00
2024-04-30 23:45:42 +00:00
function World.query(world: World, ...: i53): Query
-- breaking?
if (...) == nil then
error("Missing components")
end
2024-04-23 15:10:49 +00:00
local compatibleArchetypes = {}
local length = 0
local components = {...}
2024-04-23 15:10:49 +00:00
local archetypes = world.archetypes
local queryLength = #components
2024-04-25 03:45:33 +00:00
local firstArchetypeMap
local componentIndex = world.componentIndex
for _, componentId in components do
local map = componentIndex[componentId]
2024-04-30 23:45:42 +00:00
if not map then
return EmptyQuery
end
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
firstArchetypeMap = map
end
2024-04-24 15:32:07 +00:00
end
2024-04-23 15:10:49 +00:00
for id in firstArchetypeMap.sparse do
2024-04-23 15:10:49 +00:00
local archetype = archetypes[id]
local archetypeRecords = archetype.records
local indices = {}
2024-04-25 05:26:13 +00:00
local skip = false
for i, componentId in components do
2024-04-25 14:26:30 +00:00
local index = archetypeRecords[componentId]
if not index then
2024-04-25 05:26:13 +00:00
skip = true
break
end
indices[i] = index
2024-04-25 05:26:13 +00:00
end
2024-04-25 05:17:08 +00:00
if skip then
2024-04-25 05:26:13 +00:00
continue
2024-04-23 15:10:49 +00:00
end
length += 1
compatibleArchetypes[length] = {archetype, indices}
2024-04-23 15:10:49 +00:00
end
2024-04-28 19:00:00 +00:00
2024-04-25 05:17:08 +00:00
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then
return EmptyQuery
2024-04-25 14:26:30 +00:00
end
2024-04-28 19:00:00 +00:00
local preparedQuery = {}
preparedQuery.__index = preparedQuery
2024-04-23 15:10:49 +00:00
function preparedQuery:without(...)
local withoutComponents = {...}
for i = #compatibleArchetypes, 1, -1 do
2024-05-01 13:16:39 +00:00
local archetype = compatibleArchetypes[i][1]
local records = archetype.records
2024-04-28 19:00:00 +00:00
local shouldRemove = false
for _, componentId in withoutComponents do
if records[componentId] then
2024-04-28 19:00:00 +00:00
shouldRemove = true
break
end
end
if shouldRemove then
2024-04-28 19:00:00 +00:00
table.remove(compatibleArchetypes, i)
end
end
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then
return EmptyQuery
end
return self
2024-04-28 19:00:00 +00:00
end
local lastRow
2024-04-30 15:52:44 +00:00
local queryOutput = {}
function preparedQuery:__iter()
return function()
2024-05-01 13:16:39 +00:00
local archetype = compatibleArchetype[1]
2024-04-28 19:00:00 +00:00
local row = next(archetype.entities, lastRow)
while row == nil do
2024-04-28 19:00:00 +00:00
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
if lastArchetype == nil then
return
2024-04-28 19:00:00 +00:00
end
2024-05-01 13:16:39 +00:00
archetype = compatibleArchetype[1]
2024-04-28 19:00:00 +00:00
row = next(archetype.entities, row)
end
lastRow = row
2024-04-28 19:00:00 +00:00
local entityId = archetype.entities[row :: number]
local columns = archetype.columns
2024-05-01 13:16:39 +00:00
local tr = compatibleArchetype[2]
if queryLength == 1 then
return entityId, columns[tr[1]][row]
elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row]
elseif queryLength == 3 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
elseif queryLength == 4 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
elseif queryLength == 5 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row]
elseif queryLength == 6 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row]
elseif queryLength == 7 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
elseif queryLength == 8 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row],
columns[tr[8]][row]
2024-04-28 19:00:00 +00:00
end
2024-04-23 15:10:49 +00:00
for i in components do
2024-05-05 01:25:34 +00:00
queryOutput[i] = columns[tr[i]][row]
2024-04-28 19:00:00 +00:00
end
return entityId, unpack(queryOutput, 1, queryLength)
end
2024-04-23 23:14:43 +00:00
end
2024-04-28 19:00:00 +00:00
2024-04-30 23:45:42 +00:00
return setmetatable({}, preparedQuery) :: any
2024-04-23 15:10:49 +00:00
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.")
2024-04-28 14:46:40 +00:00
end
2024-04-30 23:09:15 +00:00
world.nextComponentId = componentId
return componentId
2024-04-28 14:46:40 +00:00
end
function World.entity(world: World)
local nextEntityId = world.nextEntityId + 1
world.nextEntityId = nextEntityId
return nextEntityId + REST
2024-04-28 14:46:40 +00:00
end
2024-05-07 19:33:42 +00:00
-- 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]
2024-05-07 19:33:42 +00:00
local archetype = record.archetype
archetypeDelete(entityIndex, archetype, record.row, true)
entityIndex[entityId] = nil
end
2024-05-05 13:22:02 +00:00
function World.__iter(world: World): () -> (number?, unknown?)
local entityIndex = world.entityIndex
local last
return function()
local entity, record = next(entityIndex, last)
if not entity then
return
end
last = entity
local archetype = record.archetype
if not archetype then
-- 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.
return entity
end
local row = record.row
local types = archetype.types
local columns = archetype.columns
local entityData = {}
for i, column in columns do
-- We use types because the key should be the component ID not the column index
entityData[types[i]] = column[row]
end
return entity, entityData
end
end
return table.freeze({
World = World;
ON_ADD = ON_ADD;
ON_REMOVE = ON_REMOVE;
ON_SET = ON_SET;
})