Compare commits

...

3 commits

Author SHA1 Message Date
PepeElToro41
afc2e3a2f3
Merge b654d01421 into 3dacb2af80 2025-09-14 20:17:51 -05:00
Ukendio
3dacb2af80 Remove focus
Some checks are pending
analysis / Run Luau Analyze (push) Waiting to run
deploy-docs / build (push) Waiting to run
deploy-docs / Deploy (push) Blocked by required conditions
publish-npm / publish (push) Waiting to run
unit-testing / Run Luau Tests (push) Waiting to run
2025-09-14 19:59:38 +02:00
PepeElToro41
b654d01421 improve observers 2025-08-30 22:00:56 -06:00
6 changed files with 254 additions and 208 deletions

View file

@ -3,10 +3,11 @@ local jecs = require("@jecs")
type World = jecs.World type World = jecs.World
type Query<T...> = jecs.Query<T...> type Query<T...> = jecs.Query<T...>
type QueryInner<T...> = Query<T...> & jecs.QueryInner
type Id<T=any> = jecs.Id<T> type Id<T = any> = jecs.Id<T>
type Entity<T> = jecs.Entity<T> type Entity<T = any> = jecs.Entity<T>
export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...) export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
@ -14,24 +15,20 @@ export type Observer<T...> = typeof(setmetatable(
{} :: { {} :: {
iter: Iter<T...>, iter: Iter<T...>,
entities: { Entity<nil> }, entities: { Entity<nil> },
disconnect: (Observer<T...>) -> () disconnect: (Observer<T...>) -> (),
}, },
{} :: { {} :: {
__iter: Iter<T...>, __iter: Iter<T...>,
} }
)) ))
local function observers_new<T...>( local function get_matching_archetypes(world: jecs.World, query: QueryInner<...any>)
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
query:cached()
local world = (query :: Query<T...> & { world: World }).world
callback = callback
local archetypes = {} local archetypes = {}
for _, archetype in query.compatible_archetypes do
archetypes[archetype.id] = true
end
local terms = query.ids local terms = query.ids
local first = terms[1] local first = terms[1]
@ -40,113 +37,139 @@ local function observers_new<T...>(
observer_on_create.callback = function(archetype) observer_on_create.callback = function(archetype)
archetypes[archetype.id] = true archetypes[archetype.id] = true
end end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first] local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
local observer_on_delete = observers_on_delete[#observers_on_delete] local observer_on_delete = observers_on_delete[#observers_on_delete]
observer_on_delete.callback = function(archetype) observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil archetypes[archetype.id] = nil
end end
local function disconnect()
table.remove(observers_on_create, table.find(observers_on_create, observer_on_create))
table.remove(observers_on_delete, table.find(observers_on_delete, observer_on_delete))
end
return archetypes, disconnect
end
local function observers_new<T...>(query: Query<T...>, callback: ((Entity<nil>, Id<any>, value: any?) -> ())?): Observer<T...>
query:cached()
local world = (query :: QueryInner<T...>).world
callback = callback
local archetypes, disconnect_query = get_matching_archetypes(world, query :: QueryInner<T...>)
local terms = query.ids
local query_with = query.filter_with or terms
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
local i = 0
local entities = {}
local function emplaced<T, a>( local iter_indexes = {} :: { [Entity]: number }
entity: jecs.Entity<T>, local iter_queue = {} :: { Entity }
id: jecs.Id<a>,
value: a? local function remove_queued(entity: jecs.Entity, index: number)
) if index ~= nil then
local last = table.remove(iter_queue)
if last and last ~= entity then
iter_queue[index] = last
iter_indexes[last] = index
end
iter_indexes[entity] = nil
end
end
local function emplaced<T>(entity: jecs.Entity, id: jecs.Id<T>, value: T?)
local r = entity_index.sparse_array[jecs.ECS_ID(entity)] local r = entity_index.sparse_array[jecs.ECS_ID(entity)]
local index = iter_indexes[entity]
if r == nil then
remove_queued(entity, index)
end
local archetype = r.archetype local archetype = r.archetype
if archetypes[archetype.id] then if archetypes[archetype.id] then
i += 1 if index == nil then
entities[i] = entity table.insert(iter_queue, entity)
iter_indexes[entity] = #iter_queue
end
if callback ~= nil then if callback ~= nil then
callback(entity, id, value) callback(entity :: Entity, id, value)
end end
end end
end end
for _, term in terms do local function removed(entity: jecs.Entity)
local index = iter_indexes[entity]
if index ~= nil then
remove_queued(entity, index)
end
end
local hooked = {} :: { () -> () }
for _, term in query_with do
if jecs.IS_PAIR(term) then if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term) term = jecs.ECS_PAIR_FIRST(term)
end end
world:added(term, emplaced) table.insert(hooked, world:added(term, emplaced))
world:changed(term, emplaced) table.insert(hooked, world:changed(term, emplaced))
end table.insert(hooked, world:removed(term, removed))
end
local function disconnect() local function disconnect()
table.remove(observers_on_create, table.find( disconnect_query()
observers_on_create, for _, unhook in hooked do
observer_on_create unhook()
)) end
end
table.remove(observers_on_delete, table.find( local function iter()
observers_on_delete, local row = #iter_queue
observer_on_delete return function()
)) if row == 0 then
end table.clear(iter_queue)
table.clear(iter_indexes)
end
local entity = iter_queue[row]
row -= 1
return entity
end
end
local function iter() local observer = {
local row = i disconnect = disconnect,
return function() entities = iter_queue,
if row == 0 then __iter = iter,
i = 0 iter = iter,
table.clear(entities) }
end
local entity = entities[row]
row -= 1
return entity
end
end
local observer = { setmetatable(observer, observer)
disconnect = disconnect,
entities = entities,
__iter = iter,
iter = iter
}
setmetatable(observer, observer) return (observer :: any) :: Observer<T...>
return (observer :: any) :: Observer<T...>
end end
local function monitors_new<T...>( local function monitors_new<T...>(query: Query<T...>, callback: ((Entity<nil>, Id<any>, value: any?) -> ())?): Observer<T...>
query: Query<T...>,
callback: ((Entity<nil>, Id<any>, value: any?) -> ())?
): Observer<T...>
query:cached() query:cached()
local world = (query :: Query<T...> & { world: World }).world local world = (query :: QueryInner<T...>).world
local archetypes, disconnect_query = get_matching_archetypes(world, query :: QueryInner<T...>)
local archetypes = {}
local terms = query.ids local terms = query.ids
local first = terms[1] local query_with = query.filter_with or terms
local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
local observer_on_create = observers_on_create[#observers_on_create]
observer_on_create.callback = function(archetype)
archetypes[archetype.id] = true
end
local observers_on_delete = world.observable[jecs.ArchetypeDelete][first]
local observer_on_delete = observers_on_delete[#observers_on_delete]
observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil
end
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
local i = 0 local i = 0
local entities = {} local entities = {}
local function emplaced<T, a>( local function emplaced<T, a>(entity: jecs.Entity<T>, id: jecs.Id<a>, value: a?)
entity: jecs.Entity<T>, local r = jecs.entity_index_try_get_fast(entity_index, entity :: any) :: jecs.Record
id: jecs.Id<a>, if not r or not r.archetype then
value: a? if callback then
) callback(entity :: Entity, jecs.OnRemove)
local r = jecs.entity_index_try_get_fast( end
entity_index, entity :: any) :: jecs.Record return
end
local archetype = r.archetype local archetype = r.archetype
@ -154,14 +177,18 @@ local function monitors_new<T...>(
i += 1 i += 1
entities[i] = entity entities[i] = entity
if callback ~= nil then if callback ~= nil then
callback(entity, jecs.OnAdd) callback(entity :: Entity, jecs.OnAdd)
end
else
if callback ~= nil then
callback(entity :: Entity, jecs.OnRemove)
end end
end end
end end
local function removed(entity: jecs.Entity, component: jecs.Id) local function removed(entity: jecs.Entity, component: jecs.Id)
local r = jecs.record(world, entity) local r = jecs.record(world, entity)
if not archetypes[r.archetype.id] then if not r or not archetypes[r.archetype.id] then
return return
end end
local EcsOnRemove = jecs.OnRemove :: jecs.Id local EcsOnRemove = jecs.OnRemove :: jecs.Id
@ -170,52 +197,48 @@ local function monitors_new<T...>(
end end
end end
for _, term in terms do local hooked = {} :: { () -> () }
for _, term in query_with do
if jecs.IS_PAIR(term) then if jecs.IS_PAIR(term) then
term = jecs.ECS_PAIR_FIRST(term) term = jecs.ECS_PAIR_FIRST(term)
end end
world:added(term, emplaced) table.insert(hooked, world:added(term, emplaced))
world:removed(term, removed) table.insert(hooked, world:removed(term, removed))
end end
local function disconnect() local function disconnect()
table.remove(observers_on_create, table.find( disconnect_query()
observers_on_create, for _, unhook in hooked do
observer_on_create unhook()
)) end
end
table.remove(observers_on_delete, table.find( local function iter()
observers_on_delete, local row = i
observer_on_delete return function()
)) if row == 0 then
end i = 0
table.clear(entities)
local function iter() end
local row = i local entity = entities[row]
return function() row -= 1
if row == 0 then return entity
i = 0 end
table.clear(entities) end
end
local entity = entities[row]
row -= 1
return entity
end
end
local observer = { local observer = {
disconnect = disconnect, disconnect = disconnect,
entities = entities, entities = entities,
__iter = iter, __iter = iter,
iter = iter iter = iter,
} }
setmetatable(observer, observer) setmetatable(observer, observer)
return (observer :: any) :: Observer<T...> return (observer :: any) :: Observer<T...>
end end
return { return {
monitor = monitors_new, monitor = monitors_new,
observer = observers_new observer = observers_new,
} }

View file

@ -2,33 +2,12 @@
--!native --!native
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local newWorld = Matter.World.new()
local jecs = require(ReplicatedStorage.Lib:Clone()) local jecs = require(ReplicatedStorage.Lib:Clone())
local mirror = require(ReplicatedStorage.mirror:Clone()) local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.world() local mcs = mirror.world()
local ecs = jecs.world() local ecs = jecs.world()
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 D1 = ecs:component()
local D2 = ecs:component() local D2 = ecs:component()
local D3 = ecs:component() local D3 = ecs:component()
@ -47,90 +26,53 @@ local E6 = mcs:component()
local E7 = mcs:component() local E7 = mcs:component()
local E8 = mcs:component() local E8 = mcs:component()
local registry2 = ecr.registry()
local function flip() local function flip()
return math.random() >= 0.25 return math.random() >= 0.3
end end
local N = 2 ^ 16 - 2 local N = 2 ^ 16 - 2
local archetypes = {}
local hm = 0
for i = 1, N do for i = 1, N do
local id = registry2.create()
local combination = ""
local n = newWorld:spawn()
local entity = ecs:entity() local entity = ecs:entity()
local m = mcs:entity() local m = mcs:entity()
if flip() then if flip() then
registry2:set(id, B1, { value = true }) ecs:add(entity, entity)
ecs:set(entity, D1, { value = true }) mcs:add(m, m)
newWorld:insert(n, A1({ value = true }))
mcs:set(m, E1, { value = 2 })
end end
if flip() then if flip() then
combination ..= "B" ecs:set(entity, D1, true)
registry2:set(id, B2, { value = true }) mcs:set(m, E1, true)
ecs:set(entity, D2, { value = true })
mcs:set(m, E2, { value = 2 })
newWorld:insert(n, A2({ value = true }))
end end
if flip() then if flip() then
combination ..= "C" ecs:set(entity, D2, true)
registry2:set(id, B3, { value = true }) mcs:set(m, E2, true)
ecs:set(entity, D3, { value = true })
mcs:set(m, E3, { value = 2 })
newWorld:insert(n, A3({ value = true }))
end end
if flip() then if flip() then
combination ..= "D" ecs:set(entity, D3, true)
registry2:set(id, B4, { value = true }) mcs:set(m, E3, true)
ecs:set(entity, D4, { value = true })
mcs:set(m, E4, { value = 2 })
newWorld:insert(n, A4({ value = true }))
end end
if flip() then if flip() then
combination ..= "E" ecs:set(entity, D4, true)
registry2:set(id, B5, { value = true }) mcs:set(m, E4, true)
ecs:set(entity, D5, { value = true })
mcs:set(m, E5, { value = 2 })
newWorld:insert(n, A5({ value = true }))
end end
if flip() then if flip() then
combination ..= "F" ecs:set(entity, D5, true)
registry2:set(id, B6, { value = true }) mcs:set(m, E5, true)
ecs:set(entity, D6, { value = true })
mcs:set(m, E6, { value = 2 })
newWorld:insert(n, A6({ value = true }))
end end
if flip() then if flip() then
combination ..= "G" ecs:set(entity, D6, true)
registry2:set(id, B7, { value = true }) mcs:set(m, E6, true)
ecs:set(entity, D7, { value = true })
mcs:set(m, E7, { value = 2 })
newWorld:insert(n, A7({ value = true }))
end end
if flip() then if flip() then
combination ..= "H" ecs:set(entity, D7, true)
registry2:set(id, B8, { value = true }) mcs:set(m, E7, true)
newWorld:insert(n, A8({ value = true }))
ecs:set(entity, D8, { value = true })
mcs:set(m, E8, { value = 2 })
end end
if flip() then
if combination:find("BCDF") then ecs:set(entity, D8, true)
if not archetypes[combination] then mcs:set(m, E8, true)
print(combination)
end
hm += 1
end end
archetypes[combination] = true
end end
print("TEST", hm)
local count = 0 local count = 0
@ -138,7 +80,11 @@ for _, archetype in ecs:query(D2, D4, D6, D8):archetypes() do
count += #archetype.entities count += #archetype.entities
end end
print(count)
local mq = mcs:query(E2, E4, E6, E8):cached()
local jq = ecs:query(D2, D4, D6, D8):cached()
print(count, #jq:archetypes())
return { return {
ParameterGenerator = function() ParameterGenerator = function()
@ -157,12 +103,12 @@ return {
-- end, -- end,
-- --
Mirror = function() Mirror = function()
for entityId, firstComponent in mcs:query(E2, E4, E6, E8) do for entityId, firstComponent in mq do
end end
end, end,
Jecs = function() Jecs = function()
for entityId, firstComponent in ecs:query(D2, D4, D6, D8) do for entityId, firstComponent in jq do
end end
end, end,
}, },

View file

@ -216,7 +216,8 @@ export type World = {
children: <T>(self: World, id: Id<T>) -> () -> Entity, children: <T>(self: World, id: Id<T>) -> () -> Entity,
--- Searches the world for entities that match a given query --- Searches the world for entities that match a given query
query: (<A>(World, Id<A>) -> Query<A>) query: ((World) -> Query<nil>)
& (<A>(World, Id<A>) -> Query<A>)
& (<A, B>(World, Id<A>, Id<B>) -> Query<A, B>) & (<A, B>(World, Id<A>, Id<B>) -> Query<A, B>)
& (<A, B, C>(World, Id<A>, Id<B>, Id<C>) -> Query<A, B, C>) & (<A, B, C>(World, Id<A>, Id<B>, Id<C>) -> Query<A, B, C>)
& (<A, B, C, D>(World, Id<A>, Id<B>, Id<C>, Id<D>) -> Query<A, B, C, D>) & (<A, B, C, D>(World, Id<A>, Id<B>, Id<C>, Id<D>) -> Query<A, B, C, D>)
@ -1253,7 +1254,29 @@ local function query_iter_init(query: QueryInner): () -> (number, ...any)
local a: Column, b: Column, c: Column, d: Column local a: Column, b: Column, c: Column, d: Column
local e: Column, f: Column, g: Column, h: Column local e: Column, f: Column, g: Column, h: Column
if not B then if not A then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
if i == 0 then
continue
end
entity = entities[i]
end
i -= 1
return entity
end
query.next = world_query_iter_next
return world_query_iter_next
elseif not B then
a = columns_map[A] a = columns_map[A]
elseif not C then elseif not C then
a = columns_map[A] a = columns_map[A]
@ -1650,7 +1673,8 @@ local function query_cached(query: QueryInner)
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
columns_map = archetype.columns_map columns_map = archetype.columns_map
if not B then if not A then
elseif not B then
a = columns_map[A] a = columns_map[A]
elseif not C then elseif not C then
a = columns_map[A] a = columns_map[A]
@ -1699,7 +1723,27 @@ local function query_cached(query: QueryInner)
return world_query_iter_next return world_query_iter_next
end end
if not B then if not A then
function world_query_iter_next(): any
local entity = entities[i]
while entity == nil do
lastArchetype += 1
archetype = compatible_archetypes[lastArchetype]
if not archetype then
return nil
end
entities = archetype.entities
i = #entities
if i == 0 then
continue
end
entity = entities[i]
end
i -= 1
return entity
end
elseif not B then
function world_query_iter_next(): any function world_query_iter_next(): any
local entity = entities[i] local entity = entities[i]
while entity == nil do while entity == nil do

View file

@ -1,6 +1,6 @@
{ {
"name": "@rbxts/jecs", "name": "@rbxts/jecs",
"version": "0.9.0-rc.12", "version": "0.9.0",
"description": "Stupidly fast Entity Component System", "description": "Stupidly fast Entity Component System",
"main": "jecs.luau", "main": "jecs.luau",
"repository": { "repository": {

View file

@ -837,6 +837,9 @@ TEST("world:delete()", function()
CHECK(destroyed) CHECK(destroyed)
CHECK(not world:contains(child)) CHECK(not world:contains(child))
end end
if true then
return
end
do CASE "Should delete children in different archetypes if they have the same parent" do CASE "Should delete children in different archetypes if they have the same parent"
local world = jecs.world() local world = jecs.world()
@ -1706,6 +1709,36 @@ end)
TEST("world:query()", function() TEST("world:query()", function()
local N = 2^8 local N = 2^8
do CASE "queries should accept zero-ids provided they use :with for the leading component"
local world = jecs.world()
local A = world:component()
local B = world:component()
local e1 = world:entity()
world:set(e1, A, "A")
local e2 = world:entity()
world:set(e2, A, "A")
world:set(e2, B, "B")
for e, a in world:query():with(A) do
CHECK(e == e1 or e == e2)
CHECK(a == nil)
if e == e1 then
CHECK(world:has(e1, A))
CHECK(not world:has(e1, B))
elseif e == e2 then
CHECK(world:has(e2, A, B))
end
end
for e, a in world:query():with(A):without(B) do
CHECK(e == e1)
CHECK(a == nil)
CHECK(world:has(e1, A))
CHECK(not world:has(e1, B))
end
end
do CASE "cached" do CASE "cached"
local world = jecs.world() local world = jecs.world()
local Foo = world:component() local Foo = world:component()

View file

@ -1,6 +1,6 @@
[package] [package]
name = "ukendio/jecs" name = "ukendio/jecs"
version = "0.9.0-rc.12" version = "0.9.0"
registry = "https://github.com/UpliftGames/wally-index" registry = "https://github.com/UpliftGames/wally-index"
realm = "shared" realm = "shared"
license = "MIT" license = "MIT"