mirror of
https://github.com/Ukendio/jecs.git
synced 2025-04-25 09:30:03 +00:00
commit
b75dc91a6a
4 changed files with 181 additions and 672 deletions
168
lib/init.lua
168
lib/init.lua
|
@ -35,9 +35,20 @@ type EntityIndex = { [i24]: Record }
|
||||||
type ComponentIndex = { [i24]: ArchetypeMap}
|
type ComponentIndex = { [i24]: ArchetypeMap}
|
||||||
|
|
||||||
type ArchetypeRecord = number
|
type ArchetypeRecord = number
|
||||||
type ArchetypeMap = { map: { [ArchetypeId]: ArchetypeRecord } , size: number }
|
type ArchetypeMap = { sparse: { [ArchetypeId]: ArchetypeRecord } , size: number }
|
||||||
type Archetypes = { [ArchetypeId]: Archetype }
|
type Archetypes = { [ArchetypeId]: Archetype }
|
||||||
|
|
||||||
|
type ArchetypeDiff = {
|
||||||
|
added: Ty,
|
||||||
|
removed: Ty,
|
||||||
|
}
|
||||||
|
|
||||||
|
local HI_COMPONENT_ID = 256
|
||||||
|
local ON_ADD = HI_COMPONENT_ID + 1
|
||||||
|
local ON_REMOVE = HI_COMPONENT_ID + 2
|
||||||
|
local ON_SET = HI_COMPONENT_ID + 3
|
||||||
|
local REST = HI_COMPONENT_ID + 4
|
||||||
|
|
||||||
local function transitionArchetype(
|
local function transitionArchetype(
|
||||||
entityIndex: EntityIndex,
|
entityIndex: EntityIndex,
|
||||||
destinationArchetype: Archetype,
|
destinationArchetype: Archetype,
|
||||||
|
@ -89,14 +100,7 @@ local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archet
|
||||||
end
|
end
|
||||||
|
|
||||||
local function hash(arr): string | number
|
local function hash(arr): string | number
|
||||||
if true then
|
return table.concat(arr, "_")
|
||||||
return table.concat(arr, "_")
|
|
||||||
end
|
|
||||||
local hashed = 5381
|
|
||||||
for i = 1, #arr do
|
|
||||||
hashed = ((bit32.lshift(hashed, 5)) + hashed) + arr[i]
|
|
||||||
end
|
|
||||||
return hashed
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
||||||
|
@ -107,11 +111,11 @@ local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archet
|
||||||
local destinationId = destinationIds[i]
|
local destinationId = destinationIds[i]
|
||||||
|
|
||||||
if not componentIndex[destinationId] then
|
if not componentIndex[destinationId] then
|
||||||
componentIndex[destinationId] = { size = 0, map = {} }
|
componentIndex[destinationId] = { size = 0, sparse = {} }
|
||||||
end
|
end
|
||||||
|
|
||||||
local archetypesMap = componentIndex[destinationId]
|
local archetypesMap = componentIndex[destinationId]
|
||||||
archetypesMap.map[to.id] = i
|
archetypesMap.sparse[to.id] = i
|
||||||
to.records[destinationId] = i
|
to.records[destinationId] = i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -152,24 +156,47 @@ function World.new()
|
||||||
componentIndex = {},
|
componentIndex = {},
|
||||||
archetypes = {},
|
archetypes = {},
|
||||||
archetypeIndex = {},
|
archetypeIndex = {},
|
||||||
ROOT_ARCHETYPE = nil :: Archetype?,
|
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
||||||
nextId = 0,
|
nextId = 0,
|
||||||
nextArchetypeId = 0
|
nextArchetypeId = 0,
|
||||||
|
hooks = {
|
||||||
|
[ON_ADD] = {}
|
||||||
|
}
|
||||||
}, World)
|
}, World)
|
||||||
self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
|
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function emit(world, eventDescription)
|
||||||
|
local event = eventDescription.event
|
||||||
|
|
||||||
|
table.insert(world.hooks[event], {
|
||||||
|
ids = eventDescription.ids,
|
||||||
|
archetype = eventDescription.archetype,
|
||||||
|
otherArchetype = eventDescription.otherArchetype,
|
||||||
|
offset = eventDescription.offset
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
||||||
|
if #added > 0 then
|
||||||
|
emit(world, {
|
||||||
|
event = ON_ADD,
|
||||||
|
ids = added,
|
||||||
|
archetype = archetype,
|
||||||
|
otherArchetype = otherArchetype,
|
||||||
|
offset = row,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
type World = typeof(World.new())
|
type World = typeof(World.new())
|
||||||
|
|
||||||
local function ensureArchetype(world: World, types, prev)
|
local function ensureArchetype(world: World, types, prev)
|
||||||
if #types < 1 then
|
if #types < 1 then
|
||||||
|
|
||||||
if not world.ROOT_ARCHETYPE then
|
|
||||||
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
|
||||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
|
||||||
return ROOT_ARCHETYPE
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
local ty = hash(types)
|
local ty = hash(types)
|
||||||
local archetype = world.archetypeIndex[ty]
|
local archetype = world.archetypeIndex[ty]
|
||||||
|
@ -213,8 +240,14 @@ local function ensureEdge(archetype: Archetype, componentId: i53)
|
||||||
return archetype.edges[componentId]
|
return archetype.edges[componentId]
|
||||||
end
|
end
|
||||||
|
|
||||||
local function archetypeTraverseAdd(world: World, componentId: i53, archetype: Archetype?): Archetype
|
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
||||||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
if not from then
|
||||||
|
if not world.ROOT_ARCHETYPE then
|
||||||
|
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
||||||
|
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
||||||
|
end
|
||||||
|
from = world.ROOT_ARCHETYPE
|
||||||
|
end
|
||||||
local edge = ensureEdge(from, componentId)
|
local edge = ensureEdge(from, componentId)
|
||||||
|
|
||||||
if not edge.add then
|
if not edge.add then
|
||||||
|
@ -224,17 +257,16 @@ local function archetypeTraverseAdd(world: World, componentId: i53, archetype: A
|
||||||
return edge.add
|
return edge.add
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.ensureRecord(world: World, entityId: i53)
|
local function ensureRecord(entityIndex, entityId: i53): Record
|
||||||
local entityIndex = world.entityIndex
|
|
||||||
local id = entityId
|
local id = entityId
|
||||||
if not entityIndex[id] then
|
if not entityIndex[id] then
|
||||||
entityIndex[id] = {} :: Record
|
entityIndex[id] = {}
|
||||||
end
|
end
|
||||||
return entityIndex[id]
|
return entityIndex[id] :: Record
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
local record = world:ensureRecord(entityId)
|
local record = ensureRecord(world.entityIndex, entityId)
|
||||||
local sourceArchetype = record.archetype
|
local sourceArchetype = record.archetype
|
||||||
local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
|
local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
|
||||||
|
|
||||||
|
@ -244,6 +276,7 @@ function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
||||||
-- if it has any components, then it wont be the root archetype
|
-- if it has any components, then it wont be the root archetype
|
||||||
if #destinationArchetype.types > 0 then
|
if #destinationArchetype.types > 0 then
|
||||||
newEntity(entityId, record, destinationArchetype)
|
newEntity(entityId, record, destinationArchetype)
|
||||||
|
onNotifyAdd(world, destinationArchetype, sourceArchetype, record.row, { componentId })
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
local archetypeRecord = destinationArchetype.records[componentId]
|
local archetypeRecord = destinationArchetype.records[componentId]
|
||||||
|
@ -265,7 +298,7 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
function World.remove(world: World, entityId: i53, componentId: i53)
|
||||||
local record = world:ensureRecord(entityId)
|
local record = ensureRecord(world.entityIndex, entityId)
|
||||||
local sourceArchetype = record.archetype
|
local sourceArchetype = record.archetype
|
||||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
||||||
|
|
||||||
|
@ -276,7 +309,7 @@ end
|
||||||
|
|
||||||
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
|
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
|
||||||
local archetype = record.archetype
|
local archetype = record.archetype
|
||||||
local archetypeRecord = componentIndex[componentId].map[archetype.id]
|
local archetypeRecord = componentIndex[componentId].sparse[archetype.id]
|
||||||
|
|
||||||
if not archetypeRecord then
|
if not archetypeRecord then
|
||||||
return nil
|
return nil
|
||||||
|
@ -308,10 +341,7 @@ function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.entity(world: World)
|
|
||||||
world.nextId += 1
|
|
||||||
return world.nextId
|
|
||||||
end
|
|
||||||
|
|
||||||
local function noop(): any
|
local function noop(): any
|
||||||
return function()
|
return function()
|
||||||
|
@ -328,10 +358,11 @@ local function getSmallestMap(componentIndex, components)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return s.map
|
return s.sparse
|
||||||
end
|
end
|
||||||
|
|
||||||
function World.query(world: World, ...: i53): (() -> (number, ...any)) | () -> ()
|
function World.query(world: World, ...: i53): (() -> (number, ...any)) | () -> ()
|
||||||
|
|
||||||
local compatibleArchetypes = {}
|
local compatibleArchetypes = {}
|
||||||
local components = { ... }
|
local components = { ... }
|
||||||
local archetypes = world.archetypes
|
local archetypes = world.archetypes
|
||||||
|
@ -451,6 +482,71 @@ function World.query(world: World, ...: i53): (() -> (number, ...any)) | () -> (
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
function World.component(world: World)
|
||||||
World = World
|
local id = world.nextId + 1
|
||||||
}
|
if id > HI_COMPONENT_ID then
|
||||||
|
error("Too many components")
|
||||||
|
end
|
||||||
|
return id
|
||||||
|
end
|
||||||
|
|
||||||
|
function World.entity(world: World)
|
||||||
|
world.nextId += 1
|
||||||
|
return world.nextId + REST
|
||||||
|
end
|
||||||
|
|
||||||
|
function World.observer(world: World, ...)
|
||||||
|
local componentIds = { ... }
|
||||||
|
|
||||||
|
return {
|
||||||
|
event = function(event)
|
||||||
|
local hook = world.hooks[event]
|
||||||
|
world.hooks[event] = nil
|
||||||
|
|
||||||
|
local last, change
|
||||||
|
return function()
|
||||||
|
last, change = next(hook, last)
|
||||||
|
if not last then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local matched = false
|
||||||
|
|
||||||
|
while not matched do
|
||||||
|
local skip = false
|
||||||
|
for _, id in change.ids do
|
||||||
|
if not table.find(componentIds, id) then
|
||||||
|
skip = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if skip then
|
||||||
|
last, change = next(hook, last)
|
||||||
|
continue
|
||||||
|
end
|
||||||
|
|
||||||
|
matched = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local queryOutput = {}
|
||||||
|
local row = change.offset
|
||||||
|
local archetype = change.archetype
|
||||||
|
local columns = archetype.columns
|
||||||
|
local archetypeRecords = archetype.records
|
||||||
|
for _, id in componentIds do
|
||||||
|
table.insert(queryOutput, columns[archetypeRecords[id]][row])
|
||||||
|
end
|
||||||
|
|
||||||
|
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.freeze({
|
||||||
|
World = World,
|
||||||
|
ON_ADD = ON_ADD,
|
||||||
|
ON_REMOVE = ON_REMOVE,
|
||||||
|
ON_SET = ON_SET
|
||||||
|
})
|
|
@ -1,7 +1,8 @@
|
||||||
local ecs = require(script.Parent).World.new()
|
local jecs = require(script.Parent)
|
||||||
|
local world = jecs.World.new()
|
||||||
|
|
||||||
local A, B, C, D = ecs:entity(), ecs:entity(), ecs:entity(), ecs:entity()
|
local A, B, C, D = world:entity(), world:entity(), world:entity(), world:entity()
|
||||||
local E, F, G, H = ecs:entity(), ecs:entity(), ecs:entity(), ecs:entity()
|
local E, F, G, H = world:entity(), world:entity(), world:entity(), world:entity()
|
||||||
print("A", A)
|
print("A", A)
|
||||||
print("B", B)
|
print("B", B)
|
||||||
print("C", C)
|
print("C", C)
|
||||||
|
@ -20,42 +21,42 @@ end
|
||||||
|
|
||||||
local hm = 0
|
local hm = 0
|
||||||
for i = 1, N do
|
for i = 1, N do
|
||||||
local entity = ecs:entity()
|
local entity = world:entity()
|
||||||
local combination = ""
|
local combination = ""
|
||||||
|
|
||||||
if flip() then
|
if flip() then
|
||||||
combination ..= "2_"
|
combination ..= "2_"
|
||||||
ecs:set(entity, B, { value = true})
|
world:set(entity, B, { value = true})
|
||||||
end
|
end
|
||||||
if flip() then
|
if flip() then
|
||||||
combination ..= "3_"
|
combination ..= "3_"
|
||||||
ecs:set(entity, C, { value = true})
|
world:set(entity, C, { value = true})
|
||||||
end
|
end
|
||||||
if flip() then
|
if flip() then
|
||||||
combination ..= "4_"
|
combination ..= "4_"
|
||||||
ecs:set(entity, D, { value = true})
|
world:set(entity, D, { value = true})
|
||||||
end
|
end
|
||||||
if flip() then
|
if flip() then
|
||||||
combination ..= "5_"
|
combination ..= "5_"
|
||||||
ecs:set(entity, E, { value = true})
|
world:set(entity, E, { value = true})
|
||||||
end
|
end
|
||||||
if flip() then
|
if flip() then
|
||||||
combination ..= "6_"
|
combination ..= "6_"
|
||||||
ecs:set(entity, F, { value = true})
|
world:set(entity, F, { value = true})
|
||||||
end
|
end
|
||||||
if flip() then
|
if flip() then
|
||||||
combination ..= "7_"
|
combination ..= "7_"
|
||||||
ecs:set(entity, G, { value = true})
|
world:set(entity, G, { value = true})
|
||||||
end
|
end
|
||||||
if flip() then
|
if flip() then
|
||||||
combination ..= "8"
|
combination ..= "8"
|
||||||
ecs:set(entity, H, { value = true})
|
world:set(entity, H, { value = true})
|
||||||
end
|
end
|
||||||
|
|
||||||
if #combination == 7 then
|
if #combination == 7 then
|
||||||
combination = "1_" .. combination
|
combination = "1_" .. combination
|
||||||
common += 1
|
common += 1
|
||||||
ecs:set(entity, A, { value = true})
|
world:set(entity, A, { value = true})
|
||||||
end
|
end
|
||||||
|
|
||||||
if combination:find("2")
|
if combination:find("2")
|
||||||
|
@ -82,42 +83,57 @@ end
|
||||||
return function()
|
return function()
|
||||||
describe("World", function()
|
describe("World", function()
|
||||||
it("should add component", function()
|
it("should add component", function()
|
||||||
local id = ecs:entity()
|
local id = world:entity()
|
||||||
ecs:set(id, A, true)
|
world:set(id, A, true)
|
||||||
ecs:set(id, B, 1)
|
world:set(id, B, 1)
|
||||||
|
|
||||||
local id1 = ecs:entity()
|
local id1 = world:entity()
|
||||||
ecs:set(id1, A, "hello")
|
world:set(id1, A, "hello")
|
||||||
expect(ecs:get(id, A)).to.equal(true)
|
expect(world:get(id, A)).to.equal(true)
|
||||||
expect(ecs:get(id, B)).to.equal(1)
|
expect(world:get(id, B)).to.equal(1)
|
||||||
expect(ecs:get(id1, A)).to.equal("hello")
|
expect(world:get(id1, A)).to.equal("hello")
|
||||||
end)
|
end)
|
||||||
it("should remove component", function()
|
it("should remove component", function()
|
||||||
local id = ecs:entity()
|
local id = world:entity()
|
||||||
ecs:set(id, A, true)
|
world:set(id, A, true)
|
||||||
ecs:set(id, B, 1000)
|
world:set(id, B, 1000)
|
||||||
ecs:remove(id, A, false)
|
world:remove(id, A, false)
|
||||||
|
|
||||||
expect(ecs:get(id, A)).to.equal(nil)
|
expect(world:get(id, A)).to.equal(nil)
|
||||||
end)
|
end)
|
||||||
it("should override component data", function()
|
it("should override component data", function()
|
||||||
|
|
||||||
local id = ecs:entity()
|
local id = world:entity()
|
||||||
ecs:set(id, A, true)
|
world:set(id, A, true)
|
||||||
expect(ecs:get(id, A)).to.equal(true)
|
expect(world:get(id, A)).to.equal(true)
|
||||||
|
|
||||||
ecs:set(id, A, false)
|
world:set(id, A, false)
|
||||||
expect(ecs:get(id, A)).to.equal(false)
|
expect(world:get(id, A)).to.equal(false)
|
||||||
|
|
||||||
end)
|
end)
|
||||||
it("query", function()
|
it("query", function()
|
||||||
local added = 0
|
local added = 0
|
||||||
for _ in ecs:query(B, C, D, F) do
|
for _ in world:query(B, C, D, F) do
|
||||||
added += 1
|
added += 1
|
||||||
end
|
end
|
||||||
expect(added).to.equal(hm)
|
expect(added).to.equal(hm)
|
||||||
print(added, hm)
|
print(added, hm)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it("track changes", function()
|
||||||
|
local Position = world:entity()
|
||||||
|
|
||||||
|
local moving = world:entity()
|
||||||
|
world:set(moving, Position, Vector3.new(1, 2, 3))
|
||||||
|
|
||||||
|
local count = 0
|
||||||
|
|
||||||
|
for e, position in world:observer(Position).event(jecs.ON_ADD) do
|
||||||
|
count += 1
|
||||||
|
expect(e).to.equal(moving)
|
||||||
|
expect(position).to.equal(Vector3.new(1, 2, 3))
|
||||||
|
end
|
||||||
|
expect(count).to.equal(1)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
|
@ -1,541 +0,0 @@
|
||||||
--!optimize 2
|
|
||||||
--!native
|
|
||||||
--!strict
|
|
||||||
--draft 4
|
|
||||||
|
|
||||||
type i53 = number
|
|
||||||
type i24 = number
|
|
||||||
|
|
||||||
type Ty = { i53 }
|
|
||||||
type ArchetypeId = number
|
|
||||||
|
|
||||||
type Column = { any }
|
|
||||||
|
|
||||||
type Archetype = {
|
|
||||||
id: number,
|
|
||||||
edges: {
|
|
||||||
[i24]: {
|
|
||||||
add: Archetype,
|
|
||||||
remove: Archetype,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
types: Ty,
|
|
||||||
type: string | number,
|
|
||||||
entities: { number },
|
|
||||||
columns: { Column },
|
|
||||||
records: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
type Record = {
|
|
||||||
archetype: Archetype,
|
|
||||||
row: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
type EntityIndex = { [i24]: Record }
|
|
||||||
type ComponentIndex = { [i24]: ArchetypeMap}
|
|
||||||
|
|
||||||
type ArchetypeRecord = number
|
|
||||||
type ArchetypeMap = { [ArchetypeId]: ArchetypeRecord }
|
|
||||||
type Archetypes = { [ArchetypeId]: Archetype }
|
|
||||||
|
|
||||||
local function transitionArchetype(
|
|
||||||
entityIndex: EntityIndex,
|
|
||||||
destinationArchetype: Archetype,
|
|
||||||
destinationRow: i24,
|
|
||||||
sourceArchetype: Archetype,
|
|
||||||
sourceRow: i24
|
|
||||||
)
|
|
||||||
local columns = sourceArchetype.columns
|
|
||||||
local sourceEntities = sourceArchetype.entities
|
|
||||||
local destinationEntities = destinationArchetype.entities
|
|
||||||
local destinationColumns = destinationArchetype.columns
|
|
||||||
|
|
||||||
for componentId, column in columns do
|
|
||||||
local targetColumn = destinationColumns[componentId]
|
|
||||||
if targetColumn then
|
|
||||||
targetColumn[destinationRow] = column[sourceRow]
|
|
||||||
end
|
|
||||||
column[sourceRow] = column[#column]
|
|
||||||
column[#column] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
destinationEntities[destinationRow] = sourceEntities[sourceRow]
|
|
||||||
local moveAway = #sourceEntities
|
|
||||||
sourceEntities[sourceRow] = sourceEntities[moveAway]
|
|
||||||
sourceEntities[moveAway] = nil
|
|
||||||
entityIndex[destinationEntities[destinationRow]].row = sourceRow
|
|
||||||
end
|
|
||||||
|
|
||||||
local function archetypeAppend(entity: i53, archetype: Archetype): i24
|
|
||||||
local entities = archetype.entities
|
|
||||||
table.insert(entities, entity)
|
|
||||||
return #entities
|
|
||||||
end
|
|
||||||
|
|
||||||
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
|
|
||||||
local row = archetypeAppend(entityId, archetype)
|
|
||||||
record.archetype = archetype
|
|
||||||
record.row = row
|
|
||||||
return record
|
|
||||||
end
|
|
||||||
|
|
||||||
local function moveEntity(entityIndex, entityId: i53, record: Record, to: Archetype)
|
|
||||||
local sourceRow = record.row
|
|
||||||
local from = record.archetype
|
|
||||||
local destinationRow = archetypeAppend(entityId, to)
|
|
||||||
transitionArchetype(entityIndex, to, destinationRow, from, sourceRow)
|
|
||||||
record.archetype = to
|
|
||||||
record.row = destinationRow
|
|
||||||
end
|
|
||||||
|
|
||||||
local function hash(arr): string | number
|
|
||||||
if true then
|
|
||||||
return table.concat(arr, "_")
|
|
||||||
end
|
|
||||||
local hashed = 5381
|
|
||||||
for i = 1, #arr do
|
|
||||||
hashed = ((bit32.lshift(hashed, 5)) + hashed) + arr[i]
|
|
||||||
end
|
|
||||||
return hashed
|
|
||||||
end
|
|
||||||
|
|
||||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
|
|
||||||
local destinationCount = #to.types
|
|
||||||
local destinationIds = to.types
|
|
||||||
|
|
||||||
for i = 1, destinationCount do
|
|
||||||
local destinationId = destinationIds[i]
|
|
||||||
|
|
||||||
if not componentIndex[destinationId] then
|
|
||||||
componentIndex[destinationId] = {}
|
|
||||||
end
|
|
||||||
componentIndex[destinationId][to.id] = i
|
|
||||||
to.records[destinationId] = i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
|
|
||||||
local ty = hash(types)
|
|
||||||
|
|
||||||
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
|
|
||||||
local id = world.nextArchetypeId
|
|
||||||
|
|
||||||
local columns = {} :: { any }
|
|
||||||
|
|
||||||
for _ in types do
|
|
||||||
table.insert(columns, {})
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = {
|
|
||||||
id = id,
|
|
||||||
types = types,
|
|
||||||
type = ty,
|
|
||||||
columns = columns,
|
|
||||||
entities = {},
|
|
||||||
edges = {},
|
|
||||||
records = {},
|
|
||||||
}
|
|
||||||
world.archetypeIndex[ty] = archetype
|
|
||||||
world.archetypes[id] = archetype
|
|
||||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
|
||||||
|
|
||||||
return archetype
|
|
||||||
end
|
|
||||||
|
|
||||||
local World = {}
|
|
||||||
World.__index = World
|
|
||||||
function World.new()
|
|
||||||
local self = setmetatable({
|
|
||||||
entityIndex = {},
|
|
||||||
componentIndex = {},
|
|
||||||
archetypes = {},
|
|
||||||
archetypeIndex = {},
|
|
||||||
ROOT_ARCHETYPE = nil :: Archetype?,
|
|
||||||
nextId = 2^8,
|
|
||||||
nextArchetypeId = 0
|
|
||||||
}, World)
|
|
||||||
self.ROOT_ARCHETYPE = archetypeOf(self, {}, nil)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
type World = typeof(World.new())
|
|
||||||
|
|
||||||
local function ensureArchetype(world: World, types, prev)
|
|
||||||
if #types < 1 then
|
|
||||||
|
|
||||||
if not world.ROOT_ARCHETYPE then
|
|
||||||
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
|
||||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
|
|
||||||
return ROOT_ARCHETYPE
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local ty = hash(types)
|
|
||||||
local archetype = world.archetypeIndex[ty]
|
|
||||||
if archetype then
|
|
||||||
return archetype
|
|
||||||
end
|
|
||||||
|
|
||||||
return archetypeOf(world, types, prev)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function findInsert(types: { i53 }, toAdd: i53)
|
|
||||||
local count = #types
|
|
||||||
for i = 1, count do
|
|
||||||
local id = types[i]
|
|
||||||
if id == toAdd then
|
|
||||||
return -1
|
|
||||||
end
|
|
||||||
if id > toAdd then
|
|
||||||
return i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return count + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
|
||||||
local types = node.types
|
|
||||||
local at = findInsert(types, componentId)
|
|
||||||
if at == -1 then
|
|
||||||
return node
|
|
||||||
end
|
|
||||||
|
|
||||||
local destinationType = table.clone(node.types)
|
|
||||||
table.insert(destinationType, at, componentId)
|
|
||||||
return ensureArchetype(world, destinationType, node)
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ensureEdge(archetype: Archetype, componentId: i53)
|
|
||||||
if not archetype.edges[componentId] then
|
|
||||||
archetype.edges[componentId] = {} :: any
|
|
||||||
end
|
|
||||||
return archetype.edges[componentId]
|
|
||||||
end
|
|
||||||
|
|
||||||
local function archetypeTraverseAdd(world: World, componentId: i53, archetype: Archetype?): Archetype
|
|
||||||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
|
||||||
local edge = ensureEdge(from, componentId)
|
|
||||||
|
|
||||||
if not edge.add then
|
|
||||||
edge.add = findArchetypeWith(world, from, componentId)
|
|
||||||
end
|
|
||||||
|
|
||||||
return edge.add
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.ensureRecord(world: World, entityId: i53)
|
|
||||||
local entityIndex = world.entityIndex
|
|
||||||
local id = entityId
|
|
||||||
if not entityIndex[id] then
|
|
||||||
entityIndex[id] = {} :: Record
|
|
||||||
end
|
|
||||||
return entityIndex[id]
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.add(world: World, entityId: i53, componentId: i53, data: unknown)
|
|
||||||
local record = world:ensureRecord(entityId)
|
|
||||||
local sourceArchetype = record.archetype
|
|
||||||
local destinationArchetype = archetypeTraverseAdd(world, componentId, sourceArchetype)
|
|
||||||
|
|
||||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
|
||||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
|
||||||
else
|
|
||||||
-- if it has any components, then it wont be the root archetype
|
|
||||||
if #destinationArchetype.types > 0 then
|
|
||||||
newEntity(entityId, record, destinationArchetype)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetypeRecord = destinationArchetype.records[componentId]
|
|
||||||
destinationArchetype.columns[archetypeRecord][record.row] = data
|
|
||||||
end
|
|
||||||
|
|
||||||
local function archetypeTraverseRemove(world: World, componentId: i53, archetype: Archetype?): Archetype
|
|
||||||
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
|
|
||||||
local edge = ensureEdge(from, componentId)
|
|
||||||
|
|
||||||
|
|
||||||
if not edge.remove then
|
|
||||||
local to = table.clone(from.types)
|
|
||||||
table.remove(to, table.find(to, componentId))
|
|
||||||
edge.remove = ensureArchetype(world, to, from)
|
|
||||||
end
|
|
||||||
|
|
||||||
return edge.remove
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.remove(world: World, entityId: i53, component: () -> () -> i53)
|
|
||||||
local componentId = component()()
|
|
||||||
local record = world:ensureRecord(entityId)
|
|
||||||
local sourceArchetype = record.archetype
|
|
||||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
|
||||||
|
|
||||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
|
||||||
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
|
|
||||||
local archetype = record.archetype
|
|
||||||
local archetypeRecord = componentIndex[componentId][archetype.id]
|
|
||||||
|
|
||||||
if not archetypeRecord then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
return archetype.columns[archetypeRecord][record.row]
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.get(
|
|
||||||
world: World,
|
|
||||||
entityId: i53,
|
|
||||||
a: () -> () -> i53,
|
|
||||||
b: () -> i53,
|
|
||||||
c: () -> i53,
|
|
||||||
d: () -> i53,
|
|
||||||
e: () -> i53
|
|
||||||
)
|
|
||||||
|
|
||||||
local id = entityId
|
|
||||||
local componentIndex = world.componentIndex
|
|
||||||
local record = world.entityIndex[id]
|
|
||||||
if not record then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local va = get(componentIndex, record, a()())
|
|
||||||
|
|
||||||
if b == nil then
|
|
||||||
return va
|
|
||||||
elseif c == nil then
|
|
||||||
return va, get(componentIndex, record, b())
|
|
||||||
elseif d == nil then
|
|
||||||
return va, get(componentIndex, record, b()), get(componentIndex, record, c())
|
|
||||||
elseif e == nil then
|
|
||||||
return va,
|
|
||||||
get(componentIndex, record, b()),
|
|
||||||
get(componentIndex, record, c()),
|
|
||||||
get(componentIndex, record, d())
|
|
||||||
else
|
|
||||||
error("args exceeded")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.entity(world: World)
|
|
||||||
world.nextId += 1
|
|
||||||
return world.nextId
|
|
||||||
end
|
|
||||||
|
|
||||||
local nextId = 0
|
|
||||||
local function component(): <T>(data: T) -> () -> (number, T)
|
|
||||||
nextId += 1
|
|
||||||
local id = nextId
|
|
||||||
return function(data)
|
|
||||||
return function()
|
|
||||||
return id, data
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.archetypesWith(world: World, componentId: i53)
|
|
||||||
local archetypes = world.archetypes
|
|
||||||
local archetypeMap = world.componentIndex[componentId]
|
|
||||||
local compatibleArchetypes = {}
|
|
||||||
for id, archetypeRecord in archetypeMap do
|
|
||||||
compatibleArchetypes[archetypes[id]] = true
|
|
||||||
end
|
|
||||||
return compatibleArchetypes
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.spawn(world: World, ...: () -> <T>() -> (number, T))
|
|
||||||
local entity = world:entity()
|
|
||||||
for i = 1, select("#", ...) do
|
|
||||||
local component = select(i, ...)
|
|
||||||
local componentId, data = component()
|
|
||||||
world:add(entity, componentId, data)
|
|
||||||
end
|
|
||||||
return entity
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.insert(world: World, entity: i53, ...: () -> <T>(data: T) -> (number, T))
|
|
||||||
for i = 1, select("#", ...) do
|
|
||||||
local component = select(i, ...)
|
|
||||||
local componentId, data = component()
|
|
||||||
world:add(entity, componentId, data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function World.query(world: World, ...: () -> () -> i53): () -> (number, ...any)
|
|
||||||
local compatibleArchetypes = {}
|
|
||||||
local components = { ... }
|
|
||||||
local archetypes = world.archetypes
|
|
||||||
local queryLength = select("#", ...)
|
|
||||||
local a: any, b: any, c: any, d: any, e: any = ...
|
|
||||||
|
|
||||||
if queryLength == 1 then
|
|
||||||
a = a()()
|
|
||||||
local archetypesMap = world.componentIndex[a]
|
|
||||||
components = { a }
|
|
||||||
local function single()
|
|
||||||
local id = next(archetypesMap)
|
|
||||||
local archetype = archetypes[id :: number]
|
|
||||||
local lastRow
|
|
||||||
|
|
||||||
return function(): any
|
|
||||||
local row, entity = next(archetype.entities, lastRow)
|
|
||||||
while row == nil do
|
|
||||||
id = next(archetypesMap, id)
|
|
||||||
if id == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
archetype = archetypes[id]
|
|
||||||
row = next(archetype.entities, row)
|
|
||||||
end
|
|
||||||
lastRow = row
|
|
||||||
|
|
||||||
return entity, archetype.columns[archetype.records[a]]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return single()
|
|
||||||
elseif queryLength == 2 then
|
|
||||||
a = a()()
|
|
||||||
b = b()()
|
|
||||||
components = { a, b }
|
|
||||||
local archetypesMap = world.componentIndex[a]
|
|
||||||
for id in archetypesMap do
|
|
||||||
local archetype = archetypes[id]
|
|
||||||
if archetype.records[b] then
|
|
||||||
table.insert(compatibleArchetypes, archetype)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function double(): any
|
|
||||||
local lastArchetype, archetype = next(compatibleArchetypes)
|
|
||||||
local lastRow
|
|
||||||
|
|
||||||
return function(): any
|
|
||||||
local row = next(archetype.entities, lastRow)
|
|
||||||
while row == nil do
|
|
||||||
lastArchetype, archetype = next(compatibleArchetypes, lastArchetype)
|
|
||||||
if lastArchetype == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
row = next(archetype.entities, row)
|
|
||||||
end
|
|
||||||
lastRow = row
|
|
||||||
|
|
||||||
local entity = archetype.entities[row::number]
|
|
||||||
local columns = archetype.columns
|
|
||||||
local archetypeRecords = archetype.records
|
|
||||||
return entity, columns[archetypeRecords[a]], columns[archetypeRecords[b]]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return double()
|
|
||||||
|
|
||||||
elseif queryLength == 3 then
|
|
||||||
a = a()()
|
|
||||||
b = b()()
|
|
||||||
c = c()()
|
|
||||||
components = { a, b, c}
|
|
||||||
|
|
||||||
elseif queryLength == 4 then
|
|
||||||
a = a()()
|
|
||||||
b = b()()
|
|
||||||
c = c()()
|
|
||||||
d = d()()
|
|
||||||
|
|
||||||
components = { a, b, c, d }
|
|
||||||
|
|
||||||
elseif queryLength == 5 then
|
|
||||||
a = a()()
|
|
||||||
b = b()()
|
|
||||||
c = c()()
|
|
||||||
d = d()()
|
|
||||||
e = e()()
|
|
||||||
components = {a,b,c,d,e}
|
|
||||||
else
|
|
||||||
for i, comp in components do
|
|
||||||
components[i] = comp()() :: any
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local firstArchetypeMap = world.componentIndex[components[1] :: any]
|
|
||||||
|
|
||||||
for id in firstArchetypeMap do
|
|
||||||
local archetype = archetypes[id]
|
|
||||||
local archetypeRecords = archetype.records
|
|
||||||
local matched = true
|
|
||||||
for i, componentId in components do
|
|
||||||
if not archetypeRecords[componentId] then
|
|
||||||
matched = false
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if matched then
|
|
||||||
table.insert(compatibleArchetypes, archetype)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local lastArchetype, archetype = next(compatibleArchetypes)
|
|
||||||
|
|
||||||
local lastRow
|
|
||||||
|
|
||||||
local function queryNext(): (...any)
|
|
||||||
local row = next(archetype.entities, lastRow)
|
|
||||||
while row == nil do
|
|
||||||
lastArchetype, archetype = next(compatibleArchetypes, lastArchetype)
|
|
||||||
if lastArchetype == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
row = next(archetype.entities, row)
|
|
||||||
end
|
|
||||||
lastRow = row
|
|
||||||
|
|
||||||
local columns = archetype.columns
|
|
||||||
local entityId = archetype.entities[row :: number]
|
|
||||||
local archetypeRecords = archetype.records
|
|
||||||
|
|
||||||
if queryLength == 1 then
|
|
||||||
return entityId, columns[archetypeRecords[a]]
|
|
||||||
elseif queryLength == 2 then
|
|
||||||
return entityId, columns[archetypeRecords[a]], columns[archetypeRecords[b]]
|
|
||||||
elseif queryLength == 3 then
|
|
||||||
return entityId,
|
|
||||||
columns[archetypeRecords[a]],
|
|
||||||
columns[archetypeRecords[b]],
|
|
||||||
columns[archetypeRecords[c]]
|
|
||||||
elseif queryLength == 4 then
|
|
||||||
return entityId,
|
|
||||||
columns[archetypeRecords[a]],
|
|
||||||
columns[archetypeRecords[b]],
|
|
||||||
columns[archetypeRecords[c]],
|
|
||||||
columns[archetypeRecords[d]]
|
|
||||||
elseif queryLength == 5 then
|
|
||||||
return entityId,
|
|
||||||
columns[archetypeRecords[a]],
|
|
||||||
columns[archetypeRecords[b]],
|
|
||||||
columns[archetypeRecords[c]],
|
|
||||||
columns[archetypeRecords[d]],
|
|
||||||
columns[archetypeRecords[e]]
|
|
||||||
end
|
|
||||||
|
|
||||||
local queryOutput = {}
|
|
||||||
for i, componentId in (components :: any) :: { number } do
|
|
||||||
queryOutput[i] = columns[archetypeRecords[componentId]]
|
|
||||||
end
|
|
||||||
|
|
||||||
return entityId, unpack(queryOutput, 1, queryLength)
|
|
||||||
end
|
|
||||||
|
|
||||||
return function()
|
|
||||||
-- consider this to be the iterator that gets invoked each iteration step
|
|
||||||
return queryNext()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
World = World,
|
|
||||||
component = component
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
local Jecs = require(script.Parent)
|
|
||||||
local component = Jecs.component
|
|
||||||
local world = Jecs.World.new()
|
|
||||||
|
|
||||||
local A, B, C, D = component(), component(), component(), component()
|
|
||||||
local E, F, G, H = component(), component(), component(), component()
|
|
||||||
print("A", A)
|
|
||||||
print("B", B)
|
|
||||||
print("C", C)
|
|
||||||
print("D", D)
|
|
||||||
print("E", E)
|
|
||||||
print("F", F)
|
|
||||||
print("G", G)
|
|
||||||
print("H", H)
|
|
||||||
|
|
||||||
for i = 1, 256 do
|
|
||||||
world:spawn(A(true), B(true), C(true), D(true))
|
|
||||||
|
|
||||||
--[[
|
|
||||||
ecs:set(entity, E, true)
|
|
||||||
ecs:set(entity, F, true)
|
|
||||||
ecs:set(entity, G, true)
|
|
||||||
ecs:set(entity, H, true)
|
|
||||||
print("end")
|
|
||||||
]]
|
|
||||||
end
|
|
||||||
|
|
||||||
return function()
|
|
||||||
describe("World", function()
|
|
||||||
it("should add component", function()
|
|
||||||
local id = world:spawn(A(true), B(1))
|
|
||||||
|
|
||||||
local id1 = world:spawn(A("hello"))
|
|
||||||
expect(world:get(id, A)).to.equal(true)
|
|
||||||
expect(world:get(id, B)).to.equal(1)
|
|
||||||
expect(world:get(id1, A)).to.equal("hello")
|
|
||||||
end)
|
|
||||||
it("should remove component", function()
|
|
||||||
local id = world:spawn(A(true), B(1000))
|
|
||||||
world:remove(id, A)
|
|
||||||
|
|
||||||
expect(world:get(id, A)).to.equal(nil)
|
|
||||||
end)
|
|
||||||
it("should override component data", function()
|
|
||||||
|
|
||||||
local id = world:spawn(A(true))
|
|
||||||
expect(world:get(id, A)).to.equal(true)
|
|
||||||
|
|
||||||
world:insert(id, A(false))
|
|
||||||
expect(world:get(id, A)).to.equal(false)
|
|
||||||
|
|
||||||
end)
|
|
||||||
it("query", function()
|
|
||||||
local added = 0
|
|
||||||
for e, a, b, c, d in world:query(A, B, C, D) do
|
|
||||||
added += 1
|
|
||||||
end
|
|
||||||
expect(added).to.equal(256)
|
|
||||||
end)
|
|
||||||
|
|
||||||
end)
|
|
||||||
end
|
|
Loading…
Reference in a new issue