mirror of
				https://github.com/Ukendio/jecs.git
				synced 2025-11-03 02:29:16 +00:00 
			
		
		
		
	Sparse set for entity records
This commit is contained in:
		
							parent
							
								
									d5414f1bc4
								
							
						
					
					
						commit
						cbe0710c37
					
				
					 2 changed files with 241 additions and 27 deletions
				
			
		
							
								
								
									
										110
									
								
								lib/init.lua
									
									
									
									
									
								
							
							
						
						
									
										110
									
								
								lib/init.lua
									
									
									
									
									
								
							| 
						 | 
					@ -29,9 +29,10 @@ type Archetype = {
 | 
				
			||||||
type Record = {
 | 
					type Record = {
 | 
				
			||||||
	archetype: Archetype,
 | 
						archetype: Archetype,
 | 
				
			||||||
	row: number,
 | 
						row: number,
 | 
				
			||||||
 | 
						dense: i24,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type EntityIndex = {[i24]: Record}
 | 
					type EntityIndex = {dense: {[i24]: i53}, sparse: {[i53]: Record}}
 | 
				
			||||||
type ComponentIndex = {[i24]: ArchetypeMap}
 | 
					type ComponentIndex = {[i24]: ArchetypeMap}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ArchetypeRecord = number
 | 
					type ArchetypeRecord = number
 | 
				
			||||||
| 
						 | 
					@ -81,10 +82,12 @@ local function transitionArchetype(
 | 
				
			||||||
		column[last] = nil
 | 
							column[last] = nil
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						local dense, sparse = entityIndex.dense, entityIndex.sparse
 | 
				
			||||||
	-- Move the entity from the source to the destination archetype.
 | 
						-- Move the entity from the source to the destination archetype.
 | 
				
			||||||
	local atSourceRow = sourceEntities[sourceRow]
 | 
						local atSourceRow = sourceEntities[sourceRow]
 | 
				
			||||||
	destinationEntities[destinationRow] = atSourceRow
 | 
						destinationEntities[destinationRow] = atSourceRow
 | 
				
			||||||
	entityIndex[atSourceRow].row = destinationRow
 | 
						local record = sparse[atSourceRow]
 | 
				
			||||||
 | 
						record.row = destinationRow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	-- Because we have swapped columns we now have to update the records
 | 
						-- Because we have swapped columns we now have to update the records
 | 
				
			||||||
	-- corresponding to the entities' rows that were swapped.
 | 
						-- corresponding to the entities' rows that were swapped.
 | 
				
			||||||
| 
						 | 
					@ -92,7 +95,7 @@ local function transitionArchetype(
 | 
				
			||||||
	if sourceRow ~= movedAway then
 | 
						if sourceRow ~= movedAway then
 | 
				
			||||||
		local atMovedAway = sourceEntities[movedAway]
 | 
							local atMovedAway = sourceEntities[movedAway]
 | 
				
			||||||
		sourceEntities[sourceRow] = atMovedAway
 | 
							sourceEntities[sourceRow] = atMovedAway
 | 
				
			||||||
		entityIndex[atMovedAway].row = sourceRow
 | 
							sparse[atMovedAway].row = sourceRow
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sourceEntities[movedAway] = nil
 | 
						sourceEntities[movedAway] = nil
 | 
				
			||||||
| 
						 | 
					@ -181,7 +184,10 @@ function World.new()
 | 
				
			||||||
		archetypeIndex = {};
 | 
							archetypeIndex = {};
 | 
				
			||||||
		archetypes = {};
 | 
							archetypes = {};
 | 
				
			||||||
		componentIndex = {};
 | 
							componentIndex = {};
 | 
				
			||||||
		entityIndex = {};
 | 
							entityIndex = {
 | 
				
			||||||
 | 
								dense = {},
 | 
				
			||||||
 | 
								sparse = {}
 | 
				
			||||||
 | 
							} :: EntityIndex;
 | 
				
			||||||
		hooks = {
 | 
							hooks = {
 | 
				
			||||||
			[ON_ADD] = {};
 | 
								[ON_ADD] = {};
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
| 
						 | 
					@ -294,15 +300,18 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet
 | 
				
			||||||
	return add
 | 
						return add
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
local function ensureRecord(entityIndex, entityId: i53): Record
 | 
					local function ensureRecord(entityIndex: EntityIndex, entityId: i53): Record
 | 
				
			||||||
	local record = entityIndex[entityId]
 | 
						local sparse = entityIndex.sparse
 | 
				
			||||||
 | 
						local dense = entityIndex.dense
 | 
				
			||||||
	if not record then
 | 
						local page = sparse[entityId]
 | 
				
			||||||
		record = {}
 | 
						if not page then
 | 
				
			||||||
		entityIndex[entityId] = record
 | 
							local i = #dense + 1
 | 
				
			||||||
 | 
							page = { dense = i } :: Record
 | 
				
			||||||
 | 
							sparse[entityId] = page
 | 
				
			||||||
 | 
							dense[i] = entityId
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return record :: Record
 | 
						return page
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
 | 
					function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
 | 
				
			||||||
| 
						 | 
					@ -374,7 +383,7 @@ end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
 | 
					function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
 | 
				
			||||||
	local id = entityId
 | 
						local id = entityId
 | 
				
			||||||
	local record = world.entityIndex[id]
 | 
						local record = world.entityIndex.sparse[id]
 | 
				
			||||||
	if not record then
 | 
						if not record then
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
| 
						 | 
					@ -587,15 +596,64 @@ function World.entity(world: World)
 | 
				
			||||||
	return nextEntityId + REST
 | 
						return nextEntityId + REST
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- 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, entityId: i53, destruct: boolean) 
 | 
				
			||||||
 | 
						local sparse = entityIndex.sparse
 | 
				
			||||||
 | 
						local dense = entityIndex.dense
 | 
				
			||||||
 | 
						local record = sparse[entityId]
 | 
				
			||||||
 | 
						local archetype = record.archetype
 | 
				
			||||||
 | 
						local row = record.row
 | 
				
			||||||
 | 
						local denseIndex = record.dense
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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 = sparse[entityToMove]
 | 
				
			||||||
 | 
							if recordToMove then 
 | 
				
			||||||
 | 
								recordToMove.row = row
 | 
				
			||||||
 | 
								record.dense = denseIndex
 | 
				
			||||||
 | 
								dense[denseIndex] = entityToMove
 | 
				
			||||||
 | 
							end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						record.archetype = nil
 | 
				
			||||||
 | 
						record.row = nil
 | 
				
			||||||
 | 
						entityIndex.sparse[entityId] = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						local atDense = record.dense
 | 
				
			||||||
 | 
						entityIndex[atDense] = nil
 | 
				
			||||||
 | 
						local columns = archetype.columns
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if not destruct then 
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						destructColumns(columns, last, row)
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function World.delete(world: World, entityId: i53) 
 | 
					function World.delete(world: World, entityId: i53) 
 | 
				
			||||||
	local entityIndex = world.entityIndex
 | 
						local entityIndex = world.entityIndex
 | 
				
			||||||
	local record = entityIndex[entityId]
 | 
						archetypeDelete(entityIndex, entityId, true)
 | 
				
			||||||
	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
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function World.observer(world: World, ...)
 | 
					function World.observer(world: World, ...)
 | 
				
			||||||
| 
						 | 
					@ -652,21 +710,23 @@ function World.observer(world: World, ...)
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function World.__iter(world: World): () -> (number?, unknown?)
 | 
					function World.__iter(world: World): () -> (number?, unknown?)
 | 
				
			||||||
	local entityIndex = world.entityIndex
 | 
						local dense = world.entityIndex.dense
 | 
				
			||||||
 | 
						local sparse = world.entityIndex.sparse
 | 
				
			||||||
	local last
 | 
						local last
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return function() 
 | 
						return function() 
 | 
				
			||||||
		local entity, record = next(entityIndex, last)
 | 
							local lastEntity, entityId = next(dense, last)
 | 
				
			||||||
		if not entity then 
 | 
							if not lastEntity then 
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
		last = entity
 | 
							last = lastEntity
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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 entity 
 | 
								return entityId
 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		local row = record.row
 | 
							local row = record.row
 | 
				
			||||||
| 
						 | 
					@ -678,7 +738,7 @@ function World.__iter(world: World): () -> (number?, unknown?)
 | 
				
			||||||
			entityData[types[i]] = column[row]
 | 
								entityData[types[i]] = column[row]
 | 
				
			||||||
		end
 | 
							end
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		return entity, entityData
 | 
							return entityId, entityData
 | 
				
			||||||
	end
 | 
						end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										154
									
								
								tests/world.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								tests/world.lua
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,154 @@
 | 
				
			||||||
 | 
					local testkit = require("../testkit")
 | 
				
			||||||
 | 
					local jecs = require("../lib/init")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					local N = 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TEST("world", function() 
 | 
				
			||||||
 | 
					    do CASE "should be iterable" 
 | 
				
			||||||
 | 
					        local world = jecs.World.new()
 | 
				
			||||||
 | 
					        local A = world:component()
 | 
				
			||||||
 | 
					        local B = world:component()
 | 
				
			||||||
 | 
					         
 | 
				
			||||||
 | 
					        local eA = world:entity()
 | 
				
			||||||
 | 
					        world:set(eA, A, true)
 | 
				
			||||||
 | 
					        local eB = world:entity()
 | 
				
			||||||
 | 
					        world:set(eB, B, true)
 | 
				
			||||||
 | 
					        local eAB = world:entity()
 | 
				
			||||||
 | 
					        world:set(eAB, A, true)
 | 
				
			||||||
 | 
					        world:set(eAB, B, true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local count = 0
 | 
				
			||||||
 | 
					        for id, data in world do
 | 
				
			||||||
 | 
					            count += 1
 | 
				
			||||||
 | 
					            if id == eA then
 | 
				
			||||||
 | 
					                CHECK(data[A] == true)
 | 
				
			||||||
 | 
					                CHECK(data[B] == nil)
 | 
				
			||||||
 | 
					            elseif id == eB then
 | 
				
			||||||
 | 
					                CHECK(data[A] == nil)
 | 
				
			||||||
 | 
					                CHECK(data[B] == true)
 | 
				
			||||||
 | 
					            elseif id == eAB then
 | 
				
			||||||
 | 
					                CHECK(data[A] == true)
 | 
				
			||||||
 | 
					                CHECK(data[B] == true)
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					                error("unknown entity", id)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CHECK(count == 3)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    do CASE "should query all matching entities"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local world = jecs.World.new()
 | 
				
			||||||
 | 
					        local A = world:component()
 | 
				
			||||||
 | 
					        local B = world:component()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local entities = {}
 | 
				
			||||||
 | 
					        for i = 1, N do
 | 
				
			||||||
 | 
					            local id = world:entity()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            world:set(id, A, true)
 | 
				
			||||||
 | 
					            if i > 5 then world:set(id, B, true) end
 | 
				
			||||||
 | 
					            entities[i] = id
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for id in world:query(A) do
 | 
				
			||||||
 | 
					            table.remove(entities, CHECK(table.find(entities, id)))
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CHECK(#entities == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    do CASE "should query all matching entities when irrelevant component is removed"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local world = jecs.World.new()
 | 
				
			||||||
 | 
					        local A = world:component()
 | 
				
			||||||
 | 
					        local B = world:component()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local entities = {}
 | 
				
			||||||
 | 
					        for i = 1, N do
 | 
				
			||||||
 | 
					            local id = world:entity()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            world:set(id, A, true)
 | 
				
			||||||
 | 
					            world:set(id, B, true)
 | 
				
			||||||
 | 
					            if i > 5 then world:remove(id, B, true) end
 | 
				
			||||||
 | 
					            entities[i] = id
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local added = 0
 | 
				
			||||||
 | 
					        for id in world:query(A) do
 | 
				
			||||||
 | 
					            added += 1
 | 
				
			||||||
 | 
					            table.remove(entities, CHECK(table.find(entities, id)))
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CHECK(added == N)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    do CASE "should query all entities without B"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local world = jecs.World.new()
 | 
				
			||||||
 | 
					        local A = world:component()
 | 
				
			||||||
 | 
					        local B = world:component()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local entities = {}
 | 
				
			||||||
 | 
					        for i = 1, N do
 | 
				
			||||||
 | 
					            local id = world:entity()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            world:set(id, A, true)
 | 
				
			||||||
 | 
					            if i < 5 then
 | 
				
			||||||
 | 
					                entities[i] = id
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					                world:set(id, B, true)
 | 
				
			||||||
 | 
					            end
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for id in world:query(A):without(B) do
 | 
				
			||||||
 | 
					            table.remove(entities, CHECK(table.find(entities, id)))
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CHECK(#entities == 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    do CASE "should allow setting components in arbitrary order" 
 | 
				
			||||||
 | 
					        local world = jecs.World.new()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local Health = world:entity()
 | 
				
			||||||
 | 
					        local Poison = world:component()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local id = world:entity()
 | 
				
			||||||
 | 
					        world:set(id, Poison, 5)
 | 
				
			||||||
 | 
					        world:set(id, Health, 50)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CHECK(world:get(id, Poison) == 5)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    do CASE "Should allow deleting components" 
 | 
				
			||||||
 | 
					        local world = jecs.World.new()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local Health = world:entity()
 | 
				
			||||||
 | 
					        local Poison = world:component()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        local id = world:entity()
 | 
				
			||||||
 | 
					        world:set(id, Poison, 5)
 | 
				
			||||||
 | 
					        world:set(id, Health, 50)
 | 
				
			||||||
 | 
					        local id1 = world:entity()
 | 
				
			||||||
 | 
					        world:set(id1, Poison, 500)
 | 
				
			||||||
 | 
					        world:set(id1, Health, 50)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        world:delete(id)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        CHECK(world:get(id, Poison) == nil)
 | 
				
			||||||
 | 
					        CHECK(world:get(id, Health) == nil)
 | 
				
			||||||
 | 
					        CHECK(world:get(id1, Poison) == 500)
 | 
				
			||||||
 | 
					        CHECK(world:get(id1, Health) == 50)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					end)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FINISH()
 | 
				
			||||||
		Loading…
	
		Reference in a new issue