mirror of
https://github.com/Ukendio/jecs.git
synced 2025-08-04 03:09:18 +00:00
lua51
This commit is contained in:
parent
0fea5a259d
commit
d9308a91e9
43 changed files with 2437 additions and 12768 deletions
1
.luacheckrc
Normal file
1
.luacheckrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
std="love+luajit"
|
2
.luaurc
2
.luaurc
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"jecs": "jecs",
|
"jecs": "jecs.luau",
|
||||||
"testkit": "tools/testkit",
|
"testkit": "tools/testkit",
|
||||||
"mirror": "mirror",
|
"mirror": "mirror",
|
||||||
"tools": "tools",
|
"tools": "tools",
|
||||||
|
|
|
@ -1,268 +0,0 @@
|
||||||
local jecs = require("@jecs")
|
|
||||||
|
|
||||||
export type PatchedWorld = jecs.World & {
|
|
||||||
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
|
|
||||||
removed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id) -> ()) -> () -> (),
|
|
||||||
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id, value: T) -> ()) -> () -> (),
|
|
||||||
observer: (
|
|
||||||
PatchedWorld,
|
|
||||||
any,
|
|
||||||
(jecs.Entity) -> ()
|
|
||||||
) -> (),
|
|
||||||
monitor: (
|
|
||||||
PatchedWorld,
|
|
||||||
any,
|
|
||||||
(jecs.Entity, jecs.Id) -> ()
|
|
||||||
) -> ()
|
|
||||||
}
|
|
||||||
|
|
||||||
local function observers_new(world, query, callback)
|
|
||||||
local terms = query.filter_with :: { jecs.Id }
|
|
||||||
if not terms then
|
|
||||||
local ids = query.ids
|
|
||||||
query.filter_with = ids
|
|
||||||
terms = ids
|
|
||||||
end
|
|
||||||
|
|
||||||
local entity_index = world.entity_index :: any
|
|
||||||
local function emplaced(entity, id, value)
|
|
||||||
local r = jecs.entity_index_try_get_fast(
|
|
||||||
entity_index, entity :: any)
|
|
||||||
|
|
||||||
if not r then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = r.archetype
|
|
||||||
|
|
||||||
if jecs.query_match(query, archetype) then
|
|
||||||
callback(entity)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, term in terms do
|
|
||||||
world:added(term, emplaced)
|
|
||||||
world:changed(term, emplaced)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function join(world, component)
|
|
||||||
local sparse_array = {}
|
|
||||||
local dense_array = {}
|
|
||||||
local values = {}
|
|
||||||
local max_id = 0
|
|
||||||
|
|
||||||
world:added(component, function(entity, id, value)
|
|
||||||
max_id += 1
|
|
||||||
sparse_array[entity] = max_id
|
|
||||||
dense_array[max_id] = entity
|
|
||||||
values[max_id] = value
|
|
||||||
end)
|
|
||||||
|
|
||||||
world:removed(component, function(entity, id)
|
|
||||||
local e_swap = dense_array[max_id]
|
|
||||||
local v_swap = values[max_id]
|
|
||||||
|
|
||||||
local dense = sparse_array[entity]
|
|
||||||
dense_array[dense] = e_swap
|
|
||||||
values[dense] = v_swap
|
|
||||||
|
|
||||||
sparse_array[entity] = nil
|
|
||||||
dense_array[max_id] = nil
|
|
||||||
values[max_id] = nil
|
|
||||||
max_id -= 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
world:changed(component, function(entity, id, value)
|
|
||||||
values[sparse_array[entity]] = value
|
|
||||||
end)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
local i = max_id
|
|
||||||
return function(): ...any
|
|
||||||
i -= 1
|
|
||||||
if i == 0 then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
local e = dense_array[i]
|
|
||||||
return e, values[i]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function monitors_new(world, query, callback)
|
|
||||||
local terms = query.filter_with :: { jecs.Id }
|
|
||||||
if not terms then
|
|
||||||
local ids = query.ids
|
|
||||||
query.filter_with = ids
|
|
||||||
terms = ids
|
|
||||||
end
|
|
||||||
|
|
||||||
local entity_index = world.entity_index :: any
|
|
||||||
local function emplaced(entity: jecs.Entity)
|
|
||||||
local r = jecs.entity_index_try_get_fast(
|
|
||||||
entity_index, entity :: any)
|
|
||||||
|
|
||||||
if not r then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = r.archetype
|
|
||||||
|
|
||||||
if jecs.query_match(query, archetype) then
|
|
||||||
callback(entity, jecs.OnAdd)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function removed(entity: jecs.Entity, component: jecs.Id)
|
|
||||||
local r = jecs.entity_index_try_get_fast(
|
|
||||||
entity_index, entity :: any)
|
|
||||||
|
|
||||||
if not r then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = r.archetype
|
|
||||||
|
|
||||||
if jecs.query_match(query, archetype) then
|
|
||||||
local EcsOnRemove = jecs.OnRemove :: jecs.Id
|
|
||||||
callback(entity, EcsOnRemove)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, term in terms do
|
|
||||||
world:added(term, emplaced)
|
|
||||||
world:removed(term, removed)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function observers_add(world: jecs.World): PatchedWorld
|
|
||||||
type Signal = { [jecs.Entity]: { (...any) -> () } }
|
|
||||||
|
|
||||||
local world_mut = world :: jecs.World & {[string]: any}
|
|
||||||
|
|
||||||
local signals = {
|
|
||||||
added = {} :: Signal,
|
|
||||||
emplaced = {} :: Signal,
|
|
||||||
removed = {} :: Signal
|
|
||||||
}
|
|
||||||
|
|
||||||
world_mut.added = function<T>(
|
|
||||||
_: jecs.World,
|
|
||||||
component: jecs.Id<T>,
|
|
||||||
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
|
|
||||||
)
|
|
||||||
local listeners = signals.added[component]
|
|
||||||
if not listeners then
|
|
||||||
listeners = {}
|
|
||||||
signals.added[component] = listeners
|
|
||||||
|
|
||||||
local function on_add(entity, id, value)
|
|
||||||
for _, listener in listeners :: any do
|
|
||||||
listener(entity, id, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local existing_hook = world:get(component, jecs.OnAdd)
|
|
||||||
if existing_hook then
|
|
||||||
table.insert(listeners, existing_hook)
|
|
||||||
end
|
|
||||||
|
|
||||||
local idr = world.component_index[component]
|
|
||||||
if idr then
|
|
||||||
idr.hooks.on_add = on_add
|
|
||||||
else
|
|
||||||
world:set(component, jecs.OnAdd, on_add)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.insert(listeners, fn)
|
|
||||||
return function()
|
|
||||||
local n = #listeners
|
|
||||||
local i = table.find(listeners, fn)
|
|
||||||
listeners[i] = listeners[n]
|
|
||||||
listeners[n] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
world_mut.changed = function<T>(
|
|
||||||
_: jecs.World,
|
|
||||||
component: jecs.Id<T>,
|
|
||||||
fn: (e: jecs.Entity, id: jecs.Id, value: T) -> ()
|
|
||||||
)
|
|
||||||
local listeners = signals.emplaced[component]
|
|
||||||
if not listeners then
|
|
||||||
listeners = {}
|
|
||||||
signals.emplaced[component] = listeners
|
|
||||||
local function on_change(entity, id, value: any)
|
|
||||||
for _, listener in listeners :: any do
|
|
||||||
listener(entity, id, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local existing_hook = world:get(component, jecs.OnChange)
|
|
||||||
if existing_hook then
|
|
||||||
table.insert(listeners, existing_hook)
|
|
||||||
end
|
|
||||||
local idr = world.component_index[component]
|
|
||||||
if idr then
|
|
||||||
idr.hooks.on_change = on_change
|
|
||||||
else
|
|
||||||
world:set(component, jecs.OnChange, on_change)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.insert(listeners, fn)
|
|
||||||
return function()
|
|
||||||
local n = #listeners
|
|
||||||
local i = table.find(listeners, fn)
|
|
||||||
listeners[i] = listeners[n]
|
|
||||||
listeners[n] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
world_mut.removed = function<T>(
|
|
||||||
_: jecs.World,
|
|
||||||
component: jecs.Id<T>,
|
|
||||||
fn: (e: jecs.Entity, id: jecs.Id) -> ()
|
|
||||||
)
|
|
||||||
local listeners = signals.removed[component]
|
|
||||||
if not listeners then
|
|
||||||
listeners = {}
|
|
||||||
signals.removed[component] = listeners
|
|
||||||
local function on_remove(entity, id)
|
|
||||||
for _, listener in listeners :: any do
|
|
||||||
listener(entity, id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
local existing_hook = world:get(component, jecs.OnRemove)
|
|
||||||
if existing_hook then
|
|
||||||
table.insert(listeners, existing_hook)
|
|
||||||
end
|
|
||||||
|
|
||||||
local idr = world.component_index[component]
|
|
||||||
if idr then
|
|
||||||
idr.hooks.on_remove = on_remove
|
|
||||||
else
|
|
||||||
world:set(component, jecs.OnRemove, on_remove)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(listeners, fn)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
local n = #listeners
|
|
||||||
local i = table.find(listeners, fn)
|
|
||||||
listeners[i] = listeners[n]
|
|
||||||
listeners[n] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
world_mut.signals = signals
|
|
||||||
|
|
||||||
world_mut.observer = observers_new
|
|
||||||
|
|
||||||
world_mut.monitor = monitors_new
|
|
||||||
|
|
||||||
world_mut.trackers = {}
|
|
||||||
|
|
||||||
return world_mut :: PatchedWorld
|
|
||||||
end
|
|
||||||
|
|
||||||
return observers_add
|
|
|
@ -1,160 +0,0 @@
|
||||||
local jecs = require("@jecs")
|
|
||||||
local mirror = require("@mirror")
|
|
||||||
|
|
||||||
type i53 = number
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE(testkit.color.white_underline("Jecs query"))
|
|
||||||
local ecs = jecs.World.new()
|
|
||||||
do
|
|
||||||
TITLE("one component in common")
|
|
||||||
|
|
||||||
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
|
|
||||||
BENCH("4 component", function()
|
|
||||||
for _ in world:query(D, C, B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local D1 = ecs:component()
|
|
||||||
local D2 = ecs:component()
|
|
||||||
local D3 = ecs:component()
|
|
||||||
local D4 = ecs:component()
|
|
||||||
local D5 = ecs:component()
|
|
||||||
local D6 = ecs:component()
|
|
||||||
local D7 = ecs:component()
|
|
||||||
local D8 = ecs:component()
|
|
||||||
|
|
||||||
local function flip()
|
|
||||||
return math.random() >= 0.15
|
|
||||||
end
|
|
||||||
|
|
||||||
local added = 0
|
|
||||||
local archetypes = {}
|
|
||||||
for i = 1, 2 ^ 16 - 2 do
|
|
||||||
local entity = ecs:entity()
|
|
||||||
|
|
||||||
local combination = ""
|
|
||||||
|
|
||||||
if flip() then
|
|
||||||
combination ..= "B"
|
|
||||||
ecs:set(entity, D2, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "C"
|
|
||||||
ecs:set(entity, D3, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "D"
|
|
||||||
ecs:set(entity, D4, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "E"
|
|
||||||
ecs:set(entity, D5, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "F"
|
|
||||||
ecs:set(entity, D6, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "G"
|
|
||||||
ecs:set(entity, D7, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "H"
|
|
||||||
ecs:set(entity, D8, { value = true })
|
|
||||||
end
|
|
||||||
|
|
||||||
if #combination == 7 then
|
|
||||||
added += 1
|
|
||||||
ecs:set(entity, D1, { value = true })
|
|
||||||
end
|
|
||||||
archetypes[combination] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local a = 0
|
|
||||||
for _ in archetypes do
|
|
||||||
a += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE(testkit.color.white_underline("Mirror query"))
|
|
||||||
local ecs = mirror.World.new()
|
|
||||||
do
|
|
||||||
TITLE("one component in common")
|
|
||||||
|
|
||||||
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
|
|
||||||
BENCH("4 component", function()
|
|
||||||
for _ in world:query(D, C, B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local D1 = ecs:component()
|
|
||||||
local D2 = ecs:component()
|
|
||||||
local D3 = ecs:component()
|
|
||||||
local D4 = ecs:component()
|
|
||||||
local D5 = ecs:component()
|
|
||||||
local D6 = ecs:component()
|
|
||||||
local D7 = ecs:component()
|
|
||||||
local D8 = ecs:component()
|
|
||||||
|
|
||||||
local function flip()
|
|
||||||
return math.random() >= 0.15
|
|
||||||
end
|
|
||||||
|
|
||||||
local added = 0
|
|
||||||
local archetypes = {}
|
|
||||||
for i = 1, 2 ^ 16 - 2 do
|
|
||||||
local entity = ecs:entity()
|
|
||||||
|
|
||||||
local combination = ""
|
|
||||||
|
|
||||||
if flip() then
|
|
||||||
combination ..= "B"
|
|
||||||
ecs:set(entity, D2, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "C"
|
|
||||||
ecs:set(entity, D3, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "D"
|
|
||||||
ecs:set(entity, D4, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "E"
|
|
||||||
ecs:set(entity, D5, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "F"
|
|
||||||
ecs:set(entity, D6, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "G"
|
|
||||||
ecs:set(entity, D7, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "H"
|
|
||||||
ecs:set(entity, D8, { value = true })
|
|
||||||
end
|
|
||||||
|
|
||||||
if #combination == 7 then
|
|
||||||
added += 1
|
|
||||||
ecs:set(entity, D1, { value = true })
|
|
||||||
end
|
|
||||||
archetypes[combination] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local a = 0
|
|
||||||
for _ in archetypes do
|
|
||||||
a += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,244 +0,0 @@
|
||||||
local jecs = require("@jecs")
|
|
||||||
local testkit = require("@testkit")
|
|
||||||
|
|
||||||
local BENCH, START = testkit.benchmark()
|
|
||||||
|
|
||||||
local function TITLE(s: string)
|
|
||||||
print()
|
|
||||||
print(testkit.color.white(s))
|
|
||||||
end
|
|
||||||
|
|
||||||
local N = 2 ^ 17
|
|
||||||
|
|
||||||
local pair = jecs.pair
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE("create")
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
BENCH("entity", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:entity()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
BENCH("pair", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
jecs.pair(A, B)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE("set")
|
|
||||||
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
local entities = table.create(N)
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
entities[i] = world:entity()
|
|
||||||
end
|
|
||||||
|
|
||||||
BENCH("add", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:set(entities[i], A, 1)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("set", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:set(entities[i], A, 2)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("remove", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:remove(entities[i], A)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- we have a separate benchmark for relationships.
|
|
||||||
-- this is due to that relationships have a very high id compared to normal
|
|
||||||
-- components, which cause them to get added into the hashmap portion.
|
|
||||||
do
|
|
||||||
TITLE("set relationship")
|
|
||||||
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
local entities = table.create(N)
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
entities[i] = world:entity()
|
|
||||||
world:set(entities[i], A, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
local pair = jecs.pair(A, world:entity())
|
|
||||||
|
|
||||||
BENCH("add", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:set(entities[i], pair, 1)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("set", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:set(entities[i], pair, 2)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("remove", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:remove(entities[i], pair)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE("get")
|
|
||||||
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
local D = world:component()
|
|
||||||
local entities = table.create(N)
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
entities[i] = world:entity()
|
|
||||||
world:set(entities[i], A, 1)
|
|
||||||
world:set(entities[i], B, 1)
|
|
||||||
world:set(entities[i], C, 1)
|
|
||||||
world:set(entities[i], D, 1)
|
|
||||||
end
|
|
||||||
|
|
||||||
BENCH("1", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:get(entities[i], A)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("2", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:get(entities[i], A, B)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("3", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:get(entities[i], A, B, C)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("4", function()
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:get(entities[i], A, B, C, D)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE("target")
|
|
||||||
|
|
||||||
BENCH("1st target", function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
local D = world:component()
|
|
||||||
local entities = table.create(N)
|
|
||||||
|
|
||||||
for i = 1, N do
|
|
||||||
local ent = world:entity()
|
|
||||||
entities[i] = ent
|
|
||||||
|
|
||||||
world:set(ent, pair(A, A))
|
|
||||||
world:set(ent, pair(A, B))
|
|
||||||
world:set(ent, pair(A, C))
|
|
||||||
world:set(ent, pair(A, D))
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 1, START(N) do
|
|
||||||
world:target(entities[i], A, 0)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- this benchmark is used to view how fragmentation affects query performance
|
|
||||||
--- we use this by determining how many entities should fit per arcehtype, instead
|
|
||||||
--- of creating x amount of archetypes. this would scale better with any amount of
|
|
||||||
--- entities.
|
|
||||||
do
|
|
||||||
TITLE(`query {N} entities`)
|
|
||||||
|
|
||||||
local function view_bench(n: number)
|
|
||||||
BENCH(`{n} entities per archetype`, function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
local D = world:component()
|
|
||||||
|
|
||||||
for i = 1, N, n do
|
|
||||||
local ct = world:entity()
|
|
||||||
for j = 1, n do
|
|
||||||
local id = world:entity()
|
|
||||||
world:set(id, A, true)
|
|
||||||
world:set(id, B, true)
|
|
||||||
world:set(id, C, true)
|
|
||||||
world:set(id, D, true)
|
|
||||||
world:add(id, ct)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local q = world:query(A, B, C, D)
|
|
||||||
START()
|
|
||||||
for id in q do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH(`inlined query`, function()
|
|
||||||
local world = jecs.World.new()
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
local C = world:component()
|
|
||||||
local D = world:component()
|
|
||||||
|
|
||||||
for i = 1, N, n do
|
|
||||||
local ct = world:entity()
|
|
||||||
for j = 1, n do
|
|
||||||
local id = world:entity()
|
|
||||||
world:set(id, A, true)
|
|
||||||
world:set(id, B, true)
|
|
||||||
world:set(id, C, true)
|
|
||||||
world:set(id, D, true)
|
|
||||||
world:add(id, ct)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetypes = world:query(A, B, C, D):archetypes()
|
|
||||||
START()
|
|
||||||
for _, archetype in archetypes do
|
|
||||||
local columns, records = archetype.columns, archetype.records
|
|
||||||
local a = columns[records[A]]
|
|
||||||
local b = columns[records[B]]
|
|
||||||
local c = columns[records[C]]
|
|
||||||
local d = columns[records[D]]
|
|
||||||
for row in archetype.entities do
|
|
||||||
local _1, _2, _3, _4 = a[row], b[row], c[row], d[row]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
for i = 13, 0, -1 do
|
|
||||||
view_bench(2 ^ i)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,246 +0,0 @@
|
||||||
--!optimize 2
|
|
||||||
--!native
|
|
||||||
|
|
||||||
local testkit = require("@testkit")
|
|
||||||
local BENCH, START = testkit.benchmark()
|
|
||||||
local function TITLE(title: string)
|
|
||||||
print()
|
|
||||||
print(testkit.color.white(title))
|
|
||||||
end
|
|
||||||
|
|
||||||
local jecs = require("@jecs")
|
|
||||||
local mirror = require("@mirror")
|
|
||||||
|
|
||||||
type i53 = number
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE(testkit.color.white_underline("Jecs query"))
|
|
||||||
local ecs = jecs.World.new()
|
|
||||||
do
|
|
||||||
TITLE("one component in common")
|
|
||||||
|
|
||||||
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
|
|
||||||
BENCH("1 component", function()
|
|
||||||
for _ in world:query(A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("2 component", function()
|
|
||||||
for _ in world:query(B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("4 component", function()
|
|
||||||
for _ in world:query(D, C, B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("8 component", function()
|
|
||||||
for _ in world:query(H, G, F, E, D, C, B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local e = world:entity()
|
|
||||||
world:set(e, A, true)
|
|
||||||
world:set(e, B, true)
|
|
||||||
world:set(e, C, true)
|
|
||||||
world:set(e, D, true)
|
|
||||||
world:set(e, E, true)
|
|
||||||
world:set(e, F, true)
|
|
||||||
world:set(e, G, true)
|
|
||||||
world:set(e, H, true)
|
|
||||||
|
|
||||||
BENCH("Update Data", function()
|
|
||||||
for _ = 1, 100 do
|
|
||||||
world:set(e, A, false)
|
|
||||||
world:set(e, B, false)
|
|
||||||
world:set(e, C, false)
|
|
||||||
world:set(e, D, false)
|
|
||||||
world:set(e, E, false)
|
|
||||||
world:set(e, F, false)
|
|
||||||
world:set(e, G, false)
|
|
||||||
world:set(e, H, false)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local D1 = ecs:component()
|
|
||||||
local D2 = ecs:component()
|
|
||||||
local D3 = ecs:component()
|
|
||||||
local D4 = ecs:component()
|
|
||||||
local D5 = ecs:component()
|
|
||||||
local D6 = ecs:component()
|
|
||||||
local D7 = ecs:component()
|
|
||||||
local D8 = ecs:component()
|
|
||||||
|
|
||||||
local function flip()
|
|
||||||
return math.random() >= 0.15
|
|
||||||
end
|
|
||||||
|
|
||||||
local added = 0
|
|
||||||
local archetypes = {}
|
|
||||||
for i = 1, 2 ^ 16 - 2 do
|
|
||||||
local entity = ecs:entity()
|
|
||||||
|
|
||||||
local combination = ""
|
|
||||||
|
|
||||||
if flip() then
|
|
||||||
combination ..= "B"
|
|
||||||
ecs:set(entity, D2, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "C"
|
|
||||||
ecs:set(entity, D3, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "D"
|
|
||||||
ecs:set(entity, D4, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "E"
|
|
||||||
ecs:set(entity, D5, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "F"
|
|
||||||
ecs:set(entity, D6, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "G"
|
|
||||||
ecs:set(entity, D7, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "H"
|
|
||||||
ecs:set(entity, D8, { value = true })
|
|
||||||
end
|
|
||||||
|
|
||||||
if #combination == 7 then
|
|
||||||
added += 1
|
|
||||||
ecs:set(entity, D1, { value = true })
|
|
||||||
end
|
|
||||||
archetypes[combination] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local a = 0
|
|
||||||
for _ in archetypes do
|
|
||||||
a += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
do
|
|
||||||
TITLE(testkit.color.white_underline("Mirror query"))
|
|
||||||
local ecs = mirror.World.new()
|
|
||||||
do
|
|
||||||
TITLE("one component in common")
|
|
||||||
|
|
||||||
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
|
|
||||||
BENCH("1 component", function()
|
|
||||||
for _ in world:query(A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("2 component", function()
|
|
||||||
for _ in world:query(B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("4 component", function()
|
|
||||||
for _ in world:query(D, C, B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
BENCH("8 component", function()
|
|
||||||
for _ in world:query(H, G, F, E, D, C, B, A) do
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
local e = world:entity()
|
|
||||||
world:set(e, A, true)
|
|
||||||
world:set(e, B, true)
|
|
||||||
world:set(e, C, true)
|
|
||||||
world:set(e, D, true)
|
|
||||||
world:set(e, E, true)
|
|
||||||
world:set(e, F, true)
|
|
||||||
world:set(e, G, true)
|
|
||||||
world:set(e, H, true)
|
|
||||||
|
|
||||||
BENCH("Update Data", function()
|
|
||||||
for _ = 1, 100 do
|
|
||||||
world:set(e, A, false)
|
|
||||||
world:set(e, B, false)
|
|
||||||
world:set(e, C, false)
|
|
||||||
world:set(e, D, false)
|
|
||||||
world:set(e, E, false)
|
|
||||||
world:set(e, F, false)
|
|
||||||
world:set(e, G, false)
|
|
||||||
world:set(e, H, false)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local D1 = ecs:component()
|
|
||||||
local D2 = ecs:component()
|
|
||||||
local D3 = ecs:component()
|
|
||||||
local D4 = ecs:component()
|
|
||||||
local D5 = ecs:component()
|
|
||||||
local D6 = ecs:component()
|
|
||||||
local D7 = ecs:component()
|
|
||||||
local D8 = ecs:component()
|
|
||||||
|
|
||||||
local function flip()
|
|
||||||
return math.random() >= 0.15
|
|
||||||
end
|
|
||||||
|
|
||||||
local added = 0
|
|
||||||
local archetypes = {}
|
|
||||||
for i = 1, 2 ^ 16 - 2 do
|
|
||||||
local entity = ecs:entity()
|
|
||||||
|
|
||||||
local combination = ""
|
|
||||||
|
|
||||||
if flip() then
|
|
||||||
combination ..= "B"
|
|
||||||
ecs:set(entity, D2, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "C"
|
|
||||||
ecs:set(entity, D3, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "D"
|
|
||||||
ecs:set(entity, D4, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "E"
|
|
||||||
ecs:set(entity, D5, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "F"
|
|
||||||
ecs:set(entity, D6, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "G"
|
|
||||||
ecs:set(entity, D7, { value = true })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "H"
|
|
||||||
ecs:set(entity, D8, { value = true })
|
|
||||||
end
|
|
||||||
|
|
||||||
if #combination == 7 then
|
|
||||||
added += 1
|
|
||||||
ecs:set(entity, D1, { value = true })
|
|
||||||
end
|
|
||||||
archetypes[combination] = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local a = 0
|
|
||||||
for _ in archetypes do
|
|
||||||
a += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,64 +0,0 @@
|
||||||
--!optimize 2
|
|
||||||
--!native
|
|
||||||
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
|
||||||
local jecs = require(ReplicatedStorage.Lib)
|
|
||||||
local pair = jecs.pair
|
|
||||||
local newWorld = Matter.World.new()
|
|
||||||
local ecs = jecs.World.new()
|
|
||||||
local mirror = require(ReplicatedStorage.mirror)
|
|
||||||
local mcs = mirror.World.new()
|
|
||||||
|
|
||||||
local C1 = ecs:component()
|
|
||||||
local C2 = ecs:entity()
|
|
||||||
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local C3 = ecs:entity()
|
|
||||||
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local C4 = ecs:entity()
|
|
||||||
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local E1 = mcs:component()
|
|
||||||
local E2 = mcs:entity()
|
|
||||||
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local E3 = mcs:entity()
|
|
||||||
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local E4 = mcs:entity()
|
|
||||||
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
|
|
||||||
local registry2 = ecr.registry()
|
|
||||||
|
|
||||||
return {
|
|
||||||
ParameterGenerator = function()
|
|
||||||
local j = ecs:entity()
|
|
||||||
ecs:set(j, C1, true)
|
|
||||||
local m = mcs:entity()
|
|
||||||
mcs:set(m, E1, true)
|
|
||||||
for i = 1, 1000 do
|
|
||||||
local friend1 = ecs:entity()
|
|
||||||
local friend2 = mcs:entity()
|
|
||||||
|
|
||||||
ecs:add(friend1, pair(C2, j))
|
|
||||||
ecs:add(friend1, pair(C3, j))
|
|
||||||
ecs:add(friend1, pair(C4, j))
|
|
||||||
|
|
||||||
mcs:add(friend2, pair(E2, m))
|
|
||||||
mcs:add(friend2, pair(E3, m))
|
|
||||||
mcs:add(friend2, pair(E4, m))
|
|
||||||
end
|
|
||||||
return {
|
|
||||||
m = m,
|
|
||||||
j = j,
|
|
||||||
}
|
|
||||||
end,
|
|
||||||
|
|
||||||
Functions = {
|
|
||||||
Mirror = function(_, a)
|
|
||||||
mcs:delete(a.m)
|
|
||||||
end,
|
|
||||||
|
|
||||||
Jecs = function(_, a)
|
|
||||||
ecs:delete(a.j)
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
--!optimize 2
|
|
||||||
--!native
|
|
||||||
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
|
||||||
local jecs = require(ReplicatedStorage.Lib:Clone())
|
|
||||||
local ecs = jecs.World.new()
|
|
||||||
local mirror = require(ReplicatedStorage.mirror:Clone())
|
|
||||||
local mcs = mirror.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:component()
|
|
||||||
local C2 = ecs:component()
|
|
||||||
local C3 = ecs:component()
|
|
||||||
local C4 = ecs:component()
|
|
||||||
local C5 = ecs:component()
|
|
||||||
local C6 = ecs:component()
|
|
||||||
local C7 = ecs:component()
|
|
||||||
local C8 = ecs:component()
|
|
||||||
local E1 = mcs:component()
|
|
||||||
local E2 = mcs:component()
|
|
||||||
local E3 = mcs:component()
|
|
||||||
local E4 = mcs:component()
|
|
||||||
local E5 = mcs:component()
|
|
||||||
local E6 = mcs:component()
|
|
||||||
local E7 = mcs:component()
|
|
||||||
local E8 = mcs:component()
|
|
||||||
|
|
||||||
local registry2 = ecr.registry()
|
|
||||||
return {
|
|
||||||
ParameterGenerator = function()
|
|
||||||
return
|
|
||||||
end,
|
|
||||||
|
|
||||||
Functions = {
|
|
||||||
Mirror = function()
|
|
||||||
local e = mcs:entity()
|
|
||||||
for i = 1, 5000 do
|
|
||||||
mcs:set(e, E1, false)
|
|
||||||
mcs:set(e, E2, false)
|
|
||||||
mcs:set(e, E3, false)
|
|
||||||
mcs:set(e, E4, false)
|
|
||||||
mcs:set(e, E5, false)
|
|
||||||
mcs:set(e, E6, false)
|
|
||||||
mcs:set(e, E7, false)
|
|
||||||
mcs:set(e, E8, false)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
Jecs = function()
|
|
||||||
local e = ecs:entity()
|
|
||||||
for i = 1, 5000 do
|
|
||||||
ecs:set(e, C1, false)
|
|
||||||
ecs:set(e, C2, false)
|
|
||||||
ecs:set(e, C3, false)
|
|
||||||
ecs:set(e, C4, false)
|
|
||||||
ecs:set(e, C5, false)
|
|
||||||
ecs:set(e, C6, false)
|
|
||||||
ecs:set(e, C7, false)
|
|
||||||
ecs:set(e, C8, false)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,164 +0,0 @@
|
||||||
--!optimize 2
|
|
||||||
--!native
|
|
||||||
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter)
|
|
||||||
local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr)
|
|
||||||
local newWorld = Matter.World.new()
|
|
||||||
|
|
||||||
local jecs = require(ReplicatedStorage.Lib)
|
|
||||||
local mirror = require(ReplicatedStorage.mirror)
|
|
||||||
local mcs = mirror.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 D1 = ecs:component()
|
|
||||||
local D2 = ecs:component()
|
|
||||||
local D3 = ecs:component()
|
|
||||||
local D4 = ecs:component()
|
|
||||||
local D5 = ecs:component()
|
|
||||||
local D6 = ecs:component()
|
|
||||||
local D7 = ecs:component()
|
|
||||||
local D8 = ecs:component()
|
|
||||||
|
|
||||||
local E1 = mcs:entity()
|
|
||||||
local E2 = mcs:entity()
|
|
||||||
local E3 = mcs:entity()
|
|
||||||
local E4 = mcs:entity()
|
|
||||||
local E5 = mcs:entity()
|
|
||||||
local E6 = mcs:entity()
|
|
||||||
local E7 = mcs:entity()
|
|
||||||
local E8 = mcs:entity()
|
|
||||||
|
|
||||||
local registry2 = ecr.registry()
|
|
||||||
|
|
||||||
local function flip()
|
|
||||||
return math.random() >= 0.25
|
|
||||||
end
|
|
||||||
|
|
||||||
local N = 2 ^ 16 - 2
|
|
||||||
local archetypes = {}
|
|
||||||
|
|
||||||
local hm = 0
|
|
||||||
for i = 1, N do
|
|
||||||
local id = registry2.create()
|
|
||||||
local combination = ""
|
|
||||||
local n = newWorld:spawn()
|
|
||||||
local entity = ecs:entity()
|
|
||||||
local m = mcs:entity()
|
|
||||||
|
|
||||||
if flip() then
|
|
||||||
registry2:set(id, B1, { value = true })
|
|
||||||
ecs:set(entity, D1, { value = true })
|
|
||||||
newWorld:insert(n, A1({ value = true }))
|
|
||||||
mcs:set(m, E1, { value = 2 })
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "B"
|
|
||||||
registry2:set(id, B2, { value = true })
|
|
||||||
ecs:set(entity, D2, { value = true })
|
|
||||||
mcs:set(m, E2, { value = 2 })
|
|
||||||
newWorld:insert(n, A2({ value = true }))
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "C"
|
|
||||||
registry2:set(id, B3, { value = true })
|
|
||||||
ecs:set(entity, D3, { value = true })
|
|
||||||
mcs:set(m, E3, { value = 2 })
|
|
||||||
newWorld:insert(n, A3({ value = true }))
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "D"
|
|
||||||
registry2:set(id, B4, { value = true })
|
|
||||||
ecs:set(entity, D4, { value = true })
|
|
||||||
mcs:set(m, E4, { value = 2 })
|
|
||||||
|
|
||||||
newWorld:insert(n, A4({ value = true }))
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "E"
|
|
||||||
registry2:set(id, B5, { value = true })
|
|
||||||
ecs:set(entity, D5, { value = true })
|
|
||||||
mcs:set(m, E5, { value = 2 })
|
|
||||||
|
|
||||||
newWorld:insert(n, A5({ value = true }))
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "F"
|
|
||||||
registry2:set(id, B6, { value = true })
|
|
||||||
ecs:set(entity, D6, { value = true })
|
|
||||||
mcs:set(m, E6, { value = 2 })
|
|
||||||
newWorld:insert(n, A6({ value = true }))
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "G"
|
|
||||||
registry2:set(id, B7, { value = true })
|
|
||||||
ecs:set(entity, D7, { value = true })
|
|
||||||
mcs:set(m, E7, { value = 2 })
|
|
||||||
newWorld:insert(n, A7({ value = true }))
|
|
||||||
end
|
|
||||||
if flip() then
|
|
||||||
combination ..= "H"
|
|
||||||
registry2:set(id, B8, { value = true })
|
|
||||||
newWorld:insert(n, A8({ value = true }))
|
|
||||||
ecs:set(entity, D8, { value = true })
|
|
||||||
mcs:set(m, E8, { value = 2 })
|
|
||||||
end
|
|
||||||
|
|
||||||
if combination:find("BCDF") then
|
|
||||||
if not archetypes[combination] then
|
|
||||||
print(combination)
|
|
||||||
end
|
|
||||||
hm += 1
|
|
||||||
end
|
|
||||||
archetypes[combination] = true
|
|
||||||
end
|
|
||||||
print("TEST", hm)
|
|
||||||
|
|
||||||
local count = 0
|
|
||||||
|
|
||||||
for _, archetype in ecs:query(D2, D4, D6, D8):archetypes() do
|
|
||||||
count += #archetype.entities
|
|
||||||
end
|
|
||||||
|
|
||||||
print(count)
|
|
||||||
|
|
||||||
return {
|
|
||||||
ParameterGenerator = function()
|
|
||||||
return
|
|
||||||
end,
|
|
||||||
|
|
||||||
Functions = {
|
|
||||||
Matter = function()
|
|
||||||
for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
ECR = function()
|
|
||||||
for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
Jecs = function()
|
|
||||||
for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
--!optimize 2
|
|
||||||
--!native
|
|
||||||
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local Matter = require(ReplicatedStorage.DevPackages.Matter)
|
|
||||||
local ecr = require(ReplicatedStorage.DevPackages.ecr)
|
|
||||||
local jecs = require(ReplicatedStorage.Lib)
|
|
||||||
local pair = jecs.pair
|
|
||||||
local ecs = jecs.World.new()
|
|
||||||
local mirror = require(ReplicatedStorage.mirror)
|
|
||||||
local mcs = mirror.World.new()
|
|
||||||
|
|
||||||
local C1 = ecs:component()
|
|
||||||
local C2 = ecs:entity()
|
|
||||||
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local C3 = ecs:entity()
|
|
||||||
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local C4 = ecs:entity()
|
|
||||||
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local E1 = mcs:component()
|
|
||||||
local E2 = mcs:entity()
|
|
||||||
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local E3 = mcs:entity()
|
|
||||||
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
local E4 = mcs:entity()
|
|
||||||
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
|
|
||||||
|
|
||||||
return {
|
|
||||||
ParameterGenerator = function()
|
|
||||||
end,
|
|
||||||
|
|
||||||
Functions = {
|
|
||||||
Mirror = function()
|
|
||||||
local m = mcs:entity()
|
|
||||||
for i = 1, 100 do
|
|
||||||
mcs:add(m, E3)
|
|
||||||
mcs:remove(m, E3)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
Jecs = function()
|
|
||||||
local j = ecs:entity()
|
|
||||||
for i = 1, 100 do
|
|
||||||
ecs:add(j, C3)
|
|
||||||
ecs:remove(j, C3)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
--!optimize 2
|
|
||||||
--!native
|
|
||||||
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local jecs = require(ReplicatedStorage.Lib:Clone())
|
|
||||||
local mirror = require(ReplicatedStorage.mirror:Clone())
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
ParameterGenerator = function()
|
|
||||||
local ecs = jecs.world()
|
|
||||||
ecs:range(1000, 20000)
|
|
||||||
local mcs = mirror.World.new()
|
|
||||||
return ecs, mcs
|
|
||||||
end,
|
|
||||||
|
|
||||||
Functions = {
|
|
||||||
Mirror = function(_, ecs, mcs)
|
|
||||||
for i = 1, 100 do
|
|
||||||
|
|
||||||
mcs:entity()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
|
|
||||||
Jecs = function(_, ecs, mcs)
|
|
||||||
for i = 1, 100 do
|
|
||||||
|
|
||||||
ecs:entity()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "private/private"
|
|
||||||
version = "0.1.0-rc.6"
|
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
|
||||||
realm = "shared"
|
|
||||||
include = ["default.project.json", "lib/**", "lib", "wally.toml", "README.md"]
|
|
||||||
exclude = ["**"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
Matter = "matter-ecs/matter@0.8.0"
|
|
||||||
ecr = "centau/ecr@0.8.0"
|
|
6
demo/.gitignore
vendored
6
demo/.gitignore
vendored
|
@ -1,6 +0,0 @@
|
||||||
# Project place file
|
|
||||||
/example.rbxlx
|
|
||||||
|
|
||||||
# Roblox Studio lock files
|
|
||||||
/*.rbxlx.lock
|
|
||||||
/*.rbxl.lock
|
|
|
@ -1,15 +0,0 @@
|
||||||
# Demo
|
|
||||||
|
|
||||||
## Build with Rojo
|
|
||||||
To build the place, run the following commands from the root of the repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd demo
|
|
||||||
rojo build -o "demo.rbxl"
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, open `demo.rbxl` in Roblox Studio and start the Rojo server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
rojo serve
|
|
||||||
```
|
|
|
@ -1,55 +0,0 @@
|
||||||
{
|
|
||||||
"name": "demo",
|
|
||||||
"emitLegacyScripts": false,
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"ReplicatedStorage": {
|
|
||||||
"$className": "ReplicatedStorage",
|
|
||||||
"$path": "src/ReplicatedStorage",
|
|
||||||
"ecs": {
|
|
||||||
"$path": "../jecs.luau"
|
|
||||||
},
|
|
||||||
"Packages": {
|
|
||||||
"$path": "Packages"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ServerScriptService": {
|
|
||||||
"$className": "ServerScriptService",
|
|
||||||
"$path": "src/ServerScriptService"
|
|
||||||
},
|
|
||||||
"Workspace": {
|
|
||||||
"$properties": {
|
|
||||||
"FilteringEnabled": true
|
|
||||||
},
|
|
||||||
"Baseplate": {
|
|
||||||
"$className": "Part",
|
|
||||||
"$properties": {
|
|
||||||
"Anchored": true,
|
|
||||||
"Color": [0.38823, 0.37254, 0.38823],
|
|
||||||
"Locked": true,
|
|
||||||
"Position": [0, -10, 0],
|
|
||||||
"Size": [512, 20, 512]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Lighting": {
|
|
||||||
"$properties": {
|
|
||||||
"Ambient": [0, 0, 0],
|
|
||||||
"Brightness": 2,
|
|
||||||
"GlobalShadows": true,
|
|
||||||
"Outlines": false,
|
|
||||||
"Technology": "Voxel"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SoundService": {
|
|
||||||
"$properties": {
|
|
||||||
"RespectFilteringEnabled": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"StarterPlayer": {
|
|
||||||
"StarterPlayerScripts": {
|
|
||||||
"$path": "src/StarterPlayer/StarterPlayerScripts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
local function collect<T...>(
|
|
||||||
signal: {
|
|
||||||
Connect: (RBXScriptSignal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
|
|
||||||
}
|
|
||||||
): () -> (T...)
|
|
||||||
local enqueued = {}
|
|
||||||
|
|
||||||
local i = 0
|
|
||||||
|
|
||||||
local connection = (signal :: any):Connect(function(...)
|
|
||||||
table.insert(enqueued, { ... })
|
|
||||||
i += 1
|
|
||||||
end)
|
|
||||||
|
|
||||||
return function(): any
|
|
||||||
if i == 0 then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
i -= 1
|
|
||||||
|
|
||||||
local args: any = table.remove(enqueued, 1)
|
|
||||||
|
|
||||||
return unpack(args)
|
|
||||||
end, connection
|
|
||||||
end
|
|
||||||
|
|
||||||
return collect
|
|
|
@ -1,36 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local jecs = require(ReplicatedStorage.ecs)
|
|
||||||
local types = require("./types")
|
|
||||||
|
|
||||||
local Networked = jecs.tag()
|
|
||||||
local NetworkedPair = jecs.tag()
|
|
||||||
|
|
||||||
local Renderable = jecs.component() :: jecs.Id<Instance>
|
|
||||||
jecs.meta(Renderable, Networked)
|
|
||||||
|
|
||||||
|
|
||||||
local Poison = jecs.component() :: jecs.Id<number>
|
|
||||||
jecs.meta(Poison, Networked)
|
|
||||||
|
|
||||||
local Health = jecs.component() :: jecs.Id<number>
|
|
||||||
jecs.meta(Health, Networked)
|
|
||||||
|
|
||||||
local Player = jecs.component() :: jecs.Id<Player>
|
|
||||||
jecs.meta(Player, Networked)
|
|
||||||
|
|
||||||
|
|
||||||
local components = {
|
|
||||||
Renderable = Renderable,
|
|
||||||
Player = Player,
|
|
||||||
Poison = Poison,
|
|
||||||
Health = Health,
|
|
||||||
|
|
||||||
Networked = Networked,
|
|
||||||
NetworkedPair = NetworkedPair,
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, component in components do
|
|
||||||
jecs.meta(component, jecs.Name, name)
|
|
||||||
end
|
|
||||||
|
|
||||||
return components
|
|
|
@ -1,13 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local jecs = require(ReplicatedStorage.ecs)
|
|
||||||
local schedule = require(ReplicatedStorage.schedule)
|
|
||||||
local observers_add = require(ReplicatedStorage.observers_add)
|
|
||||||
|
|
||||||
local SYSTEM = schedule.SYSTEM
|
|
||||||
local RUN = schedule.RUN
|
|
||||||
require(ReplicatedStorage.components)
|
|
||||||
local world = observers_add(jecs.world())
|
|
||||||
|
|
||||||
local systems = ReplicatedStorage.systems
|
|
||||||
SYSTEM(world, systems.receive_replication)
|
|
||||||
RUN(world)
|
|
|
@ -1,190 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local jecs = require(ReplicatedStorage.ecs)
|
|
||||||
|
|
||||||
type Observer<T...> = {
|
|
||||||
callback: (jecs.Entity) -> (),
|
|
||||||
query: jecs.Query<T...>,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PatchedWorld = jecs.World & {
|
|
||||||
added: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
|
|
||||||
removed: (PatchedWorld, jecs.Id, (e: jecs.Entity, id: jecs.Id) -> ()) -> (),
|
|
||||||
changed: <T>(PatchedWorld, jecs.Id<T>, (e: jecs.Entity, id: jecs.Id<T>, value: T) -> ()) -> (),
|
|
||||||
-- deleted: (PatchedWorld, () -> ()) -> () -> (),
|
|
||||||
observer: (PatchedWorld, Observer<any>) -> (),
|
|
||||||
monitor: (PatchedWorld, Observer<any>) -> (),
|
|
||||||
}
|
|
||||||
|
|
||||||
local function observers_new(world, description)
|
|
||||||
local query = description.query
|
|
||||||
local callback = description.callback
|
|
||||||
local terms = query.filter_with :: { jecs.Id }
|
|
||||||
if not terms then
|
|
||||||
local ids = query.ids
|
|
||||||
query.filter_with = ids
|
|
||||||
terms = ids
|
|
||||||
end
|
|
||||||
|
|
||||||
local entity_index = world.entity_index :: any
|
|
||||||
local function emplaced(entity: jecs.Entity)
|
|
||||||
local r = jecs.entity_index_try_get_fast(
|
|
||||||
entity_index, entity :: any)
|
|
||||||
|
|
||||||
if not r then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = r.archetype
|
|
||||||
|
|
||||||
if jecs.query_match(query, archetype) then
|
|
||||||
callback(entity)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, term in terms do
|
|
||||||
world:added(term, emplaced)
|
|
||||||
world:changed(term, emplaced)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function monitors_new(world, description)
|
|
||||||
local query = description.query
|
|
||||||
local callback = description.callback
|
|
||||||
local terms = query.filter_with :: { jecs.Id }
|
|
||||||
if not terms then
|
|
||||||
local ids = query.ids
|
|
||||||
query.filter_with = ids
|
|
||||||
terms = ids
|
|
||||||
end
|
|
||||||
|
|
||||||
local entity_index = world.entity_index :: any
|
|
||||||
local function emplaced(entity: jecs.Entity)
|
|
||||||
local r = jecs.entity_index_try_get_fast(
|
|
||||||
entity_index, entity :: any)
|
|
||||||
|
|
||||||
if not r then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = r.archetype
|
|
||||||
|
|
||||||
if jecs.query_match(query, archetype) then
|
|
||||||
callback(entity, jecs.OnAdd)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function removed(entity: jecs.Entity, component: jecs.Id)
|
|
||||||
local r = jecs.entity_index_try_get_fast(
|
|
||||||
entity_index, entity :: any)
|
|
||||||
|
|
||||||
if not r then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = r.archetype
|
|
||||||
|
|
||||||
if jecs.query_match(query, archetype) then
|
|
||||||
callback(entity, jecs.OnRemove)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, term in terms do
|
|
||||||
world:added(term, emplaced)
|
|
||||||
world:removed(term, removed)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function observers_add(world: jecs.World): PatchedWorld
|
|
||||||
local signals = {
|
|
||||||
added = {},
|
|
||||||
emplaced = {},
|
|
||||||
removed = {},
|
|
||||||
deleted = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
world = world :: jecs.World & {[string]: any}
|
|
||||||
|
|
||||||
world.added = function(_, component, fn)
|
|
||||||
local listeners = signals.added[component]
|
|
||||||
if not listeners then
|
|
||||||
listeners = {}
|
|
||||||
signals.added[component] = listeners
|
|
||||||
|
|
||||||
local idr = jecs.id_record_ensure(world :: any, component :: any)
|
|
||||||
local rw = jecs.pair(component, jecs.Wildcard)
|
|
||||||
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
|
|
||||||
local function on_add(entity: number, id: number, value: any)
|
|
||||||
for _, listener in listeners do
|
|
||||||
listener(entity, id, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
world:set(component, jecs.OnAdd, on_add)
|
|
||||||
idr.hooks.on_add = on_add :: any
|
|
||||||
idr_r.hooks.on_add = on_add :: any
|
|
||||||
end
|
|
||||||
table.insert(listeners, fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
world.changed = function(_, component, fn)
|
|
||||||
local listeners = signals.emplaced[component]
|
|
||||||
if not listeners then
|
|
||||||
listeners = {}
|
|
||||||
signals.emplaced[component] = listeners
|
|
||||||
local idr = jecs.id_record_ensure(world :: any, component :: any)
|
|
||||||
local rw = jecs.pair(component, jecs.Wildcard)
|
|
||||||
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
|
|
||||||
local function on_change(entity: number, id: number, value: any)
|
|
||||||
for _, listener in listeners do
|
|
||||||
listener(entity, id, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
world:set(component, jecs.OnChange, on_change)
|
|
||||||
idr.hooks.on_change = on_change :: any
|
|
||||||
idr_r.hooks.on_change = on_change :: any
|
|
||||||
end
|
|
||||||
table.insert(listeners, fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
world.removed = function(_, component, fn)
|
|
||||||
local listeners = signals.removed[component]
|
|
||||||
if not listeners then
|
|
||||||
listeners = {}
|
|
||||||
signals.removed[component] = listeners
|
|
||||||
local idr = jecs.id_record_ensure(world :: any, component :: any)
|
|
||||||
local rw = jecs.pair(component, jecs.Wildcard)
|
|
||||||
local idr_r = jecs.id_record_ensure(world :: any, rw :: any)
|
|
||||||
local function on_remove(entity: number, id: number, value: any)
|
|
||||||
for _, listener in listeners do
|
|
||||||
listener(entity, id, value)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
world:set(component, jecs.OnRemove, on_remove)
|
|
||||||
idr.hooks.on_remove = on_remove :: any
|
|
||||||
idr_r.hooks.on_remove = on_remove :: any
|
|
||||||
end
|
|
||||||
table.insert(listeners, fn)
|
|
||||||
end
|
|
||||||
|
|
||||||
world.signals = signals
|
|
||||||
|
|
||||||
world.observer = observers_new
|
|
||||||
|
|
||||||
world.monitor = monitors_new
|
|
||||||
|
|
||||||
-- local world_delete = world.delete
|
|
||||||
|
|
||||||
-- world.deleted = function(_, fn)
|
|
||||||
-- local listeners = signals.deleted
|
|
||||||
-- table.insert(listeners, fn)
|
|
||||||
-- end
|
|
||||||
-- world.delete = function(world, entity)
|
|
||||||
-- world_delete(world, entity)
|
|
||||||
-- for _, fn in signals.deleted do
|
|
||||||
-- fn(entity)
|
|
||||||
-- end
|
|
||||||
-- end
|
|
||||||
|
|
||||||
return world :: PatchedWorld
|
|
||||||
end
|
|
||||||
|
|
||||||
return observers_add
|
|
|
@ -1,50 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local types = require("../ReplicatedStorage/types")
|
|
||||||
|
|
||||||
type Signal<T...> = {
|
|
||||||
Connect: (Signal<T...>, fn: (T...) -> ()) -> RBXScriptConnection
|
|
||||||
}
|
|
||||||
type Remote<T...> = {
|
|
||||||
FireClient: (Remote<T...>, T...) -> (),
|
|
||||||
FireAllClients: (Remote<T...>, T...) -> (),
|
|
||||||
FireServer: (Remote<T...>) -> (),
|
|
||||||
OnServerEvent: {
|
|
||||||
Connect: (any, fn: (Player, T...) -> () ) -> ()
|
|
||||||
},
|
|
||||||
OnClientEvent: {
|
|
||||||
Connect: (any, fn: (T...) -> () ) -> ()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
local function stream_ensure(name): Remote<any>
|
|
||||||
local remote = ReplicatedStorage:FindFirstChild(name)
|
|
||||||
if not remote then
|
|
||||||
remote = Instance.new("RemoteEvent")
|
|
||||||
remote.Name = name
|
|
||||||
remote.Parent = ReplicatedStorage
|
|
||||||
end
|
|
||||||
return remote :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
local function datagram_ensure(name): Remote<any>
|
|
||||||
local remote = ReplicatedStorage:FindFirstChild(name)
|
|
||||||
if not remote then
|
|
||||||
remote = Instance.new("UnreliableRemoteEvent")
|
|
||||||
remote.Name = name
|
|
||||||
remote.Parent = ReplicatedStorage
|
|
||||||
end
|
|
||||||
return remote :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
input = datagram_ensure("input") :: Remote<string>,
|
|
||||||
replication = stream_ensure("replication") :: Remote<{
|
|
||||||
[string]: {
|
|
||||||
set: { types.Entity }?,
|
|
||||||
values: { any }?,
|
|
||||||
removed: { types.Entity }?
|
|
||||||
}
|
|
||||||
}>,
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local jabby = require(ReplicatedStorage.Packages.jabby)
|
|
||||||
local jecs = require(ReplicatedStorage.ecs)
|
|
||||||
|
|
||||||
jabby.set_check_function(function() return true end)
|
|
||||||
|
|
||||||
local scheduler = jabby.scheduler.create("jabby scheduler")
|
|
||||||
|
|
||||||
jabby.register({
|
|
||||||
applet = jabby.applets.scheduler,
|
|
||||||
name = "Scheduler",
|
|
||||||
configuration = {
|
|
||||||
scheduler = scheduler,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
local ContextActionService = game:GetService("ContextActionService")
|
|
||||||
|
|
||||||
local function create_widget(_, state: Enum.UserInputState)
|
|
||||||
local client = jabby.obtain_client()
|
|
||||||
if state ~= Enum.UserInputState.Begin then return end
|
|
||||||
client.spawn_app(client.apps.home, nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
local RunService = game:GetService("RunService")
|
|
||||||
|
|
||||||
local System = jecs.component() :: jecs.Id<{
|
|
||||||
fn: () -> (),
|
|
||||||
name: string,
|
|
||||||
}>
|
|
||||||
local DependsOn = jecs.component()
|
|
||||||
local Phase = jecs.tag()
|
|
||||||
local Event = jecs.component() :: jecs.Id<RBXScriptSignal>
|
|
||||||
|
|
||||||
local pair = jecs.pair
|
|
||||||
|
|
||||||
local types = require(ReplicatedStorage.types)
|
|
||||||
|
|
||||||
local function ECS_PHASE(world, after: types.Entity)
|
|
||||||
local phase = world:entity()
|
|
||||||
world:add(phase, Phase)
|
|
||||||
if after then
|
|
||||||
local dependency = pair(DependsOn, after)
|
|
||||||
world:add(phase, dependency)
|
|
||||||
end
|
|
||||||
|
|
||||||
return phase
|
|
||||||
end
|
|
||||||
|
|
||||||
local Heartbeat = jecs.tag()
|
|
||||||
jecs.meta(Heartbeat, Phase)
|
|
||||||
jecs.meta(Heartbeat, Event, RunService.Heartbeat)
|
|
||||||
|
|
||||||
local PreSimulation = jecs.tag()
|
|
||||||
jecs.meta(PreSimulation, Phase)
|
|
||||||
jecs.meta(PreSimulation, Event, RunService.PreSimulation)
|
|
||||||
|
|
||||||
local PreAnimation = jecs.tag()
|
|
||||||
jecs.meta(PreAnimation, Phase)
|
|
||||||
jecs.meta(PreAnimation, Event, RunService.PreAnimation)
|
|
||||||
|
|
||||||
local PreRender = jecs.tag()
|
|
||||||
jecs.meta(PreRender, Phase)
|
|
||||||
jecs.meta(PreRender, Event, RunService.PreRender)
|
|
||||||
|
|
||||||
local function ECS_SYSTEM(world: types.World, mod: ModuleScript, phase: types.Entity?)
|
|
||||||
local system = world:entity()
|
|
||||||
local p = phase or Heartbeat
|
|
||||||
local fn = require(mod) :: (...any) -> ()
|
|
||||||
world:set(system, System, {
|
|
||||||
fn = fn(world, 0) or fn,
|
|
||||||
name = mod.Name,
|
|
||||||
})
|
|
||||||
|
|
||||||
local depends_on = DependsOn :: jecs.Entity
|
|
||||||
world:add(system, pair(depends_on, p))
|
|
||||||
end
|
|
||||||
local function find_systems_w_phase(world: types.World, systems, phase: types.Entity)
|
|
||||||
local phase_name = world:get(phase, jecs.Name) :: string
|
|
||||||
for _, s in world:query(System):with(pair(DependsOn, phase)) do
|
|
||||||
table.insert(systems, {
|
|
||||||
id = scheduler:register_system({
|
|
||||||
phase = phase_name,
|
|
||||||
name = s.name,
|
|
||||||
}),
|
|
||||||
fn = s.fn
|
|
||||||
})
|
|
||||||
end
|
|
||||||
for after in world:query(Phase, pair(DependsOn, phase)) do
|
|
||||||
find_systems_w_phase(world, systems, after)
|
|
||||||
end
|
|
||||||
return systems
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ECS_RUN(world: types.World)
|
|
||||||
|
|
||||||
jabby.register({
|
|
||||||
applet = jabby.applets.world,
|
|
||||||
name = "MyWorld",
|
|
||||||
configuration = {
|
|
||||||
world = world,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if RunService:IsClient() then
|
|
||||||
ContextActionService:BindAction("Open Jabby Home", create_widget, false, Enum.KeyCode.F4)
|
|
||||||
end
|
|
||||||
|
|
||||||
for phase, event in world:query(Event, Phase) do
|
|
||||||
local systems = find_systems_w_phase(world, {}, phase)
|
|
||||||
event:Connect(function(...)
|
|
||||||
for _, system in systems do
|
|
||||||
scheduler:run(system.id, system.fn, world, ...)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
return {
|
|
||||||
PHASE = ECS_PHASE,
|
|
||||||
SYSTEM = ECS_SYSTEM,
|
|
||||||
RUN = ECS_RUN,
|
|
||||||
phases = {
|
|
||||||
Heartbeat = Heartbeat,
|
|
||||||
PreSimulation = PreSimulation,
|
|
||||||
PreAnimation = PreAnimation,
|
|
||||||
PreRender = PreRender
|
|
||||||
},
|
|
||||||
components = {
|
|
||||||
System = System,
|
|
||||||
DependsOn = DependsOn,
|
|
||||||
Phase = Phase,
|
|
||||||
Event = Event,
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
local types = require("../types")
|
|
||||||
local jecs = require(game:GetService("ReplicatedStorage").ecs)
|
|
||||||
local remotes = require("../remotes")
|
|
||||||
local collect = require("../collect")
|
|
||||||
local client_ids = {}
|
|
||||||
|
|
||||||
|
|
||||||
local function ecs_map_get(world, id)
|
|
||||||
local deserialised_id = client_ids[id]
|
|
||||||
|
|
||||||
if not deserialised_id then
|
|
||||||
if world:has(id, jecs.Name) then
|
|
||||||
deserialised_id = world:entity(id)
|
|
||||||
else
|
|
||||||
deserialised_id = world:entity()
|
|
||||||
end
|
|
||||||
|
|
||||||
client_ids[id] = deserialised_id
|
|
||||||
end
|
|
||||||
|
|
||||||
-- local deserialised_id = client_ids[id]
|
|
||||||
-- if not deserialised_id then
|
|
||||||
-- if world:has(id, jecs.Name) then
|
|
||||||
-- deserialised_id = world:entity(id)
|
|
||||||
-- else
|
|
||||||
-- if world:exists(id) then
|
|
||||||
-- deserialised_id = world:entity()
|
|
||||||
-- else
|
|
||||||
-- deserialised_id = world:entity(id)
|
|
||||||
-- end
|
|
||||||
-- end
|
|
||||||
-- client_ids[id] = deserialised_id
|
|
||||||
-- end
|
|
||||||
|
|
||||||
return deserialised_id
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ecs_make_alive_id(world, id)
|
|
||||||
local rel = jecs.ECS_PAIR_FIRST(id)
|
|
||||||
local tgt = jecs.ECS_PAIR_SECOND(id)
|
|
||||||
|
|
||||||
rel = ecs_map_get(world, rel)
|
|
||||||
tgt = ecs_map_get(world, tgt)
|
|
||||||
|
|
||||||
return jecs.pair(rel, tgt)
|
|
||||||
end
|
|
||||||
|
|
||||||
local snapshots = collect(remotes.replication.OnClientEvent)
|
|
||||||
|
|
||||||
return function(world: types.World)
|
|
||||||
for snapshot in snapshots do
|
|
||||||
for id, map in snapshot do
|
|
||||||
id = tonumber(id)
|
|
||||||
if jecs.IS_PAIR(id) then
|
|
||||||
id = ecs_make_alive_id(world, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
local set = map.set
|
|
||||||
if set then
|
|
||||||
if jecs.is_tag(world, id) then
|
|
||||||
for _, entity in set do
|
|
||||||
entity = ecs_map_get(world, entity)
|
|
||||||
world:add(entity, id)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
local values = map.values
|
|
||||||
for i, entity in set do
|
|
||||||
entity = ecs_map_get(world, entity)
|
|
||||||
world:set(entity, id, values[i])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local removed = map.removed
|
|
||||||
|
|
||||||
if removed then
|
|
||||||
for i, e in removed do
|
|
||||||
if not world:contains(e) then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
world:remove(e, id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,15 +0,0 @@
|
||||||
local jecs = require(game:GetService("ReplicatedStorage").ecs)
|
|
||||||
local observers_add = require("../ReplicatedStorage/observers_add")
|
|
||||||
|
|
||||||
export type World = typeof(observers_add(jecs.world()))
|
|
||||||
export type Entity = jecs.Entity
|
|
||||||
export type Id<T> = jecs.Id<T>
|
|
||||||
export type Snapshot = {
|
|
||||||
[string]: {
|
|
||||||
set: { jecs.Entity }?,
|
|
||||||
values: { any }?,
|
|
||||||
removed: { jecs.Entity }?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
|
|
@ -1,19 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local ServerScriptService = game:GetService("ServerScriptService")
|
|
||||||
local jecs = require(ReplicatedStorage.ecs)
|
|
||||||
local schedule = require(ReplicatedStorage.schedule)
|
|
||||||
local observers_add = require(ReplicatedStorage.observers_add)
|
|
||||||
|
|
||||||
local SYSTEM = schedule.SYSTEM
|
|
||||||
local RUN = schedule.RUN
|
|
||||||
|
|
||||||
require(ReplicatedStorage.components)
|
|
||||||
local world = observers_add(jecs.world())
|
|
||||||
|
|
||||||
local systems = ServerScriptService.systems
|
|
||||||
|
|
||||||
SYSTEM(world, systems.replication)
|
|
||||||
SYSTEM(world, systems.players_added)
|
|
||||||
SYSTEM(world, systems.poison_hurts)
|
|
||||||
SYSTEM(world, systems.life_is_painful)
|
|
||||||
RUN(world, 0)
|
|
|
@ -1,12 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local ct = require(ReplicatedStorage.components)
|
|
||||||
local types = require(ReplicatedStorage.types)
|
|
||||||
|
|
||||||
return function(world: types.World, dt: number)
|
|
||||||
for e in world:query(ct.Player):without(ct.Health) do
|
|
||||||
world:set(e, ct.Health, 100)
|
|
||||||
end
|
|
||||||
for e in world:query(ct.Player, ct.Health):without(ct.Poison) do
|
|
||||||
world:set(e, ct.Poison, 10)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,20 +0,0 @@
|
||||||
local collect = require("../../ReplicatedStorage/collect")
|
|
||||||
local types = require("../../ReplicatedStorage/types")
|
|
||||||
local ct = require("../../ReplicatedStorage/components")
|
|
||||||
local Players = game:GetService("Players")
|
|
||||||
|
|
||||||
local player_added = collect(Players.PlayerAdded)
|
|
||||||
return function(world: types.World, dt: number)
|
|
||||||
for player in player_added do
|
|
||||||
local entity = world:entity()
|
|
||||||
world:set(entity, ct.Player, player)
|
|
||||||
end
|
|
||||||
|
|
||||||
for entity, player in world:query(ct.Player):without(ct.Renderable) do
|
|
||||||
local character = player.Character
|
|
||||||
if character then
|
|
||||||
if not character.Parent then
|
|
||||||
world:set(entity, ct.Renderable, character)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,12 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local ct = require(ReplicatedStorage.components)
|
|
||||||
return function(world, dt)
|
|
||||||
for e, poison, health in world:query(ct.Poison, ct.Health) do
|
|
||||||
local health_after_tick = health - poison * dt * 0.05
|
|
||||||
if health_after_tick < 0 then
|
|
||||||
world:remove(e, ct.Health)
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
world:set(e, ct.Health, health_after_tick)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,190 +0,0 @@
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
local types = require("../../ReplicatedStorage/types")
|
|
||||||
local ct = require("../../ReplicatedStorage/components")
|
|
||||||
local jecs = require(ReplicatedStorage.ecs)
|
|
||||||
local remotes = require("../../ReplicatedStorage/remotes")
|
|
||||||
local components = ct :: {[string]: jecs.Entity }
|
|
||||||
|
|
||||||
return function(world: ty.World)
|
|
||||||
|
|
||||||
--- integration test
|
|
||||||
|
|
||||||
-- for _ = 1, 10 do
|
|
||||||
-- local e = world:entity()
|
|
||||||
-- world:set(e, ct.TestA, true)
|
|
||||||
-- end
|
|
||||||
|
|
||||||
local storages = {} :: { [jecs.Entity]: {[jecs.Entity]: any }}
|
|
||||||
local networked_components = {}
|
|
||||||
local networked_pairs = {}
|
|
||||||
|
|
||||||
for component in world:each(ct.Networked) do
|
|
||||||
local name = world:get(component, jecs.Name) :: string
|
|
||||||
if components[name] == nil then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
storages[component] = {}
|
|
||||||
|
|
||||||
table.insert(networked_components, component)
|
|
||||||
end
|
|
||||||
|
|
||||||
for relation in world:each(ct.NetworkedPair) do
|
|
||||||
local name = world:get(relation, jecs.Name) :: string
|
|
||||||
if not components[name] then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
table.insert(networked_pairs, relation)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, component in networked_components do
|
|
||||||
local name = world:get(component, jecs.Name) :: string
|
|
||||||
if not components[name] then
|
|
||||||
error(`Networked Component (%id{component}%name{name})`)
|
|
||||||
end
|
|
||||||
local is_tag = jecs.is_tag(world, component)
|
|
||||||
local storage = storages[component]
|
|
||||||
if is_tag then
|
|
||||||
world:added(component, function(entity)
|
|
||||||
storage[entity] = true
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
world:added(component, function(entity, _, value)
|
|
||||||
storage[entity] = value
|
|
||||||
end)
|
|
||||||
world:changed(component, function(entity, _, value)
|
|
||||||
storage[entity] = value
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
world:removed(component, function(entity)
|
|
||||||
storage[entity] = "jecs.Remove"
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, relation in networked_pairs do
|
|
||||||
world:added(relation, function(entity, id, value)
|
|
||||||
local is_tag = jecs.is_tag(world, id)
|
|
||||||
local storage = storages[id]
|
|
||||||
if not storage then
|
|
||||||
storage = {}
|
|
||||||
storages[id] = storage
|
|
||||||
end
|
|
||||||
if is_tag then
|
|
||||||
storage[entity] = true
|
|
||||||
else
|
|
||||||
storage[entity] = value
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
world:changed(relation, function(entity, id, value)
|
|
||||||
local is_tag = jecs.is_tag(world, id)
|
|
||||||
if is_tag then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local storage = storages[id]
|
|
||||||
if not storage then
|
|
||||||
storage = {}
|
|
||||||
storages[id] = storage
|
|
||||||
end
|
|
||||||
|
|
||||||
storage[entity] = value
|
|
||||||
end)
|
|
||||||
|
|
||||||
world:removed(relation, function(entity, id)
|
|
||||||
local storage = storages[id]
|
|
||||||
if not storage then
|
|
||||||
storage = {}
|
|
||||||
storages[id] = storage
|
|
||||||
end
|
|
||||||
|
|
||||||
storage[entity] = "jecs.Remove"
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
local players_added = collect(Players.PlayerAdded)
|
|
||||||
|
|
||||||
return function()
|
|
||||||
local snapshot_lazy: ty.Snapshot
|
|
||||||
local set_ids_lazy: { jecs.Entity }
|
|
||||||
|
|
||||||
for player in players_added do
|
|
||||||
if not snapshot_lazy then
|
|
||||||
snapshot_lazy, set_ids_lazy = {}, {}
|
|
||||||
|
|
||||||
for component, storage in storages do
|
|
||||||
local set_values = {}
|
|
||||||
local set_n = 0
|
|
||||||
|
|
||||||
local q = world:query(component)
|
|
||||||
local is_tag = jecs.is_tag(world, component)
|
|
||||||
for _, archetype in q:archetypes() do
|
|
||||||
local entities = archetype.entities
|
|
||||||
local entities_len = #entities
|
|
||||||
table.move(entities, 1, entities_len, set_n + 1, set_ids_lazy)
|
|
||||||
if is_tag then
|
|
||||||
set_values = table.create(entities_len, true)
|
|
||||||
else
|
|
||||||
local column = archetype.columns[archetype.records[component]]
|
|
||||||
table.move(column, 1, entities_len, set_n + 1, set_values)
|
|
||||||
end
|
|
||||||
|
|
||||||
set_n += entities_len
|
|
||||||
end
|
|
||||||
|
|
||||||
local set = table.move(set_ids_lazy, 1, set_n, 1, {})
|
|
||||||
|
|
||||||
snapshot_lazy[tostring(component)] = {
|
|
||||||
set = if set_n > 0 then set else nil,
|
|
||||||
values = if set_n > 0 then set_values else nil,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
remotes.replication:FireClient(player, snapshot_lazy)
|
|
||||||
end
|
|
||||||
|
|
||||||
local snapshot = {} :: ty.Snapshot
|
|
||||||
|
|
||||||
local set_ids = {}
|
|
||||||
local removed_ids = {}
|
|
||||||
|
|
||||||
for component, storage in storages do
|
|
||||||
local set_values = {} :: { any }
|
|
||||||
local set_n = 0
|
|
||||||
local removed_n = 0
|
|
||||||
for e, v in storage do
|
|
||||||
if v ~= "jecs.Remove" then
|
|
||||||
set_n += 1
|
|
||||||
set_ids[set_n] = e
|
|
||||||
set_values[set_n] = v or true
|
|
||||||
elseif not world:contains(e) then
|
|
||||||
removed_n += 1
|
|
||||||
removed_ids[removed_n] = e
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
table.clear(storage)
|
|
||||||
|
|
||||||
local dirty = false
|
|
||||||
|
|
||||||
if set_n > 0 or removed_n > 0 then
|
|
||||||
dirty = true
|
|
||||||
end
|
|
||||||
|
|
||||||
if dirty then
|
|
||||||
local removed = table.move(removed_ids, 1, removed_n, 1, {}) :: { jecs.Entity }
|
|
||||||
local set = table.move(set_ids, 1, set_n, 1, {}) :: { jecs.Entity }
|
|
||||||
snapshot[tostring(component)] = {
|
|
||||||
set = if set_n > 0 then set else nil,
|
|
||||||
values = if set_n > 0 then set_values else nil,
|
|
||||||
removed = if removed_n > 0 then removed else nil
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if next(snapshot) ~= nil then
|
|
||||||
remotes.replication:FireAllClients(snapshot)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,8 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "marcus/demo"
|
|
||||||
version = "0.1.0"
|
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
|
||||||
realm = "shared"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
jabby = "alicesaidhi/jabby@0.2.2"
|
|
310
jecs.d.ts
vendored
310
jecs.d.ts
vendored
|
@ -1,310 +0,0 @@
|
||||||
/**
|
|
||||||
* 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<TData = unknown> = number & {
|
|
||||||
readonly __nominal_Entity: unique symbol;
|
|
||||||
readonly __type_TData: TData;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An entity with no associated data when used as a component
|
|
||||||
*/
|
|
||||||
export type Tag = Entity<undefined>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A pair of entities:
|
|
||||||
* - `pred` is the type of the "predicate" entity.
|
|
||||||
* - `obj` is the type of the "object" entity.
|
|
||||||
*/
|
|
||||||
export type Pair<P = unknown, O = unknown> = number & {
|
|
||||||
readonly __nominal_Pair: unique symbol;
|
|
||||||
readonly __pred: P;
|
|
||||||
readonly __obj: O;
|
|
||||||
};
|
|
||||||
/**
|
|
||||||
* An `Id` can be either a single Entity or a Pair of Entities.
|
|
||||||
* By providing `TData`, you can specifically require an Id that yields that type.
|
|
||||||
*/
|
|
||||||
export type Id<TData = unknown> = Entity<TData> | Pair<TData, unknown> | Pair<undefined, TData>;
|
|
||||||
|
|
||||||
export type InferComponent<E> = E extends Entity<infer D>
|
|
||||||
? D
|
|
||||||
: E extends Pair<infer P, infer O>
|
|
||||||
? P extends undefined
|
|
||||||
? O
|
|
||||||
: P
|
|
||||||
: never;
|
|
||||||
|
|
||||||
type FlattenTuple<T extends unknown[]> = T extends [infer U] ? U : LuaTuple<T>;
|
|
||||||
type Nullable<T extends unknown[]> = { [K in keyof T]: T[K] | undefined };
|
|
||||||
type InferComponents<A extends Id[]> = { [K in keyof A]: InferComponent<A[K]> };
|
|
||||||
|
|
||||||
type ArchetypeId = number;
|
|
||||||
type Column = unknown[];
|
|
||||||
|
|
||||||
export type Archetype = {
|
|
||||||
id: number;
|
|
||||||
types: number[];
|
|
||||||
type: string;
|
|
||||||
entities: number[];
|
|
||||||
columns: Column[];
|
|
||||||
records: number[];
|
|
||||||
counts: number[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
|
|
||||||
|
|
||||||
export type CachedQuery<T extends unknown[]> = {
|
|
||||||
/**
|
|
||||||
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
|
|
||||||
*/
|
|
||||||
iter(): Iter<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the matched archetypes of the query
|
|
||||||
* @returns An array of archetypes of the query
|
|
||||||
*/
|
|
||||||
archetypes(): Archetype[];
|
|
||||||
} & Iter<T>;
|
|
||||||
|
|
||||||
export type Query<T extends unknown[]> = {
|
|
||||||
/**
|
|
||||||
* Returns an iterator that produces a tuple of [Entity, ...queriedComponents].
|
|
||||||
*/
|
|
||||||
iter(): Iter<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and returns a cached version of this query for efficient reuse.
|
|
||||||
* Call refinement methods (with/without) on the query before caching.
|
|
||||||
* @returns A cached query
|
|
||||||
*/
|
|
||||||
cached(): CachedQuery<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modifies the query to include specified components.
|
|
||||||
* @param components The components to include.
|
|
||||||
* @returns A new Query with the inclusion applied.
|
|
||||||
*/
|
|
||||||
with(...components: Id[]): Query<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modifies the Query to exclude specified components.
|
|
||||||
* @param components The components to exclude.
|
|
||||||
* @returns A new Query with the exclusion applied.
|
|
||||||
*/
|
|
||||||
without(...components: Id[]): Query<T>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the matched archetypes of the query
|
|
||||||
* @returns An array of archetypes of the query
|
|
||||||
*/
|
|
||||||
archetypes(): Archetype[];
|
|
||||||
} & Iter<T>;
|
|
||||||
|
|
||||||
export class World {
|
|
||||||
/**
|
|
||||||
* Creates a new World.
|
|
||||||
*/
|
|
||||||
constructor();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enforces a check for entities to be created within a desired range.
|
|
||||||
* @param range_begin The starting point
|
|
||||||
* @param range_end The end point (optional)
|
|
||||||
*/
|
|
||||||
range(range_begin: number, range_end?: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new entity.
|
|
||||||
* @returns An entity (Tag) with no data.
|
|
||||||
*/
|
|
||||||
entity(): Tag;
|
|
||||||
entity<T extends Entity>(id: T): InferComponent<T> extends undefined ? Tag : T;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new entity in the first 256 IDs, typically used for static
|
|
||||||
* components that need fast access.
|
|
||||||
* @returns A typed Entity with `TData`.
|
|
||||||
*/
|
|
||||||
component<TData = unknown>(): Entity<TData>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the target of a relationship. For example, if we say
|
|
||||||
* `world.target(entity, ChildOf)`, this returns the parent entity.
|
|
||||||
* @param entity The entity using a relationship pair.
|
|
||||||
* @param relation The "relationship" component/tag (e.g., ChildOf).
|
|
||||||
* @param index If multiple targets exist, specify an index. Defaults to 0.
|
|
||||||
*/
|
|
||||||
target(entity: Entity, relation: Entity, index?: number): Entity | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an entity (and its components/relationships) from the world entirely.
|
|
||||||
* @param entity The entity to delete.
|
|
||||||
*/
|
|
||||||
delete(entity: Entity): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a component (with no value) to the entity.
|
|
||||||
* @param entity The target entity.
|
|
||||||
* @param component The component (or tag) to add.
|
|
||||||
*/
|
|
||||||
add<C>(entity: Entity, component: undefined extends InferComponent<C> ? C : Id<undefined>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Installs a hook on the given component.
|
|
||||||
* @param component The target component.
|
|
||||||
* @param hook The hook to install.
|
|
||||||
* @param value The hook callback.
|
|
||||||
*/
|
|
||||||
set<T>(component: Entity<T>, hook: StatefulHook, value: (e: Entity<T>, id: Id<T>, data: T) => void): void;
|
|
||||||
set<T>(component: Entity<T>, hook: StatelessHook, value: (e: Entity<T>, id: Id<T>) => void): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assigns a value to a component on the given entity.
|
|
||||||
* @param entity The target entity.
|
|
||||||
* @param component The component definition (could be a Pair or Entity).
|
|
||||||
* @param value The value to store with that component.
|
|
||||||
*/
|
|
||||||
set<E extends Id<unknown>>(entity: Entity, component: E, value: InferComponent<E>): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up the world by removing empty archetypes and rebuilding the archetype collections.
|
|
||||||
* This helps maintain memory efficiency by removing unused archetype definitions.
|
|
||||||
*/
|
|
||||||
cleanup(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears all components and relationships from the given entity, but
|
|
||||||
* does not delete the entity from the world.
|
|
||||||
* @param entity The entity to clear.
|
|
||||||
*/
|
|
||||||
clear(entity: Entity): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a component from the given entity.
|
|
||||||
* @param entity The target entity.
|
|
||||||
* @param component The component to remove.
|
|
||||||
*/
|
|
||||||
remove(entity: Entity, component: Id): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the values of up to 4 components on a given entity. Missing
|
|
||||||
* components will return `undefined`.
|
|
||||||
* @param entity The entity to query.
|
|
||||||
* @param components Up to 4 components/tags to retrieve.
|
|
||||||
* @returns A tuple of data (or a single value), each possibly undefined.
|
|
||||||
*/
|
|
||||||
get<T extends [Id] | [Id, Id] | [Id, Id, Id] | [Id, Id, Id, Id]>(
|
|
||||||
entity: Entity,
|
|
||||||
...components: T
|
|
||||||
): FlattenTuple<Nullable<InferComponents<T>>>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns `true` if the given entity has all of the specified components.
|
|
||||||
* A maximum of 4 components can be checked at once.
|
|
||||||
* @param entity The entity to check.
|
|
||||||
* @param components Upto 4 components to check for.
|
|
||||||
*/
|
|
||||||
has(entity: Entity, ...components: Id[]): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the parent (the target of a `ChildOf` relationship) for an entity,
|
|
||||||
* if such a relationship exists.
|
|
||||||
* @param entity The entity whose parent is queried.
|
|
||||||
*/
|
|
||||||
parent(entity: Entity): Entity | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an entity exists in the world.
|
|
||||||
* @param entity The entity to verify.
|
|
||||||
*/
|
|
||||||
contains(entity: Entity): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an entity with the given ID is currently alive, ignoring its generation.
|
|
||||||
* @param entity The entity to verify.
|
|
||||||
* @returns boolean true if any entity with the given ID exists (ignoring generation), false otherwise
|
|
||||||
*/
|
|
||||||
exists(entity: Entity): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an iterator that yields all entities that have the specified component or relationship.
|
|
||||||
* @param id The component or relationship ID to search for
|
|
||||||
* @returns An iterator function that yields entities
|
|
||||||
*/
|
|
||||||
each(id: Id): IterableFunction<Entity>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an iterator that yields all child entities of the specified parent entity.
|
|
||||||
* Uses the ChildOf relationship internally.
|
|
||||||
* @param parent The parent entity to get children for
|
|
||||||
* @returns An iterator function that yields child entities
|
|
||||||
*/
|
|
||||||
children(parent: Entity): IterableFunction<Entity>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Searches the world for entities that match specified components.
|
|
||||||
* @param components The list of components to query.
|
|
||||||
* @returns A Query object to iterate over results.
|
|
||||||
*/
|
|
||||||
query<T extends Id[]>(...components: T): Query<InferComponents<T>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function component<T>(): Entity<T>;
|
|
||||||
|
|
||||||
export function tag(): Tag;
|
|
||||||
|
|
||||||
// note: original types had id: Entity, id: Id<T>, which does not work with TS.
|
|
||||||
export function meta<T>(e: Entity, id: Id<T>, value?: T): Entity<T>;
|
|
||||||
|
|
||||||
export function is_tag(world: World, id: Id): boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a composite key (pair)
|
|
||||||
* @param pred The first entity (predicate)
|
|
||||||
* @param obj The second entity (object)
|
|
||||||
* @returns The composite key (pair)
|
|
||||||
*/
|
|
||||||
export function pair<P, O>(pred: Entity<P>, obj: Entity<O>): Pair<P, O>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the entity is a composite key (pair)
|
|
||||||
* @param value The entity to check
|
|
||||||
* @returns If the entity is a pair
|
|
||||||
*/
|
|
||||||
export function IS_PAIR(value: Id): value is Pair;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the first entity (predicate) of a pair
|
|
||||||
* @param pair The pair to get the first entity from
|
|
||||||
* @returns The first entity (predicate) of the pair
|
|
||||||
*/
|
|
||||||
export function pair_first<P, O>(world: World, p: Pair<P, O>): Entity<P>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the second entity (object) of a pair
|
|
||||||
* @param pair The pair to get the second entity from
|
|
||||||
* @returns The second entity (object) of the pair
|
|
||||||
*/
|
|
||||||
export function pair_second<P, O>(world: World, p: Pair<P, O>): Entity<O>;
|
|
||||||
|
|
||||||
type StatefulHook = Entity<<T>(e: Entity<T>, id: Id<T>, data: T) => void> & {
|
|
||||||
readonly __nominal_StatefulHook: unique symbol,
|
|
||||||
}
|
|
||||||
type StatelessHook = Entity<<T>(e: Entity<T>, id: Id<T>) => void> & {
|
|
||||||
readonly __nominal_StatelessHook: unique symbol,
|
|
||||||
}
|
|
||||||
|
|
||||||
export declare const OnAdd: StatefulHook;
|
|
||||||
export declare const OnRemove: StatelessHook;
|
|
||||||
export declare const OnChange: StatefulHook;
|
|
||||||
export declare const ChildOf: Tag;
|
|
||||||
export declare const Wildcard: Entity;
|
|
||||||
export declare const w: Entity;
|
|
||||||
export declare const OnDelete: Tag;
|
|
||||||
export declare const OnDeleteTarget: Tag;
|
|
||||||
export declare const Delete: Tag;
|
|
||||||
export declare const Remove: Tag;
|
|
||||||
export declare const Name: Entity<string>;
|
|
||||||
export declare const Rest: Entity;
|
|
5152
jecs.luau → jecs.lua
5152
jecs.luau → jecs.lua
File diff suppressed because it is too large
Load diff
659
mirror.luau
659
mirror.luau
|
@ -1,659 +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 = { sparse: { [ArchetypeId]: ArchetypeRecord }, size: number }
|
|
||||||
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(
|
|
||||||
entityIndex: EntityIndex,
|
|
||||||
to: Archetype,
|
|
||||||
destinationRow: i24,
|
|
||||||
from: Archetype,
|
|
||||||
sourceRow: i24
|
|
||||||
)
|
|
||||||
local columns = from.columns
|
|
||||||
local sourceEntities = from.entities
|
|
||||||
local destinationEntities = to.entities
|
|
||||||
local destinationColumns = to.columns
|
|
||||||
local tr = to.records
|
|
||||||
local types = from.types
|
|
||||||
|
|
||||||
for i, column in columns do
|
|
||||||
-- Retrieves the new column index from the source archetype's record from each component
|
|
||||||
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
|
|
||||||
local targetColumn = destinationColumns[tr[types[i]]]
|
|
||||||
|
|
||||||
-- Sometimes target column may not exist, e.g. when you remove a component.
|
|
||||||
if targetColumn then
|
|
||||||
targetColumn[destinationRow] = column[sourceRow]
|
|
||||||
end
|
|
||||||
-- If the entity is the last row in the archetype then swapping it would be meaningless.
|
|
||||||
local last = #column
|
|
||||||
if sourceRow ~= last then
|
|
||||||
-- Swap rempves columns to ensure there are no holes in the archetype.
|
|
||||||
column[sourceRow] = column[last]
|
|
||||||
end
|
|
||||||
column[last] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Move the entity from the source to the destination archetype.
|
|
||||||
local atSourceRow = sourceEntities[sourceRow]
|
|
||||||
destinationEntities[destinationRow] = atSourceRow
|
|
||||||
entityIndex[atSourceRow].row = destinationRow
|
|
||||||
|
|
||||||
-- Because we have swapped columns we now have to update the records
|
|
||||||
-- corresponding to the entities' rows that were swapped.
|
|
||||||
local movedAway = #sourceEntities
|
|
||||||
if sourceRow ~= movedAway then
|
|
||||||
local atMovedAway = sourceEntities[movedAway]
|
|
||||||
sourceEntities[sourceRow] = atMovedAway
|
|
||||||
entityIndex[atMovedAway].row = sourceRow
|
|
||||||
end
|
|
||||||
|
|
||||||
sourceEntities[movedAway] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local function archetypeAppend(entity: number, archetype: Archetype): number
|
|
||||||
local entities = archetype.entities
|
|
||||||
local length = #entities + 1
|
|
||||||
entities[length] = entity
|
|
||||||
return length
|
|
||||||
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
|
|
||||||
return table.concat(arr, "_")
|
|
||||||
end
|
|
||||||
|
|
||||||
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
|
|
||||||
local destinationIds = to.types
|
|
||||||
local records = to.records
|
|
||||||
local id = to.id
|
|
||||||
|
|
||||||
for i, destinationId in destinationIds do
|
|
||||||
local archetypesMap = componentIndex[destinationId]
|
|
||||||
|
|
||||||
if not archetypesMap then
|
|
||||||
archetypesMap = { size = 0, sparse = {} }
|
|
||||||
componentIndex[destinationId] = archetypesMap
|
|
||||||
end
|
|
||||||
|
|
||||||
archetypesMap.sparse[id] = i
|
|
||||||
records[destinationId] = i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function archetypeOf(world: World, types: { i24 }, prev: Archetype?): Archetype
|
|
||||||
local ty = hash(types)
|
|
||||||
|
|
||||||
local id = world.nextArchetypeId + 1
|
|
||||||
world.nextArchetypeId = id
|
|
||||||
|
|
||||||
local length = #types
|
|
||||||
local columns = table.create(length) :: { any }
|
|
||||||
|
|
||||||
for index in types do
|
|
||||||
columns[index] = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetype = {
|
|
||||||
columns = columns,
|
|
||||||
edges = {},
|
|
||||||
entities = {},
|
|
||||||
id = id,
|
|
||||||
records = {},
|
|
||||||
type = ty,
|
|
||||||
types = types,
|
|
||||||
}
|
|
||||||
world.archetypeIndex[ty] = archetype
|
|
||||||
world.archetypes[id] = archetype
|
|
||||||
if length > 0 then
|
|
||||||
createArchetypeRecords(world.componentIndex, archetype, prev)
|
|
||||||
end
|
|
||||||
|
|
||||||
return archetype
|
|
||||||
end
|
|
||||||
|
|
||||||
local World = {}
|
|
||||||
World.__index = World
|
|
||||||
function World.new()
|
|
||||||
local self = setmetatable({
|
|
||||||
archetypeIndex = {},
|
|
||||||
archetypes = {},
|
|
||||||
componentIndex = {},
|
|
||||||
entityIndex = {},
|
|
||||||
hooks = {
|
|
||||||
[ON_ADD] = {},
|
|
||||||
},
|
|
||||||
nextArchetypeId = 0,
|
|
||||||
nextComponentId = 0,
|
|
||||||
nextEntityId = 0,
|
|
||||||
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
|
|
||||||
}, World)
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
local function emit(world, eventDescription)
|
|
||||||
local event = eventDescription.event
|
|
||||||
|
|
||||||
table.insert(world.hooks[event], {
|
|
||||||
archetype = eventDescription.archetype,
|
|
||||||
ids = eventDescription.ids,
|
|
||||||
offset = eventDescription.offset,
|
|
||||||
otherArchetype = eventDescription.otherArchetype,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
|
|
||||||
if #added > 0 then
|
|
||||||
emit(world, {
|
|
||||||
archetype = archetype,
|
|
||||||
event = ON_ADD,
|
|
||||||
ids = added,
|
|
||||||
offset = row,
|
|
||||||
otherArchetype = otherArchetype,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
export type World = typeof(World.new())
|
|
||||||
|
|
||||||
local function ensureArchetype(world: World, types, prev)
|
|
||||||
if #types < 1 then
|
|
||||||
return world.ROOT_ARCHETYPE
|
|
||||||
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)
|
|
||||||
for i, id in types do
|
|
||||||
if id == toAdd then
|
|
||||||
return -1
|
|
||||||
end
|
|
||||||
if id > toAdd then
|
|
||||||
return i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return #types + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
|
|
||||||
local types = node.types
|
|
||||||
-- Component IDs are added incrementally, so inserting and sorting
|
|
||||||
-- them each time would be expensive. Instead this insertion sort can find the insertion
|
|
||||||
-- point in the types array.
|
|
||||||
local at = findInsert(types, componentId)
|
|
||||||
if at == -1 then
|
|
||||||
-- If it finds a duplicate, it just means it is the same archetype so it can return it
|
|
||||||
-- directly instead of needing to hash types for a lookup to the archetype.
|
|
||||||
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)
|
|
||||||
local edges = archetype.edges
|
|
||||||
local edge = edges[componentId]
|
|
||||||
if not edge then
|
|
||||||
edge = {} :: any
|
|
||||||
edges[componentId] = edge
|
|
||||||
end
|
|
||||||
return edge
|
|
||||||
end
|
|
||||||
|
|
||||||
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
|
|
||||||
if not from then
|
|
||||||
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
|
|
||||||
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
|
|
||||||
if not ROOT_ARCHETYPE then
|
|
||||||
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
|
|
||||||
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
|
|
||||||
end
|
|
||||||
from = ROOT_ARCHETYPE
|
|
||||||
end
|
|
||||||
|
|
||||||
local edge = ensureEdge(from, componentId)
|
|
||||||
local add = edge.add
|
|
||||||
if not add then
|
|
||||||
-- Save an edge using the component ID to the archetype to allow
|
|
||||||
-- faster traversals to adjacent archetypes.
|
|
||||||
add = findArchetypeWith(world, from, componentId)
|
|
||||||
edge.add = add :: never
|
|
||||||
end
|
|
||||||
|
|
||||||
return add
|
|
||||||
end
|
|
||||||
|
|
||||||
local function ensureRecord(entityIndex, entityId: i53): Record
|
|
||||||
local record = entityIndex[entityId]
|
|
||||||
|
|
||||||
if not record then
|
|
||||||
record = {}
|
|
||||||
entityIndex[entityId] = record
|
|
||||||
end
|
|
||||||
|
|
||||||
return record :: Record
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
|
|
||||||
local record = ensureRecord(world.entityIndex, entityId)
|
|
||||||
local from = record.archetype
|
|
||||||
local to = archetypeTraverseAdd(world, componentId, from)
|
|
||||||
|
|
||||||
if from == to then
|
|
||||||
-- If the archetypes are the same it can avoid moving the entity
|
|
||||||
-- and just set the data directly.
|
|
||||||
local archetypeRecord = to.records[componentId]
|
|
||||||
from.columns[archetypeRecord][record.row] = data
|
|
||||||
-- Should fire an OnSet event here.
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if from then
|
|
||||||
-- If there was a previous archetype, then the entity needs to move the archetype
|
|
||||||
moveEntity(world.entityIndex, entityId, record, to)
|
|
||||||
else
|
|
||||||
if #to.types > 0 then
|
|
||||||
-- When there is no previous archetype it should create the archetype
|
|
||||||
newEntity(entityId, record, to)
|
|
||||||
onNotifyAdd(world, to, from, record.row, { componentId })
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetypeRecord = to.records[componentId]
|
|
||||||
to.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)
|
|
||||||
|
|
||||||
local remove = edge.remove
|
|
||||||
if not remove then
|
|
||||||
local to = table.clone(from.types)
|
|
||||||
table.remove(to, table.find(to, componentId))
|
|
||||||
remove = ensureArchetype(world, to, from)
|
|
||||||
edge.remove = remove :: never
|
|
||||||
end
|
|
||||||
|
|
||||||
return remove
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.remove(world: World, entityId: i53, componentId: i53)
|
|
||||||
local entityIndex = world.entityIndex
|
|
||||||
local record = ensureRecord(entityIndex, entityId)
|
|
||||||
local sourceArchetype = record.archetype
|
|
||||||
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
|
|
||||||
|
|
||||||
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
|
|
||||||
moveEntity(entityIndex, entityId, record, destinationArchetype)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Keeping the function as small as possible to enable inlining
|
|
||||||
local function get(record: Record, componentId: i24)
|
|
||||||
local archetype = record.archetype
|
|
||||||
local archetypeRecord = archetype.records[componentId]
|
|
||||||
|
|
||||||
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 record = world.entityIndex[id]
|
|
||||||
if not record then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local va = get(record, a)
|
|
||||||
|
|
||||||
if b == nil then
|
|
||||||
return va
|
|
||||||
elseif c == nil then
|
|
||||||
return va, get(record, b)
|
|
||||||
elseif d == nil then
|
|
||||||
return va, get(record, b), get(record, c)
|
|
||||||
elseif e == nil then
|
|
||||||
return va, get(record, b), get(record, c), get(record, d)
|
|
||||||
else
|
|
||||||
error("args exceeded")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- the less creation the better
|
|
||||||
local function actualNoOperation() end
|
|
||||||
local function noop(_self: Query, ...: i53): () -> (number, ...any)
|
|
||||||
return actualNoOperation :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
local EmptyQuery = {
|
|
||||||
__iter = noop,
|
|
||||||
without = noop,
|
|
||||||
}
|
|
||||||
EmptyQuery.__index = EmptyQuery
|
|
||||||
setmetatable(EmptyQuery, EmptyQuery)
|
|
||||||
|
|
||||||
export type Query = typeof(EmptyQuery)
|
|
||||||
|
|
||||||
function World.query(world: World, ...: i53): Query
|
|
||||||
-- breaking?
|
|
||||||
if (...) == nil then
|
|
||||||
error("Missing components")
|
|
||||||
end
|
|
||||||
|
|
||||||
local compatibleArchetypes = {}
|
|
||||||
local length = 0
|
|
||||||
|
|
||||||
local components = { ... }
|
|
||||||
local archetypes = world.archetypes
|
|
||||||
local queryLength = #components
|
|
||||||
|
|
||||||
local firstArchetypeMap
|
|
||||||
local componentIndex = world.componentIndex
|
|
||||||
|
|
||||||
for _, componentId in components do
|
|
||||||
local map = componentIndex[componentId]
|
|
||||||
if not map then
|
|
||||||
return EmptyQuery
|
|
||||||
end
|
|
||||||
|
|
||||||
if firstArchetypeMap == nil or map.size < firstArchetypeMap.size then
|
|
||||||
firstArchetypeMap = map
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for id in firstArchetypeMap.sparse do
|
|
||||||
local archetype = archetypes[id]
|
|
||||||
local archetypeRecords = archetype.records
|
|
||||||
local indices = {}
|
|
||||||
local skip = false
|
|
||||||
|
|
||||||
for i, componentId in components do
|
|
||||||
local index = archetypeRecords[componentId]
|
|
||||||
if not index then
|
|
||||||
skip = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
indices[i] = index
|
|
||||||
end
|
|
||||||
|
|
||||||
if skip then
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
length += 1
|
|
||||||
compatibleArchetypes[length] = { archetype, indices }
|
|
||||||
end
|
|
||||||
|
|
||||||
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
|
||||||
if not lastArchetype then
|
|
||||||
return EmptyQuery
|
|
||||||
end
|
|
||||||
|
|
||||||
local preparedQuery = {}
|
|
||||||
preparedQuery.__index = preparedQuery
|
|
||||||
|
|
||||||
function preparedQuery:without(...)
|
|
||||||
local withoutComponents = { ... }
|
|
||||||
for i = #compatibleArchetypes, 1, -1 do
|
|
||||||
local archetype = compatibleArchetypes[i][1]
|
|
||||||
local records = archetype.records
|
|
||||||
local shouldRemove = false
|
|
||||||
|
|
||||||
for _, componentId in withoutComponents do
|
|
||||||
if records[componentId] then
|
|
||||||
shouldRemove = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if shouldRemove then
|
|
||||||
table.remove(compatibleArchetypes, i)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes)
|
|
||||||
if not lastArchetype then
|
|
||||||
return EmptyQuery
|
|
||||||
end
|
|
||||||
|
|
||||||
return self
|
|
||||||
end
|
|
||||||
|
|
||||||
local lastRow
|
|
||||||
local queryOutput = {}
|
|
||||||
|
|
||||||
function preparedQuery:__iter()
|
|
||||||
return function()
|
|
||||||
local archetype = compatibleArchetype[1]
|
|
||||||
local row = next(archetype.entities, lastRow)
|
|
||||||
while row == nil do
|
|
||||||
lastArchetype, compatibleArchetype = next(compatibleArchetypes, lastArchetype)
|
|
||||||
if lastArchetype == nil then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
archetype = compatibleArchetype[1]
|
|
||||||
row = next(archetype.entities, row)
|
|
||||||
end
|
|
||||||
lastRow = row
|
|
||||||
|
|
||||||
local entityId = archetype.entities[row :: number]
|
|
||||||
local columns = archetype.columns
|
|
||||||
local tr = compatibleArchetype[2]
|
|
||||||
|
|
||||||
if queryLength == 1 then
|
|
||||||
return entityId, columns[tr[1]][row]
|
|
||||||
elseif queryLength == 2 then
|
|
||||||
return entityId, columns[tr[1]][row], columns[tr[2]][row]
|
|
||||||
elseif queryLength == 3 then
|
|
||||||
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
|
|
||||||
elseif queryLength == 4 then
|
|
||||||
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
|
|
||||||
elseif queryLength == 5 then
|
|
||||||
return entityId,
|
|
||||||
columns[tr[1]][row],
|
|
||||||
columns[tr[2]][row],
|
|
||||||
columns[tr[3]][row],
|
|
||||||
columns[tr[4]][row],
|
|
||||||
columns[tr[5]][row]
|
|
||||||
elseif queryLength == 6 then
|
|
||||||
return entityId,
|
|
||||||
columns[tr[1]][row],
|
|
||||||
columns[tr[2]][row],
|
|
||||||
columns[tr[3]][row],
|
|
||||||
columns[tr[4]][row],
|
|
||||||
columns[tr[5]][row],
|
|
||||||
columns[tr[6]][row]
|
|
||||||
elseif queryLength == 7 then
|
|
||||||
return entityId,
|
|
||||||
columns[tr[1]][row],
|
|
||||||
columns[tr[2]][row],
|
|
||||||
columns[tr[3]][row],
|
|
||||||
columns[tr[4]][row],
|
|
||||||
columns[tr[5]][row],
|
|
||||||
columns[tr[6]][row],
|
|
||||||
columns[tr[7]][row]
|
|
||||||
elseif queryLength == 8 then
|
|
||||||
return entityId,
|
|
||||||
columns[tr[1]][row],
|
|
||||||
columns[tr[2]][row],
|
|
||||||
columns[tr[3]][row],
|
|
||||||
columns[tr[4]][row],
|
|
||||||
columns[tr[5]][row],
|
|
||||||
columns[tr[6]][row],
|
|
||||||
columns[tr[7]][row],
|
|
||||||
columns[tr[8]][row]
|
|
||||||
end
|
|
||||||
|
|
||||||
for i in components do
|
|
||||||
queryOutput[i] = columns[tr[i]][row]
|
|
||||||
end
|
|
||||||
|
|
||||||
return entityId, unpack(queryOutput, 1, queryLength)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return setmetatable({}, preparedQuery) :: any
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.component(world: World)
|
|
||||||
local componentId = world.nextComponentId + 1
|
|
||||||
if componentId > HI_COMPONENT_ID then
|
|
||||||
-- IDs are partitioned into ranges because component IDs are not nominal,
|
|
||||||
-- so it needs to error when IDs intersect into the entity range.
|
|
||||||
error("Too many components, consider using world:entity() instead to create components.")
|
|
||||||
end
|
|
||||||
world.nextComponentId = componentId
|
|
||||||
return componentId
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.entity(world: World)
|
|
||||||
local nextEntityId = world.nextEntityId + 1
|
|
||||||
world.nextEntityId = nextEntityId
|
|
||||||
return nextEntityId + REST
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.delete(world: World, entityId: i53)
|
|
||||||
local entityIndex = world.entityIndex
|
|
||||||
local record = entityIndex[entityId]
|
|
||||||
moveEntity(entityIndex, entityId, record, world.ROOT_ARCHETYPE)
|
|
||||||
-- Since we just appended an entity to the ROOT_ARCHETYPE we have to remove it from
|
|
||||||
-- the entities array and delete the record. We know there won't be the hole since
|
|
||||||
-- we are always removing the last row.
|
|
||||||
--world.ROOT_ARCHETYPE.entities[record.row] = nil
|
|
||||||
--entityIndex[entityId] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function World.observer(world: World, ...)
|
|
||||||
local componentIds = { ... }
|
|
||||||
local idsCount = #componentIds
|
|
||||||
local hooks = world.hooks
|
|
||||||
|
|
||||||
return {
|
|
||||||
event = function(event)
|
|
||||||
local hook = hooks[event]
|
|
||||||
hooks[event] = nil
|
|
||||||
|
|
||||||
local last, change
|
|
||||||
return function()
|
|
||||||
last, change = next(hook, last)
|
|
||||||
if not last then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local matched = false
|
|
||||||
local ids = change.ids
|
|
||||||
|
|
||||||
while not matched do
|
|
||||||
local skip = false
|
|
||||||
for _, id in ids do
|
|
||||||
if not table.find(componentIds, id) then
|
|
||||||
skip = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if skip then
|
|
||||||
last, change = next(hook, last)
|
|
||||||
ids = change.ids
|
|
||||||
continue
|
|
||||||
end
|
|
||||||
|
|
||||||
matched = true
|
|
||||||
end
|
|
||||||
|
|
||||||
local queryOutput = table.create(idsCount)
|
|
||||||
local row = change.offset
|
|
||||||
local archetype = change.archetype
|
|
||||||
local columns = archetype.columns
|
|
||||||
local archetypeRecords = archetype.records
|
|
||||||
for index, id in componentIds do
|
|
||||||
queryOutput[index] = columns[archetypeRecords[id]][row]
|
|
||||||
end
|
|
||||||
|
|
||||||
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
return table.freeze({
|
|
||||||
World = World,
|
|
||||||
ON_ADD = ON_ADD,
|
|
||||||
ON_REMOVE = ON_REMOVE,
|
|
||||||
ON_SET = ON_SET,
|
|
||||||
})
|
|
4464
package-lock.json
generated
4464
package-lock.json
generated
File diff suppressed because it is too large
Load diff
47
package.json
47
package.json
|
@ -1,47 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@rbxts/jecs",
|
|
||||||
"version": "0.6.0",
|
|
||||||
"description": "Stupidly fast Entity Component System",
|
|
||||||
"main": "jecs.luau",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/ukendio/jecs.git"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "Ukendio",
|
|
||||||
"contributors": [
|
|
||||||
"Ukendio",
|
|
||||||
"EncodedVenom"
|
|
||||||
],
|
|
||||||
"homepage": "https://github.com/ukendio/jecs",
|
|
||||||
"license": "MIT",
|
|
||||||
"types": "jecs.d.ts",
|
|
||||||
"files": [
|
|
||||||
"jecs.luau",
|
|
||||||
"jecs.d.ts",
|
|
||||||
"LICENSE.md",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"publishConfig": {
|
|
||||||
"access": "public"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rbxts/compiler-types": "^2.3.0-types.1",
|
|
||||||
"@rbxts/types": "^1.0.781",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.8.0",
|
|
||||||
"@typescript-eslint/parser": "^5.8.0",
|
|
||||||
"eslint": "^8.5.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"eslint-plugin-roblox-ts": "^0.0.32",
|
|
||||||
"prettier": "^2.5.1",
|
|
||||||
"roblox-ts": "^3.0.0",
|
|
||||||
"typescript": "^5.4.2",
|
|
||||||
"vitepress": "^1.3.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"docs:dev": "vitepress dev docs",
|
|
||||||
"docs:build": "vitepress build docs",
|
|
||||||
"docs:preview": "vitepress preview docs"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
[tools]
|
|
||||||
wally = "upliftgames/wally@0.3.2"
|
|
||||||
rojo = "rojo-rbx/rojo@7.4.4"
|
|
||||||
stylua = "johnnymorganz/stylua@2.0.1"
|
|
||||||
Blink = "1Axen/Blink@0.14.1"
|
|
||||||
wally-package-types = "JohnnyMorganz/wally-package-types@1.4.2"
|
|
|
@ -1,136 +0,0 @@
|
||||||
local jecs = require("@jecs")
|
|
||||||
local testkit = require("@testkit")
|
|
||||||
local test = testkit.test()
|
|
||||||
local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
|
|
||||||
local observers_add = require("@addons/observers")
|
|
||||||
|
|
||||||
TEST("addons/observers", function()
|
|
||||||
local world = observers_add(jecs.world())
|
|
||||||
|
|
||||||
local Test = world:component() :: jecs.Id<number>
|
|
||||||
type Q<T...> = () -> (jecs.Entity, T...)
|
|
||||||
local query:
|
|
||||||
(<A>(jecs.World, jecs.Id<A>) -> Q<A>)
|
|
||||||
& (<A, B>(jecs.World, jecs.Id<A>, jecs.Id<B>) -> Q<A, B>)
|
|
||||||
& (<A, B, C>(jecs.World, jecs.Id<A>, jecs.Id<B>, jecs.Id<C>) -> Q<A, B, C>)
|
|
||||||
= 1 :: never
|
|
||||||
for e, a in query(world, Test) do
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "Should work even if set after the component has been used"
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
world:set(world:entity(), A, 2)
|
|
||||||
local ran = true
|
|
||||||
world:added(A, function()
|
|
||||||
ran = false
|
|
||||||
end)
|
|
||||||
|
|
||||||
local entity = world:entity()
|
|
||||||
world:set(entity, A, 3)
|
|
||||||
|
|
||||||
CHECK(ran)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "Should not override hook"
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
local count = 1
|
|
||||||
local function counter()
|
|
||||||
count += 2
|
|
||||||
end
|
|
||||||
|
|
||||||
world:set(A, jecs.OnAdd, counter)
|
|
||||||
world:added(A, counter)
|
|
||||||
world:set(world:entity(), A, false)
|
|
||||||
CHECK(count == 3)
|
|
||||||
world:set(world:entity(), A, false)
|
|
||||||
|
|
||||||
CHECK(count == 5)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "Ensure ordering between signals and observers"
|
|
||||||
local A = world:component()
|
|
||||||
local B = world:component()
|
|
||||||
|
|
||||||
local count = 1
|
|
||||||
local function counter()
|
|
||||||
count += 2
|
|
||||||
end
|
|
||||||
|
|
||||||
world:observer(world:query(A, B), counter)
|
|
||||||
|
|
||||||
world:added(A, counter)
|
|
||||||
world:added(A, counter)
|
|
||||||
|
|
||||||
local e = world:entity()
|
|
||||||
world:add(e, A)
|
|
||||||
CHECK(count == 3)
|
|
||||||
|
|
||||||
world:add(e, B)
|
|
||||||
CHECK(count == 4)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "Rematch entities in observers"
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
local count = 1
|
|
||||||
local function counter()
|
|
||||||
count += 2
|
|
||||||
end
|
|
||||||
|
|
||||||
world:observer(world:query(A), counter)
|
|
||||||
|
|
||||||
local e = world:entity()
|
|
||||||
world:set(e, A, false)
|
|
||||||
CHECK(count == 2)
|
|
||||||
world:remove(e, A)
|
|
||||||
CHECK(count == 2)
|
|
||||||
world:set(e, A, false)
|
|
||||||
CHECK(count == 3)
|
|
||||||
world:set(e, A, false)
|
|
||||||
CHECK(count == 4)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "Don't report changed components in monitor"
|
|
||||||
local A = world:component()
|
|
||||||
local count = 1
|
|
||||||
local function counter()
|
|
||||||
count += 2
|
|
||||||
end
|
|
||||||
|
|
||||||
world:monitor(world:query(A), counter)
|
|
||||||
|
|
||||||
local e = world:entity()
|
|
||||||
world:set(e, A, false)
|
|
||||||
CHECK(count == 2)
|
|
||||||
world:remove(e, A)
|
|
||||||
CHECK(count == 3)
|
|
||||||
world:set(e, A, false)
|
|
||||||
CHECK(count == 4)
|
|
||||||
world:set(e, A, false)
|
|
||||||
CHECK(count == 4)
|
|
||||||
end
|
|
||||||
|
|
||||||
do CASE "Call off pairs"
|
|
||||||
local A = world:component()
|
|
||||||
|
|
||||||
local callcount = 1
|
|
||||||
world:added(A, function(entity)
|
|
||||||
callcount += 2
|
|
||||||
end)
|
|
||||||
world:added(A, function(entity)
|
|
||||||
callcount += 2
|
|
||||||
end)
|
|
||||||
|
|
||||||
local e = world:entity()
|
|
||||||
local e2 = world:entity()
|
|
||||||
|
|
||||||
world:add(e2, jecs.pair(A, e))
|
|
||||||
world:add(e, jecs.pair(A, e2))
|
|
||||||
CHECK(callcount == 5)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
return FINISH()
|
|
158
test/lol.luau
158
test/lol.luau
|
@ -1,158 +0,0 @@
|
||||||
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)
|
|
||||||
print("*dellocated", pe(id))
|
|
||||||
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,122 +0,0 @@
|
||||||
local RunService = game:GetService("RunService")
|
|
||||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
|
||||||
_G.__JECS_HI_COMPONENT_ID = 300
|
|
||||||
local ecs = require(ReplicatedStorage.ecs)
|
|
||||||
|
|
||||||
-- 500 entities
|
|
||||||
-- 2-30 components on each entity
|
|
||||||
-- 300 unique components
|
|
||||||
-- 200 systems
|
|
||||||
-- 1-10 components to query per system
|
|
||||||
|
|
||||||
local startTime = os.clock()
|
|
||||||
|
|
||||||
local world = ecs.World.new()
|
|
||||||
|
|
||||||
local components = {}
|
|
||||||
|
|
||||||
for i = 1, 300 do -- 300 components
|
|
||||||
components[i] = world:component()
|
|
||||||
end
|
|
||||||
|
|
||||||
local archetypes = {}
|
|
||||||
for i = 1, 50 do -- 50 archetypes
|
|
||||||
local archetype = {}
|
|
||||||
|
|
||||||
for _ = 1, math.random(2, 30) do
|
|
||||||
local componentId = math.random(1, #components)
|
|
||||||
|
|
||||||
table.insert(archetype, components[componentId])
|
|
||||||
end
|
|
||||||
|
|
||||||
archetypes[i] = archetype
|
|
||||||
end
|
|
||||||
|
|
||||||
for _ = 1, 1000 do -- 1000 entities in the world
|
|
||||||
local componentsToAdd = {}
|
|
||||||
|
|
||||||
local archetypeId = math.random(1, #archetypes)
|
|
||||||
local e = world:entity()
|
|
||||||
for _, component in ipairs(archetypes[archetypeId]) do
|
|
||||||
world:set(e, component, {
|
|
||||||
DummyData = math.random(1, 5000),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local function values(t)
|
|
||||||
local array = {}
|
|
||||||
for _, v in t do
|
|
||||||
table.insert(array, v)
|
|
||||||
end
|
|
||||||
return array
|
|
||||||
end
|
|
||||||
|
|
||||||
local contiguousComponents = values(components)
|
|
||||||
local systemComponentsToQuery = {}
|
|
||||||
|
|
||||||
for _ = 1, 200 do -- 200 systems
|
|
||||||
local numComponentsToQuery = math.random(1, 10)
|
|
||||||
local componentsToQuery = {}
|
|
||||||
|
|
||||||
for _ = 1, numComponentsToQuery do
|
|
||||||
table.insert(componentsToQuery, contiguousComponents[math.random(1, #contiguousComponents)])
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(systemComponentsToQuery, componentsToQuery)
|
|
||||||
end
|
|
||||||
|
|
||||||
local worldCreateTime = os.clock() - startTime
|
|
||||||
local results = {}
|
|
||||||
startTime = os.clock()
|
|
||||||
|
|
||||||
RunService.Heartbeat:Connect(function()
|
|
||||||
local added = 0
|
|
||||||
local systemStartTime = os.clock()
|
|
||||||
debug.profilebegin("systems")
|
|
||||||
for _, componentsToQuery in ipairs(systemComponentsToQuery) do
|
|
||||||
debug.profilebegin("system")
|
|
||||||
for entityId, firstComponent in world:query(unpack(componentsToQuery)) do
|
|
||||||
world:set(
|
|
||||||
entityId,
|
|
||||||
{
|
|
||||||
DummyData = firstComponent.DummyData + 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
added += 1
|
|
||||||
end
|
|
||||||
debug.profileend()
|
|
||||||
end
|
|
||||||
debug.profileend()
|
|
||||||
|
|
||||||
if os.clock() - startTime < 4 then
|
|
||||||
-- discard first 4 seconds
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if results == nil then
|
|
||||||
return
|
|
||||||
elseif #results < 1000 then
|
|
||||||
table.insert(results, os.clock() - systemStartTime)
|
|
||||||
else
|
|
||||||
print("added", added)
|
|
||||||
print("World created in", worldCreateTime * 1000, "ms")
|
|
||||||
local sum = 0
|
|
||||||
for _, result in ipairs(results) do
|
|
||||||
sum += result
|
|
||||||
end
|
|
||||||
print(("Average frame time: %fms"):format((sum / #results) * 1000))
|
|
||||||
|
|
||||||
results = nil
|
|
||||||
|
|
||||||
local n = #world.archetypes
|
|
||||||
|
|
||||||
print(
|
|
||||||
("X entities\n%d components\n%d systems\n%d archetypes"):format(
|
|
||||||
#components,
|
|
||||||
#systemComponentsToQuery,
|
|
||||||
n
|
|
||||||
)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end)
|
|
|
@ -1,19 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Stress",
|
|
||||||
"tree": {
|
|
||||||
"$className": "DataModel",
|
|
||||||
"ReplicatedStorage": {
|
|
||||||
"$className": "ReplicatedStorage",
|
|
||||||
"$path": "DevPackages",
|
|
||||||
"ecs": {
|
|
||||||
"$path": "jecs.luau"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ReplicatedFirst": {
|
|
||||||
"$className": "ReplicatedFirst",
|
|
||||||
"stres": {
|
|
||||||
"$path": "stress.client.luau"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
1855
test/tests.luau
1855
test/tests.luau
File diff suppressed because it is too large
Load diff
|
@ -1,24 +0,0 @@
|
||||||
local jecs = require("@jecs")
|
|
||||||
local pair = jecs.pair
|
|
||||||
local ChildOf = jecs.ChildOf
|
|
||||||
local lifetime_tracker_add = require("@tools/lifetime_tracker")
|
|
||||||
local world = lifetime_tracker_add(jecs.world(), {padding_enabled=false})
|
|
||||||
local FriendsWith = world:component()
|
|
||||||
world:print_snapshot()
|
|
||||||
local e1 = world:entity()
|
|
||||||
local e2 = world:entity()
|
|
||||||
world:delete(e2)
|
|
||||||
|
|
||||||
world:print_snapshot()
|
|
||||||
local e3 = world:entity()
|
|
||||||
world:add(e3, pair(ChildOf, e1))
|
|
||||||
local e4 = world:entity()
|
|
||||||
world:add(e4, pair(FriendsWith, e3))
|
|
||||||
world:print_snapshot()
|
|
||||||
world:delete(e1)
|
|
||||||
world:delete(e3)
|
|
||||||
world:print_snapshot()
|
|
||||||
world:print_entity_index()
|
|
||||||
world:entity()
|
|
||||||
world:entity()
|
|
||||||
world:print_snapshot()
|
|
|
@ -1,24 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"downlevelIteration": true,
|
|
||||||
"jsx": "react",
|
|
||||||
"jsxFactory": "Roact.createElement",
|
|
||||||
"jsxFragmentFactory": "Roact.Fragment",
|
|
||||||
"module": "commonjs",
|
|
||||||
"moduleResolution": "Node",
|
|
||||||
"noLib": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"strict": true,
|
|
||||||
"target": "ESNext",
|
|
||||||
"typeRoots": [
|
|
||||||
"node_modules/@rbxts"
|
|
||||||
],
|
|
||||||
"rootDir": "lib",
|
|
||||||
"outDir": "out",
|
|
||||||
"baseUrl": "lib",
|
|
||||||
"incremental": true,
|
|
||||||
"tsBuildInfoFile": "out/tsconfig.tsbuildinfo",
|
|
||||||
"moduleDetection": "force"
|
|
||||||
}
|
|
||||||
}
|
|
15
wally.toml
15
wally.toml
|
@ -1,15 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "ukendio/jecs"
|
|
||||||
version = "0.6.0"
|
|
||||||
registry = "https://github.com/UpliftGames/wally-index"
|
|
||||||
realm = "shared"
|
|
||||||
license = "MIT"
|
|
||||||
include = [
|
|
||||||
"default.project.json",
|
|
||||||
"jecs.luau",
|
|
||||||
"wally.toml",
|
|
||||||
"README.md",
|
|
||||||
"CHANGELOG.md",
|
|
||||||
"LICENSE",
|
|
||||||
]
|
|
||||||
exclude = ["**"]
|
|
Loading…
Reference in a new issue