From ba74d6b471fe757314e7056d3e7469007e11a485 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 07:38:38 +0100 Subject: [PATCH 01/12] Initial commit --- jecs.luau | 168 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 101 insertions(+), 67 deletions(-) diff --git a/jecs.luau b/jecs.luau index ff93b00..a94568b 100644 --- a/jecs.luau +++ b/jecs.luau @@ -89,7 +89,8 @@ local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 local EcsDelete = HI_COMPONENT_ID + 9 local EcsRemove = HI_COMPONENT_ID + 10 local EcsName = HI_COMPONENT_ID + 11 -local EcsRest = HI_COMPONENT_ID + 12 +local EcsTableCreate = HI_COMPONENT_ID + 12 +local EcsRest = HI_COMPONENT_ID + 13 local ECS_PAIR_FLAG = 0x8 local ECS_ID_FLAGS_MASK = 0x10 @@ -250,6 +251,40 @@ local function ecs_pair_second(world, e) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) end +local function query_match(query, archetype) + local matches = true + + local records = archetype.records + for _, id in query.ids do + if not records[id] then + matches = false + break + end + end + + return matches +end + +local function observer_invoke(observer, event) + table.insert(observer.query.compatible_archetypes, event.archetype) +end + +local function emit(world: World, event) + local map = world.observerable[event.id] + if not map then + return + end + local observer_list: {[string]: any} = map[event.component] + if not observer_list then + return + end + for _, observer in observer_list do + if query_match(observer.query, event.archetype) then + observer_invoke(observer, event) + end + end +end + local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) local src_columns = from.columns local dst_columns = to.columns @@ -535,15 +570,49 @@ local function archetype_append_to_records( end end -local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): Archetype +local function create_observer_uni(world: World, component: number, event) + local map = world.observerable[event] + if not map then + map = {} + world.observerable[event] = map + end + + local observer_list = map[component] + if not observer_list then + observer_list = {} + map[component] = observer_list + end + + local observer = {} + + table.insert(observer_list, observer) + + return observer +end + +local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype local archetype_id = (world.nextArchetypeId :: number) + 1 world.nextArchetypeId = archetype_id - local length = #types + local length = #id_types local columns = (table.create(length) :: any) :: { Column } local records: { ArchetypeRecord } = {} - for i, componentId in types do + + local archetype: Archetype = { + columns = columns, + entities = {}, + id = archetype_id, + records = records, + type = ty, + types = id_types, + + add = {}, + remove = {}, + refs = {} :: GraphEdge, + } + + for i, componentId in id_types do local idr = id_record_ensure(world, componentId) archetype_append_to_records(idr, archetype_id, records, componentId, i) @@ -564,21 +633,10 @@ local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): A else columns[i] = NULL_ARRAY end + + emit(world, { id = EcsTableCreate, component = componentId, archetype = archetype}) end - local archetype: Archetype = { - columns = columns, - entities = {}, - id = archetype_id, - records = records, - type = ty, - types = types, - - add = {}, - remove = {}, - refs = {} :: GraphEdge, - } - world.archetypeIndex[ty] = archetype world.archetypes[archetype_id] = archetype @@ -1477,6 +1535,15 @@ local function query_archetypes(query) return query.compatible_archetypes end +local function query_cached(query) + for _, component in query.ids do + local observer = create_observer_uni(query.world, component, EcsTableCreate) + observer.query = query + end + + return query +end + local Query = {} Query.__index = Query Query.__iter = query_iter @@ -1484,6 +1551,7 @@ Query.iter = query_iter_init Query.without = query_without Query.with = query_with Query.archetypes = query_archetypes +Query.cached = query_cached local function world_query(world: World, ...) local compatible_archetypes = {} @@ -1496,10 +1564,16 @@ local function world_query(world: World, ...) local idr: IdRecord? local componentIndex = world.componentIndex + local q = setmetatable({ + ids = ids, + compatible_archetypes = compatible_archetypes, + world = world, + }, Query) + for _, id in ids do local map = componentIndex[id] if not map then - return EMPTY_QUERY + return q end if idr == nil or map.size < idr.size then @@ -1508,7 +1582,7 @@ local function world_query(world: World, ...) end if not idr then - return EMPTY_QUERY + return q end for archetype_id in idr.cache do @@ -1536,15 +1610,6 @@ local function world_query(world: World, ...) compatible_archetypes[length] = compatibleArchetype end - if length == 0 then - return EMPTY_QUERY - end - - local q = setmetatable({ - compatible_archetypes = compatible_archetypes, - ids = ids, - }, Query) :: any - return q end @@ -1730,6 +1795,7 @@ function World.new() nextComponentId = 0 :: number, nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, + observerable = {} }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}, "") @@ -1793,13 +1859,14 @@ export type Entity = number & { __T: T } type Iter = (query: Query) -> () -> (Entity, T...) -type Query = typeof(setmetatable({}, { +export type Query = typeof(setmetatable({}, { __iter = (nil :: any) :: Iter, })) & { iter: Iter, - with: (self: Query, ...i53) -> Query, - without: (self: Query, ...i53) -> Query, + with: (self: Query, ...Id) -> Query, + without: (self: Query, ...Id) -> Query, archetypes: (self: Query) -> { Archetype }, + cached: (self: Query) -> Query } export type World = { @@ -1812,6 +1879,8 @@ export type World = { nextComponentId: number, nextEntityId: number, nextArchetypeId: number, + + observerable: { [string]: { [Id]: { query: Query } } } } & { --- Creates a new entity entity: (self: World) -> Entity, @@ -1854,42 +1923,7 @@ export type World = { children: (self: World, id: Id) -> () -> Entity, --- Searches the world for entities that match a given query - query: ((self: World, Id) -> Query) - & ((self: World, Id, Id) -> Query) - & ((self: World, Id, Id, Id) -> Query) - & ((self: World, Id, Id, Id, Id) -> Query) - & ((self: World, Id, Id, Id, Id, Id) -> Query) - & (( - self: World, - Id, - Id, - Id, - Id, - Id, - Id - ) -> Query) - & (( - self: World, - Id, - Id, - Id, - Id, - Id, - Id, - Id - ) -> Query) - & (( - self: World, - Id, - Id, - Id, - Id, - Id, - Id, - Id, - Id, - ...Id - ) -> Query), + query: ((self: World, a: { __T: A }) -> Query) } return { From a0aac721a9a31bab9965ad77c11df1cdc462df5b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 07:38:45 +0100 Subject: [PATCH 02/12] Add tests --- test/tests.luau | 51 +++++++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 34 deletions(-) diff --git a/test/tests.luau b/test/tests.luau index 2f47089..f67b6fe 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -62,6 +62,10 @@ local function debug_world_inspect(world: World) } end +local function name(world, e) + return world:get(e, jecs.Name) +end + TEST("archetype", function() local archetype_append_to_records = jecs.archetype_append_to_records local id_record_ensure = jecs.id_record_ensure @@ -353,6 +357,19 @@ TEST("world:add()", function() end) TEST("world:query()", function() + do CASE "cached" + local world = world_new() + local Foo = world:component() + local Bar = world:component() + local e = world:entity() + world:set(e, Foo, true) + local q = world:query(Foo):cached() + world:set(e, Bar, false) + for _, e in q do + CHECK(true) + end + CHECK(#q.compatible_archetypes == 2) + end do CASE("multiple iter") local world = jecs.World.new() local A = world:component() @@ -808,40 +825,6 @@ TEST("world:query()", function() CHECK(withoutCount == 0) end - do - CASE("Empty Query") - do - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local e1 = world:entity() - world:add(e1, A) - - local query = world:query(B) - CHECK(query:without() == query) - CHECK(query:with() == query) - -- They always return the same EMPTY_LIST - CHECK(query:archetypes() == world:query(B):archetypes()) - end - - do - local world = jecs.World.new() - local A = world:component() - local B = world:component() - - local e1 = world:entity() - world:add(e1, A) - - local count = 0 - for id in world:query(B) do - count += 1 - end - - CHECK(count == 0) - end - end - do CASE("without") do From 670a27711ff63f9f6cf061bc74dde9afd9ab14f0 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 08:10:16 +0100 Subject: [PATCH 03/12] Dedup observers --- jecs.luau | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/jecs.luau b/jecs.luau index a94568b..5c226e0 100644 --- a/jecs.luau +++ b/jecs.luau @@ -634,7 +634,10 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) columns[i] = NULL_ARRAY end - emit(world, { id = EcsTableCreate, component = componentId, archetype = archetype}) + end + + for _, id in id_types do + emit(world, { id = EcsTableCreate, component = id, archetype = archetype}) end world.archetypeIndex[ty] = archetype @@ -1536,11 +1539,8 @@ local function query_archetypes(query) end local function query_cached(query) - for _, component in query.ids do - local observer = create_observer_uni(query.world, component, EcsTableCreate) - observer.query = query - end - + local observer = create_observer_uni(query.world, query.ids[1], EcsTableCreate) + observer.query = query return query end From 927bee30cd31b19361ac8bf2163ecf6b223b8eb0 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 08:24:45 +0100 Subject: [PATCH 04/12] Handle filters on table creation --- jecs.luau | 91 +++++++++++++++++++++++++++++++------------------ test/tests.luau | 7 ++-- 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/jecs.luau b/jecs.luau index 5c226e0..d6232b1 100644 --- a/jecs.luau +++ b/jecs.luau @@ -252,17 +252,34 @@ local function ecs_pair_second(world, e) end local function query_match(query, archetype) - local matches = true - local records = archetype.records for _, id in query.ids do if not records[id] then - matches = false - break + return false end end - return matches + local filters = query.filters + if filters then + local without = filters.without + if without then + for _, id in filters.without do + if records[id] then + return false + end + end + end + local with = filters.with + if with then + for _, id in filters.without do + if not records[id] then + return false + end + end + end + end + + return true end local function observer_invoke(observer, event) @@ -1470,32 +1487,35 @@ local function query_iter(query): () -> (number, ...any) end local function query_without(query: { compatible_archetypes: { Archetype } }, ...) + local filters = query.filters + local without = { ... } + if not filters then + filters = {} + query.filters = filters + end + filters.without = without local compatible_archetypes = query.compatible_archetypes - local N = select("#", ...) for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] local records = archetype.records - local shouldRemove = false + local matches = true - for j = 1, N do - local id = select(j, ...) + for _, id in without do if records[id] then - shouldRemove = true + matches = false break end end - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any + if matches then + continue end - end - if #compatible_archetypes == 0 then - return EMPTY_QUERY + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any end return query :: any @@ -1503,31 +1523,36 @@ end local function query_with(query: { compatible_archetypes: { Archetype } }, ...) local compatible_archetypes = query.compatible_archetypes - local N = select("#", ...) + local filters = query.filters + local with = { ... } + if not filters then + filters = {} + query.filters = filters + end + filters.with = with for i = #compatible_archetypes, 1, -1 do local archetype = compatible_archetypes[i] local records = archetype.records - local shouldRemove = false + local matches = true - for j = 1, N do - local id = select(j, ...) + for _, id in with do if not records[id] then - shouldRemove = true + matches = false break end end - if shouldRemove then - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any + if matches then + continue end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any end - if #compatible_archetypes == 0 then - return EMPTY_QUERY - end + return query :: any end diff --git a/test/tests.luau b/test/tests.luau index f67b6fe..f437b66 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -361,14 +361,17 @@ TEST("world:query()", function() local world = world_new() local Foo = world:component() local Bar = world:component() + local Baz = world:component() local e = world:entity() + local q = world:query(Foo, Bar):without(Baz):cached() world:set(e, Foo, true) - local q = world:query(Foo):cached() world:set(e, Bar, false) + world:set(e, Baz, true) for _, e in q do CHECK(true) end - CHECK(#q.compatible_archetypes == 2) + CHECK(#q:archetypes() == 1) + CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")])) end do CASE("multiple iter") local world = jecs.World.new() From 6234dd1bc215922bfcbb3c885767edb16aa9c649 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 21:18:54 +0100 Subject: [PATCH 05/12] Handle Archetype deletion --- jecs.luau | 122 ++++++++++++++++++++++++++++-------------------- test/tests.luau | 3 ++ 2 files changed, 74 insertions(+), 51 deletions(-) diff --git a/jecs.luau b/jecs.luau index d6232b1..55dcb7d 100644 --- a/jecs.luau +++ b/jecs.luau @@ -78,31 +78,32 @@ type EntityIndex = { local HI_COMPONENT_ID = _G.__JECS_HI_COMPONENT_ID or 256 -- stylua: ignore start -local EcsOnAdd = HI_COMPONENT_ID + 1 -local EcsOnRemove = HI_COMPONENT_ID + 2 -local EcsOnSet = HI_COMPONENT_ID + 3 -local EcsWildcard = HI_COMPONENT_ID + 4 -local EcsChildOf = HI_COMPONENT_ID + 5 -local EcsComponent = HI_COMPONENT_ID + 6 -local EcsOnDelete = HI_COMPONENT_ID + 7 -local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 -local EcsDelete = HI_COMPONENT_ID + 9 -local EcsRemove = HI_COMPONENT_ID + 10 -local EcsName = HI_COMPONENT_ID + 11 -local EcsTableCreate = HI_COMPONENT_ID + 12 -local EcsRest = HI_COMPONENT_ID + 13 +local EcsOnAdd = HI_COMPONENT_ID + 1 +local EcsOnRemove = HI_COMPONENT_ID + 2 +local EcsOnSet = HI_COMPONENT_ID + 3 +local EcsWildcard = HI_COMPONENT_ID + 4 +local EcsChildOf = HI_COMPONENT_ID + 5 +local EcsComponent = HI_COMPONENT_ID + 6 +local EcsOnDelete = HI_COMPONENT_ID + 7 +local EcsOnDeleteTarget = HI_COMPONENT_ID + 8 +local EcsDelete = HI_COMPONENT_ID + 9 +local EcsRemove = HI_COMPONENT_ID + 10 +local EcsName = HI_COMPONENT_ID + 11 +local EcsArchetypeCreate = HI_COMPONENT_ID + 12 +local EcsArchetypeDelete = HI_COMPONENT_ID + 13 +local EcsRest = HI_COMPONENT_ID + 14 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) -local ECS_ID_DELETE = 0b0000_0001 -local ECS_ID_IS_TAG = 0b0000_0010 -local ECS_ID_HAS_ON_ADD = 0b0000_0100 -local ECS_ID_HAS_ON_SET = 0b0000_1000 -local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 -local ECS_ID_MASK = 0b0000_0000 +local ECS_ID_DELETE = 0b0000_0001 +local ECS_ID_IS_TAG = 0b0000_0010 +local ECS_ID_HAS_ON_ADD = 0b0000_0100 +local ECS_ID_HAS_ON_SET = 0b0000_1000 +local ECS_ID_HAS_ON_REMOVE = 0b0001_0000 +local ECS_ID_MASK = 0b0000_0000 -- stylua: ignore end local NULL_ARRAY = table.freeze({}) :: Column @@ -251,7 +252,7 @@ local function ecs_pair_second(world, e) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) end -local function query_match(query, archetype) +local function query_match(query: any, archetype: Archetype) local records = archetype.records for _, id in query.ids do if not records[id] then @@ -282,22 +283,19 @@ local function query_match(query, archetype) return true end -local function observer_invoke(observer, event) - table.insert(observer.query.compatible_archetypes, event.archetype) -end - -local function emit(world: World, event) - local map = world.observerable[event.id] - if not map then +local function emit(world: World, event, component, archetype: Archetype) + local cache = world.observerable[event] + if not cache then return end - local observer_list: {[string]: any} = map[event.component] + local observer_list = cache[component] if not observer_list then return end + for _, observer in observer_list do - if query_match(observer.query, event.archetype) then - observer_invoke(observer, event) + if query_match(observer.query, archetype) then + observer.callback(archetype) end end end @@ -650,11 +648,10 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) else columns[i] = NULL_ARRAY end - end for _, id in id_types do - emit(world, { id = EcsTableCreate, component = id, archetype = archetype}) + emit(world, EcsArchetypeCreate, id, archetype) end world.archetypeIndex[ty] = archetype @@ -1078,6 +1075,10 @@ local function archetype_destroy(world: World, archetype: Archetype) world.archetypeIndex[archetype.type] = nil :: any local records = archetype.records + for id in records do + emit(world, EcsArchetypeDelete, id, archetype) + end + for id in records do local idr = component_index[id] idr.cache[archetype_id] = nil :: any @@ -1136,23 +1137,30 @@ do local idr = component_index[delete] if idr then - local children = {} - for archetype_id in idr.cache do - local idr_archetype = archetypes[archetype_id] - - for i, child in idr_archetype.entities do - table.insert(children, child) - end - end local flags = idr.flags if bit32.band(flags, ECS_ID_DELETE) ~= 0 then - for _, child in children do - -- Cascade deletion to children - world_delete(world, child) + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_delete(world, entities[i]) + end end else - for _, child in children do - world_remove(world, child, delete) + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + local entities = idr_archetype.entities + local n = #entities + for i = n, 1, -1 do + world_remove(world, entities[i], delete) + end + end + + for archetype_id in idr.cache do + local idr_archetype = archetypes[archetype_id] + archetype_destroy(world, idr_archetype) end end end @@ -1564,8 +1572,20 @@ local function query_archetypes(query) end local function query_cached(query) - local observer = create_observer_uni(query.world, query.ids[1], EcsTableCreate) - observer.query = query + local archetypes = query.compatible_archetypes + local observer_1 = create_observer_uni(query.world, query.ids[1], EcsArchetypeCreate) + observer_1.query = query + observer_1.callback = function(archetype) + table.insert(archetypes, archetype) + end + local observer_2 = create_observer_uni(query.world, query.ids[1], EcsArchetypeDelete) + observer_2.query = query + observer_2.callback = function(archetype) + local i = table.find(archetypes, archetype) + local n = #archetypes + archetypes[i] = archetypes[n] + archetypes[n] = nil + end return query end @@ -1820,7 +1840,7 @@ function World.new() nextComponentId = 0 :: number, nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, - observerable = {} + observerable = {}, }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}, "") @@ -1891,7 +1911,7 @@ export type Query = typeof(setmetatable({}, { with: (self: Query, ...Id) -> Query, without: (self: Query, ...Id) -> Query, archetypes: (self: Query) -> { Archetype }, - cached: (self: Query) -> Query + cached: (self: Query) -> Query, } export type World = { @@ -1905,7 +1925,7 @@ export type World = { nextEntityId: number, nextArchetypeId: number, - observerable: { [string]: { [Id]: { query: Query } } } + observerable: { [i53]: { [i53]: { { query: Query } } } }, } & { --- Creates a new entity entity: (self: World) -> Entity, @@ -1948,7 +1968,7 @@ export type World = { children: (self: World, id: Id) -> () -> Entity, --- Searches the world for entities that match a given query - query: ((self: World, a: { __T: A }) -> Query) + query: (self: World, a: { __T: A }) -> Query, } return { diff --git a/test/tests.luau b/test/tests.luau index f437b66..7f64172 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -372,6 +372,9 @@ TEST("world:query()", function() end CHECK(#q:archetypes() == 1) CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")])) + world:delete(Foo) + print(#q:archetypes()) + CHECK(#q:archetypes() == 0) end do CASE("multiple iter") local world = jecs.World.new() From c37930b7e8964946a6dd3c62eb8201c6abbd8f41 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 21:21:27 +0100 Subject: [PATCH 06/12] Remove print --- test/tests.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/test/tests.luau b/test/tests.luau index 7f64172..04f0aef 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -373,7 +373,6 @@ TEST("world:query()", function() CHECK(#q:archetypes() == 1) CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")])) world:delete(Foo) - print(#q:archetypes()) CHECK(#q:archetypes() == 0) end do CASE("multiple iter") From d5d275cc17facdb4a89af7440a4321226888421f Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 22:17:58 +0100 Subject: [PATCH 07/12] Fix type errors --- jecs.luau | 48 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/jecs.luau b/jecs.luau index 55dcb7d..1a9792b 100644 --- a/jecs.luau +++ b/jecs.luau @@ -585,7 +585,9 @@ local function archetype_append_to_records( end end -local function create_observer_uni(world: World, component: number, event) + + +local function create_observer_uni(world: World, component: number, event): ecs_partial_t local map = world.observerable[event] if not map then map = {} @@ -602,7 +604,7 @@ local function create_observer_uni(world: World, component: number, event) table.insert(observer_list, observer) - return observer + return observer :: any end local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype @@ -1494,12 +1496,12 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: { compatible_archetypes: { Archetype } }, ...) - local filters = query.filters +local function query_without(query: QueryInner, ...: i53) + local filters: { without: { i53 } } = query.filters :: any local without = { ... } if not filters then filters = {} - query.filters = filters + query.filters = filters :: any end filters.without = without local compatible_archetypes = query.compatible_archetypes @@ -1529,13 +1531,13 @@ local function query_without(query: { compatible_archetypes: { Archetype } }, .. return query :: any end -local function query_with(query: { compatible_archetypes: { Archetype } }, ...) +local function query_with(query: QueryInner, ...) local compatible_archetypes = query.compatible_archetypes - local filters = query.filters + local filters: { with: { i53 } } = query.filters :: any local with = { ... } if not filters then filters = {} - query.filters = filters + query.filters = filters :: any end filters.with = with for i = #compatible_archetypes, 1, -1 do @@ -1571,14 +1573,15 @@ local function query_archetypes(query) return query.compatible_archetypes end -local function query_cached(query) +local function query_cached(query: QueryInner) local archetypes = query.compatible_archetypes - local observer_1 = create_observer_uni(query.world, query.ids[1], EcsArchetypeCreate) + local world = query.world :: World + local observer_1 = create_observer_uni(world, query.ids[1], EcsArchetypeCreate) observer_1.query = query observer_1.callback = function(archetype) table.insert(archetypes, archetype) end - local observer_2 = create_observer_uni(query.world, query.ids[1], EcsArchetypeDelete) + local observer_2 = create_observer_uni(world, query.ids[1], EcsArchetypeDelete) observer_2.query = query observer_2.callback = function(archetype) local i = table.find(archetypes, archetype) @@ -1914,6 +1917,29 @@ export type Query = typeof(setmetatable({}, { cached: (self: Query) -> Query, } +type QueryInner = { + compatible_archetypes: { Archetype }, + filters: { + without: { i53 }?, + with: { i53 }?, + }?, + ids: { i53 }, + world: {} -- Downcasted to be serializable by the analyzer +} + +type Observer = { + callback: (archetype: Archetype) -> (), + query: QueryInner, +} + +type function ecs_partial_t(ty) + local output = types.newtable() + for k, v in ty:properties() do + output:setproperty(k, types.unionof(v.write, types.singleton(nil))) + end + return output +end + export type World = { archetypeIndex: { [string]: Archetype }, archetypes: Archetypes, From 7c2cd6061e95cf07bbfc052dcad3e3087d1c4a06 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 22:22:57 +0100 Subject: [PATCH 08/12] Cleanup code --- jecs.luau | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/jecs.luau b/jecs.luau index 1a9792b..5d9f31f 100644 --- a/jecs.luau +++ b/jecs.luau @@ -684,8 +684,8 @@ local function archetype_ensure(world: World, types): Archetype return archetype_create(world, types, ty) end -local function find_insert(types: { i53 }, toAdd: i53): number - for i, id in types do +local function find_insert(id_types: { i53 }, toAdd: i53): number + for i, id in id_types do if id == toAdd then return -1 end @@ -693,17 +693,17 @@ local function find_insert(types: { i53 }, toAdd: i53): number return i end end - return #types + 1 + return #id_types + 1 end local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype - local types = node.types + local id_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 dst = table.clone(node.types) :: { i53 } - local at = find_insert(types, id) + local at = find_insert(id_types, id) 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. @@ -715,13 +715,13 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch end local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype - local types = node.types - local at = table.find(types, id) + local id_types = node.types + local at = table.find(id_types, id) if at == nil then return node end - local dst = table.clone(types) + local dst = table.clone(id_types) table.remove(dst, at) return archetype_ensure(world, dst) @@ -1576,15 +1576,18 @@ end local function query_cached(query: QueryInner) local archetypes = query.compatible_archetypes local world = query.world :: World - local observer_1 = create_observer_uni(world, query.ids[1], EcsArchetypeCreate) - observer_1.query = query - observer_1.callback = function(archetype) + -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively + -- because the event will be emitted for all components of that Archetype. + local first = query.ids[1] + local observer_for_create = create_observer_uni(world, first, EcsArchetypeCreate) + observer_for_create.query = query + observer_for_create.callback = function(archetype) table.insert(archetypes, archetype) end - local observer_2 = create_observer_uni(world, query.ids[1], EcsArchetypeDelete) - observer_2.query = query - observer_2.callback = function(archetype) - local i = table.find(archetypes, archetype) + local observer_for_delete = create_observer_uni(world, first, EcsArchetypeDelete) + observer_for_delete.query = query + observer_for_delete.callback = function(archetype) + local i = table.find(archetypes, archetype) :: number local n = #archetypes archetypes[i] = archetypes[n] archetypes[n] = nil From 694a569b6cda52427ac61f424ca109c6f1620a42 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 23:36:43 +0100 Subject: [PATCH 09/12] Manually inline code --- jecs.luau | 147 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 89 insertions(+), 58 deletions(-) diff --git a/jecs.luau b/jecs.luau index 4ca2baf..3491f89 100644 --- a/jecs.luau +++ b/jecs.luau @@ -252,52 +252,60 @@ local function ecs_pair_second(world, e) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) end -local function query_match(query: any, archetype: Archetype) - local records = archetype.records - for _, id in query.ids do +local function query_match_filter_with(records: { ArchetypeRecord }, with) + if not with then + return true + end + + for _, id in with do if not records[id] then return false end end + return true +end + +local function query_match_filter_without(records: { ArchetypeRecord }, without) + if not without then + return true + end + + for _, id in without do + if records[id] then + return false + end + end + return true +end +local function query_match(query: any, archetype: Archetype) + local records = archetype.records + if not query_match_filter_with(records, query.ids) then + return false + end local filters = query.filters if filters then - local without = filters.without - if without then - for _, id in filters.without do - if records[id] then - return false - end - end + local matched_without = query_match_filter_without( + records, filters.without) + if not matched_without then + return false end - local with = filters.with - if with then - for _, id in filters.without do - if not records[id] then - return false - end - end + local matched_with = query_match_filter_with( + records, filters.with) + if not matched_with then + return false end end return true end -local function emit(world: World, event, component, archetype: Archetype) +local function find_observers(world: World, event, component): { Observer }? local cache = world.observerable[event] if not cache then - return - end - local observer_list = cache[component] - if not observer_list then - return - end - - for _, observer in observer_list do - if query_match(observer.query, archetype) then - observer.callback(archetype) - end + return nil end + return cache[component] :: any end local function archetype_move(entity_index: EntityIndex, to: Archetype, dst_row: i24, from: Archetype, src_row: i24) @@ -591,26 +599,6 @@ local function archetype_append_to_records( end end -local function create_observer_uni(world: World, component: number, event): ecs_partial_t - local map = world.observerable[event] - if not map then - map = {} - world.observerable[event] = map - end - - local observer_list = map[component] - if not observer_list then - observer_list = {} - map[component] = observer_list - end - - local observer = {} - - table.insert(observer_list, observer) - - return observer :: any -end - local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?): Archetype local archetype_id = (world.nextArchetypeId :: number) + 1 world.nextArchetypeId = archetype_id @@ -657,7 +645,15 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) end for _, id in id_types do - emit(world, EcsArchetypeCreate, id, archetype) + local observer_list = find_observers(world, EcsArchetypeCreate, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end end world.archetypeIndex[ty] = archetype @@ -1082,7 +1078,15 @@ local function archetype_destroy(world: World, archetype: Archetype) local records = archetype.records for id in records do - emit(world, EcsArchetypeDelete, id, archetype) + local observer_list = find_observers(world, EcsArchetypeDelete, id) + if not observer_list then + continue + end + for _, observer in observer_list do + if query_match(observer.query, archetype) then + observer.callback(archetype) + end + end end for id in records do @@ -1583,19 +1587,46 @@ local function query_cached(query: QueryInner) -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively -- because the event will be emitted for all components of that Archetype. local first = query.ids[1] - local observer_for_create = create_observer_uni(world, first, EcsArchetypeCreate) - observer_for_create.query = query - observer_for_create.callback = function(archetype) + local observerable = world.observerable + local on_create_action = observerable[EcsArchetypeCreate] + if not on_create_action then + on_create_action = {} + observerable[EcsArchetypeCreate] = on_create_action + end + local query_cache_on_create = on_create_action[first] + if not query_cache_on_create then + query_cache_on_create = {} + on_create_action[first] = query_cache_on_create + end + + local on_delete_action = observerable[EcsArchetypeDelete] + if not on_delete_action then + on_delete_action = {} + observerable[EcsArchetypeDelete] = on_delete_action + end + local query_cache_on_delete = on_delete_action[first] + if not query_cache_on_delete then + query_cache_on_delete = {} + on_delete_action[first] = query_cache_on_delete + end + + local function on_create_callback(archetype) table.insert(archetypes, archetype) end - local observer_for_delete = create_observer_uni(world, first, EcsArchetypeDelete) - observer_for_delete.query = query - observer_for_delete.callback = function(archetype) + + local function on_delete_callback(archetype) local i = table.find(archetypes, archetype) :: number local n = #archetypes archetypes[i] = archetypes[n] archetypes[n] = nil end + + local observer_for_create = { query = query, callback = on_create_callback } + local observer_for_delete = { query = query, callback = on_delete_callback } + + table.insert(query_cache_on_create, observer_for_create) + table.insert(query_cache_on_delete, observer_for_delete) + return query end @@ -1896,7 +1927,7 @@ type function ecs_entity_t(entity) return entity:components()[2]:readproperty(types.singleton("__T")) end -export type function Pair(first, second) +type function Pair(first, second) local thing = first:components()[2] if thing:readproperty(types.singleton("__T")):is("nil") then From 1f6f03d2b0f85e5400414fed74f2f789b7b1974b Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 25 Dec 2024 01:05:27 +0100 Subject: [PATCH 10/12] Build terms for cached queries --- jecs.luau | 74 +++++++++++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/jecs.luau b/jecs.luau index 3491f89..f21b637 100644 --- a/jecs.luau +++ b/jecs.luau @@ -252,47 +252,14 @@ local function ecs_pair_second(world, e) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) end -local function query_match_filter_with(records: { ArchetypeRecord }, with) - if not with then - return true - end - - for _, id in with do - if not records[id] then - return false - end - end - return true -end - -local function query_match_filter_without(records: { ArchetypeRecord }, without) - if not without then - return true - end - - for _, id in without do - if records[id] then - return false - end - end - return true -end -local function query_match(query: any, archetype: Archetype) +local function query_match(terms: { {i53 }}, archetype: Archetype) local records = archetype.records - if not query_match_filter_with(records, query.ids) then - return false - end - local filters = query.filters - if filters then - local matched_without = query_match_filter_without( - records, filters.without) - if not matched_without then - return false - end - local matched_with = query_match_filter_with( - records, filters.with) - if not matched_with then + for _, term in terms do + local id = term[1] + local out = term[2] + local has = records[id] ~= nil + if has ~= not out then return false end end @@ -650,7 +617,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) continue end for _, observer in observer_list do - if query_match(observer.query, archetype) then + if query_match(observer.terms, archetype) then observer.callback(archetype) end end @@ -1083,7 +1050,7 @@ local function archetype_destroy(world: World, archetype: Archetype) continue end for _, observer in observer_list do - if query_match(observer.query, archetype) then + if query_match(observer.terms, archetype) then observer.callback(archetype) end end @@ -1621,8 +1588,29 @@ local function query_cached(query: QueryInner) archetypes[n] = nil end - local observer_for_create = { query = query, callback = on_create_callback } - local observer_for_delete = { query = query, callback = on_delete_callback } + local terms = {} + + for _, id in query.ids do + table.insert(terms, { id }) + end + local filters = query.filters + if filters then + local with = filters.with + if with then + for _, id in with do + table.insert(terms, { id }) + end + end + local without = filters.without + if without then + for _, id in without do + table.insert(terms, { id, true }) + end + end + end + + local observer_for_create = { query = query, callback = on_create_callback, terms = terms } + local observer_for_delete = { query = query, callback = on_delete_callback, terms = terms } table.insert(query_cache_on_create, observer_for_create) table.insert(query_cache_on_delete, observer_for_delete) From ecae34229df4e2589b2248957c03b2abc7d6f204 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Wed, 25 Dec 2024 22:00:46 +0100 Subject: [PATCH 11/12] Specialized cached query iterator --- jecs.luau | 570 ++++++++++++++++++++++++++++++++++-------------- test/tests.luau | 21 +- 2 files changed, 420 insertions(+), 171 deletions(-) diff --git a/jecs.luau b/jecs.luau index f21b637..34493a6 100644 --- a/jecs.luau +++ b/jecs.luau @@ -252,18 +252,25 @@ local function ecs_pair_second(world, e) return entity_index_get_alive(world.entity_index, ECS_ENTITY_T_LO(e)) end -local function query_match(terms: { {i53 }}, archetype: Archetype) +local function query_match(query, archetype: Archetype) local records = archetype.records + local with = query.filter_with - for _, term in terms do - local id = term[1] - local out = term[2] - local has = records[id] ~= nil - if has ~= not out then + for _, id in with do + if not records[id] then return false end end + local without = query.filter_without + if not without then + for _, id in without do + if records[id] then + return false + end + end + end + return true end @@ -617,7 +624,7 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) continue end for _, observer in observer_list do - if query_match(observer.terms, archetype) then + if query_match(observer.query, archetype) then observer.callback(archetype) end end @@ -1050,7 +1057,7 @@ local function archetype_destroy(world: World, archetype: Archetype) continue end for _, observer in observer_list do - if query_match(observer.terms, archetype) then + if query_match(observer.query, archetype) then observer.callback(archetype) end end @@ -1224,7 +1231,7 @@ local EMPTY_QUERY = { setmetatable(EMPTY_QUERY, EMPTY_QUERY) -local function query_iter_init(query): () -> (number, ...any) +local function query_iter_init(query: QueryInner): () -> (number, ...any) local world_query_iter_next local compatible_archetypes = query.compatible_archetypes @@ -1303,7 +1310,326 @@ local function query_iter_init(query): () -> (number, ...any) i = #entities entityId = entities[i] columns = archetype.columns - local records = archetype.records + records = archetype.records + a = columns[records[A].column] + end + + local row = i + i -= 1 + + return entityId, a[row] + end + elseif not C then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + end + + local row = i + i -= 1 + + return entityId, a[row], b[row] + end + elseif not D then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + end + + local row = i + i -= 1 + + return entityId, a[row], b[row], c[row] + end + elseif not E then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + records = archetype.records + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + end + + local row = i + i -= 1 + + return entityId, a[row], b[row], c[row], d[row] + end + else + local queryOutput = {} + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + records = archetype.records + + if not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] + end + end + + local row = i + i -= 1 + + if not F then + return entityId, a[row], b[row], c[row], d[row], e[row] + elseif not G then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row] + elseif not H then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row] + elseif not I then + return entityId, a[row], b[row], c[row], d[row], e[row], f[row], g[row], h[row] + end + + local records = archetype.records + for j, id in ids do + queryOutput[j] = columns[records[id].column][row] + end + + return entityId, unpack(queryOutput) + end + end + + query.next = world_query_iter_next + return world_query_iter_next +end + +local function query_iter(query): () -> (number, ...any) + local query_next = query.next + if not query_next then + query_next = query_iter_init(query) + end + return query_next +end + +local function query_without(query: QueryInner, ...: i53) + local without = { ... } + query.filter_without = without + local compatible_archetypes = query.compatible_archetypes + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local records = archetype.records + local matches = true + + for _, id in without do + if records[id] then + matches = false + break + end + end + + if matches then + continue + end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any + end + + return query :: any +end + +local function query_with(query: QueryInner, ...: i53) + local compatible_archetypes = query.compatible_archetypes + local with = { ... } + query.filter_with = with + + for i = #compatible_archetypes, 1, -1 do + local archetype = compatible_archetypes[i] + local records = archetype.records + local matches = true + + for _, id in with do + if not records[id] then + matches = false + break + end + end + + if matches then + continue + end + + local last = #compatible_archetypes + if last ~= i then + compatible_archetypes[i] = compatible_archetypes[last] + end + compatible_archetypes[last] = nil :: any + end + + return query :: any +end + +-- Meant for directly iterating over archetypes to minimize +-- function call overhead. Should not be used unless iterating over +-- hundreds of thousands of entities in bulk. +local function query_archetypes(query) + return query.compatible_archetypes +end + +local function query_cached(query: QueryInner) + local archetypes = query.compatible_archetypes + local world = query.world :: World + -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively + -- because the event will be emitted for all components of that Archetype. + local first = query.ids[1] + local observerable = world.observerable + local on_create_action = observerable[EcsArchetypeCreate] + if not on_create_action then + on_create_action = {} + observerable[EcsArchetypeCreate] = on_create_action + end + local query_cache_on_create = on_create_action[first] + if not query_cache_on_create then + query_cache_on_create = {} + on_create_action[first] = query_cache_on_create + end + + local on_delete_action = observerable[EcsArchetypeDelete] + if not on_delete_action then + on_delete_action = {} + observerable[EcsArchetypeDelete] = on_delete_action + end + local query_cache_on_delete = on_delete_action[first] + if not query_cache_on_delete then + query_cache_on_delete = {} + on_delete_action[first] = query_cache_on_delete + end + + local function on_create_callback(archetype) + table.insert(archetypes, archetype) + end + + local function on_delete_callback(archetype) + local i = table.find(archetypes, archetype) :: number + local n = #archetypes + archetypes[i] = archetypes[n] + archetypes[n] = nil + end + + local with = query.filter_with + local ids = query.ids + if with then + table.move(ids, 1, #ids, #with, with) + else + query.filter_with = ids + end + + local observer_for_create = { query = query, callback = on_create_callback } + local observer_for_delete = { query = query, callback = on_delete_callback } + + table.insert(query_cache_on_create, observer_for_create) + table.insert(query_cache_on_delete, observer_for_delete) + + local compatible_archetypes = query.compatible_archetypes + local lastArchetype = 1 + + local ids = query.ids + local A, B, C, D, E, F, G, H, I = unpack(ids) + local a: Column, b: Column, c: Column, d: Column + local e: Column, f: Column, g: Column, h: Column + + local world_query_iter_next + local columns: { Column } + local entities: { i53 } + local i: number + local archetype: Archetype + local records: { ArchetypeRecord } + + if not B then + function world_query_iter_next(): any + local entityId = entities[i] + while entityId == nil do + lastArchetype += 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return nil + end + + entities = archetype.entities + i = #entities + entityId = entities[i] + columns = archetype.columns + records = archetype.records a = columns[records[A].column] end @@ -1459,163 +1785,72 @@ local function query_iter_init(query): () -> (number, ...any) end end - query.next = world_query_iter_next - return world_query_iter_next -end - -local function query_iter(query): () -> (number, ...any) - local query_next = query.next - if not query_next then - query_next = query_iter_init(query) - end - return query_next -end - -local function query_without(query: QueryInner, ...: i53) - local filters: { without: { i53 } } = query.filters :: any - local without = { ... } - if not filters then - filters = {} - query.filters = filters :: any - end - filters.without = without - local compatible_archetypes = query.compatible_archetypes - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local matches = true - - for _, id in without do - if records[id] then - matches = false - break - end + local function cached_query_iter() + lastArchetype = 1 + archetype = compatible_archetypes[lastArchetype] + if not archetype then + return NOOP + end + entities = archetype.entities + i = #entities + records = archetype.records + columns = archetype.columns + if not B then + a = columns[records[A].column] + elseif not C then + a = columns[records[A].column] + b = columns[records[B].column] + elseif not D then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + elseif not E then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + elseif not F then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + elseif not G then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + elseif not H then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + elseif not I then + a = columns[records[A].column] + b = columns[records[B].column] + c = columns[records[C].column] + d = columns[records[D].column] + e = columns[records[E].column] + f = columns[records[F].column] + g = columns[records[G].column] + h = columns[records[H].column] end - if matches then - continue - end - - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any + return world_query_iter_next end - return query :: any -end - -local function query_with(query: QueryInner, ...) - local compatible_archetypes = query.compatible_archetypes - local filters: { with: { i53 } } = query.filters :: any - local with = { ... } - if not filters then - filters = {} - query.filters = filters :: any - end - filters.with = with - for i = #compatible_archetypes, 1, -1 do - local archetype = compatible_archetypes[i] - local records = archetype.records - local matches = true - - for _, id in with do - if not records[id] then - matches = false - break - end - end - - if matches then - continue - end - - local last = #compatible_archetypes - if last ~= i then - compatible_archetypes[i] = compatible_archetypes[last] - end - compatible_archetypes[last] = nil :: any - end - - return query :: any -end - --- Meant for directly iterating over archetypes to minimize --- function call overhead. Should not be used unless iterating over --- hundreds of thousands of entities in bulk. -local function query_archetypes(query) - return query.compatible_archetypes -end - -local function query_cached(query: QueryInner) - local archetypes = query.compatible_archetypes - local world = query.world :: World - -- Only need one observer for EcsArchetypeCreate and EcsArchetypeDelete respectively - -- because the event will be emitted for all components of that Archetype. - local first = query.ids[1] - local observerable = world.observerable - local on_create_action = observerable[EcsArchetypeCreate] - if not on_create_action then - on_create_action = {} - observerable[EcsArchetypeCreate] = on_create_action - end - local query_cache_on_create = on_create_action[first] - if not query_cache_on_create then - query_cache_on_create = {} - on_create_action[first] = query_cache_on_create - end - - local on_delete_action = observerable[EcsArchetypeDelete] - if not on_delete_action then - on_delete_action = {} - observerable[EcsArchetypeDelete] = on_delete_action - end - local query_cache_on_delete = on_delete_action[first] - if not query_cache_on_delete then - query_cache_on_delete = {} - on_delete_action[first] = query_cache_on_delete - end - - local function on_create_callback(archetype) - table.insert(archetypes, archetype) - end - - local function on_delete_callback(archetype) - local i = table.find(archetypes, archetype) :: number - local n = #archetypes - archetypes[i] = archetypes[n] - archetypes[n] = nil - end - - local terms = {} - - for _, id in query.ids do - table.insert(terms, { id }) - end - local filters = query.filters - if filters then - local with = filters.with - if with then - for _, id in with do - table.insert(terms, { id }) - end - end - local without = filters.without - if without then - for _, id in without do - table.insert(terms, { id, true }) - end - end - end - - local observer_for_create = { query = query, callback = on_create_callback, terms = terms } - local observer_for_delete = { query = query, callback = on_delete_callback, terms = terms } - - table.insert(query_cache_on_create, observer_for_create) - table.insert(query_cache_on_delete, observer_for_delete) - - return query + return setmetatable(query, { + __index = { + archetypes = query_archetypes, + __iter = cached_query_iter, + iter = cached_query_iter + } + }) end local Query = {} @@ -1943,12 +2178,11 @@ export type Query = typeof(setmetatable({}, { type QueryInner = { compatible_archetypes: { Archetype }, - filters: { - without: { i53 }?, - with: { i53 }?, - }?, + filter_with: { i53 }?, + filter_without: { i53 }?, ids: { i53 }, - world: {} -- Downcasted to be serializable by the analyzer + world: {}, -- Downcasted to be serializable by the analyzer + next: () -> Item } type Observer = { diff --git a/test/tests.luau b/test/tests.luau index 1037dce..2c797e5 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -371,10 +371,25 @@ TEST("world:query()", function() local q = world:query(Foo, Bar):without(Baz):cached() world:set(e, Foo, true) world:set(e, Bar, false) - world:set(e, Baz, true) - for _, e in q do - CHECK(true) + local i = 0 + + for _, e in q:iter() do + i=1 end + CHECK(i == 1) + for _, e in q:iter() do + i=2 + end + CHECK(i == 2) + for _, e in q do + i=3 + end + CHECK(i == 3) + for _, e in q do + i=4 + end + CHECK(i == 4) + CHECK(#q:archetypes() == 1) CHECK(not table.find(q:archetypes(), world.archetypes[table.concat({Foo, Bar, Baz}, "_")])) world:delete(Foo) From bfce07c8810a99d49323285b09329277b1f99f9d Mon Sep 17 00:00:00 2001 From: Ukendio Date: Thu, 26 Dec 2024 01:16:20 +0100 Subject: [PATCH 12/12] Remove shadowed variable --- jecs.luau | 1 - 1 file changed, 1 deletion(-) diff --git a/jecs.luau b/jecs.luau index 753b0ee..c1e31f6 100644 --- a/jecs.luau +++ b/jecs.luau @@ -1603,7 +1603,6 @@ local function query_cached(query: QueryInner) local compatible_archetypes = query.compatible_archetypes local lastArchetype = 1 - local ids = query.ids local A, B, C, D, E, F, G, H, I = unpack(ids) local a: Column, b: Column, c: Column, d: Column local e: Column, f: Column, g: Column, h: Column