Compare commits

..

No commits in common. "main" and "v0.9.0" have entirely different histories.
main ... v0.9.0

7 changed files with 174 additions and 517 deletions

View file

@ -12,10 +12,6 @@ export type Iter<T...> = (Observer<T...>) -> () -> (jecs.Entity, T...)
export type Observer<T...> = { export type Observer<T...> = {
disconnect: (Observer<T...>) -> (), disconnect: (Observer<T...>) -> (),
}
export type Monitor<T...> = {
disconnect: (Observer<T...>) -> (),
added: ((jecs.Entity) -> ()) -> (), added: ((jecs.Entity) -> ()) -> (),
removed: ((jecs.Entity) -> ()) -> () removed: ((jecs.Entity) -> ()) -> ()
} }
@ -26,12 +22,11 @@ local function observers_new<T...>(
): Observer<T...> ): Observer<T...>
query:cached() query:cached()
local world = (query :: Query<T...> & { world: World }).world local world = (query :: Query<T...> & { world: World }).world
callback = callback callback = callback
local archetypes = {} local archetypes = {}
local terms = query.filter_with :: { jecs.Id } local terms = query.ids
local first = terms[1] local first = terms[1]
local observers_on_create = world.observable[jecs.ArchetypeCreate][first] local observers_on_create = world.observable[jecs.ArchetypeCreate][first]
@ -47,10 +42,6 @@ local function observers_new<T...>(
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
for _, archetype in query:archetypes() do
archetypes[archetype.id] = true
end
local function emplaced<T, a>( local function emplaced<T, a>(
entity: jecs.Entity<T>, entity: jecs.Entity<T>,
id: jecs.Id<a>, id: jecs.Id<a>,
@ -81,38 +72,19 @@ local function observers_new<T...>(
if without then if without then
for _, term in without do for _, term in without do
if jecs.IS_PAIR(term) then if jecs.IS_PAIR(term) then
local rel = jecs.ECS_PAIR_FIRST(term) term = jecs.ECS_PAIR_FIRST(term)
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onremoved = world:removed(rel, function(entity, id)
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback(entity)
end
end
end)
table.insert(cleanup, onremoved)
else
local onremoved = world:removed(term, function(entity, id)
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback(entity)
end
end
end)
table.insert(cleanup, onremoved)
end end
local onremoved = world:removed(term, function(entity, id)
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback(entity)
end
end
end)
table.insert(cleanup, onremoved)
end end
end end
@ -141,7 +113,7 @@ local function observers_new<T...>(
return (observer :: any) :: Observer<T...> return (observer :: any) :: Observer<T...>
end end
local function monitors_new<T...>(query: Query<T...>): Monitor<T...> local function monitors_new<T...>(query: Query<T...>): Observer<T...>
query:cached() query:cached()
local world = (query :: Query<T...> & { world: World }).world local world = (query :: Query<T...> & { world: World }).world
@ -160,10 +132,6 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
observer_on_delete.callback = function(archetype) observer_on_delete.callback = function(archetype)
archetypes[archetype.id] = nil archetypes[archetype.id] = nil
end end
for _, archetype in query:archetypes() do
archetypes[archetype.id] = true
end
local entity_index = world.entity_index :: any local entity_index = world.entity_index :: any
local callback_added: ((jecs.Entity) -> ())? local callback_added: ((jecs.Entity) -> ())?
@ -215,73 +183,24 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
if without then if without then
for _, term in without do for _, term in without do
if jecs.IS_PAIR(term) then if jecs.IS_PAIR(term) then
local rel = jecs.ECS_PAIR_FIRST(term) term = jecs.ECS_PAIR_FIRST(term)
local tgt = jecs.ECS_PAIR_SECOND(term)
local wc = tgt == jecs.w
local onadded = world:added(rel, function(entity, id)
if callback_removed == nil then
return
end
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_removed(entity)
end
end
end)
local onremoved = world:removed(rel, function(entity, id)
if callback_added == nil then
return
end
if not wc and id ~= term then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_added(entity)
end
end
end)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
else
local onadded = world:added(term, function(entity, id)
if callback_removed == nil then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_removed(entity)
end
end
end)
local onremoved = world:removed(term, function(entity, id)
if callback_added == nil then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_added(entity)
end
end
end)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
end end
local onadded = world:added(term, removed)
local onremoved = world:removed(term, function(entity, id)
if callback_added == nil then
return
end
local r = jecs.record(world, entity)
local archetype = r.archetype
if archetype then
local dst = jecs.archetype_traverse_remove(world, id, archetype)
if archetypes[dst.id] then
callback_added(entity)
end
end
end)
table.insert(cleanup, onadded)
table.insert(cleanup, onremoved)
end end
end end
@ -317,7 +236,7 @@ local function monitors_new<T...>(query: Query<T...>): Monitor<T...>
removed = monitor_removed removed = monitor_removed
} }
return (observer :: any) :: Monitor<T...> return (observer :: any) :: Observer<T...>
end end
return { return {

View file

@ -1,60 +1,60 @@
-- Using world:target is the recommended way to grab the target in a wildcard -- Using world:target is the recommended way to grab the target in a wildcard
-- query. However the random access can add up in very hot paths. Accessing its -- query. However the random access can add up in very hot paths. Accessing its
-- column can drastically improve performance, especially when there are -- column can drastically improve performance, especially when there are
-- multiple adjacent pairs. -- multiple adjacent pairs.
local jecs = require("@jecs") local jecs = require("@jecs")
local pair = jecs.pair local pair = jecs.pair
local __ = jecs.Wildcard local __ = jecs.Wildcard
local world = jecs.world() local world = jecs.world()
local Likes = world:entity() local Likes = world:entity()
local function name(e, name: string): string local function name(e, name: string): string
if name then if name then
world:set(e, jecs.Name, name) world:set(e, jecs.Name, name)
return name return name
end end
return assert(world:get(e, jecs.Name)) return assert(world:get(e, jecs.Name))
end end
local e1 = world:entity() local e1 = world:entity()
name(e1, "e1") name(e1, "e1")
local e2 = world:entity() local e2 = world:entity()
name(e2, "e2") name(e2, "e2")
local e3 = world:entity() local e3 = world:entity()
name(e3, "e3") name(e3, "e3")
world:add(e1, pair(Likes, e2)) world:add(e1, pair(Likes, e2))
world:add(e1, pair(Likes, e3)) world:add(e1, pair(Likes, e3))
local likes = jecs.component_record(world, pair(Likes, __)) local likes = jecs.component_record(world, pair(Likes, __))
assert(likes) assert(likes)
local likes_cr = likes.records local likes_cr = likes.records
local likes_counts = likes.counts local likes_counts = likes.counts
local archetypes = world:query(pair(Likes, __)):archetypes() local archetypes = world:query(pair(Likes, __)):archetypes()
for _, archetype in archetypes do for _, archetype in archetypes do
local types = archetype.types local types = archetype.types
-- Get the starting index which is what the (R, *) alias is at -- Get the starting index which is what the (R, *) alias is at
local wc = likes_cr[archetype.id] local wc = likes_cr[archetype.id]
local count = likes_counts[archetype.id] local count = likes_counts[archetype.id]
local entities = archetype.entities local entities = archetype.entities
for i = #entities, 1, -1 do for i = #entities, 1, -1 do
-- It is generally a good idea to iterate backwards on arrays if you -- It is generally a good idea to iterate backwards on arrays if you
-- need to delete the iterated entity to prevent iterator invalidation -- need to delete the iterated entity to prevent iterator invalidation
local entity = entities[i] local entity = entities[i]
for cr = wc, wc + count - 1 do for cr = wc, wc + count - 1 do
local person = jecs.pair_second(world, types[cr]) local person = jecs.pair_second(world, types[cr])
print(`entity ${entity} likes ${person}`) print(`entity ${entity} likes ${person}`)
end end
end end
end end
-- Output: -- Output:
-- entity $273 likes $275 -- entity $273 likes $275
-- entity $273 likes $274 -- entity $273 likes $274

View file

@ -1,44 +1,44 @@
-- To get the most out of performance, you can lift the inner loop of queries to -- To get the most out of performance, you can lift the inner loop of queries to
-- the system in which you can do archetype-specific optimizations like finding -- the system in which you can do archetype-specific optimizations like finding
-- the parent once per archetype rather than per entity. -- the parent once per archetype rather than per entity.
local jecs = require("@jecs") local jecs = require("@jecs")
local pair = jecs.pair local pair = jecs.pair
local ChildOf = jecs.ChildOf local ChildOf = jecs.ChildOf
local __ = jecs.Wildcard local __ = jecs.Wildcard
local world = jecs.world() local world = jecs.world()
local Position = world:component() :: jecs.Id<vector> local Position = world:component() :: jecs.Id<vector>
local Visible = world:entity() local Visible = world:entity()
local parent = world:entity() local parent = world:entity()
world:set(parent, Position, vector.zero) world:set(parent, Position, vector.zero)
world:add(parent, Visible) world:add(parent, Visible)
local child = world:entity() local child = world:entity()
world:set(child, Position, vector.one) world:set(child, Position, vector.one)
world:add(child, pair(ChildOf, parent)) world:add(child, pair(ChildOf, parent))
local parents = jecs.component_record(world, pair(ChildOf, __)) local parents = jecs.component_record(world, pair(ChildOf, __))
assert(parents) assert(parents)
local parent_cr = parents.records local parent_cr = parents.records
local archetypes = world:query(Position, pair(ChildOf, __)):archetypes() local archetypes = world:query(Position, pair(ChildOf, __)):archetypes()
for _, archetype in archetypes do for _, archetype in archetypes do
local types = archetype.types local types = archetype.types
local p = jecs.pair_second(world, types[parent_cr[archetype.id]]) local p = jecs.pair_second(world, types[parent_cr[archetype.id]])
if world:has(p, Visible) then if world:has(p, Visible) then
local columns = archetype.columns_map local columns = archetype.columns_map
local positions = columns[Position] local positions = columns[Position]
for row, entity in archetype.entities do for row, entity in archetype.entities do
local pos = positions[row] local pos = positions[row]
print(`Child ${entity} of ${p} is visible at {pos}`) print(`Child ${entity} of ${p} is visible at {pos}`)
end end
end end
end end
-- Output: -- Output:
-- Child $274 of $273 is visibile at 1,1,1 -- Child $274 of $273 is visibile at 1,1,1

15
jecs.d.ts vendored
View file

@ -236,12 +236,6 @@ export class World {
*/ */
contains(entity: Entity): boolean; contains(entity: Entity): boolean;
/**
* Checks if an entity exists in the world.
* @param entity The entity to verify.
*/
contains(entity: number): entity is Entity;
/** /**
* Checks if an entity with the given ID is currently alive, ignoring its generation. * Checks if an entity with the given ID is currently alive, ignoring its generation.
* @param entity The entity to verify. * @param entity The entity to verify.
@ -350,19 +344,12 @@ export type ComponentRecord = {
export function component_record(world: World, id: Id): ComponentRecord; export function component_record(world: World, id: Id): ComponentRecord;
type TagToUndefined<T> = T extends TagDiscriminator ? undefined : T type TagToUndefined<T> = T extends TagDiscriminator ? undefined : T
type TrimOptional<T extends unknown[]> = T extends [...infer L, infer R]
? unknown extends R
? L | T | TrimOptional<L>
: R extends undefined
? L | T | TrimOptional<L>
: T
: T
export function bulk_insert<const C extends Id[]>( export function bulk_insert<const C extends Id[]>(
world: World, world: World,
entity: Entity, entity: Entity,
ids: C, ids: C,
values: TrimOptional<{ [K in keyof C]: TagToUndefined<InferComponent<C[K]>> }>, values: { [K in keyof C]: TagToUndefined<InferComponent<C[K]>> },
): void; ): void;
export function bulk_remove(world: World, entity: Entity, ids: Id[]): void; export function bulk_remove(world: World, entity: Entity, ids: Id[]): void;

View file

@ -1607,9 +1607,6 @@ local function query_cached(query: QueryInner)
local lastArchetype = 1 local lastArchetype = 1
local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id }) local A, B, C, D, E, F, G, H, I = unpack(ids :: { Id })
if not A then
A = query.filter_with[1]
end
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
@ -3247,7 +3244,8 @@ local function world_new()
return world return world
end end
-- type function ecs_id_t(ty) -- type function ecs_id_t(entity)
-- local ty = entity:components()[2]
-- local __T = ty:readproperty(types.singleton("__T")) -- local __T = ty:readproperty(types.singleton("__T"))
-- if not __T then -- if not __T then
-- return ty:readproperty(types.singleton("__jecs_pair_value")) -- return ty:readproperty(types.singleton("__jecs_pair_value"))

View file

@ -5,109 +5,21 @@ local CASE, TEST, FINISH, CHECK = test.CASE, test.TEST, test.FINISH, test.CHECK
local FOCUS = test.FOCUS local FOCUS = test.FOCUS
local ob = require("@addons/ob") local ob = require("@addons/ob")
TEST("addons/ob::observer", function() TEST("addons/ob", function()
local world = jecs.world() local world = jecs.world()
do CASE "should match against archetypes that were already created" do CASE "Should support query:without()"
local A = world:component()
local e1 = world:entity()
world:add(e1, A)
local c = 1
ob.observer(world:query(A), function()
c+=1
end)
world:remove(e1, A)
world:add(e1, A)
CHECK(c==2)
end
do CASE "Should enter observer at query:without(pair(R, t1)) when adding pair(R, t2)"
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1 local c = 1
local monitor = ob.monitor(world:query(A):without(B))
ob.observer(world:query(A):without(jecs.pair(B, C)), function() monitor.added(function()
c += 1 c += 1
end) end)
monitor.removed(function()
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
end
do CASE "Should enter observer at query:without(pair(R, t1)) when removing pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1
ob.observer(world:query(A):without(jecs.pair(B, D)), function()
c += 1 c += 1
end) end)
local child = world:entity()
world:add(child, jecs.pair(B, C))
world:add(child, jecs.pair(B, D))
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, jecs.pair(B, C))
CHECK(c==1)
world:add(child, jecs.pair(B, C))
CHECK(c==1)
world:remove(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
world:remove(child, jecs.pair(B, C))
CHECK(c==2)
world:remove(child, jecs.pair(B, D))
CHECK(c==3)
end
do CASE "Should enter observer at query:without(pair(R, *)) when adding pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local c = 1
ob.observer(world:query(A):without(jecs.pair(B, jecs.w)), function() c+= 1 end)
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
end
do CASE "Should enter observer at query:without(t1) when removing t1"
local A = world:component()
local B = world:component()
local c = 1
ob.observer(world:query(A):without(B), function() c+= 1 end)
local child = world:entity() local child = world:entity()
world:add(child, B) world:add(child, B)
CHECK(c==1) CHECK(c==1)
@ -116,22 +28,28 @@ TEST("addons/ob::observer", function()
world:remove(child, B) world:remove(child, B)
CHECK(c==2) CHECK(c==2)
world:remove(child, A) world:remove(child, A)
CHECK(c==2) CHECK(c==3)
end end
do CASE "observers should accept pairs" do CASE "monitors should accept pairs"
local A = world:component() local A = world:component()
local B = world:component() local B = world:component()
local c = 1 local c = 1
ob.observer(world:query(jecs.pair(A, B)), function() c+= 1 end) local monitor = ob.monitor(world:query(jecs.pair(A, B)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
c += 1
end)
local child = world:entity() local child = world:entity()
world:add(child, jecs.pair(A, B)) world:add(child, jecs.pair(A, B))
CHECK(c == 2) CHECK(c == 2)
world:remove(child, jecs.pair(A, B)) world:remove(child, jecs.pair(A, B))
CHECK(c == 2) CHECK(c == 3)
end end
do CASE "Ensure ordering between signals and observers" do CASE "Ensure ordering between signals and observers"
@ -181,191 +99,6 @@ TEST("addons/ob::observer", function()
CHECK(count == 4) CHECK(count == 4)
end end
do CASE "Call off pairs"
local A = world:component()
local callcount = 1
world:added(A, function(entity)
callcount += 1
end)
world:added(A, function(entity)
callcount += 1
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 == 1 + 2 * 2)
end
end)
TEST("addons/ob::monitor", function()
local world = jecs.world()
do CASE "should match against archetypes that were already created"
local A = world:component()
local e1 = world:entity()
world:add(e1, A)
local monitor = ob.monitor(world:query(A))
local c = 1
monitor.added(function()
c += 1
end)
world:remove(e1, A)
world:add(e1, A)
CHECK(c==2)
end
do CASE "Should enter monitor at query:without(pair(R, t1)) when adding pair(R, t2)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1
local r = 1
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, C)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
r += 1
end)
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
CHECK(r==1)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
CHECK(r==2)
end
do CASE "Should enter monitor at query:without(pair(R, t1)) when removing pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, jecs.pair(B, D))
local c = 1
local r = 1
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, D)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
r += 1
end)
local child = world:entity()
world:add(child, jecs.pair(B, C))
world:add(child, jecs.pair(B, D))
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, jecs.pair(B, C))
CHECK(c==1)
world:add(child, jecs.pair(B, C))
CHECK(c==1)
world:remove(child, jecs.pair(B, D))
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
CHECK(r==2)
world:remove(child, jecs.pair(B, C))
CHECK(c==2)
CHECK(r==2)
world:remove(child, jecs.pair(B, D))
CHECK(c==3)
CHECK(r==2)
end
do CASE "Should enter monitor at query:without(pair(R, *)) when adding pair(R, t1)"
local A = world:component()
local B = world:component()
local C = world:component()
local D = world:component()
local c = 1
local r = 1
local monitor = ob.monitor(world:query(A):without(jecs.pair(B, jecs.w)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
r += 1
end)
local child = world:entity()
world:add(child, A)
CHECK(c==2)
world:add(child, jecs.pair(B, D))
CHECK(c==2)
CHECK(r==2)
world:add(child, jecs.pair(B, C))
CHECK(c==2)
CHECK(r==2)
end
do CASE "Should enter monitor at query:without(t1) when removing t1"
local A = world:component()
local B = world:component()
local c = 1
local monitor = ob.monitor(world:query(A):without(B))
monitor.added(function()
c += 1
end)
monitor.removed(function()
c += 1
end)
local child = world:entity()
world:add(child, B)
CHECK(c==1)
world:add(child, A)
CHECK(c==1)
world:remove(child, B)
CHECK(c==2)
world:remove(child, A)
CHECK(c==3)
end
do CASE "monitors should accept pairs"
local A = world:component()
local B = world:component()
local c = 1
local monitor = ob.monitor(world:query(jecs.pair(A, B)))
monitor.added(function()
c += 1
end)
monitor.removed(function()
c += 1
end)
local child = world:entity()
world:add(child, jecs.pair(A, B))
CHECK(c == 2)
world:remove(child, jecs.pair(A, B))
CHECK(c == 3)
end
do CASE "Don't report changed components in monitor" do CASE "Don't report changed components in monitor"
local A = world:component() local A = world:component()
local count = 1 local count = 1
@ -387,6 +120,25 @@ TEST("addons/ob::monitor", function()
world:set(e, A, false) world:set(e, A, false)
CHECK(count == 4) CHECK(count == 4)
end end
do CASE "Call off pairs"
local A = world:component()
local callcount = 1
world:added(A, function(entity)
callcount += 1
end)
world:added(A, function(entity)
callcount += 1
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 == 1 + 2 * 2)
end
end) end)
return FINISH() return FINISH()

View file

@ -1365,6 +1365,7 @@ TEST("world:added", function()
end end
do CASE "" do CASE ""
local world = jecs.world()
local IsNearby = world:component() local IsNearby = world:component()
world:set(IsNearby, jecs.Name, "IsNearby") world:set(IsNearby, jecs.Name, "IsNearby")
local person1, person2 = world:entity(), world:entity() local person1, person2 = world:entity(), world:entity()