mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-24 17:10:03 +00:00
Merge branch 'Ukendio:main' into packaging/pesde
This commit is contained in:
commit
ea52b1a6c8
9 changed files with 555 additions and 98 deletions
5
.github/workflows/unit-testing.yaml
vendored
5
.github/workflows/unit-testing.yaml
vendored
|
@ -13,7 +13,10 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Luau
|
||||
uses: encodedvenom/install-luau@v2.1
|
||||
uses: encodedvenom/install-luau@v4.2
|
||||
with:
|
||||
version: '0.651'
|
||||
verbose: 'true'
|
||||
|
||||
- name: Run Unit Tests
|
||||
id: run_tests
|
||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@rbxts/jecs",
|
||||
"version": "0.3.3",
|
||||
"version": "0.4.0-rc.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@rbxts/jecs",
|
||||
"version": "0.3.3",
|
||||
"version": "0.4.0-rc.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@rbxts/compiler-types": "^2.3.0-types.1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@rbxts/jecs",
|
||||
"version": "0.3.3",
|
||||
"version": "0.4.0-rc.0",
|
||||
"description": "Stupidly fast Entity Component System",
|
||||
"main": "src",
|
||||
"repository": {
|
||||
|
|
2
src/index.d.ts
vendored
2
src/index.d.ts
vendored
|
@ -2,7 +2,7 @@
|
|||
* A unique identifier in the world, entity.
|
||||
* The generic type T defines the data type when this entity is used as a component
|
||||
*/
|
||||
export type Entity<T = unknown> = number & { __jecs_value: T };
|
||||
export type Entity<T = undefined> = number & { __jecs_value: T };
|
||||
|
||||
/**
|
||||
* An entity with no associated data when used as a component
|
||||
|
|
251
src/init.luau
251
src/init.luau
|
@ -44,11 +44,6 @@ type Record = {
|
|||
dense: i24,
|
||||
}
|
||||
|
||||
type EntityIndex = {
|
||||
dense: Map<i24, i53>,
|
||||
sparse: Map<i53, Record>,
|
||||
}
|
||||
|
||||
type ArchetypeRecord = {
|
||||
count: number,
|
||||
column: number,
|
||||
|
@ -74,6 +69,13 @@ type ArchetypeDiff = {
|
|||
removed: Ty,
|
||||
}
|
||||
|
||||
type EntityIndex = {
|
||||
dense_array: Map<i24, i53>,
|
||||
sparse_array: Map<i53, Record>,
|
||||
alive_count: number,
|
||||
max_id: number,
|
||||
}
|
||||
|
||||
local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256
|
||||
-- stylua: ignore start
|
||||
local EcsOnAdd = HI_COMPONENT_ID + 1
|
||||
|
@ -89,8 +91,8 @@ local EcsRemove = HI_COMPONENT_ID + 10
|
|||
local EcsName = HI_COMPONENT_ID + 11
|
||||
local EcsRest = HI_COMPONENT_ID + 12
|
||||
|
||||
local ECS_PAIR_FLAG = 0x8
|
||||
local ECS_ID_FLAGS_MASK = 0x10
|
||||
local ECS_PAIR_FLAG = 0x8
|
||||
local ECS_ID_FLAGS_MASK = 0x10
|
||||
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
|
||||
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
|
||||
|
||||
|
@ -141,7 +143,12 @@ local function ECS_GENERATION_INC(e: i53)
|
|||
local id = flags // ECS_ENTITY_MASK
|
||||
local generation = flags % ECS_GENERATION_MASK
|
||||
|
||||
return ECS_COMBINE(id, generation + 1) + flags
|
||||
local next_gen = generation + 1
|
||||
if next_gen > ECS_GENERATION_MASK then
|
||||
return id
|
||||
end
|
||||
|
||||
return ECS_COMBINE(id, next_gen)
|
||||
end
|
||||
return ECS_COMBINE(e, 1)
|
||||
end
|
||||
|
@ -164,49 +171,83 @@ local function ECS_PAIR(pred: i53, obj: i53): i53
|
|||
return ECS_COMBINE(ECS_ENTITY_T_LO(obj), ECS_ENTITY_T_LO(pred)) + FLAGS_ADD(--[[isPair]] true) :: i53
|
||||
end
|
||||
|
||||
local ERROR_ENTITY_NOT_ALIVE = "Entity is not alive"
|
||||
local ERROR_GENERATION_INVALID = "INVALID GENERATION"
|
||||
|
||||
local function entity_index_get_alive(index: EntityIndex, e: i24): i53
|
||||
local denseArray = index.dense
|
||||
local id = denseArray[ECS_ENTITY_T_LO(e)]
|
||||
|
||||
if id then
|
||||
local currentGeneration = ECS_GENERATION(id)
|
||||
local gen = ECS_GENERATION(e)
|
||||
if gen == currentGeneration then
|
||||
return id
|
||||
end
|
||||
|
||||
error(ERROR_GENERATION_INVALID)
|
||||
local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record?
|
||||
local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
|
||||
if not r then
|
||||
return nil
|
||||
end
|
||||
|
||||
error(ERROR_ENTITY_NOT_ALIVE)
|
||||
if not r or r.dense == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
local function _entity_index_sparse_get(entityIndex, id)
|
||||
return entityIndex.sparse[entity_index_get_alive(entityIndex, id)]
|
||||
local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record?
|
||||
local r = entity_index_try_get_any(entity_index, entity)
|
||||
if r then
|
||||
local r_dense = r.dense
|
||||
if r_dense > entity_index.alive_count then
|
||||
return nil
|
||||
end
|
||||
if entity_index.dense_array[r_dense] ~= entity then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
local function entity_index_try_get_fast(entity_index: EntityIndex, entity: number): Record?
|
||||
local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
|
||||
if r then
|
||||
if entity_index.dense_array[r.dense] ~= entity then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
local function entity_index_get_alive(index: EntityIndex, e: i24): i53
|
||||
local r = entity_index_try_get_any(index, e)
|
||||
if r then
|
||||
return index.dense_array[r.dense]
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function entity_index_is_alive(entity_index: EntityIndex, entity: number)
|
||||
return entity_index_try_get(entity_index, entity) ~= nil
|
||||
end
|
||||
|
||||
local function entity_index_new_id(entity_index: EntityIndex, data): i53
|
||||
local dense_array = entity_index.dense_array
|
||||
local alive_count = entity_index.alive_count
|
||||
if alive_count ~= #dense_array then
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
local id = dense_array[alive_count]
|
||||
return id
|
||||
end
|
||||
|
||||
local id = entity_index.max_id + 1
|
||||
entity_index.max_id = id
|
||||
alive_count += 1
|
||||
entity_index.alive_count = alive_count
|
||||
dense_array[alive_count] = id
|
||||
entity_index.sparse_array[id] = { dense = alive_count } :: Record
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
-- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits
|
||||
local function ecs_pair_first(world, e)
|
||||
return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_HI(e))
|
||||
return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_HI(e))
|
||||
end
|
||||
|
||||
-- ECS_PAIR_SECOND gets the relationship / pred / LOW bits
|
||||
local function ecs_pair_second(world, e)
|
||||
return entity_index_get_alive(world.entityIndex, ECS_ENTITY_T_LO(e))
|
||||
end
|
||||
|
||||
local function entity_index_new_id(entityIndex: EntityIndex, index: i24): i53
|
||||
--local id = ECS_COMBINE(index, 0)
|
||||
local id = index
|
||||
entityIndex.sparse[id] = {
|
||||
dense = index,
|
||||
} :: Record
|
||||
entityIndex.dense[index] = id
|
||||
|
||||
return id
|
||||
return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e))
|
||||
end
|
||||
|
||||
local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24)
|
||||
|
@ -239,7 +280,6 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row:
|
|||
column[last] = nil
|
||||
end
|
||||
|
||||
local sparse = entity_index.sparse
|
||||
local moved = #src_entities
|
||||
|
||||
-- Move the entity from the source to the destination archetype.
|
||||
|
@ -255,9 +295,10 @@ local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row:
|
|||
src_entities[moved] = nil :: any
|
||||
dst_entities[dst_row] = e1
|
||||
|
||||
local record1 = sparse[e1]
|
||||
local record2 = sparse[e2]
|
||||
local sparse_array = entity_index.sparse_array
|
||||
|
||||
local record1 = sparse_array[ECS_ENTITY_T_LO(e1)]
|
||||
local record2 = sparse_array[ECS_ENTITY_T_LO(e2)]
|
||||
record1.row = dst_row
|
||||
record2.row = src_row
|
||||
end
|
||||
|
@ -307,7 +348,7 @@ do
|
|||
end
|
||||
|
||||
function world_get(world: World, entity: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any
|
||||
local record = world.entityIndex.sparse[entity]
|
||||
local record = entity_index_try_get_fast(world.entity_index, entity)
|
||||
if not record then
|
||||
return nil
|
||||
end
|
||||
|
@ -338,7 +379,7 @@ do
|
|||
end
|
||||
|
||||
local function world_get_one_inline(world: World, entity: i53, id: i53): any
|
||||
local record = world.entityIndex.sparse[entity]
|
||||
local record = entity_index_try_get_fast(world.entity_index, entity)
|
||||
if not record then
|
||||
return nil
|
||||
end
|
||||
|
@ -356,7 +397,7 @@ local function world_get_one_inline(world: World, entity: i53, id: i53): any
|
|||
end
|
||||
|
||||
local function world_has_one_inline(world: World, entity: number, id: i53): boolean
|
||||
local record = world.entityIndex.sparse[entity]
|
||||
local record = entity_index_try_get_fast(world.entity_index, entity)
|
||||
if not record then
|
||||
return false
|
||||
end
|
||||
|
@ -372,7 +413,7 @@ local function world_has_one_inline(world: World, entity: number, id: i53): bool
|
|||
end
|
||||
|
||||
local function world_has(world: World, entity: number, ...: i53): boolean
|
||||
local record = world.entityIndex.sparse[entity]
|
||||
local record = entity_index_try_get_fast(world.entity_index, entity)
|
||||
if not record then
|
||||
return false
|
||||
end
|
||||
|
@ -395,7 +436,11 @@ end
|
|||
|
||||
local function world_target(world: World, entity: i53, relation: i24, index: number?): i24?
|
||||
local nth = index or 0
|
||||
local record = world.entityIndex.sparse[entity]
|
||||
local record = entity_index_try_get_fast(world.entity_index, entity)
|
||||
if not record then
|
||||
return nil
|
||||
end
|
||||
|
||||
local archetype = record.archetype
|
||||
if not archetype then
|
||||
return nil
|
||||
|
@ -437,7 +482,10 @@ local function id_record_ensure(world: World, id: number): IdRecord
|
|||
|
||||
if not idr then
|
||||
local flags = ECS_ID_MASK
|
||||
local relation = ECS_ENTITY_T_HI(id)
|
||||
local relation = id
|
||||
if ECS_IS_PAIR(id) then
|
||||
relation = ecs_pair_first(world, id)
|
||||
end
|
||||
|
||||
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
|
||||
local cleanup_policy_target = world_target(world, relation, EcsOnDeleteTarget, 0)
|
||||
|
@ -543,9 +591,7 @@ local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): A
|
|||
end
|
||||
|
||||
local function world_entity(world: World): i53
|
||||
local entityId = (world.nextEntityId :: number) + 1
|
||||
world.nextEntityId = entityId
|
||||
return entity_index_new_id(world.entityIndex, entityId + EcsRest)
|
||||
return entity_index_new_id(world.entity_index)
|
||||
end
|
||||
|
||||
local function world_parent(world: World, entity: i53)
|
||||
|
@ -701,15 +747,19 @@ local function invoke_hook(action, entity, data)
|
|||
end
|
||||
|
||||
local function world_add(world: World, entity: i53, id: i53): ()
|
||||
local entityIndex = world.entityIndex
|
||||
local record = entityIndex.sparse[entity]
|
||||
local entity_index = world.entity_index
|
||||
local record = entity_index_try_get_fast(entity_index, entity)
|
||||
if not record then
|
||||
return
|
||||
end
|
||||
|
||||
local from = record.archetype
|
||||
local to = archetype_traverse_add(world, id, from)
|
||||
if from == to then
|
||||
return
|
||||
end
|
||||
if from then
|
||||
entity_move(entityIndex, entity, record, to)
|
||||
entity_move(entity_index, entity, record, to)
|
||||
else
|
||||
if #to.types > 0 then
|
||||
new_entity(entity, record, to)
|
||||
|
@ -725,8 +775,12 @@ local function world_add(world: World, entity: i53, id: i53): ()
|
|||
end
|
||||
|
||||
local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
|
||||
local entityIndex = world.entityIndex
|
||||
local record = entityIndex.sparse[entity]
|
||||
local entity_index = world.entity_index
|
||||
local record = entity_index_try_get_fast(entity_index, entity)
|
||||
if not record then
|
||||
return
|
||||
end
|
||||
|
||||
local from: Archetype = record.archetype
|
||||
local to: Archetype = archetype_traverse_add(world, id, from)
|
||||
local idr = world.componentIndex[id]
|
||||
|
@ -741,7 +795,8 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
|
|||
-- If the archetypes are the same it can avoid moving the entity
|
||||
-- and just set the data directly.
|
||||
local tr = to.records[id]
|
||||
from.columns[tr.column][record.row] = data
|
||||
local column = from.columns[tr.column]
|
||||
column[record.row] = data
|
||||
local on_set = idr_hooks.on_set
|
||||
if on_set then
|
||||
on_set(entity, data)
|
||||
|
@ -752,7 +807,7 @@ local function world_set(world: World, entity: i53, id: i53, data: unknown): ()
|
|||
|
||||
if from then
|
||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
||||
entity_move(entityIndex, entity, record, to)
|
||||
entity_move(entity_index, entity, record, to)
|
||||
else
|
||||
if #to.types > 0 then
|
||||
-- When there is no previous archetype it should create the archetype
|
||||
|
@ -788,15 +843,18 @@ local function world_component(world: World): i53
|
|||
error("Too many components, consider using world:entity() instead to create components.")
|
||||
end
|
||||
world.nextComponentId = componentId
|
||||
local id = entity_index_new_id(world.entityIndex, componentId)
|
||||
world_add(world, id, EcsComponent)
|
||||
return id
|
||||
|
||||
return componentId
|
||||
end
|
||||
|
||||
local function world_remove(world: World, entity: i53, id: i53)
|
||||
local entity_index = world.entityIndex
|
||||
local record = entity_index.sparse[entity]
|
||||
local entity_index = world.entity_index
|
||||
local record = entity_index_try_get_fast(entity_index, entity)
|
||||
if not record then
|
||||
return
|
||||
end
|
||||
local from = record.archetype
|
||||
|
||||
if not from then
|
||||
return
|
||||
end
|
||||
|
@ -831,7 +889,7 @@ local function archetype_fast_delete(columns: { Column }, column_count: number,
|
|||
end
|
||||
|
||||
local function archetype_delete(world: World, archetype: Archetype, row: number, destruct: boolean?)
|
||||
local entityIndex = world.entityIndex
|
||||
local entityIndex = world.entity_index
|
||||
local columns = archetype.columns
|
||||
local types = archetype.types
|
||||
local entities = archetype.entities
|
||||
|
@ -844,7 +902,7 @@ local function archetype_delete(world: World, archetype: Archetype, row: number,
|
|||
|
||||
if row ~= last then
|
||||
-- TODO: should be "entity_index_sparse_get(entityIndex, move)"
|
||||
local record_to_move = entityIndex.sparse[move]
|
||||
local record_to_move = entity_index_try_get_any(entityIndex, move)
|
||||
if record_to_move then
|
||||
record_to_move.row = row
|
||||
end
|
||||
|
@ -868,7 +926,7 @@ end
|
|||
|
||||
local function world_clear(world: World, entity: i53)
|
||||
--TODO: use sparse_get (stashed)
|
||||
local record = world.entityIndex.sparse[entity]
|
||||
local record = entity_index_try_get(world.entity_index, entity)
|
||||
if not record then
|
||||
return
|
||||
end
|
||||
|
@ -983,10 +1041,8 @@ end
|
|||
local world_delete: (world: World, entity: i53, destruct: boolean?) -> ()
|
||||
do
|
||||
function world_delete(world: World, entity: i53, destruct: boolean?)
|
||||
local entityIndex = world.entityIndex
|
||||
local sparse_array = entityIndex.sparse
|
||||
|
||||
local record = sparse_array[entity]
|
||||
local entity_index = world.entity_index
|
||||
local record = entity_index_try_get(entity_index, entity)
|
||||
if not record then
|
||||
return
|
||||
end
|
||||
|
@ -1044,7 +1100,7 @@ do
|
|||
if not ECS_IS_PAIR(id) then
|
||||
continue
|
||||
end
|
||||
local object = ECS_ENTITY_T_LO(id)
|
||||
local object = ecs_pair_second(world, id)
|
||||
if object == delete then
|
||||
local id_record = component_index[id]
|
||||
local flags = id_record.flags
|
||||
|
@ -1067,13 +1123,25 @@ do
|
|||
end
|
||||
end
|
||||
|
||||
local dense_array = entity_index.dense_array
|
||||
local index_of_deleted_entity = record.dense
|
||||
local index_of_last_alive_entity = entity_index.alive_count
|
||||
entity_index.alive_count = index_of_last_alive_entity - 1
|
||||
|
||||
local last_alive_entity = dense_array[index_of_last_alive_entity]
|
||||
local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: Record
|
||||
r_swap.dense = index_of_deleted_entity
|
||||
record.archetype = nil :: any
|
||||
sparse_array[entity] = nil :: any
|
||||
record.row = nil :: any
|
||||
record.dense = index_of_last_alive_entity
|
||||
|
||||
dense_array[index_of_deleted_entity] = last_alive_entity
|
||||
dense_array[index_of_last_alive_entity] = ECS_GENERATION_INC(entity)
|
||||
end
|
||||
end
|
||||
|
||||
local function world_contains(world: World, entity): boolean
|
||||
return world.entityIndex.sparse[entity] ~= nil
|
||||
return entity_index_is_alive(world.entity_index, entity)
|
||||
end
|
||||
|
||||
local function NOOP() end
|
||||
|
@ -1543,8 +1611,11 @@ if _G.__JECS_DEBUG then
|
|||
end
|
||||
end
|
||||
|
||||
local function ID_IS_TAG(world, id)
|
||||
return not world_has_one_inline(world, ECS_ENTITY_T_HI(id), EcsComponent)
|
||||
local function ID_IS_TAG(world: World, id)
|
||||
if ECS_IS_PAIR(id) then
|
||||
id = ecs_pair_first(world, id)
|
||||
end
|
||||
return not world_has_one_inline(world, id, EcsComponent)
|
||||
end
|
||||
|
||||
World.query = function(world: World, ...)
|
||||
|
@ -1555,14 +1626,12 @@ if _G.__JECS_DEBUG then
|
|||
World.set = function(world: World, entity: i53, id: i53, value: any): ()
|
||||
local is_tag = ID_IS_TAG(world, id)
|
||||
if is_tag and value == nil then
|
||||
world_add(world, entity, id)
|
||||
local _1 = get_name(world, entity)
|
||||
local _2 = get_name(world, id)
|
||||
local why = "cannot set component value to nil"
|
||||
throw(why)
|
||||
return
|
||||
elseif value ~= nil and is_tag then
|
||||
world_add(world, entity, id)
|
||||
local _1 = get_name(world, entity)
|
||||
local _2 = get_name(world, id)
|
||||
local why = `cannot set a component value because {_2} is a tag`
|
||||
|
@ -1579,7 +1648,6 @@ if _G.__JECS_DEBUG then
|
|||
local _1 = get_name(world, entity)
|
||||
local _2 = get_name(world, id)
|
||||
throw("You provided a value when none was expected. " .. `Did you mean to use "world:add({_1}, {_2})"`)
|
||||
return
|
||||
end
|
||||
|
||||
world_add(world, entity, id)
|
||||
|
@ -1609,14 +1677,17 @@ if _G.__JECS_DEBUG then
|
|||
end
|
||||
|
||||
function World.new()
|
||||
local entity_index: EntityIndex = {
|
||||
dense_array = {} :: { [i24]: i53 },
|
||||
sparse_array = {} :: { [i53]: Record },
|
||||
alive_count = 0,
|
||||
max_id = 0,
|
||||
}
|
||||
local self = setmetatable({
|
||||
archetypeIndex = {} :: { [string]: Archetype },
|
||||
archetypes = {} :: Archetypes,
|
||||
componentIndex = {} :: ComponentIndex,
|
||||
entityIndex = {
|
||||
dense = {} :: { [i24]: i53 },
|
||||
sparse = {} :: { [i53]: Record },
|
||||
} :: EntityIndex,
|
||||
entity_index = entity_index,
|
||||
nextArchetypeId = 0 :: number,
|
||||
nextComponentId = 0 :: number,
|
||||
nextEntityId = 0 :: number,
|
||||
|
@ -1625,9 +1696,14 @@ function World.new()
|
|||
|
||||
self.ROOT_ARCHETYPE = archetype_create(self, {}, "")
|
||||
|
||||
for i = 1, HI_COMPONENT_ID do
|
||||
local e = entity_index_new_id(entity_index)
|
||||
world_add(self, e, EcsComponent)
|
||||
end
|
||||
|
||||
for i = HI_COMPONENT_ID + 1, EcsRest do
|
||||
-- Initialize built-in components
|
||||
entity_index_new_id(self.entityIndex, i)
|
||||
entity_index_new_id(entity_index)
|
||||
end
|
||||
|
||||
world_add(self, EcsName, EcsComponent)
|
||||
|
@ -1692,7 +1768,7 @@ export type World = {
|
|||
archetypeIndex: { [string]: Archetype },
|
||||
archetypes: Archetypes,
|
||||
componentIndex: ComponentIndex,
|
||||
entityIndex: EntityIndex,
|
||||
entity_index: EntityIndex,
|
||||
ROOT_ARCHETYPE: Archetype,
|
||||
|
||||
nextComponentId: number,
|
||||
|
@ -1791,7 +1867,7 @@ return {
|
|||
Name = EcsName :: Entity<string>,
|
||||
Rest = EcsRest :: Entity,
|
||||
|
||||
pair = ECS_PAIR,
|
||||
pair = ECS_PAIR :: <P, O>(first: P, second: O) -> Pair<P, O>,
|
||||
|
||||
-- Inwards facing API for testing
|
||||
ECS_ID = ECS_ENTITY_T_LO,
|
||||
|
@ -1819,4 +1895,9 @@ return {
|
|||
create_edge_for_remove = create_edge_for_remove,
|
||||
archetype_traverse_add = archetype_traverse_add,
|
||||
archetype_traverse_remove = archetype_traverse_remove,
|
||||
|
||||
entity_index_try_get = entity_index_try_get,
|
||||
entity_index_try_get_any = entity_index_try_get_any,
|
||||
entity_index_is_alive = entity_index_is_alive,
|
||||
entity_index_new_id = entity_index_new_id,
|
||||
}
|
||||
|
|
183
test/gen.luau
Normal file
183
test/gen.luau
Normal file
|
@ -0,0 +1,183 @@
|
|||
type i53 = number
|
||||
type i24 = number
|
||||
|
||||
type Ty = { i53 }
|
||||
type ArchetypeId = number
|
||||
|
||||
type Column = { any }
|
||||
|
||||
type Map<K, V> = { [K]: V }
|
||||
|
||||
type GraphEdge = {
|
||||
from: Archetype,
|
||||
to: Archetype?,
|
||||
prev: GraphEdge?,
|
||||
next: GraphEdge?,
|
||||
id: number,
|
||||
}
|
||||
|
||||
type GraphEdges = Map<i53, GraphEdge>
|
||||
|
||||
type GraphNode = {
|
||||
add: GraphEdges,
|
||||
remove: GraphEdges,
|
||||
refs: GraphEdge,
|
||||
}
|
||||
|
||||
type ArchetypeRecord = {
|
||||
count: number,
|
||||
column: number,
|
||||
}
|
||||
|
||||
export type Archetype = {
|
||||
id: number,
|
||||
node: GraphNode,
|
||||
types: Ty,
|
||||
type: string,
|
||||
entities: { number },
|
||||
columns: { Column },
|
||||
records: { ArchetypeRecord },
|
||||
}
|
||||
type Record = {
|
||||
archetype: Archetype,
|
||||
row: number,
|
||||
dense: i24,
|
||||
}
|
||||
|
||||
type EntityIndex = {
|
||||
dense_array: Map<i24, i53>,
|
||||
sparse_array: Map<i53, Record>,
|
||||
sparse_count: number,
|
||||
alive_count: number,
|
||||
max_id: number,
|
||||
}
|
||||
|
||||
local ECS_PAIR_FLAG = 0x8
|
||||
local ECS_ID_FLAGS_MASK = 0x10
|
||||
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
|
||||
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
|
||||
|
||||
-- HIGH 24 bits LOW 24 bits
|
||||
local function ECS_GENERATION(e: i53): i24
|
||||
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
|
||||
end
|
||||
|
||||
local function ECS_COMBINE(source: number, target: number): i53
|
||||
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
|
||||
end
|
||||
|
||||
local function ECS_GENERATION_INC(e: i53)
|
||||
if e > ECS_ENTITY_MASK then
|
||||
local flags = e // ECS_ID_FLAGS_MASK
|
||||
local id = flags // ECS_ENTITY_MASK
|
||||
local generation = flags % ECS_GENERATION_MASK
|
||||
print(generation)
|
||||
return ECS_COMBINE(id, generation + 1)
|
||||
end
|
||||
return ECS_COMBINE(e, 1)
|
||||
end
|
||||
local function ECS_ENTITY_T_LO(e: i53): i24
|
||||
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
|
||||
end
|
||||
|
||||
local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record?
|
||||
local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
|
||||
|
||||
if not r or r.dense == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return r
|
||||
end
|
||||
|
||||
local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record?
|
||||
local r = entity_index_try_get_any(entity_index, entity)
|
||||
if r then
|
||||
local r_dense = r.dense
|
||||
if r_dense > entity_index.alive_count then
|
||||
return nil
|
||||
end
|
||||
if entity_index.dense_array[r_dense] ~= entity then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
local function entity_index_get_alive(entity_index: EntityIndex, entity: number): number
|
||||
local r = entity_index_try_get_any(entity_index, entity)
|
||||
if r then
|
||||
return entity_index.dense_array[r.dense]
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function entity_index_remove(entity_index: EntityIndex, entity: number)
|
||||
local r = entity_index_try_get(entity_index, entity)
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
local dense_array = entity_index.dense_array
|
||||
local index_of_deleted_entity = r.dense
|
||||
local last_entity_alive_at_index = entity_index.alive_count
|
||||
entity_index.alive_count -= 1
|
||||
|
||||
local last_alive_entity = dense_array[last_entity_alive_at_index]
|
||||
local r_swap = entity_index_try_get_any(entity_index, last_alive_entity) :: Record
|
||||
r_swap.dense = index_of_deleted_entity
|
||||
r.archetype = nil :: any
|
||||
r.row = nil :: any
|
||||
r.dense = last_entity_alive_at_index
|
||||
|
||||
dense_array[index_of_deleted_entity] = last_alive_entity
|
||||
dense_array[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
|
||||
end
|
||||
|
||||
local function entity_index_new_id(entity_index: EntityIndex): i53
|
||||
local dense_array = entity_index.dense_array
|
||||
if entity_index.alive_count ~= #dense_array then
|
||||
entity_index.alive_count += 1
|
||||
local id = dense_array[entity_index.alive_count]
|
||||
return id
|
||||
end
|
||||
|
||||
entity_index.max_id += 1
|
||||
local id = entity_index.max_id
|
||||
entity_index.alive_count += 1
|
||||
|
||||
dense_array[entity_index.alive_count] = id
|
||||
entity_index.sparse_array[id] = {
|
||||
dense = entity_index.alive_count,
|
||||
} :: Record
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
local function entity_index_is_alive(entity_index: EntityIndex, entity: number)
|
||||
return entity_index_try_get(entity_index, entity) ~= nil
|
||||
end
|
||||
|
||||
local eidx = {
|
||||
alive_count = 0,
|
||||
max_id = 0,
|
||||
sparse_array = {} :: { Record },
|
||||
sparse_count = 0,
|
||||
dense_array = {} :: { i53 },
|
||||
}
|
||||
local e1v0 = entity_index_new_id(eidx, "e1v0")
|
||||
local e2v0 = entity_index_new_id(eidx, "e2v0")
|
||||
local e3v0 = entity_index_new_id(eidx, "e3v0")
|
||||
local e4v0 = entity_index_new_id(eidx, "e4v0")
|
||||
local e5v0 = entity_index_new_id(eidx, "e5v0")
|
||||
|
||||
local e6v0 = entity_index_new_id(eidx)
|
||||
entity_index_remove(eidx, e6v0)
|
||||
local e6v1 = entity_index_new_id(eidx)
|
||||
entity_index_remove(eidx, e6v1)
|
||||
local e6v2 = entity_index_new_id(eidx)
|
||||
print(ECS_ENTITY_T_LO(e6v2), ECS_GENERATION(e6v2))
|
||||
|
||||
print("-----")
|
||||
local e2 = ECS_GENERATION_INC(ECS_GENERATION_INC(269))
|
||||
print("-----")
|
||||
print(ECS_ENTITY_T_LO(e2), ECS_GENERATION(e2))
|
157
test/lol.luau
Normal file
157
test/lol.luau
Normal file
|
@ -0,0 +1,157 @@
|
|||
local c = {
|
||||
white_underline = function(s: any)
|
||||
return `\27[1;4m{s}\27[0m`
|
||||
end,
|
||||
|
||||
white = function(s: any)
|
||||
return `\27[37;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
green = function(s: any)
|
||||
return `\27[32;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
red = function(s: any)
|
||||
return `\27[31;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
yellow = function(s: any)
|
||||
return `\27[33;1m{s}\27[0m`
|
||||
end,
|
||||
|
||||
red_highlight = function(s: any)
|
||||
return `\27[41;1;30m{s}\27[0m`
|
||||
end,
|
||||
|
||||
green_highlight = function(s: any)
|
||||
return `\27[42;1;30m{s}\27[0m`
|
||||
end,
|
||||
|
||||
gray = function(s: any)
|
||||
return `\27[30;1m{s}\27[0m`
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
local ECS_PAIR_FLAG = 0x8
|
||||
local ECS_ID_FLAGS_MASK = 0x10
|
||||
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
|
||||
local ECS_GENERATION_MASK = bit32.lshift(1, 16)
|
||||
|
||||
type i53 = number
|
||||
type i24 = number
|
||||
|
||||
local function ECS_ENTITY_T_LO(e: i53): i24
|
||||
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
|
||||
end
|
||||
|
||||
local function ECS_GENERATION(e: i53): i24
|
||||
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
|
||||
end
|
||||
|
||||
local ECS_ID = ECS_ENTITY_T_LO
|
||||
|
||||
local function ECS_COMBINE(source: number, target: number): i53
|
||||
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
|
||||
end
|
||||
|
||||
local function ECS_GENERATION_INC(e: i53)
|
||||
if e > ECS_ENTITY_MASK then
|
||||
local flags = e // ECS_ID_FLAGS_MASK
|
||||
local id = flags // ECS_ENTITY_MASK
|
||||
local generation = flags % ECS_GENERATION_MASK
|
||||
|
||||
local next_gen = generation + 1
|
||||
if next_gen > ECS_GENERATION_MASK then
|
||||
return id
|
||||
end
|
||||
|
||||
return ECS_COMBINE(id, next_gen) + flags
|
||||
end
|
||||
return ECS_COMBINE(e, 1)
|
||||
end
|
||||
|
||||
local function bl()
|
||||
print("")
|
||||
end
|
||||
|
||||
local function pe(e)
|
||||
local gen = ECS_GENERATION(e)
|
||||
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
|
||||
end
|
||||
|
||||
local function dprint(tbl: { [number]: number })
|
||||
bl()
|
||||
print("--------")
|
||||
for i, e in tbl do
|
||||
print("| "..pe(e).." |")
|
||||
print("--------")
|
||||
end
|
||||
bl()
|
||||
end
|
||||
|
||||
local max_id = 0
|
||||
local alive_count = 0
|
||||
local dense = {}
|
||||
local sparse = {}
|
||||
local function alloc()
|
||||
if alive_count ~= #dense then
|
||||
alive_count += 1
|
||||
print("*recycled", pe(dense[alive_count]))
|
||||
return dense[alive_count]
|
||||
end
|
||||
max_id += 1
|
||||
local id = max_id
|
||||
alive_count += 1
|
||||
dense[alive_count] = id
|
||||
sparse[id] = {
|
||||
dense = alive_count
|
||||
}
|
||||
print("*allocated", pe(id))
|
||||
return id
|
||||
end
|
||||
|
||||
local function remove(entity)
|
||||
local id = ECS_ID(entity)
|
||||
local r = sparse[id]
|
||||
local index_of_deleted_entity = r.dense
|
||||
local last_entity_alive_at_index = alive_count -- last entity alive
|
||||
alive_count -= 1
|
||||
local last_alive_entity = dense[last_entity_alive_at_index]
|
||||
local r_swap = sparse[ECS_ID(last_alive_entity)]
|
||||
r_swap.dense = r.dense
|
||||
r.dense = last_entity_alive_at_index
|
||||
dense[index_of_deleted_entity] = last_alive_entity
|
||||
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
|
||||
end
|
||||
|
||||
local function alive(e)
|
||||
local r = sparse[ECS_ID(e)]
|
||||
|
||||
return dense[r.dense] == e
|
||||
end
|
||||
|
||||
local function pa(e)
|
||||
print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`)
|
||||
end
|
||||
|
||||
local tprint = require("@testkit").print
|
||||
local e1v0 = alloc()
|
||||
local e2v0 = alloc()
|
||||
local e3v0 = alloc()
|
||||
local e4v0 = alloc()
|
||||
local e5v0 = alloc()
|
||||
pa(e1v0)
|
||||
pa(e4v0)
|
||||
remove(e5v0)
|
||||
pa(e5v0)
|
||||
|
||||
local e5v1 = alloc()
|
||||
pa(e5v0)
|
||||
pa(e5v1)
|
||||
pa(e2v0)
|
||||
print(ECS_ID(e2v0))
|
||||
|
||||
dprint(dense)
|
||||
remove(e2v0)
|
||||
dprint(dense)
|
|
@ -1,4 +1,4 @@
|
|||
local jecs: typeof(require("../jecs/src")) = require("@jecs")
|
||||
local jecs = require("@jecs")
|
||||
|
||||
local testkit = require("@testkit")
|
||||
local BENCH, START = testkit.benchmark()
|
||||
|
@ -7,9 +7,11 @@ 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 pair = jecs.pair
|
||||
local getAlive = jecs.entity_index_get_alive
|
||||
local ecs_pair_first = jecs.pair_first
|
||||
local ecs_pair_second = jecs.pair_second
|
||||
local entity_index_try_get_any = jecs.entity_index_try_get_any
|
||||
local entity_index_get_alive = jecs.entity_index_get_alive
|
||||
local entity_index_is_alive = jecs.entity_index_is_alive
|
||||
local world_new = jecs.World.new
|
||||
|
||||
local TEST, CASE, CHECK, FINISH, SKIP, FOCUS = testkit.test()
|
||||
|
@ -29,7 +31,7 @@ type World = jecs.WorldShim
|
|||
|
||||
local function debug_world_inspect(world)
|
||||
local function record(e)
|
||||
return world.entityIndex.sparse[e]
|
||||
return entity_index_try_get_any(world.entity_index, e)
|
||||
end
|
||||
local function tbl(e)
|
||||
return record(e).archetype
|
||||
|
@ -168,7 +170,6 @@ TEST("world:entity()", function()
|
|||
local world = jecs.World.new()
|
||||
local e = world:entity()
|
||||
CHECK(ECS_ID(e) == 1 + jecs.Rest)
|
||||
CHECK(getAlive(world.entityIndex, ECS_ID(e)) == e)
|
||||
CHECK(ECS_GENERATION(e) == 0) -- 0
|
||||
e = ECS_GENERATION_INC(e)
|
||||
CHECK(ECS_GENERATION(e) == 1) -- 1
|
||||
|
@ -190,6 +191,36 @@ TEST("world:entity()", function()
|
|||
CHECK(ecs_pair_first(world, pair) == e2)
|
||||
CHECK(ecs_pair_second(world, pair) == e3)
|
||||
end
|
||||
|
||||
do CASE "Recycling"
|
||||
local world = world_new()
|
||||
local e = world:entity()
|
||||
world:delete(e)
|
||||
local e1 = world:entity()
|
||||
world:delete(e1)
|
||||
local e2 = world:entity()
|
||||
CHECK(ECS_ID(e2) == e)
|
||||
CHECK(ECS_GENERATION(e2) == 2)
|
||||
CHECK(world:contains(e2))
|
||||
CHECK(not world:contains(e1))
|
||||
CHECK(not world:contains(e))
|
||||
end
|
||||
|
||||
do CASE "Recycling max generation"
|
||||
local world = world_new()
|
||||
local pin = jecs.Rest + 1
|
||||
for i = 1, 2^16-1 do
|
||||
local e = world:entity()
|
||||
world:delete(e)
|
||||
end
|
||||
local e = world:entity()
|
||||
CHECK(ECS_ID(e) == pin)
|
||||
CHECK(ECS_GENERATION(e) == 2^16-1)
|
||||
world:delete(e)
|
||||
e = world:entity()
|
||||
CHECK(ECS_ID(e) == pin)
|
||||
CHECK(ECS_GENERATION(e) == 0)
|
||||
end
|
||||
end)
|
||||
|
||||
TEST("world:set()", function()
|
||||
|
@ -878,9 +909,10 @@ TEST("world:clear()", function()
|
|||
CHECK(archetype_entities[1] == _e)
|
||||
CHECK(archetype_entities[2] == _e1)
|
||||
|
||||
local sparse_array = world.entityIndex.sparse
|
||||
local e_record = sparse_array[e]
|
||||
local e1_record = sparse_array[e1]
|
||||
local e_record = entity_index_try_get_any(
|
||||
world.entity_index, e)
|
||||
local e1_record = entity_index_try_get_any(
|
||||
world.entity_index, e1)
|
||||
CHECK(e_record.archetype == archetype)
|
||||
CHECK(e1_record.archetype == archetype)
|
||||
CHECK(e1_record.row == 2)
|
||||
|
@ -1085,6 +1117,7 @@ TEST("world:delete", function()
|
|||
for i, friend in friends do
|
||||
CHECK(not world:has(friend, pair(FriendsWith, e)))
|
||||
CHECK(world:has(friend, Health))
|
||||
CHECK(world:contains(friend))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "ukendio/jecs"
|
||||
version = "0.3.2"
|
||||
version = "0.4.0-rc.0"
|
||||
registry = "https://github.com/UpliftGames/wally-index"
|
||||
realm = "shared"
|
||||
license = "MIT"
|
||||
|
|
Loading…
Reference in a new issue