Index indices after archetype matched

This commit is contained in:
Ukendio 2024-05-01 15:16:39 +02:00
parent 14507ac775
commit 14d53e03c1
4 changed files with 298 additions and 160 deletions

View file

@ -8,7 +8,7 @@ local function TITLE(title: string)
print(testkit.color.white(title)) print(testkit.color.white(title))
end end
local jecs = require("../lib/init") local jecs = require("../mirror/init")
local ecs = jecs.World.new() local ecs = jecs.World.new()

View file

@ -170,9 +170,9 @@ return {
end, end,
Functions = { Functions = {
Matter = function() Mirror = function()
local matched = 0 local matched = 0
for entityId, firstComponent in newWorld:query(A1, A4, A6, A8) do for entityId, firstComponent in mcs:query(E1, E4, E6, E8) do
matched += 1 matched += 1
end end
end, end,

View file

@ -406,11 +406,7 @@ function World.query(world: World, ...: i53): Query
if skip then if skip then
continue continue
end end
table.insert(compatibleArchetypes, { archetype, indices })
table.insert(compatibleArchetypes, {
archetype = archetype,
indices = indices
})
end end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes) local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
@ -424,7 +420,7 @@ function World.query(world: World, ...: i53): Query
function preparedQuery:without(...) function preparedQuery:without(...)
local components = { ... } local components = { ... }
for i = #compatibleArchetypes, 1, -1 do for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i].archetype local archetype = compatibleArchetypes[i][1]
local shouldRemove = false local shouldRemove = false
for _, componentId in components do for _, componentId in components do
if archetype.records[componentId] then if archetype.records[componentId] then
@ -451,21 +447,21 @@ function World.query(world: World, ...: i53): Query
function preparedQuery:__iter() function preparedQuery:__iter()
return function() return function()
local archetype = compatibleArchetype.archetype local archetype = compatibleArchetype[1]
local tr = compatibleArchetype.indices
local row = next(archetype.entities, lastRow) local row = next(archetype.entities, lastRow)
while row == nil do while row == nil do
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
if lastArchetype == nil then if lastArchetype == nil then
return return
end end
archetype = compatibleArchetype.archetype archetype = compatibleArchetype[1]
row = next(archetype.entities, row) row = next(archetype.entities, row)
end end
lastRow = row lastRow = row
local entityId = archetype.entities[row :: number] local entityId = archetype.entities[row :: number]
local columns = archetype.columns local columns = archetype.columns
local tr = compatibleArchetype[2]
if queryLength == 1 then if queryLength == 1 then
return entityId, columns[tr[1]][row] return entityId, columns[tr[1]][row]

View file

@ -35,8 +35,19 @@ type EntityIndex = { [i24]: Record }
type ComponentIndex = { [i24]: ArchetypeMap} type ComponentIndex = { [i24]: ArchetypeMap}
type ArchetypeRecord = number type ArchetypeRecord = number
type ArchetypeMap = { map: { [ArchetypeId]: ArchetypeRecord } , size: number } type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
type Archetypes = { [ArchetypeId]: Archetype } type Archetypes = { [ArchetypeId]: Archetype }
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
local function transitionArchetype( local function transitionArchetype(
entityIndex: EntityIndex, entityIndex: EntityIndex,
@ -59,11 +70,13 @@ local function transitionArchetype(
column[#column] = nil column[#column] = nil
end end
destinationEntities[destinationRow] = sourceEntities[sourceRow] destinationEntities[destinationRow] = sourceEntities[sourceRow]
local moveAway = #sourceEntities entityIndex[sourceEntities[sourceRow]].row = destinationRow
sourceEntities[sourceRow] = sourceEntities[moveAway]
sourceEntities[moveAway] = nil local movedAway = #sourceEntities
entityIndex[destinationEntities[destinationRow]].row = sourceRow sourceEntities[sourceRow] = sourceEntities[movedAway]
entityIndex[sourceEntities[movedAway]].row = sourceRow
sourceEntities[movedAway] = nil
end end
local function archetypeAppend(entity: i53, archetype: Archetype): i24 local function archetypeAppend(entity: i53, archetype: Archetype): i24
@ -89,14 +102,7 @@ local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archet
end end
local function hash(arr): string | number local function hash(arr): string | number
if true then return table.concat(arr, "_")
return table.concat(arr, "_")
end
local hashed = 5381
for i = 1, #arr do
hashed = ((bit32.lshift(hashed, 5)) + hashed) + arr[i]
end
return hashed
end end
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?) local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
@ -107,11 +113,11 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet
local destinationId = destinationIds[i] local destinationId = destinationIds[i]
if not componentIndex[destinationId] then if not componentIndex[destinationId] then
componentIndex[destinationId] = { size = 0, map = {} } componentIndex[destinationId] = { size = 0, sparse = {} }
end end
local archetypesMap = componentIndex[destinationId] local archetypesMap = componentIndex[destinationId]
archetypesMap.map[to.id] = i archetypesMap.sparse[to.id] = i
to.records[destinationId] = i to.records[destinationId] = i
end end
end end
@ -152,24 +158,48 @@ function World.new()
componentIndex = {}, componentIndex = {},
archetypes = {}, archetypes = {},
archetypeIndex = {}, archetypeIndex = {},
ROOT_ARCHETYPE = nil :: Archetype?, ROOT_ARCHETYPE = (nil :: any) :: Archetype,
nextId = 0, nextEntityId = 0,
nextArchetypeId = 0 nextComponentId = 0,
nextArchetypeId = 0,
hooks = {
[ON_ADD] = {}
}
}, World) }, World)
self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
return self return self
end end
type World = typeof(World.new()) 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())
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
if not world.ROOT_ARCHETYPE then
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
return ROOT_ARCHETYPE
end
end end
local ty = hash(types) local ty = hash(types)
local archetype = world.archetypeIndex[ty] local archetype = world.archetypeIndex[ty]
@ -213,8 +243,14 @@ local function ensureEdge(archetype: Archetype, componentId: i53)
return archetype.edges[componentId] return archetype.edges[componentId]
end end
local function archetypeTraverseAdd(world: World, componentId: i53, archetype: Archetype?): Archetype local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype if not from then
if not world.ROOT_ARCHETYPE then
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
end
from = world.ROOT_ARCHETYPE
end
local edge = ensureEdge(from, componentId) local edge = ensureEdge(from, componentId)
if not edge.add then if not edge.add then
@ -224,28 +260,34 @@ local function archetypeTraverseAdd(world: World, componentId: i53, archetype: A
return edge.add return edge.add
end end
function World.ensureRecord(world: World, entityId: i53) local function ensureRecord(entityIndex, entityId: i53): Record
local entityIndex = world.entityIndex
local id = entityId local id = entityId
if not entityIndex[id] then if not entityIndex[id] then
entityIndex[id] = {} :: Record entityIndex[id] = {}
end end
return entityIndex[id] return entityIndex[id] :: Record
end end
function World.set(world: World, entityId: i53, componentId: i53, data: unknown) function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
local record = world:ensureRecord(entityId) local record = ensureRecord(world.entityIndex, entityId)
local sourceArchetype = record.archetype local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype) local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then if sourceArchetype == destinationArchetype then
local archetypeRecord = destinationArchetype.records[componentId]
destinationArchetype.columns[archetypeRecord][record.row] = data
return
end
if sourceArchetype then
moveEntity(world.entityIndex, entityId, record, destinationArchetype) moveEntity(world.entityIndex, entityId, record, destinationArchetype)
else else
-- if it has any components, then it wont be the root archetype
if #destinationArchetype.types > 0 then if #destinationArchetype.types > 0 then
newEntity(entityId, record, destinationArchetype) newEntity(entityId, record, destinationArchetype)
onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId })
end end
end end
local archetypeRecord = destinationArchetype.records[componentId] local archetypeRecord = destinationArchetype.records[componentId]
destinationArchetype.columns[archetypeRecord][record.row] = data destinationArchetype.columns[archetypeRecord][record.row] = data
end end
@ -265,7 +307,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
end end
function World.remove(world: World, entityId: i53, componentId: i53) function World.remove(world: World, entityId: i53, componentId: i53)
local record = world:ensureRecord(entityId) local record = ensureRecord(world.entityIndex, entityId)
local sourceArchetype = record.archetype local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
@ -276,7 +318,7 @@ end
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24) local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
local archetype = record.archetype local archetype = record.archetype
local archetypeRecord = componentIndex[componentId].map[archetype.id] local archetypeRecord = componentIndex[componentId].sparse[archetype.id]
if not archetypeRecord then if not archetypeRecord then
return nil return nil
@ -308,149 +350,249 @@ function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53
end end
end end
function World.entity(world: World) local function noop(self: Query, ...: i53): () -> (number, ...any)
world.nextId += 1 return function()
return world.nextId end :: any
end end
local function noop(): any local EmptyQuery = {
return function() __iter = noop,
end without = noop
end }
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
local function getSmallestMap(componentIndex, components) export type Query = typeof(EmptyQuery)
local s: any
for i, componentId in components do function World.query(world: World, ...: i53): Query
local map = componentIndex[componentId]
if s == nil or map.size < s.size then
s = map
end
end
return s.map
end
function World.query(world: World, ...: i53): (() -> (number, ...any)) | () -> ()
local compatibleArchetypes = {} local compatibleArchetypes = {}
local components = { ... } local components = { ... }
local archetypes = world.archetypes local archetypes = world.archetypes
local queryLength = #components local queryLength = #components
local firstArchetypeMap = getSmallestMap(world.componentIndex, components)
if not firstArchetypeMap then if queryLength == 0 then
return noop() error("Missing components")
end end
for id in firstArchetypeMap do 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
local i = 0
for id in firstArchetypeMap.sparse do
local archetype = archetypes[id] local archetype = archetypes[id]
local columns = archetype.columns
local archetypeRecords = archetype.records local archetypeRecords = archetype.records
local indices = {} local indices = {}
local skip = false local skip = false
for i, componentId in components do for j, componentId in components do
local index = archetypeRecords[componentId] local index = archetypeRecords[componentId]
if not index then if not index then
skip = true skip = true
break break
end end
indices[i] = columns[index] indices[j] = archetypeRecords[componentId]
end end
if skip then if skip then
continue continue
end end
i += 1
table.insert(compatibleArchetypes, { table.insert(compatibleArchetypes, { archetype, indices })
archetype = archetype,
indices = indices
})
end end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes) local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not compatibleArchetype then if not lastArchetype then
return noop() return EmptyQuery
end end
local lastRow local preparedQuery = {}
preparedQuery.__index = preparedQuery
return function()
local archetype = compatibleArchetype.archetype function preparedQuery:without(...)
local indices = compatibleArchetype.indices local components = { ... }
local row = next(archetype.entities, lastRow) for i = #compatibleArchetypes, 1, -1 do
while row == nil do local archetype = compatibleArchetypes[i][1]
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype) local shouldRemove = false
if lastArchetype == nil then for _, componentId in components do
return if archetype.records[componentId] then
shouldRemove = true
break
end
end end
archetype = compatibleArchetype.archetype if shouldRemove then
row = next(archetype.entities, row) table.remove(compatibleArchetypes, i)
end
end
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
if not lastArchetype then
return EmptyQuery
end end
lastRow = row
local entityId = archetype.entities[row :: number] return self
if queryLength == 1 then
return entityId, indices[1][row]
elseif queryLength == 2 then
return entityId, indices[1][row], indices[2][row]
elseif queryLength == 3 then
return entityId,
indices[1][row],
indices[2][row],
indices[3][row]
elseif queryLength == 4 then
return entityId,
indices[1][row],
indices[2][row],
indices[3][row],
indices[4][row]
elseif queryLength == 5 then
return entityId,
indices[1][row],
indices[2][row],
indices[3][row],
indices[4][row]
elseif queryLength == 6 then
return entityId,
indices[1][row],
indices[2][row],
indices[3][row],
indices[4][row],
indices[5][row],
indices[6][row]
elseif queryLength == 7 then
return entityId,
indices[1][row],
indices[2][row],
indices[3][row],
indices[4][row],
indices[5][row],
indices[6][row],
indices[7][row]
elseif queryLength == 8 then
return entityId,
indices[1][row],
indices[2][row],
indices[3][row],
indices[4][row],
indices[5][row],
indices[6][row],
indices[7][row],
indices[8][row]
end
local queryOutput = {}
for i, componentId in components do
queryOutput[i] = indices[i][row]
end
return entityId, unpack(queryOutput, 1, queryLength)
end 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]
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
for i in components do
queryOutput[i] = tr[i][row]
end
return entityId, unpack(queryOutput, 1, queryLength)
end
end
return setmetatable({}, preparedQuery) :: any
end end
return { function World.component(world: World)
World = World local componentId = world.nextComponentId + 1
} if componentId > HI_COMPONENT_ID then
error("Too many components")
end
world.nextComponentId = componentId
return componentId
end
function World.entity(world: World)
world.nextEntityId += 1
return world.nextEntityId + REST
end
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
}
end
return table.freeze({
World = World,
ON_ADD = ON_ADD,
ON_REMOVE = ON_REMOVE,
ON_SET = ON_SET
})