diff --git a/jecs.luau b/jecs.luau index ff93b00..d6232b1 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,57 @@ 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 records = archetype.records + for _, id in query.ids do + if not records[id] then + return false + end + 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 + 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) + 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 +587,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,20 +650,12 @@ local function archetype_create(world: World, types: { i24 }, ty, prev: i53?): A else columns[i] = NULL_ARRAY end + end - local archetype: Archetype = { - columns = columns, - entities = {}, - id = archetype_id, - records = records, - type = ty, - types = types, - - add = {}, - remove = {}, - refs = {} :: GraphEdge, - } + for _, id in id_types do + emit(world, { id = EcsTableCreate, component = id, archetype = archetype}) + end world.archetypeIndex[ty] = archetype world.archetypes[archetype_id] = archetype @@ -1409,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 @@ -1442,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 @@ -1477,6 +1563,12 @@ local function query_archetypes(query) return query.compatible_archetypes end +local function query_cached(query) + local observer = create_observer_uni(query.world, query.ids[1], EcsTableCreate) + observer.query = query + return query +end + local Query = {} Query.__index = Query Query.__iter = query_iter @@ -1484,6 +1576,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 +1589,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 +1607,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 +1635,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 +1820,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 +1884,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 +1904,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 +1948,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 { diff --git a/test/tests.luau b/test/tests.luau index 2f47089..f437b66 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,22 @@ 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 Baz = world:component() + local e = world:entity() + 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) + end + 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() local A = world:component() @@ -808,40 +828,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