From ba74d6b471fe757314e7056d3e7469007e11a485 Mon Sep 17 00:00:00 2001 From: Ukendio Date: Tue, 24 Dec 2024 07:38:38 +0100 Subject: [PATCH 1/4] 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 2/4] 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 3/4] 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 4/4] 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()