Initial commit

This commit is contained in:
Ukendio 2024-05-12 03:49:46 +02:00
parent 77bbe3e285
commit ebb81749fb
2 changed files with 168 additions and 130 deletions

View file

@ -44,12 +44,115 @@ type ArchetypeDiff = {
removed: Ty, removed: Ty,
} }
local FLAGS_PAIR = 0x8
local HI_COMPONENT_ID = 256 local HI_COMPONENT_ID = 256
local ON_ADD = HI_COMPONENT_ID + 1 local ON_ADD = HI_COMPONENT_ID + 1
local ON_REMOVE = HI_COMPONENT_ID + 2 local ON_REMOVE = HI_COMPONENT_ID + 2
local ON_SET = HI_COMPONENT_ID + 3 local ON_SET = HI_COMPONENT_ID + 3
local WILDCARD = HI_COMPONENT_ID + 4 local WILDCARD = HI_COMPONENT_ID + 4
local REST = HI_COMPONENT_ID + 5 local REST = HI_COMPONENT_ID + 5
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
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 newId(source: number, target: number)
local e = source * 2^28 + target * ECS_ID_FLAGS_MASK
return e
end
local function ECS_IS_PAIR(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
if source == WILDCARD then
id = newId(ECS_PAIR_SECOND(target), WILDCARD)
elseif target == WILDCARD then
id = newId(WILDCARD, ECS_PAIR_SECOND(source))
else
id = newId(ECS_PAIR_SECOND(target), ECS_PAIR_SECOND(source))
end
return id + addFlags({ isPair = true })
end
local function getAlive(entityIndex: EntityIndex, id: i53)
return entityIndex.dense[id]
end
local function ecs_get_source(entityIndex, e)
assert(ECS_IS_PAIR(e))
return getAlive(entityIndex, ECS_PAIR_FIRST(e))
end
local function ecs_get_target(entityIndex, e)
assert(ECS_IS_PAIR(e))
return getAlive(entityIndex, ECS_PAIR_SECOND(e))
end
local function nextEntityId(entityIndex, index: i24)
local id = newId(index, 0)
entityIndex.sparse[id] = {
dense = index
} :: Record
entityIndex.dense[index] = id
return id
end
local function transitionArchetype( local function transitionArchetype(
entityIndex: EntityIndex, entityIndex: EntityIndex,
@ -158,10 +261,10 @@ local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archet
world.nextArchetypeId = id world.nextArchetypeId = id
local length = #types local length = #types
local columns = table.create(length) :: {any} local columns = {}
for index in types do for index, componentId in types do
columns[index] = {} table.insert(columns, {})
end end
local archetype = { local archetype = {
@ -204,103 +307,6 @@ function World.new()
return self return self
end end
local FLAGS_PAIR = 0x8
local function ADD_FLAGS(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 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 NEW_ID(source: number, target: number)
local e = source * 2^28 + target * ECS_ID_FLAGS_MASK
return e
end
local function ECS_IS_PAIR(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 NEW_ID(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 = NEW_ID(ECS_PAIR_SECOND(target), ECS_PAIR_SECOND(source)) + ADD_FLAGS({ isPair = true })
return id
end
local function getAlive(entityIndex: EntityIndex, id: i53)
return assert(entityIndex.dense[id], id .. "is not alive")
end
local function ecs_get_source(entityIndex, e)
assert(ECS_IS_PAIR(e))
return getAlive(entityIndex, ECS_PAIR_FIRST(e))
end
local function ecs_get_target(entityIndex, e)
assert(ECS_IS_PAIR(e))
return getAlive(entityIndex, ECS_PAIR_SECOND(e))
end
local function nextEntityId(entityIndex, index: i24)
local id = NEW_ID(index, 0)
entityIndex.sparse[id] = {
dense = index
} :: Record
entityIndex.dense[index] = id
return id
end
function World.component(world: World) function World.component(world: World)
local componentId = world.nextComponentId + 1 local componentId = world.nextComponentId + 1
if componentId > HI_COMPONENT_ID then if componentId > HI_COMPONENT_ID then
@ -398,19 +404,37 @@ local function findInsert(types: {i53}, toAdd: i53)
end end
local function findArchetypeWith(world: World, node: Archetype, componentId: i53) local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
local entityIndex = world.entityIndex
local types = node.types local types = node.types
-- Component IDs are added incrementally, so inserting and sorting -- Component IDs are added incrementally, so inserting and sorting
-- them each time would be expensive. Instead this insertion sort can find the insertion -- them each time would be expensive. Instead this insertion sort can find the insertion
-- point in the types array. -- point in the types array.
local destinationType = table.clone(node.types)
local at = findInsert(types, componentId) local at = findInsert(types, componentId)
if at == -1 then if at == -1 then
-- If it finds a duplicate, it just means it is the same archetype so it can return it -- 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. -- directly instead of needing to hash types for a lookup to the archetype.
return node return node
end end
local destinationType = table.clone(node.types)
table.insert(destinationType, at, componentId) table.insert(destinationType, at, componentId)
if ECS_IS_PAIR(componentId) then
local source = ECS_PAIR(
ecs_get_source(entityIndex, componentId), WILDCARD)
local sourceAt = findInsert(destinationType, source)
if sourceAt ~= -1 then
table.insert(destinationType, sourceAt, source)
end
local target = ECS_PAIR(
WILDCARD, ecs_get_target(entityIndex, componentId))
local targetAt = findInsert(destinationType, target)
if targetAt ~= -1 then
table.insert(destinationType, targetAt, target)
end
end
return ensureArchetype(world, destinationType, node) return ensureArchetype(world, destinationType, node)
end end
@ -583,7 +607,6 @@ function World.query(world: World, ...: i53): Query
local firstArchetypeMap local firstArchetypeMap
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
local entityIndex = world.entityIndex
for _, componentId in components do for _, componentId in components do
local map = componentIndex[componentId] local map = componentIndex[componentId]
@ -603,22 +626,6 @@ function World.query(world: World, ...: i53): Query
local skip = false local skip = false
for i, componentId in components do for i, componentId in components do
if ECS_IS_PAIR(componentId) then
local source = ecs_get_source(entityIndex, componentId)
local target = ecs_get_target(entityIndex, componentId)
if target == WILDCARD then
local matched = false
for c in archetypeRecords do
if ecs_get_source(entityIndex, c) == source then
matched = true
end
end
if matched then
fr
end
end
end
local index = archetypeRecords[componentId] local index = archetypeRecords[componentId]
if not index then if not index then
skip = true skip = true
@ -785,11 +792,13 @@ return table.freeze({
ON_REMOVE = ON_REMOVE; ON_REMOVE = ON_REMOVE;
ON_SET = ON_SET; ON_SET = ON_SET;
ECS_ID = ECS_ID, ECS_ID = ECS_ID,
ECS_IS_PAIR = ECS_IS_PAIR, IS_PAIR = ECS_IS_PAIR,
ECS_PAIR = ECS_PAIR, ECS_PAIR = ECS_PAIR,
ECS_GENERATION = ECS_GENERATION, ECS_GENERATION = ECS_GENERATION,
ECS_GENERATION_INC = ECS_GENERATION_INC, ECS_GENERATION_INC = ECS_GENERATION_INC,
getAlive = getAlive, getAlive = getAlive,
ecs_get_target = ecs_get_target, ecs_get_target = ecs_get_target,
ecs_get_source = ecs_get_source ecs_get_source = ecs_get_source,
Wildcard = WILDCARD,
REST = REST
}) })

View file

@ -2,7 +2,7 @@ local testkit = require("../testkit")
local jecs = require("../lib/init") local jecs = require("../lib/init")
local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION
local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC
local IS_PAIR = jecs.ECS_IS_PAIR local IS_PAIR = jecs.IS_PAIR
local ECS_PAIR = jecs.ECS_PAIR local ECS_PAIR = jecs.ECS_PAIR
local getAlive = jecs.getAlive local getAlive = jecs.getAlive
local ecs_get_source = jecs.ecs_get_source local ecs_get_source = jecs.ecs_get_source
@ -14,6 +14,7 @@ local TEST, CASE, CHECK, FINISH, SKIP = testkit.test()
local N = 10 local N = 10
TEST("world", function() TEST("world", function()
--[[
do CASE "should be iterable" do CASE "should be iterable"
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
@ -48,7 +49,6 @@ TEST("world", function()
end end
do CASE "should query all matching entities" do CASE "should query all matching entities"
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -71,7 +71,6 @@ TEST("world", function()
end end
do CASE "should query all matching entities when irrelevant component is removed" do CASE "should query all matching entities when irrelevant component is removed"
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -99,7 +98,6 @@ TEST("world", function()
end end
do CASE "should query all entities without B" do CASE "should query all entities without B"
local world = jecs.World.new() local world = jecs.World.new()
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
@ -171,14 +169,13 @@ TEST("world", function()
world:remove(id, Poison) world:remove(id, Poison)
CHECK(world:get(id, Poison) == nil) CHECK(world:get(id, Poison) == nil)
print(world:get(id, Health))
CHECK(world:get(id, Health) == 50) CHECK(world:get(id, Health) == 50)
end end
do CASE "should increment generation" do CASE "should increment generation"
local world = jecs.World.new() local world = jecs.World.new()
local e = world:entity() local e = world:entity()
CHECK(ECS_ID(e) == 1 + REST) CHECK(ECS_ID(e) == 1 + jecs.REST)
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e) CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
CHECK(ECS_GENERATION(e) == 0) -- 0 CHECK(ECS_GENERATION(e) == 0) -- 0
e = ECS_GENERATION_INC(e) e = ECS_GENERATION_INC(e)
@ -190,8 +187,8 @@ TEST("world", function()
local _e = world:entity() local _e = world:entity()
local e2 = world:entity() local e2 = world:entity()
local e3 = world:entity() local e3 = world:entity()
CHECK(ECS_ID(e2) == 2 + REST) CHECK(ECS_ID(e2) == 2 +jecs.REST)
CHECK(ECS_ID(e3) == 3 + REST) CHECK(ECS_ID(e3) == 3 + jecs.REST)
CHECK(ECS_GENERATION(e2) == 0) CHECK(ECS_GENERATION(e2) == 0)
CHECK(ECS_GENERATION(e3) == 0) CHECK(ECS_GENERATION(e3) == 0)
@ -203,6 +200,38 @@ TEST("world", function()
CHECK(ecs_get_target(world.entityIndex, pair) == e3) CHECK(ecs_get_target(world.entityIndex, pair) == e3)
end end
do CASE "should allow querying for relations"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), true)
for e in world:query(ECS_PAIR(Eats, Apples)) do
CHECK(e == bob)
end
end
]]
do CASE "should allow wildcards in queries"
local world = jecs.World.new()
local Eats = world:entity()
local Apples = world:entity()
local bob = world:entity()
world:set(bob, ECS_PAIR(Eats, Apples), true)
--testkit.print(world.componentIndex)
local w = jecs.Wildcard
for e, bool in world:query(ECS_PAIR(Eats, w)) do
CHECK(e == bob)
CHECK(bool)
end
for e, bool in world:query(ECS_PAIR(w, Apples)) do
CHECK(e == bob)
CHECK(bool)
end
end
end) end)
FINISH() FINISH()