diff --git a/lib/init.lua b/lib/init.lua index 9a0a0ac..203ca97 100644 --- a/lib/init.lua +++ b/lib/init.lua @@ -199,27 +199,168 @@ function World.new() return self end -local function emit(world, eventDescription) - local event = eventDescription.event +local FLAGS_PAIR = 0x8 - table.insert(world.hooks[event], { - archetype = eventDescription.archetype; - ids = eventDescription.ids; - offset = eventDescription.offset; - otherArchetype = eventDescription.otherArchetype; - }) +local function addFlags(flags) + local typeFlags = 0x0 + if flags.isPair then + typeFlags = bit32.bor(typeFlags, FLAGS_PAIR) -- HIGHEST bit in the ID. + end + if false then + typeFlags = bit32.bor(typeFlags, 0x4) -- Set the second flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x2) -- Set the third flag to true + end + if false then + typeFlags = bit32.bor(typeFlags, 0x1) -- LAST BIT in the ID. + end + + return typeFlags 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; - }) +local ECS_ID_FLAGS_MASK = 0x10 + +-- ECS_ENTITY_MASK (0xFFFFFFFFull << 28) +local ECS_ENTITY_MASK = bit32.lshift(1, 24) + +-- ECS_GENERATION_MASK (0xFFFFull << 24) +local ECS_GENERATION_MASK = bit32.lshift(1, 16) + +local function newId(source: number, target: number) + local e = source * 2^28 + target * ECS_ID_FLAGS_MASK + return e +end + +local function isPair(e: number) + return (e % 2^4) // FLAGS_PAIR ~= 0 +end + +function separate(entity: number) + local _typeFlags = entity % 0x10 + entity //= ECS_ID_FLAGS_MASK + return entity // ECS_ENTITY_MASK, entity % ECS_GENERATION_MASK, _typeFlags +end + +-- HIGH 24 bits LOW 24 bits +local function ECS_GENERATION(e: i53) + e //= 0x10 + return e % ECS_GENERATION_MASK +end + +local function ECS_ID(e: i53) + e //= 0x10 + return e // ECS_ENTITY_MASK +end + +local function ECS_GENERATION_INC(e: i53) + local id, generation, flags = separate(e) + + return newId(id, generation + 1) + flags +end + +-- gets the high ID +local function ECS_PAIR_FIRST(entity: i53): i24 + entity //= 0x10 + local first = entity % ECS_ENTITY_MASK + return first +end + +-- gets the low ID +local ECS_PAIR_SECOND = ECS_ID + +local function ECS_PAIR(source: number, target: number) + local id = newId(ECS_PAIR_SECOND(target), ECS_PAIR_SECOND(source)) + addFlags({ isPair = true }) + return id +end + +local function getAlive(entityIndex, id) + return entityIndex.dense[id] +end + +local function ecs_pair_first(entityIndex, e) + assert(isPair(e)) + return getAlive(entityIndex, ECS_PAIR_FIRST(e)) +end +local function ecs_pair_second(entityIndex, e) + assert(isPair(e)) + return getAlive(entityIndex, ECS_PAIR_SECOND(e)) +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.") end + world.nextComponentId = componentId + return componentId +end + +function World.entity(world: World) + local nextEntityId = world.nextEntityId + 1 + world.nextEntityId = nextEntityId + local index = nextEntityId + REST + local id = newId(index, 0) + local entityIndex = world.entityIndex + entityIndex.sparse[id] = { + dense = index + } :: Record + entityIndex.dense[index] = id + + return id +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, record: Record, entityId: i53, destruct: boolean) + local sparse, dense = entityIndex.sparse, entityIndex.dense + local archetype = record.archetype + local row = record.row + local entities = archetype.entities + local last = #entities + + local entityToMove = entities[last] + + if row ~= last then + dense[record.dense] = entityToMove + sparse[entityToMove] = record + end + + sparse[entityId] = nil + dense[#dense] = nil + + entities[row], entities[last] = entities[last], nil + + 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.sparse[entityId] + if not record then + return + end + archetypeDelete(entityIndex, record, entityId, true) end export type World = typeof(World.new()) @@ -300,22 +441,8 @@ local function archetypeTraverseAdd(world: World, componentId: i53, from: Archet return add end -local function ensureRecord(entityIndex: EntityIndex, entityId: i53): Record - local sparse = entityIndex.sparse - local dense = entityIndex.dense - local page = sparse[entityId] - if not page then - local i = #dense + 1 - page = { dense = i } :: Record - sparse[entityId] = page - dense[i] = entityId - end - - return page -end - function World.set(world: World, entityId: i53, componentId: i53, data: unknown) - local record = ensureRecord(world.entityIndex, entityId) + local record = world.entityIndex.sparse[entityId] local from = record.archetype local to = archetypeTraverseAdd(world, componentId, from) @@ -335,7 +462,6 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown) 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 @@ -360,7 +486,7 @@ end function World.remove(world: World, entityId: i53, componentId: i53) local entityIndex = world.entityIndex - local record = ensureRecord(entityIndex, entityId) + local record = entityIndex.sparse[entityId] local sourceArchetype = record.archetype local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype) @@ -579,129 +705,6 @@ function World.query(world: World, ...: i53): Query return setmetatable({}, preparedQuery) :: any 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.") - end - world.nextComponentId = componentId - return componentId -end - -function World.entity(world: World) - local nextEntityId = world.nextEntityId + 1 - world.nextEntityId = nextEntityId - return nextEntityId + REST -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, record: Record, entityId: i53, destruct: boolean) - local sparse, dense = entityIndex.sparse, entityIndex.dense - local archetype = record.archetype - local row = record.row - local entities = archetype.entities - local last = #entities - - local entityToMove = entities[last] - - if row ~= last then - dense[record.dense] = entityToMove - sparse[entityToMove] = record - end - - sparse[entityId] = nil - dense[#dense] = nil - - entities[row], entities[last] = entities[last], nil - - 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.sparse[entityId] - if not record then - return - end - archetypeDelete(entityIndex, record, entityId, true) -end - -function World.observer(world: World, ...) - local componentIds = {...} - local idsCount = #componentIds - local hooks = world.hooks - - return { - event = function(event) - local hook = hooks[event] - hooks[event] = nil - - local last, change - return function() - last, change = next(hook, last) - if not last then - return - end - - local matched = false - local ids = change.ids - - while not matched do - local skip = false - for _, id in ids do - if not table.find(componentIds, id) then - skip = true - break - end - end - - if skip then - last, change = next(hook, last) - ids = change.ids - continue - end - - matched = true - end - - local queryOutput = table.create(idsCount) - local row = change.offset - local archetype = change.archetype - local columns = archetype.columns - local archetypeRecords = archetype.records - for index, id in componentIds do - queryOutput[index] = columns[archetypeRecords[id]][row] - end - - return archetype.entities[row], unpack(queryOutput, 1, idsCount) - end - end; - } -end - function World.__iter(world: World): () -> (number?, unknown?) local dense = world.entityIndex.dense local sparse = world.entityIndex.sparse @@ -740,4 +743,12 @@ return table.freeze({ ON_ADD = ON_ADD; ON_REMOVE = ON_REMOVE; ON_SET = ON_SET; + ECS_ID = ECS_ID, + IS_PAIR = isPair, + ECS_PAIR = ECS_PAIR, + ECS_GENERATION = ECS_GENERATION, + ECS_GENERATION_INC = ECS_GENERATION_INC, + getAlive = getAlive, + ecs_pair_first = ecs_pair_first, + ecs_pair_second = ecs_pair_second }) diff --git a/tests/test1.lua b/tests/test1.lua deleted file mode 100644 index 7ff3b5a..0000000 --- a/tests/test1.lua +++ /dev/null @@ -1,148 +0,0 @@ -local testkit = require("../testkit") -local jecs = require("../lib/init") - -local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() - -local N = 10 - -TEST("world:query", function() - 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) - world:delete(id) - - CHECK(world:get(id, Poison) == nil) - CHECK(world:get(id, Health) == nil) - end - - do CASE "Should allow iterating the whole world" - local world = jecs.World.new() - - local A, B = world:entity(), world:entity() - - 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[B] == true) - CHECK(data[A] == nil) - elseif id == eAB then - CHECK(data[A] == true) - CHECK(data[B] == true) - else - error("unknown entity", id) - end - end - - CHECK(count == 3) - end - -end) - -FINISH() \ No newline at end of file diff --git a/tests/world.lua b/tests/world.lua index a917f65..d8ebb6f 100644 --- a/tests/world.lua +++ b/tests/world.lua @@ -1,5 +1,12 @@ local testkit = require("../testkit") local jecs = require("../lib/init") +local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION +local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC +local IS_PAIR = jecs.IS_PAIR +local ECS_PAIR = jecs.ECS_PAIR +local getAlive = jecs.getAlive +local ecs_pair_first = jecs.ecs_pair_first +local ecs_pair_second = jecs.ecs_pair_second local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() @@ -127,7 +134,7 @@ TEST("world", function() CHECK(world:get(id, Poison) == 5) end - do CASE "Should allow deleting components" + do CASE "should allow deleting components" local world = jecs.World.new() local Health = world:entity() @@ -149,6 +156,35 @@ TEST("world", function() end + do CASE "should increment generation" + local world = jecs.World.new() + local e = world:entity() + local REST = 256 + 4 + CHECK(ECS_ID(e) == 1 + REST) + CHECK(ECS_GENERATION(e) == 0) -- 0 + e = ECS_GENERATION_INC(e) + CHECK(ECS_GENERATION(e) == 1) -- 1 + end + + do CASE "relations" + local world = jecs.World.new() + local _e = world:entity() + local e2 = world:entity() + local e3 = world:entity() + local REST = 256 + 4 + CHECK(ECS_ID(e2) == 2 + REST) + CHECK(ECS_ID(e3) == 3 + REST) + CHECK(ECS_GENERATION(e2) == 0) + CHECK(ECS_GENERATION(e3) == 0) + + CHECK(IS_PAIR(world:entity()) == false) + + local pair = ECS_PAIR(e2, e3) + CHECK(IS_PAIR(pair) == true) + CHECK(ecs_pair_first(world.entityIndex, pair) == e2) + CHECK(ecs_pair_second(world.entityIndex, pair) == e3) + end + end) FINISH() \ No newline at end of file