2024-04-25 05:26:38 +00:00
|
|
|
--!optimize 2
|
|
|
|
--!native
|
|
|
|
--!strict
|
|
|
|
--draft 4
|
|
|
|
|
|
|
|
type i53 = number
|
|
|
|
type i24 = number
|
|
|
|
|
|
|
|
type Ty = { i53 }
|
|
|
|
type ArchetypeId = number
|
|
|
|
|
|
|
|
type Column = { any }
|
|
|
|
|
|
|
|
type Archetype = {
|
|
|
|
id: number,
|
|
|
|
edges: {
|
|
|
|
[i24]: {
|
|
|
|
add: Archetype,
|
|
|
|
remove: Archetype,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
types: Ty,
|
|
|
|
type: string | number,
|
|
|
|
entities: { number },
|
|
|
|
columns: { Column },
|
|
|
|
records: {},
|
|
|
|
}
|
|
|
|
|
|
|
|
type Record = {
|
|
|
|
archetype: Archetype,
|
|
|
|
row: number,
|
|
|
|
}
|
|
|
|
|
|
|
|
type EntityIndex = { [i24]: Record }
|
|
|
|
type ComponentIndex = { [i24]: ArchetypeMap}
|
|
|
|
|
|
|
|
type ArchetypeRecord = number
|
2024-05-01 13:16:39 +00:00
|
|
|
type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
|
2024-04-25 05:26:38 +00:00
|
|
|
type Archetypes = { [ArchetypeId]: Archetype }
|
2024-05-01 13:16:39 +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-25 05:26:38 +00:00
|
|
|
|
|
|
|
local function transitionArchetype(
|
|
|
|
entityIndex: EntityIndex,
|
2024-05-05 00:48:34 +00:00
|
|
|
to: Archetype,
|
2024-04-25 05:26:38 +00:00
|
|
|
destinationRow: i24,
|
2024-05-05 00:48:34 +00:00
|
|
|
from: Archetype,
|
2024-04-25 05:26:38 +00:00
|
|
|
sourceRow: i24
|
|
|
|
)
|
2024-05-05 00:48:34 +00:00
|
|
|
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.
|
2024-04-25 05:26:38 +00:00
|
|
|
if targetColumn then
|
|
|
|
targetColumn[destinationRow] = column[sourceRow]
|
|
|
|
end
|
2024-05-05 00:48:34 +00:00
|
|
|
-- 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-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
2024-05-05 00:48:34 +00:00
|
|
|
-- Move the entity from the source to the destination archetype.
|
2024-05-01 13:16:39 +00:00
|
|
|
destinationEntities[destinationRow] = sourceEntities[sourceRow]
|
|
|
|
entityIndex[sourceEntities[sourceRow]].row = destinationRow
|
|
|
|
|
2024-05-05 00:48:34 +00:00
|
|
|
-- Because we have swapped columns we now have to update the records
|
|
|
|
-- corresponding to the entities' rows that were swapped.
|
2024-05-01 13:16:39 +00:00
|
|
|
local movedAway = #sourceEntities
|
2024-05-05 00:48:34 +00:00
|
|
|
if sourceRow ~= movedAway then
|
|
|
|
sourceEntities[sourceRow] = sourceEntities[movedAway]
|
|
|
|
entityIndex[sourceEntities[movedAway]].row = sourceRow
|
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
sourceEntities[movedAway] = nil
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local function archetypeAppend(entity: i53, archetype: Archetype): i24
|
|
|
|
local entities = archetype.entities
|
|
|
|
table.insert(entities, entity)
|
|
|
|
return #entities
|
|
|
|
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-05-01 13:16:39 +00:00
|
|
|
return table.concat(arr, "_")
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
|
|
|
local destinationCount = #to.types
|
|
|
|
local destinationIds = to.types
|
|
|
|
|
|
|
|
for i = 1, destinationCount do
|
|
|
|
local destinationId = destinationIds[i]
|
|
|
|
|
|
|
|
if not componentIndex[destinationId] then
|
2024-05-01 13:16:39 +00:00
|
|
|
componentIndex[destinationId] = { size = 0, sparse = {} }
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local archetypesMap = componentIndex[destinationId]
|
2024-05-01 13:16:39 +00:00
|
|
|
archetypesMap.sparse[to.id] = i
|
2024-04-25 05:26:38 +00:00
|
|
|
to.records[destinationId] = i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
|
|
|
|
local ty = hash(types)
|
|
|
|
|
|
|
|
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
|
|
|
|
local id = world.nextArchetypeId
|
|
|
|
|
|
|
|
local columns = {} :: { any }
|
|
|
|
|
|
|
|
for _ in types do
|
|
|
|
table.insert(columns, {})
|
|
|
|
end
|
|
|
|
|
|
|
|
local archetype = {
|
|
|
|
id = id,
|
|
|
|
types = types,
|
|
|
|
type = ty,
|
|
|
|
columns = columns,
|
|
|
|
entities = {},
|
|
|
|
edges = {},
|
|
|
|
records = {},
|
|
|
|
}
|
|
|
|
world.archetypeIndex[ty] = archetype
|
|
|
|
world.archetypes[id] = archetype
|
2024-05-05 00:48:34 +00:00
|
|
|
if #types > 0 then
|
|
|
|
createArchetypeRecords(world.componentIndex, archetype, prev)
|
|
|
|
end
|
2024-04-25 05:26:38 +00:00
|
|
|
|
|
|
|
return archetype
|
|
|
|
end
|
|
|
|
|
|
|
|
local World = {}
|
|
|
|
World.__index = World
|
|
|
|
function World.new()
|
|
|
|
local self = setmetatable({
|
|
|
|
entityIndex = {},
|
|
|
|
componentIndex = {},
|
|
|
|
archetypes = {},
|
|
|
|
archetypeIndex = {},
|
2024-05-01 13:16:39 +00:00
|
|
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
|
|
|
nextEntityId = 0,
|
|
|
|
nextComponentId = 0,
|
|
|
|
nextArchetypeId = 0,
|
|
|
|
hooks = {
|
|
|
|
[ON_ADD] = {}
|
|
|
|
}
|
2024-04-25 05:26:38 +00:00
|
|
|
}, World)
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
local function emit(world, eventDescription)
|
|
|
|
local event = eventDescription.event
|
|
|
|
|
|
|
|
table.insert(world.hooks[event], {
|
|
|
|
ids = eventDescription.ids,
|
|
|
|
archetype = eventDescription.archetype,
|
|
|
|
otherArchetype = eventDescription.otherArchetype,
|
|
|
|
offset = eventDescription.offset
|
|
|
|
})
|
|
|
|
end
|
|
|
|
|
|
|
|
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
|
|
|
if #added > 0 then
|
|
|
|
emit(world, {
|
|
|
|
event = ON_ADD,
|
|
|
|
ids = added,
|
|
|
|
archetype = archetype,
|
|
|
|
otherArchetype = otherArchetype,
|
|
|
|
offset = row,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
export type World = typeof(World.new())
|
2024-04-25 05:26:38 +00:00
|
|
|
|
|
|
|
local function ensureArchetype(world: World, types, prev)
|
|
|
|
if #types < 1 then
|
2024-05-01 13:16:39 +00:00
|
|
|
return world.ROOT_ARCHETYPE
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
2024-05-05 00:48:34 +00:00
|
|
|
|
2024-04-25 05:26:38 +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)
|
|
|
|
local count = #types
|
|
|
|
for i = 1, count do
|
|
|
|
local id = types[i]
|
|
|
|
if id == toAdd then
|
|
|
|
return -1
|
|
|
|
end
|
|
|
|
if id > toAdd then
|
|
|
|
return i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return count + 1
|
|
|
|
end
|
|
|
|
|
|
|
|
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
|
|
|
local types = node.types
|
2024-05-05 00:48:34 +00:00
|
|
|
-- 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-25 05:26:38 +00:00
|
|
|
local at = findInsert(types, componentId)
|
|
|
|
if at == -1 then
|
2024-05-05 00:48:34 +00:00
|
|
|
-- 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-25 05:26:38 +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)
|
|
|
|
if not archetype.edges[componentId] then
|
|
|
|
archetype.edges[componentId] = {} :: any
|
|
|
|
end
|
|
|
|
return archetype.edges[componentId]
|
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
|
|
|
if not from then
|
2024-05-05 00:48:34 +00:00
|
|
|
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
|
2024-05-01 13:16:39 +00:00
|
|
|
if not world.ROOT_ARCHETYPE then
|
|
|
|
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
|
|
|
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
|
|
|
end
|
|
|
|
from = world.ROOT_ARCHETYPE
|
|
|
|
end
|
2024-04-25 05:26:38 +00:00
|
|
|
local edge = ensureEdge(from, componentId)
|
|
|
|
|
|
|
|
if not edge.add then
|
2024-05-05 00:48:34 +00:00
|
|
|
-- Save an edge using the component ID to the archetype to allow
|
|
|
|
-- faster traversals to adjacent archetypes.
|
2024-04-25 05:26:38 +00:00
|
|
|
edge.add = findArchetypeWith(world, from, componentId)
|
|
|
|
end
|
|
|
|
|
|
|
|
return edge.add
|
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
local function ensureRecord(entityIndex, entityId: i53): Record
|
2024-04-25 05:26:38 +00:00
|
|
|
local id = entityId
|
|
|
|
if not entityIndex[id] then
|
2024-05-01 13:16:39 +00:00
|
|
|
entityIndex[id] = {}
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
2024-05-01 13:16:39 +00:00
|
|
|
return entityIndex[id] :: Record
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
2024-05-01 13:16:39 +00:00
|
|
|
local record = ensureRecord(world.entityIndex, entityId)
|
2024-05-05 00:48:34 +00:00
|
|
|
local from = record.archetype
|
|
|
|
local to = archetypeTraverseAdd(world, componentId, from)
|
|
|
|
|
|
|
|
if from == to then
|
|
|
|
-- If the archetypes are the same it can avoid moving the entity
|
|
|
|
-- and just set the data directly.
|
|
|
|
local archetypeRecord = to.records[componentId]
|
|
|
|
from.columns[archetypeRecord][record.row] = data
|
|
|
|
-- Should fire an OnSet event here.
|
2024-05-01 13:16:39 +00:00
|
|
|
return
|
|
|
|
end
|
|
|
|
|
2024-05-05 00:48:34 +00:00
|
|
|
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-25 05:26:38 +00:00
|
|
|
else
|
2024-05-05 00:48:34 +00:00
|
|
|
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-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
end
|
2024-05-01 13:16:39 +00:00
|
|
|
|
2024-05-05 00:48:34 +00:00
|
|
|
local archetypeRecord = to.records[componentId]
|
|
|
|
to.columns[archetypeRecord][record.row] = data
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
|
|
|
|
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
|
|
|
local edge = ensureEdge(from, componentId)
|
|
|
|
|
|
|
|
|
|
|
|
if not edge.remove then
|
|
|
|
local to = table.clone(from.types)
|
|
|
|
table.remove(to, table.find(to, componentId))
|
|
|
|
edge.remove = ensureArchetype(world, to, from)
|
|
|
|
end
|
|
|
|
|
|
|
|
return edge.remove
|
|
|
|
end
|
|
|
|
|
|
|
|
function World.remove(world: World, entityId: i53, componentId: i53)
|
2024-05-01 13:16:39 +00:00
|
|
|
local record = ensureRecord(world.entityIndex, entityId)
|
2024-04-25 05:26:38 +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)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-05 00:48:34 +00:00
|
|
|
-- Keeping the function as small as possible to enable inlining
|
2024-04-25 05:26:38 +00:00
|
|
|
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
|
|
|
|
local archetype = record.archetype
|
2024-05-05 00:48:34 +00:00
|
|
|
local archetypeRecord = archetype.records[componentId]
|
2024-04-25 05:26:38 +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 componentIndex = world.componentIndex
|
|
|
|
local record = world.entityIndex[id]
|
|
|
|
if not record then
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
|
|
|
local va = get(componentIndex, record, a)
|
|
|
|
|
|
|
|
if b == nil then
|
|
|
|
return va
|
|
|
|
elseif c == nil then
|
|
|
|
return va, get(componentIndex, record, b)
|
|
|
|
elseif d == nil then
|
|
|
|
return va, get(componentIndex, record, b), get(componentIndex, record, c)
|
|
|
|
elseif e == nil then
|
|
|
|
return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
|
|
|
|
else
|
|
|
|
error("args exceeded")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
local function noop(self: Query, ...: i53): () -> (number, ...any)
|
|
|
|
return function()
|
|
|
|
end :: any
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
local EmptyQuery = {
|
|
|
|
__iter = noop,
|
|
|
|
without = noop
|
|
|
|
}
|
|
|
|
EmptyQuery.__index = EmptyQuery
|
|
|
|
setmetatable(EmptyQuery, EmptyQuery)
|
2024-04-25 05:26:38 +00:00
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
export type Query = typeof(EmptyQuery)
|
2024-04-25 05:26:38 +00:00
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
function World.query(world: World, ...: i53): Query
|
2024-04-25 05:26:38 +00:00
|
|
|
local compatibleArchetypes = {}
|
|
|
|
local components = { ... }
|
|
|
|
local archetypes = world.archetypes
|
|
|
|
local queryLength = #components
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
if queryLength == 0 then
|
|
|
|
error("Missing components")
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
local firstArchetypeMap
|
|
|
|
local componentIndex = world.componentIndex
|
|
|
|
|
|
|
|
for i, componentId in components do
|
|
|
|
local map = componentIndex[componentId]
|
|
|
|
if not map then
|
|
|
|
return EmptyQuery
|
|
|
|
end
|
|
|
|
|
|
|
|
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
|
|
|
|
firstArchetypeMap = map
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
for id in firstArchetypeMap.sparse do
|
2024-04-25 05:26:38 +00:00
|
|
|
local archetype = archetypes[id]
|
|
|
|
local archetypeRecords = archetype.records
|
|
|
|
local indices = {}
|
2024-04-25 14:26:30 +00:00
|
|
|
local skip = false
|
|
|
|
|
2024-05-05 00:48:34 +00:00
|
|
|
for i, componentId in components do
|
2024-04-25 14:26:30 +00:00
|
|
|
local index = archetypeRecords[componentId]
|
|
|
|
if not index then
|
|
|
|
skip = true
|
|
|
|
break
|
|
|
|
end
|
2024-05-05 00:48:34 +00:00
|
|
|
indices[i] = archetypeRecords[componentId]
|
2024-04-25 14:26:30 +00:00
|
|
|
end
|
2024-04-25 05:26:38 +00:00
|
|
|
|
2024-04-25 14:26:30 +00:00
|
|
|
if skip then
|
|
|
|
continue
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
2024-05-01 13:16:39 +00:00
|
|
|
table.insert(compatibleArchetypes, { archetype, indices })
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
2024-05-01 13:16:39 +00:00
|
|
|
|
2024-04-25 05:26:38 +00:00
|
|
|
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
2024-05-01 13:16:39 +00:00
|
|
|
if not lastArchetype then
|
|
|
|
return EmptyQuery
|
2024-04-25 14:26:30 +00:00
|
|
|
end
|
2024-04-25 05:26:38 +00:00
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
local preparedQuery = {}
|
|
|
|
preparedQuery.__index = preparedQuery
|
|
|
|
|
|
|
|
function preparedQuery:without(...)
|
|
|
|
local components = { ... }
|
|
|
|
for i = #compatibleArchetypes, 1, -1 do
|
|
|
|
local archetype = compatibleArchetypes[i][1]
|
|
|
|
local shouldRemove = false
|
|
|
|
for _, componentId in components do
|
|
|
|
if archetype.records[componentId] then
|
|
|
|
shouldRemove = true
|
|
|
|
break
|
|
|
|
end
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
2024-05-01 13:16:39 +00:00
|
|
|
if shouldRemove then
|
|
|
|
table.remove(compatibleArchetypes, i)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
|
|
|
if not lastArchetype then
|
|
|
|
return EmptyQuery
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
return self
|
|
|
|
end
|
|
|
|
|
|
|
|
local lastRow
|
|
|
|
local queryOutput = {}
|
|
|
|
|
|
|
|
|
|
|
|
function preparedQuery:__iter()
|
|
|
|
return function()
|
|
|
|
local archetype = compatibleArchetype[1]
|
|
|
|
local row = next(archetype.entities, lastRow)
|
|
|
|
while row == nil do
|
|
|
|
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
|
|
|
if lastArchetype == nil then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
archetype = compatibleArchetype[1]
|
|
|
|
row = next(archetype.entities, row)
|
|
|
|
end
|
|
|
|
lastRow = row
|
|
|
|
|
|
|
|
local entityId = archetype.entities[row :: number]
|
|
|
|
local columns = archetype.columns
|
|
|
|
local tr = compatibleArchetype[2]
|
2024-05-05 00:48:34 +00:00
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
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]
|
|
|
|
end
|
2024-04-25 05:26:38 +00:00
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
for i in components do
|
|
|
|
queryOutput[i] = tr[i][row]
|
|
|
|
end
|
|
|
|
|
|
|
|
return entityId, unpack(queryOutput, 1, queryLength)
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
2024-05-01 13:16:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return setmetatable({}, preparedQuery) :: any
|
|
|
|
end
|
2024-04-25 05:26:38 +00:00
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
function World.component(world: World)
|
|
|
|
local componentId = world.nextComponentId + 1
|
|
|
|
if componentId > HI_COMPONENT_ID then
|
2024-05-05 00:48:34 +00:00
|
|
|
-- 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-25 05:26:38 +00:00
|
|
|
end
|
2024-05-01 13:16:39 +00:00
|
|
|
world.nextComponentId = componentId
|
|
|
|
return componentId
|
|
|
|
end
|
|
|
|
|
|
|
|
function World.entity(world: World)
|
|
|
|
world.nextEntityId += 1
|
|
|
|
return world.nextEntityId + REST
|
|
|
|
end
|
|
|
|
|
2024-05-05 00:48:34 +00:00
|
|
|
function World.delete(world: World, entityId: i53)
|
|
|
|
local entityIndex = world.entityIndex
|
|
|
|
local record = entityIndex[entityId]
|
|
|
|
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
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
function World.observer(world: World, ...)
|
|
|
|
local componentIds = { ... }
|
|
|
|
|
|
|
|
return {
|
|
|
|
event = function(event)
|
|
|
|
local hook = world.hooks[event]
|
|
|
|
world.hooks[event] = nil
|
|
|
|
|
|
|
|
local last, change
|
|
|
|
return function()
|
|
|
|
last, change = next(hook, last)
|
|
|
|
if not last then
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
local matched = false
|
|
|
|
|
|
|
|
while not matched do
|
|
|
|
local skip = false
|
|
|
|
for _, id in change.ids do
|
|
|
|
if not table.find(componentIds, id) then
|
|
|
|
skip = true
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if skip then
|
|
|
|
last, change = next(hook, last)
|
|
|
|
continue
|
|
|
|
end
|
|
|
|
|
|
|
|
matched = true
|
|
|
|
end
|
|
|
|
|
|
|
|
local queryOutput = {}
|
|
|
|
local row = change.offset
|
|
|
|
local archetype = change.archetype
|
|
|
|
local columns = archetype.columns
|
|
|
|
local archetypeRecords = archetype.records
|
|
|
|
for _, id in componentIds do
|
|
|
|
table.insert(queryOutput, columns[archetypeRecords[id]][row])
|
|
|
|
end
|
|
|
|
|
|
|
|
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
}
|
2024-04-25 05:26:38 +00:00
|
|
|
end
|
|
|
|
|
2024-05-01 13:16:39 +00:00
|
|
|
return table.freeze({
|
|
|
|
World = World,
|
|
|
|
ON_ADD = ON_ADD,
|
|
|
|
ON_REMOVE = ON_REMOVE,
|
|
|
|
ON_SET = ON_SET
|
|
|
|
})
|