diff --git a/mirror/init.lua b/mirror/init.lua index e10d9de..35a9b9c 100644 --- a/mirror/init.lua +++ b/mirror/init.lua @@ -51,31 +51,48 @@ local REST = HI_COMPONENT_ID + 4 local function transitionArchetype( entityIndex: EntityIndex, - destinationArchetype: Archetype, + to: Archetype, destinationRow: i24, - sourceArchetype: Archetype, + from: Archetype, sourceRow: i24 ) - local columns = sourceArchetype.columns - local sourceEntities = sourceArchetype.entities - local destinationEntities = destinationArchetype.entities - local destinationColumns = destinationArchetype.columns + 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 componentId, column in columns do - local targetColumn = destinationColumns[componentId] + 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 targetColumn[destinationRow] = column[sourceRow] end - column[sourceRow] = column[#column] - column[#column] = nil + -- 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 end + -- Move the entity from the source to the destination archetype. destinationEntities[destinationRow] = sourceEntities[sourceRow] entityIndex[sourceEntities[sourceRow]].row = destinationRow + -- Because we have swapped columns we now have to update the records + -- corresponding to the entities' rows that were swapped. local movedAway = #sourceEntities - sourceEntities[sourceRow] = sourceEntities[movedAway] - entityIndex[sourceEntities[movedAway]].row = sourceRow + if sourceRow ~= movedAway then + sourceEntities[sourceRow] = sourceEntities[movedAway] + entityIndex[sourceEntities[movedAway]].row = sourceRow + end + sourceEntities[movedAway] = nil end @@ -145,7 +162,9 @@ local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Arch } world.archetypeIndex[ty] = archetype world.archetypes[id] = archetype - createArchetypeRecords(world.componentIndex, archetype, prev) + if #types > 0 then + createArchetypeRecords(world.componentIndex, archetype, prev) + end return archetype end @@ -180,8 +199,6 @@ local function emit(world, eventDescription) }) end - - local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty) if #added > 0 then emit(world, { @@ -194,13 +211,13 @@ local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: end end - export type World = typeof(World.new()) local function ensureArchetype(world: World, types, prev) if #types < 1 then return world.ROOT_ARCHETYPE end + local ty = hash(types) local archetype = world.archetypeIndex[ty] if archetype then @@ -226,8 +243,13 @@ 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. 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. return node end @@ -245,6 +267,7 @@ end local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype if not from then + -- If there was no source archetype then it should return the ROOT_ARCHETYPE if not world.ROOT_ARCHETYPE then local ROOT_ARCHETYPE = archetypeOf(world, {}, nil) world.ROOT_ARCHETYPE = ROOT_ARCHETYPE @@ -254,6 +277,8 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet local edge = ensureEdge(from, componentId) if not edge.add then + -- Save an edge using the component ID to the archetype to allow + -- faster traversals to adjacent archetypes. edge.add = findArchetypeWith(world, from, componentId) end @@ -270,26 +295,31 @@ end function World.set(world: World, entityId: i53, componentId: i53, data: unknown) local record = ensureRecord(world.entityIndex, entityId) - local sourceArchetype = record.archetype - local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype) + local from = record.archetype + local to = archetypeTraverseAdd(world, componentId, from) - if sourceArchetype == destinationArchetype then - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = data + 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. return end - if sourceArchetype then - moveEntity(world.entityIndex, entityId, record, destinationArchetype) + if from then + -- If there was a previous archetype, then the entity needs to move the archetype + moveEntity(world.entityIndex, entityId, record, to) else - if #destinationArchetype.types > 0 then - newEntity(entityId, record, destinationArchetype) - onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId }) + 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 }) end end - local archetypeRecord = destinationArchetype.records[componentId] - destinationArchetype.columns[archetypeRecord][record.row] = data + local archetypeRecord = to.records[componentId] + to.columns[archetypeRecord][record.row] = data end local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype @@ -316,9 +346,10 @@ function World.remove(world: World, entityId: i53, componentId: i53) end end +-- Keeping the function as small as possible to enable inlining local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24) local archetype = record.archetype - local archetypeRecord = componentIndex[componentId].sparse[archetype.id] + local archetypeRecord = archetype.records[componentId] if not archetypeRecord then return nil @@ -388,26 +419,24 @@ function World.query(world: World, ...: i53): Query end end - local i = 0 for id in firstArchetypeMap.sparse do local archetype = archetypes[id] local archetypeRecords = archetype.records local indices = {} local skip = false - for j, componentId in components do + for i, componentId in components do local index = archetypeRecords[componentId] if not index then skip = true break end - indices[j] = archetypeRecords[componentId] + indices[i] = archetypeRecords[componentId] end if skip then continue end - i += 1 table.insert(compatibleArchetypes, { archetype, indices }) end @@ -464,7 +493,7 @@ function World.query(world: World, ...: i53): Query 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 @@ -530,7 +559,9 @@ end function World.component(world: World) local componentId = world.nextComponentId + 1 if componentId > HI_COMPONENT_ID then - error("Too many components") + -- 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 @@ -541,6 +572,17 @@ function World.entity(world: World) return world.nextEntityId + REST end +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 + function World.observer(world: World, ...) local componentIds = { ... }