Rebase
52
.gitignore
vendored
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
# Compiled Lua sources
|
||||||
|
luac.out
|
||||||
|
|
||||||
|
# luarocks build files
|
||||||
|
*.src.rock
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rbxm
|
||||||
|
|
||||||
|
# Object files
|
||||||
|
*.o
|
||||||
|
*.os
|
||||||
|
*.ko
|
||||||
|
*.obj
|
||||||
|
*.elf
|
||||||
|
|
||||||
|
# Precompiled Headers
|
||||||
|
*.gch
|
||||||
|
*.pch
|
||||||
|
|
||||||
|
# Libraries
|
||||||
|
*.lib
|
||||||
|
*.a
|
||||||
|
*.la
|
||||||
|
*.lo
|
||||||
|
*.def
|
||||||
|
*.exp
|
||||||
|
|
||||||
|
# Shared objects (inc. Windows DLLs)
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.so.*
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
*.app
|
||||||
|
*.i*86
|
||||||
|
*.x86_64
|
||||||
|
*.hex
|
||||||
|
|
||||||
|
# Wally files
|
||||||
|
DevPackages
|
||||||
|
Packages
|
||||||
|
wally.lock
|
||||||
|
WallyPatches
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
roblox.toml
|
||||||
|
sourcemap.json
|
||||||
|
drafts
|
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Just an ECS
|
||||||
|
|
||||||
|
jecs provides
|
6
aftman.toml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
[tools]
|
||||||
|
wally = "upliftgames/wally@0.3.1"
|
||||||
|
rojo = "rojo-rbx/rojo@7.4.1"
|
||||||
|
stylua = "johnnymorganz/stylua@0.19.1"
|
||||||
|
selene = "kampfkarren/selene@0.26.1"
|
||||||
|
wally-patch-package="Barocena/wally-patch-package@1.2.1"
|
96
benches/insertion.bench.lua
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
--!optimize 2
|
||||||
|
--!native
|
||||||
|
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local rgb = require(ReplicatedStorage.rgb)
|
||||||
|
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||||
|
local jecs = require(ReplicatedStorage.Lib)
|
||||||
|
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||||
|
local newWorld = Matter.World.new()
|
||||||
|
local ecs = jecs.World.new()
|
||||||
|
|
||||||
|
local A1 = Matter.component()
|
||||||
|
local A2 = Matter.component()
|
||||||
|
local A3 = Matter.component()
|
||||||
|
local A4 = Matter.component()
|
||||||
|
local A5 = Matter.component()
|
||||||
|
local A6 = Matter.component()
|
||||||
|
local A7 = Matter.component()
|
||||||
|
local A8 = Matter.component()
|
||||||
|
|
||||||
|
local B1 = ecr.component()
|
||||||
|
local B2 = ecr.component()
|
||||||
|
local B3 = ecr.component()
|
||||||
|
local B4 = ecr.component()
|
||||||
|
local B5 = ecr.component()
|
||||||
|
local B6 = ecr.component()
|
||||||
|
local B7 = ecr.component()
|
||||||
|
local B8 = ecr.component()
|
||||||
|
|
||||||
|
local C1 = ecs:entity()
|
||||||
|
local C2 = ecs:entity()
|
||||||
|
local C3 = ecs:entity()
|
||||||
|
local C4 = ecs:entity()
|
||||||
|
local C5 = ecs:entity()
|
||||||
|
local C6 = ecs:entity()
|
||||||
|
local C7 = ecs:entity()
|
||||||
|
local C8 = ecs:entity()
|
||||||
|
|
||||||
|
local registry2 = ecr.registry()
|
||||||
|
return {
|
||||||
|
ParameterGenerator = function()
|
||||||
|
return
|
||||||
|
end,
|
||||||
|
|
||||||
|
Functions = {
|
||||||
|
Matter = function()
|
||||||
|
for i = 1, 50 do
|
||||||
|
newWorld:spawn(
|
||||||
|
A1({ value = true }),
|
||||||
|
A2({ value = true }),
|
||||||
|
A3({ value = true }),
|
||||||
|
A4({ value = true }),
|
||||||
|
A5({ value = true }),
|
||||||
|
A6({ value = true }),
|
||||||
|
A7({ value = true }),
|
||||||
|
A8({ value = true })
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
|
||||||
|
ECR = function()
|
||||||
|
for i = 1, 50 do
|
||||||
|
local e = registry2.create()
|
||||||
|
registry2:set(e, B1, {value = false})
|
||||||
|
registry2:set(e, B2, {value = false})
|
||||||
|
registry2:set(e, B3, {value = false})
|
||||||
|
registry2:set(e, B4, {value = false})
|
||||||
|
registry2:set(e, B5, {value = false})
|
||||||
|
registry2:set(e, B6, {value = false})
|
||||||
|
registry2:set(e, B7, {value = false})
|
||||||
|
registry2:set(e, B8, {value = false})
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
|
||||||
|
Jecs = function()
|
||||||
|
|
||||||
|
local e = ecs:entity()
|
||||||
|
|
||||||
|
for i = 1, 50 do
|
||||||
|
|
||||||
|
ecs:add(e, C1, {value = false})
|
||||||
|
ecs:add(e, C2, {value = false})
|
||||||
|
ecs:add(e, C3, {value = false})
|
||||||
|
ecs:add(e, C4, {value = false})
|
||||||
|
ecs:add(e, C5, {value = false})
|
||||||
|
ecs:add(e, C6, {value = false})
|
||||||
|
ecs:add(e, C7, {value = false})
|
||||||
|
ecs:add(e, C8, {value = false})
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
162
benches/query.bench.lua
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
--!optimize 2
|
||||||
|
--!native
|
||||||
|
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local rgb = require(ReplicatedStorage.rgb)
|
||||||
|
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||||
|
local jecs = require(ReplicatedStorage.Lib)
|
||||||
|
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||||
|
local newWorld = Matter.World.new()
|
||||||
|
local ecs = jecs.World.new()
|
||||||
|
|
||||||
|
local A1 = Matter.component()
|
||||||
|
local A2 = Matter.component()
|
||||||
|
local A3 = Matter.component()
|
||||||
|
local A4 = Matter.component()
|
||||||
|
local A5 = Matter.component()
|
||||||
|
local A6 = Matter.component()
|
||||||
|
local A7 = Matter.component()
|
||||||
|
local A8 = Matter.component()
|
||||||
|
|
||||||
|
local B1 = ecr.component()
|
||||||
|
local B2 = ecr.component()
|
||||||
|
local B3 = ecr.component()
|
||||||
|
local B4 = ecr.component()
|
||||||
|
local B5 = ecr.component()
|
||||||
|
local B6 = ecr.component()
|
||||||
|
local B7 = ecr.component()
|
||||||
|
local B8 = ecr.component()
|
||||||
|
|
||||||
|
local C1 = ecs:entity()
|
||||||
|
local C2 = ecs:entity()
|
||||||
|
local C3 = ecs:entity()
|
||||||
|
local C4 = ecs:entity()
|
||||||
|
local C5 = ecs:entity()
|
||||||
|
local C6 = ecs:entity()
|
||||||
|
local C7 = ecs:entity()
|
||||||
|
local C8 = ecs:entity()
|
||||||
|
|
||||||
|
local registry2 = ecr.registry()
|
||||||
|
|
||||||
|
local function flip()
|
||||||
|
return math.random() >= 0.15
|
||||||
|
end
|
||||||
|
|
||||||
|
local common = 0
|
||||||
|
local N = 2^16-2
|
||||||
|
local archetypes = {}
|
||||||
|
for i = 1, N do
|
||||||
|
local id = registry2.create()
|
||||||
|
local combination = ""
|
||||||
|
local n = newWorld:spawn()
|
||||||
|
|
||||||
|
local entity = ecs:entity()
|
||||||
|
|
||||||
|
if flip() then
|
||||||
|
combination ..= "B"
|
||||||
|
registry2:set(id, B2, {value = true})
|
||||||
|
ecs:add(entity, C2, { value = true})
|
||||||
|
newWorld:insert(n, A2({value = true}))
|
||||||
|
end
|
||||||
|
if flip() then
|
||||||
|
combination ..= "C"
|
||||||
|
registry2:set(id, B3, {value = true})
|
||||||
|
ecs:add(entity, C3, { value = true})
|
||||||
|
newWorld:insert(n, A3({value = true}))
|
||||||
|
end
|
||||||
|
if flip() then
|
||||||
|
combination ..= "D"
|
||||||
|
registry2:set(id, B4, {value = true})
|
||||||
|
ecs:add(entity, C4, { value = true})
|
||||||
|
newWorld:insert(n, A4({value = true}))
|
||||||
|
end
|
||||||
|
if flip() then
|
||||||
|
combination ..= "E"
|
||||||
|
registry2:set(id, B5, {value = true})
|
||||||
|
ecs:add(entity, C5, { value = true})
|
||||||
|
newWorld:insert(n, A5({value = true}))
|
||||||
|
end
|
||||||
|
if flip() then
|
||||||
|
combination ..= "F"
|
||||||
|
registry2:set(id, B6, {value = true})
|
||||||
|
ecs:add(entity, C6, { value = true})
|
||||||
|
newWorld:insert(n, A6({value = true}))
|
||||||
|
end
|
||||||
|
if flip() then
|
||||||
|
combination ..= "G"
|
||||||
|
registry2:set(id, B7, {value = true})
|
||||||
|
ecs:add(entity, C7, { value = true})
|
||||||
|
newWorld:insert(n, A7({value = true}))
|
||||||
|
end
|
||||||
|
if flip() then
|
||||||
|
combination ..= "H"
|
||||||
|
registry2:set(id, B8, {value = true})
|
||||||
|
ecs:add(entity, C8, { value = true})
|
||||||
|
newWorld:insert(n, A8({value = true}))
|
||||||
|
end
|
||||||
|
|
||||||
|
if #combination == 7 then
|
||||||
|
combination = "A" .. combination
|
||||||
|
common += 1
|
||||||
|
registry2:set(id, B1, {value = true})
|
||||||
|
ecs:add(entity, C1, { value = true})
|
||||||
|
newWorld:insert(n, A1({value = true}))
|
||||||
|
end
|
||||||
|
|
||||||
|
archetypes[combination] = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local white = rgb.white
|
||||||
|
local yellow = rgb.yellow
|
||||||
|
local gray = rgb.gray
|
||||||
|
local green = rgb.green
|
||||||
|
|
||||||
|
local WALL = gray(" │ ")
|
||||||
|
|
||||||
|
local numberOfArchetypes = 0
|
||||||
|
for _ in archetypes do
|
||||||
|
numberOfArchetypes += 1
|
||||||
|
end
|
||||||
|
print(common)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"N entities "..yellow(N)
|
||||||
|
..WALL
|
||||||
|
.."with common components: "
|
||||||
|
..yellow(tostring(common).."/"..tostring(N)).." "
|
||||||
|
..yellow("("..string.format("%.2f", (common / (2^16 - 2)* 100)).."%)")
|
||||||
|
..WALL
|
||||||
|
..yellow("Total Archetypes: "..numberOfArchetypes)
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
ParameterGenerator = function()
|
||||||
|
return
|
||||||
|
end,
|
||||||
|
|
||||||
|
Functions = {
|
||||||
|
Matter = function()
|
||||||
|
local matched = 0
|
||||||
|
for entityId, firstComponent in newWorld:query(A1, A2, A3, A4) do
|
||||||
|
matched += 1
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
|
||||||
|
ECR = function()
|
||||||
|
local matched = 0
|
||||||
|
for entityId, firstComponent in registry2:view(B1, B2, B3, B4) do
|
||||||
|
matched += 1
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
|
||||||
|
Jecs = function()
|
||||||
|
local matched = 0
|
||||||
|
for entityId, firstComponent in ecs:query(C1, C2, C3, C4) do
|
||||||
|
matched += 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
42
benches/spawn.bench.lua
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
--!optimize 2
|
||||||
|
--!native
|
||||||
|
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
local rgb = require(ReplicatedStorage.rgb)
|
||||||
|
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
||||||
|
local jecs = require(ReplicatedStorage.Lib)
|
||||||
|
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
||||||
|
local newWorld = Matter.World.new()
|
||||||
|
local ecs = jecs.World.new()
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
ParameterGenerator = function()
|
||||||
|
local registry2 = ecr.registry()
|
||||||
|
|
||||||
|
return registry2
|
||||||
|
end,
|
||||||
|
|
||||||
|
Functions = {
|
||||||
|
Matter = function()
|
||||||
|
for i = 1, 1000 do
|
||||||
|
newWorld:spawn()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
|
||||||
|
ECR = function(_, registry2)
|
||||||
|
for i = 1, 1000 do
|
||||||
|
registry2.create()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
|
||||||
|
|
||||||
|
Jecs = function()
|
||||||
|
for i = 1, 1000 do
|
||||||
|
ecs:entity()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
6
default.project.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"name": "jecs",
|
||||||
|
"tree": {
|
||||||
|
"$path": "lib"
|
||||||
|
}
|
||||||
|
}
|
405
lib/init.lua
Normal file
|
@ -0,0 +1,405 @@
|
||||||
|
--!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 = 0,
|
||||||
|
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, componentId: i53)
|
||||||
|
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
|
||||||
|
|
||||||
|
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.query(world: World, ...: i53): () -> (number, ...any)
|
||||||
|
local compatibleArchetypes = {}
|
||||||
|
local components = { ... }
|
||||||
|
local archetypes = world.archetypes
|
||||||
|
local queryLength = #components
|
||||||
|
local a, b, c, d, e = ...
|
||||||
|
local firstArchetypeMap = world.componentIndex[components[1]]
|
||||||
|
|
||||||
|
for id in firstArchetypeMap do
|
||||||
|
local archetype = archetypes[id]
|
||||||
|
local archetypeRecords = archetype.records
|
||||||
|
local matched = true
|
||||||
|
for _, 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]]
|
||||||
|
elseif queryLength == 3 then
|
||||||
|
return entityId, columns[archetypeRecords[a]]
|
||||||
|
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 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
|
||||||
|
}
|
70
lib/init.spec.lua
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
local ecs = require(script.Parent).World.new()
|
||||||
|
|
||||||
|
local A, B, C, D = ecs:entity(), ecs:entity(), ecs:entity(), ecs:entity()
|
||||||
|
local E, F, G, H = ecs:entity(), ecs:entity(), ecs:entity(), ecs:entity()
|
||||||
|
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
|
||||||
|
local entity = ecs:entity()
|
||||||
|
ecs:add(entity, A, true)
|
||||||
|
ecs:add(entity, B, true)
|
||||||
|
ecs:add(entity, C, true)
|
||||||
|
ecs:add(entity, D, true)
|
||||||
|
|
||||||
|
--[[
|
||||||
|
ecs:add(entity, E, true)
|
||||||
|
ecs:add(entity, F, true)
|
||||||
|
ecs:add(entity, G, true)
|
||||||
|
ecs:add(entity, H, true)
|
||||||
|
print("end")
|
||||||
|
]]
|
||||||
|
end
|
||||||
|
|
||||||
|
return function()
|
||||||
|
describe("World", function()
|
||||||
|
it("should add component", function()
|
||||||
|
local id = ecs:entity()
|
||||||
|
ecs:add(id, A, true)
|
||||||
|
ecs:add(id, B, 1)
|
||||||
|
|
||||||
|
local id1 = ecs:entity()
|
||||||
|
ecs:add(id1, A, "hello")
|
||||||
|
expect(ecs:get(id, A)).to.equal(true)
|
||||||
|
expect(ecs:get(id, B)).to.equal(1)
|
||||||
|
expect(ecs:get(id1, A)).to.equal("hello")
|
||||||
|
end)
|
||||||
|
it("should remove component", function()
|
||||||
|
local id = ecs:entity()
|
||||||
|
ecs:add(id, A, true)
|
||||||
|
ecs:add(id, B, 1000)
|
||||||
|
ecs:remove(id, A, false)
|
||||||
|
|
||||||
|
expect(ecs:get(id, A)).to.equal(nil)
|
||||||
|
end)
|
||||||
|
it("should override component data", function()
|
||||||
|
|
||||||
|
local id = ecs:entity()
|
||||||
|
ecs:add(id, A, true)
|
||||||
|
expect(ecs:get(id, A)).to.equal(true)
|
||||||
|
|
||||||
|
ecs:add(id, A, false)
|
||||||
|
expect(ecs:get(id, A)).to.equal(false)
|
||||||
|
|
||||||
|
end)
|
||||||
|
it("query", function()
|
||||||
|
local added = 0
|
||||||
|
for e, a, b, c, d in ecs:query(A, B, C, D) do
|
||||||
|
added += 1
|
||||||
|
end
|
||||||
|
expect(added).to.equal(256)
|
||||||
|
end)
|
||||||
|
|
||||||
|
end)
|
||||||
|
end
|
33
rgb.lua
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
return {
|
||||||
|
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,
|
||||||
|
}
|
38
test.project.json
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"name": "jecs-test",
|
||||||
|
"tree": {
|
||||||
|
"$className": "DataModel",
|
||||||
|
"StarterPlayer": {
|
||||||
|
"$className": "StarterPlayer",
|
||||||
|
"StarterPlayerScripts": {
|
||||||
|
"$className": "StarterPlayerScripts",
|
||||||
|
"$path": "tests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ReplicatedStorage": {
|
||||||
|
"$className": "ReplicatedStorage",
|
||||||
|
"DevPackages": {
|
||||||
|
"$path": "DevPackages"
|
||||||
|
},
|
||||||
|
"Lib": {
|
||||||
|
"$path": "lib"
|
||||||
|
},
|
||||||
|
"rgb": {
|
||||||
|
"$path": "rgb.lua"
|
||||||
|
},
|
||||||
|
"benches": {
|
||||||
|
"$path": "benches"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"TestService": {
|
||||||
|
"$properties": {
|
||||||
|
"ExecuteWithStudioRun": true
|
||||||
|
},
|
||||||
|
"$className": "TestService",
|
||||||
|
"run": {
|
||||||
|
"$path": "tests.server.lua"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
24
testez.d.lua
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
declare function afterAll(callback: () -> ()): ()
|
||||||
|
declare function afterEach(callback: () -> ()): ()
|
||||||
|
|
||||||
|
declare function beforeAll(callback: () -> ()): ()
|
||||||
|
declare function beforeEach(callback: () -> ()): ()
|
||||||
|
|
||||||
|
declare function describe(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function describeFOCUS(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function fdescribe(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function describeSKIP(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function xdescribe(phrase: string, callback: () -> ()): ()
|
||||||
|
|
||||||
|
declare function expect(value: any): any
|
||||||
|
|
||||||
|
declare function FIXME(optionalMessage: string?): ()
|
||||||
|
declare function FOCUS(): ()
|
||||||
|
declare function SKIP(): ()
|
||||||
|
|
||||||
|
declare function it(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function itFOCUS(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function fit(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function itSKIP(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function xit(phrase: string, callback: () -> ()): ()
|
||||||
|
declare function itFIXME(phrase: string, callback: () -> ()): ()
|
9
tests.server.lua
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||||
|
|
||||||
|
require(ReplicatedStorage.DevPackages.TestEZ).TestBootstrap:run({
|
||||||
|
ReplicatedStorage.Lib,
|
||||||
|
nil,
|
||||||
|
{
|
||||||
|
noXpcallByDefault = true,
|
||||||
|
},
|
||||||
|
})
|
BIN
thesis/images/archetype_graph.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
thesis/images/chrome_IdcpbCveiD.png
Normal file
After Width: | Height: | Size: 142 KiB |
BIN
thesis/images/chrome_f5DTavXIka.png
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
thesis/images/chrome_giChmd5W4Z.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
thesis/images/insertion.png
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
thesis/images/queries.png
Normal file
After Width: | Height: | Size: 201 KiB |
BIN
thesis/images/random_access.png
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
thesis/images/removed.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
thesis/images/sparseset.png
Normal file
After Width: | Height: | Size: 9.5 KiB |
10
wally.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "marcus/jade"
|
||||||
|
version = "0.1.0"
|
||||||
|
registry = "https://github.com/UpliftGames/wally-index"
|
||||||
|
realm = "shared"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
TestEZ = "roblox/testez@0.4.1"
|
||||||
|
Matter = "matter-ecs/matter@0.7.1"
|
||||||
|
ecr = "centau/ecr@0.8.0"
|