## QueryBuilder[br] ## A utility class for constructing and executing queries to retrieve entities based on their components. ## ## The QueryBuilder supports filtering entities that have all, any, or exclude specific components, ## as well as filtering by enabled/disabled status using high-performance group indexing. ## [codeblock] ## var enabled_entities = ECS.world.query ## .with_all([Transform, Velocity]) ## .with_any([Health]) ## .with_none([Inactive]) ## .enabled(true) ## .execute() ## ## var disabled_entities = ECS.world.query.enabled(false).execute() ## var all_entities = ECS.world.query.enabled(null).execute() ##[/codeblock] ## This will efficiently query entities using indexed group lookups rather than ## filtering the entire entity list. class_name QueryBuilder extends RefCounted # The world instance to query against. var _world: World # Components that an entity must have all of. var _all_components: Array = [] # Components that an entity must have at least one of. var _any_components: Array = [] # Components that an entity must not have. var _exclude_components: Array = [] # Relationships that entities must have var _relationships: Array = [] # (Retained for entity-level filtering only; NOT part of cache key) var _exclude_relationships: Array = [] # Components queries that an entity must match var _all_components_queries: Array = [] # Components queries that an entity must match for any components var _any_components_queries: Array = [] # Groups that an entity must be in var _groups: Array = [] # Groups that an entity must not be in var _exclude_groups: Array = [] # Enabled/disabled filter: true = enabled only, false = disabled only, null = all var _enabled_filter = null # Components to iterate in archetype mode (ordered array of component types) var _iterate_components: Array = [] # Add fields for query result caching var _cache_valid: bool = false var _cached_result: Array = [] # OPTIMIZATION: Cache the query hash key to avoid recalculating FNV-1a hash every frame var _cache_key: int = -1 var _cache_key_valid: bool = false ## Initializes the QueryBuilder with the specified [param world] func _init(world: World = null): _world = world as World ## Allow setting the world after creation for editor time creation func set_world(world: World): _world = world ## Clears the query criteria, resetting all filters. Mostly used in testing ## [param returns] - The current instance of the QueryBuilder for chaining. func clear(): _all_components = [] _any_components = [] _exclude_components = [] _relationships = [] _exclude_relationships = [] _all_components_queries = [] _any_components_queries = [] _groups = [] _exclude_groups = [] _enabled_filter = null _iterate_components = [] _cache_valid = false _cache_key_valid = false return self ## Finds entities with all of the provided components.[br] ## [param components] An [Array] of [Component] classes.[br] ## [param returns]: [QueryBuilder] instance for chaining. func with_all(components: Array = []) -> QueryBuilder: var processed = ComponentQueryMatcher.process_component_list(components) _all_components = processed.components _all_components_queries = processed.queries _cache_valid = false _cache_key_valid = false return self ## Entities must have at least one of the provided components.[br] ## [param components] An [Array] of [Component] classes.[br] ## [param reutrns] [QueryBuilder] instance for chaining. func with_any(components: Array = []) -> QueryBuilder: var processed = ComponentQueryMatcher.process_component_list(components) _any_components = processed.components _any_components_queries = processed.queries _cache_valid = false _cache_key_valid = false return self ## Entities must not have any of the provided components.[br] ## Params: [param components] An [Array] of [Component] classes.[br] ## [param reutrns] [QueryBuilder] instance for chaining. func with_none(components: Array = []) -> QueryBuilder: # Don't process queries for with_none, just take the components directly _exclude_components = components.map( func(comp): return comp if not comp is Dictionary else comp.keys()[0] ) _cache_valid = false _cache_key_valid = false return self ## Finds entities with specific relationships using weak matching by default (component type and queries). ## [br][b]Weak Matching (default):[/b] Components match by type and component queries are evaluated. ## [br]For strong matching (exact component data), use [method Entity.has_relationship] with [code]weak=false[/code]. func with_relationship(relationships: Array = []) -> QueryBuilder: _relationships = relationships _cache_valid = false # Cache key unaffected by relationships (structural only) return self ## Entities must not have any of the provided relationships using weak matching by default (component type and queries). ## [br][b]Weak Matching (default):[/b] Components match by type and component queries are evaluated. ## [br]For strong matching (exact component data), use [method Entity.has_relationship] with [code]weak=false[/code]. func without_relationship(relationships: Array = []) -> QueryBuilder: _exclude_relationships = relationships _cache_valid = false return self ## Query for entities that are targets of specific relationships func with_reverse_relationship(relationships: Array = []) -> QueryBuilder: for rel in relationships: if rel.relation != null: var rev_key = "reverse_" + rel.relation.get_script().resource_path if _world.reverse_relationship_index.has(rev_key): return self.with_all(_world.reverse_relationship_index[rev_key]) _cache_valid = false return self ## Finds entities with specific groups. func with_group(groups: Array[String] = []) -> QueryBuilder: _groups.append_array(groups) _cache_valid = false _cache_key_valid = false return self ## Entities must not have any of the provided groups. func without_group(groups: Array[String] = []) -> QueryBuilder: _exclude_groups.append_array(groups) _cache_valid = false _cache_key_valid = false return self ## Filter to only enabled entities using internal arrays for optimal performance.[br] ## [param returns] [QueryBuilder] instance for chaining. func enabled() -> QueryBuilder: _enabled_filter = true _cache_valid = false _cache_key_valid = false return self ## Filter to only disabled entities using internal arrays for optimal performance.[br] ## [param returns] [QueryBuilder] instance for chaining. func disabled() -> QueryBuilder: _enabled_filter = false _cache_valid = false _cache_key_valid = false return self ## Specifies the component order for batch processing iteration.[br] ## This determines the order of component arrays passed to System.process_batch()[br] ## [param components] An array of component types in the desired iteration order[br] ## [param returns] [QueryBuilder] instance for chaining.[br][br] ## [b]Example:[/b] ## [codeblock] ## func query() -> QueryBuilder: ## return q.with_all([C_Velocity, C_Timer]).enabled().iterate([C_Velocity, C_Timer]) ## ## func process_batch(entities: Array[Entity], components: Array, delta: float) -> void: ## var velocities = components[0] # C_Velocity (first in iterate) ## var timers = components[1] # C_Timer (second in iterate) ## [/codeblock] func iterate(components: Array) -> QueryBuilder: _iterate_components = components return self func execute_one() -> Entity: # Execute the query and return the first matching entity var result = execute() if result.size() > 0: return result[0] return null ## Executes the constructed query and retrieves matching entities.[br] ## [param returns] - An [Array] of [Entity] that match the query criteria. func execute() -> Array: # For relationship or group filters we need fresh filtering every call (no stale cached filtered result) var uses_relationship_filters := (not _relationships.is_empty() or not _exclude_relationships.is_empty()) var uses_group_filters := (not _groups.is_empty() or not _exclude_groups.is_empty()) var structural_result: Array if _cache_valid and not uses_relationship_filters and not uses_group_filters: # Safe to reuse full cached result only for purely structural component queries structural_result = _cached_result else: # Recompute base structural/group result (without relationship filtering caching) structural_result = _internal_execute() # Only cache if no dynamic relationship/group filters are present if not uses_relationship_filters and not uses_group_filters: _cached_result = structural_result _cache_valid = true else: _cache_valid = false # force recompute next call var result = structural_result # Apply component property queries (post structural) if not _all_components_queries.is_empty() and _has_actual_queries(_all_components_queries): result = _filter_entities_by_queries(result, _all_components, _all_components_queries, true) if not _any_components_queries.is_empty() and _has_actual_queries(_any_components_queries): result = _filter_entities_by_queries(result, _any_components, _any_components_queries, false) return result func _internal_execute() -> Array: # If we have groups or exclude groups, gather entities from those groups if not _groups.is_empty() or not _exclude_groups.is_empty(): var entities_in_group = [] # Use Godot's optimized get_nodes_in_group() instead of filtering if not _groups.is_empty(): # For multiple groups, use set operations for efficiency var group_set: Set for i in range(_groups.size()): var group_name = _groups[i] var nodes_in_group = _world.get_tree().get_nodes_in_group(group_name) # Filter to only Entity nodes var entities_in_this_group = nodes_in_group.filter(func(n): return n is Entity) if i == 0: # First group - start with these entities group_set = Set.new(entities_in_this_group) else: # Subsequent groups - intersect (entity must be in ALL groups) group_set = group_set.intersect(Set.new(entities_in_this_group)) entities_in_group = group_set.to_array() if group_set else [] else: # If no required groups but we have exclude_groups, start with ALL entities from component query # This handles the case of "without_group" queries entities_in_group = ( _world._query(_all_components, _any_components, _exclude_components, _enabled_filter, get_cache_key()) as Array[Entity] ) # Filter out entities in excluded groups if not _exclude_groups.is_empty(): var exclude_set = Set.new() for group_name in _exclude_groups: var nodes_in_group = _world.get_tree().get_nodes_in_group(group_name) var entities_in_excluded = nodes_in_group.filter(func(n): return n is Entity) exclude_set = exclude_set.union(Set.new(entities_in_excluded)) # Remove excluded entities var result_set = Set.new(entities_in_group) entities_in_group = result_set.difference(exclude_set).to_array() # match the entities in the group with the query return matches(entities_in_group) # Otherwise, query the world with enabled filter for optimal performance # OPTIMIZATION: Pass pre-calculated cache key to avoid rehashing var result = ( _world._query(_all_components, _any_components, _exclude_components, _enabled_filter, get_cache_key()) as Array[Entity] ) # Handle relationship filtering if not _relationships.is_empty() or not _exclude_relationships.is_empty(): var filtered_entities: Array = [] for entity in result: var matches = true # Required relationships for relationship in _relationships: if not entity.has_relationship(relationship): matches = false break # Excluded relationships if matches: for ex_relationship in _exclude_relationships: if entity.has_relationship(ex_relationship): matches = false break if matches: filtered_entities.append(entity) result = filtered_entities # Return the structural query result (caching handled in execute()) # Note: enabled/disabled filtering is now handled in World._query for optimal performance return result ## Check if any query in the array has actual property filters (not just empty {}) func _has_actual_queries(queries: Array) -> bool: for query in queries: if not query.is_empty(): return true return false ## Filter entities based on component queries func _filter_entities_by_queries( entities: Array, components: Array, queries: Array, require_all: bool ) -> Array: var filtered = [] for entity in entities: if entity == null: continue if require_all: # Must match all queries var matches = true for i in range(components.size()): var component = entity.get_component(components[i]) var query = queries[i] if not ComponentQueryMatcher.matches_query(component, query): matches = false break if matches: filtered.append(entity) else: # Must match any query for i in range(components.size()): var component = entity.get_component(components[i]) var query = queries[i] if component and ComponentQueryMatcher.matches_query(component, query): filtered.append(entity) break return filtered ## Check if entity matches any of the queries func _entity_matches_any_query(entity: Entity, components: Array, queries: Array) -> bool: for i in range(components.size()): var component = entity.get_component(components[i]) if component and ComponentQueryMatcher.matches_query(component, queries[i]): return true return false ## Filters a provided list of entities using the current query criteria.[br] ## Unlike execute(), this doesn't query the world but instead filters the provided entities.[br][br] ## [param entities] Array of entities to filter[br] ## [param returns] Array of entities that match the query criteria[br] func matches(entities: Array) -> Array: # if the query is empty all entities match if is_empty(): return entities var result = [] for entity in entities: # If it's null skip it if entity == null: continue assert(entity is Entity, "Must be an entity") var matches = true # Check all required components for component in _all_components: if not entity.has_component(component): matches = false break # If still matching and we have any_components, check those if matches and not _any_components.is_empty(): matches = false for component in _any_components: if entity.has_component(component): matches = true break # Check excluded components if matches: for component in _exclude_components: if entity.has_component(component): matches = false break # Check required relationships if matches and not _relationships.is_empty(): for relationship in _relationships: if not entity.has_relationship(relationship): matches = false break # Check excluded relationships if matches and not _exclude_relationships.is_empty(): for relationship in _exclude_relationships: if entity.has_relationship(relationship): matches = false break if matches: result.append(entity) return result func combine(other: QueryBuilder) -> QueryBuilder: _all_components += other._all_components _all_components_queries += other._all_components_queries _any_components += other._any_components _any_components_queries += other._any_components_queries _exclude_components += other._exclude_components _relationships += other._relationships _exclude_relationships += other._exclude_relationships _groups += other._groups _exclude_groups += other._exclude_groups _cache_valid = false return self func as_array() -> Array: return [ _all_components, _any_components, _exclude_components, _relationships, _exclude_relationships ] func is_empty() -> bool: return ( _all_components.is_empty() and _any_components.is_empty() and _exclude_components.is_empty() and _relationships.is_empty() and _exclude_relationships.is_empty() ) func _to_string() -> String: var parts = [] if not _all_components.is_empty(): parts.append("with_all(" + _format_components(_all_components) + ")") if not _any_components.is_empty(): parts.append("with_any(" + _format_components(_any_components) + ")") if not _exclude_components.is_empty(): parts.append("with_none(" + _format_components(_exclude_components) + ")") if not _relationships.is_empty(): parts.append("with_relationship(" + _format_relationships(_relationships) + ")") if not _exclude_relationships.is_empty(): parts.append("without_relationship(" + _format_relationships(_exclude_relationships) + ")") if not _groups.is_empty(): parts.append("with_group(" + str(_groups) + ")") if not _exclude_groups.is_empty(): parts.append("without_group(" + str(_exclude_groups) + ")") if _enabled_filter != null: if _enabled_filter: parts.append("enabled()") else: parts.append("disabled()") if not _all_components_queries.is_empty(): parts.append("component_queries(" + _format_component_queries(_all_components_queries) + ")") if not _any_components_queries.is_empty(): parts.append("any_component_queries(" + _format_component_queries(_any_components_queries) + ")") if parts.is_empty(): return "ECS.world.query" return "ECS.world.query." + ".".join(parts) func _format_components(components: Array) -> String: var names = [] for component in components: if component is Script: names.append(component.get_global_name()) else: names.append(str(component)) return "[" + ", ".join(names) + "]" func _format_relationships(relationships: Array) -> String: var names = [] for relationship in relationships: if relationship.has_method("to_string"): names.append(relationship.to_string()) else: names.append(str(relationship)) return "[" + ", ".join(names) + "]" func _format_component_queries(queries: Array) -> String: var formatted = [] for query in queries: if query.has_method("to_string"): formatted.append(query.to_string()) else: formatted.append(str(query)) return "[" + ", ".join(formatted) + "]" func compile(query: String) -> QueryBuilder: return QueryBuilder.new(_world) func invalidate_cache(): _cache_valid = false _cache_key_valid = false ## Called when a relationship is added or removed (only for queries using relationships) ## Relationship changes do NOT affect structural cache key; queries only re-filter at execute time func _on_relationship_changed(_entity: Entity, _relationship: Relationship): _cache_valid = false # only result cache ## Get the cached query hash key, calculating it only once ## OPTIMIZATION: Avoids recalculating FNV-1a hash every frame in hot path queries func get_cache_key() -> int: # Structural cache key excludes relationships/groups (matches 6.0.0 behavior) if not _cache_key_valid: if _world: _cache_key = QueryCacheKey.build(_all_components, _any_components, _exclude_components) _cache_key_valid = true else: return -1 return _cache_key ## Get matching archetypes directly for column-based iteration ## OPTIMIZATION: Skip entity flattening, return archetypes directly for cache-friendly processing ## [br][br] ## [b]Example:[/b] ## [codeblock] ## func process_all(entities: Array, delta: float): ## for archetype in query().archetypes(): ## var transforms = archetype.get_column(transform_path) ## for i in range(transforms.size()): ## # Process transform directly from packed array ## [/codeblock] func archetypes() -> Array[Archetype]: return _world.get_matching_archetypes(self )