diff --git a/jecs.luau b/jecs.luau index 1aed04f..f21b637 100644 --- a/jecs.luau +++ b/jecs.luau @@ -78,30 +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 EcsRest = HI_COMPONENT_ID + 12 +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 @@ -250,6 +252,29 @@ 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 records = archetype.records + + 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 + + return true +end + +local function find_observers(world: World, event, component): { Observer }? + local cache = world.observerable[event] + if not cache then + 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) local src_columns = from.columns local dst_columns = to.columns @@ -549,6 +574,20 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) local columns = (table.create(length) :: any) :: { Column } local records: { ArchetypeRecord } = {} + + 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) @@ -572,18 +611,17 @@ local function archetype_create(world: World, id_types: { i24 }, ty, prev: i53?) end end - local archetype: Archetype = { - columns = columns, - entities = {}, - id = archetype_id, - records = records, - type = ty, - types = id_types, - - add = {}, - remove = {}, - refs = {} :: GraphEdge, - } + for _, id in id_types do + 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.terms, archetype) then + observer.callback(archetype) + end + end + end world.archetypeIndex[ty] = archetype world.archetypes[archetype_id] = archetype @@ -626,13 +664,13 @@ local function find_insert(id_types: { i53 }, toAdd: i53): number 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. @@ -644,13 +682,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) @@ -1006,6 +1044,18 @@ local function archetype_destroy(world: World, archetype: Archetype) world.archetypeIndex[archetype.type] = nil :: any local records = archetype.records + for id in records do + 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.terms, archetype) then + observer.callback(archetype) + end + end + end + for id in records do local idr = component_index[id] idr.cache[archetype_id] = nil :: any @@ -1064,23 +1114,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 @@ -1414,65 +1471,73 @@ local function query_iter(query): () -> (number, ...any) return query_next end -local function query_without(query: { compatible_archetypes: { Archetype } }, ...) +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 - 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 end -local function query_with(query: { compatible_archetypes: { Archetype } }, ...) +local function query_with(query: QueryInner, ...) local compatible_archetypes = query.compatible_archetypes - local N = select("#", ...) + 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 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 @@ -1483,6 +1548,76 @@ 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 +end + local Query = {} Query.__index = Query Query.__iter = query_iter @@ -1490,6 +1625,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 = {} @@ -1502,10 +1638,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 @@ -1514,7 +1656,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 @@ -1542,15 +1684,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 @@ -1736,6 +1869,7 @@ function World.new() nextComponentId = 0 :: number, nextEntityId = 0 :: number, ROOT_ARCHETYPE = (nil :: any) :: Archetype, + observerable = {}, }, World) :: any self.ROOT_ARCHETYPE = archetype_create(self, {}, "") @@ -1781,7 +1915,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 @@ -1797,15 +1931,39 @@ 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, } +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, @@ -1816,6 +1974,8 @@ export type World = { nextComponentId: number, nextEntityId: number, nextArchetypeId: number, + + observerable: { [i53]: { [i53]: { { query: Query } } } }, } & { --- Creates a new entity entity: (self: World) -> Entity, diff --git a/test/tests.luau b/test/tests.luau index 12d60b1..1037dce 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 @@ -358,6 +362,24 @@ 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}, "_")])) + world:delete(Foo) + CHECK(#q:archetypes() == 0) + end do CASE("multiple iter") local world = jecs.World.new() local A = world:component() @@ -813,40 +835,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