## Entity[br] ## ## Represents an entity within the [_ECS] framework.[br] ## An entity is a container that can hold multiple [Component]s. ## ## Entities serve as the fundamental building block for game objects, allowing for flexible and modular design.[br] ##[br] ## Entities can have [Component]s added or removed dynamically, enabling the behavior and properties of game objects to change at runtime.[br] ## Entities can have [Relationship]s added or removed dynamically, allowing for a deep hierarchical query system.[br] ##[br] ## Example: ##[codeblock] ## var entity = Entity.new() ## var transform = Transform.new() ## entity.add_component(transform) ## entity.component_added.connect(_on_component_added) ## ## func _on_component_added(entity: Entity, component_key: String) -> void: ## print("Component added:", component_key) ##[/codeblock] @icon("res://addons/gecs/assets/entity.svg") @tool class_name Entity extends CharacterBody3D #region Signals ## Emitted when a [Component] is added to the entity. signal component_added(entity: Entity, component: Resource) ## Emitted when a [Component] is removed from the entity. signal component_removed(entity: Entity, component: Resource) ## Emitted when a [Component] property is changed. signal component_property_changed( entity: Entity, component: Resource, property_name: String, old_value: Variant, new_value: Variant ) ## Emit when a [Relationship] is added to the [Entity] signal relationship_added(entity: Entity, relationship: Relationship) ## Emit when a [Relationship] is removed from the [Entity] signal relationship_removed(entity: Entity, relationship: Relationship) #endregion Signals #region Exported Variables ## The id of the entity either UUID or custom string. ## This must be unique within a [World]. If left blank, a UUID will be generated when the entity is added to a world. @export var id: String ## Is this entity active? (Will show up in queries) @export var enabled: bool = true: set(value): if enabled != value: var old_enabled = enabled enabled = value # Notify world to move entity between enabled/disabled archetypes _on_enabled_changed(old_enabled, value) ## [Component]s to be attached to the entity set in the editor. These will be loaded for you and added to the [Entity] @export var component_resources: Array[Component] = [] ## Serialization config override for this specific entity (optional) @export var serialize_config: GECSSerializeConfig #endregion Exported Variables #region Public Variables ## [Component]s attached to the [Entity] in the form of Dict[resource_path:String, Component] var components: Dictionary = {} ## Relationships attached to the entity var relationships: Array[Relationship] = [] ## Cache for component resource paths to avoid repeated .get_script().resource_path calls var _component_path_cache: Dictionary = {} ## Logger for entities to only log to a specific domain var _entityLogger = GECSLogger.new().domain("Entity") ## We can store ephemeral state on the entity var _state = {} #endregion Public Variables #region Built-in Virtual Methods ## Called to initialize the entity and its components. ## This is called automatically by [method World.add_entity][br] func _initialize(_components: Array = []) -> void: _entityLogger.trace("Entity Initializing Components: ", self.name) # because components can be added before the entity is added to the world # replay adding components here so signals pick them up and the index is updated var temp_comps = components.values().duplicate_deep() components.clear() for comp in temp_comps: add_component(comp) # Add components defined in code to comp resources component_resources.append_array(define_components()) # remove any component_resources that are already defined in components # This is useful for when you instantiate an entity from a scene and want to overide components component_resources = component_resources.filter(func(comp): return not has_component(comp.get_script())) # Add components passed in directly to the _initialize method to override everything else component_resources.append_array(_components) # Initialize components for res in component_resources: add_component(res.duplicate(true)) # Call the lifecycle method on_ready on_ready() #endregion Built-in Virtual Methods ## Get the effective serialization config for this entity ## Returns entity-specific config if set, otherwise falls back to world default func get_effective_serialize_config() -> GECSSerializeConfig: if serialize_config != null: return serialize_config if ECS.world != null and ECS.world.default_serialize_config != null: return ECS.world.default_serialize_config # Fallback if no world or no default config var fallback = GECSSerializeConfig.new() return fallback #region Components ## Adds a single component to the entity.[br] ## [param component] The subclass of [Component] to add.[br] ## [b]Example[/b]: ## [codeblock]entity.add_component(HealthComponent)[/codeblock] func add_component(component: Resource) -> void: # Cache the resource path to avoid repeated calls var resource_path = component.get_script().resource_path # If a component of this type already exists, remove it first if components.has(resource_path): var existing_component = components[resource_path] remove_component(existing_component) _component_path_cache[component] = resource_path components[resource_path] = component component.parent = self if not component.property_changed.is_connected(_on_component_property_changed): component.property_changed.connect(_on_component_property_changed) ## Adding components happens through a signal component_added.emit(self , component) _entityLogger.trace("Added Component: ", resource_path) func _on_component_property_changed( component: Resource, property_name: String, old_value: Variant, new_value: Variant ) -> void: # Pass this signal on to the world component_property_changed.emit(self , component, property_name, old_value, new_value) ## Adds multiple components to the entity.[br] ## [param _components] An [Array] of [Component]s to add.[br] ## [b]Example:[/b] ## [codeblock]entity.add_components([TransformComponent, VelocityComponent])[/codeblock] func add_components(_components: Array): # OPTIMIZATION: Batch component additions to avoid multiple archetype transitions # Instead of moving archetype once per component, calculate the final archetype once if _components.is_empty(): return # Add all components to local storage first (no signals yet) var added_components = [] for component in _components: if component == null: continue var component_path = component.get_script().resource_path if not components.has(component_path): components[component_path] = component added_components.append(component) # If no new components were actually added, return early if added_components.is_empty(): return # OPTIMIZATION: Move to final archetype only once, after all components are added if ECS.world and ECS.world.entity_to_archetype.has(self ): var old_archetype = ECS.world.entity_to_archetype[ self ] var new_signature = ECS.world._calculate_entity_signature(self ) var comp_types = components.keys() var new_archetype = ECS.world._get_or_create_archetype(new_signature, comp_types) # Only move if we actually need a different archetype if old_archetype != new_archetype: # Remove from old archetype old_archetype.remove_entity(self ) # Add to new archetype new_archetype.add_entity(self ) ECS.world.entity_to_archetype[ self ] = new_archetype # Clean up empty old archetype if old_archetype.is_empty(): old_archetype.add_edges.clear() old_archetype.remove_edges.clear() ECS.world.archetypes.erase(old_archetype.signature) else: # Same archetype - just update the column data for new components for component in added_components: var comp_path = component.get_script().resource_path var entity_index = old_archetype.entity_to_index[ self ] old_archetype.columns[comp_path][entity_index] = component # Emit signals for all added components for component in added_components: component_added.emit(self , component) ## Removes a single component from the entity.[br] ## [param component] The [Component] subclass to remove.[br] ## [b]Example:[/b] ## [codeblock]entity.remove_component(HealthComponent)[/codeblock] func remove_component(component: Resource) -> void: # Use cached path if available, otherwise get it from the component class var resource_path: String if _component_path_cache.has(component): resource_path = _component_path_cache[component] _component_path_cache.erase(component) else: # Component parameter should be a class/script, consistent with has_component resource_path = component.resource_path if components.has(resource_path): var component_instance = components[resource_path] components.erase(resource_path) # Clean up cache entry for the component instance _component_path_cache.erase(component_instance) component_removed.emit(self , component_instance) # ARCHETYPE: Signal handler (_on_entity_component_removed) handles archetype update _entityLogger.trace("Removed Component: ", resource_path) func deferred_remove_component(component: Resource) -> void: call_deferred_thread_group("remove_component", component) ## Removes multiple components from the entity.[br] ## [param _components] An array of components to remove.[br] ## ## [b]Example:[/b] ## [codeblock]entity.remove_components([transform_component, velocity_component])[/codeblock] func remove_components(_components: Array): # OPTIMIZATION: Batch component removals to avoid multiple archetype transitions # Instead of moving archetype once per component, calculate the final archetype once if _components.is_empty(): return # Remove all components from local storage first (no signals yet) var removed_components = [] for _component in _components: if _component == null: continue var comp_to_remove: Resource = null # Handle both Scripts and Resource instances # NOTE: Check Script first since Script inherits from Resource if _component is Script: comp_to_remove = get_component(_component) elif _component is Resource: comp_to_remove = _component if comp_to_remove: var component_path = comp_to_remove.get_script().resource_path if components.has(component_path): components.erase(component_path) removed_components.append(comp_to_remove) # If no components were actually removed, return early if removed_components.is_empty(): return # OPTIMIZATION: Move to final archetype only once, after all components are removed if ECS.world and ECS.world.entity_to_archetype.has(self ): var old_archetype = ECS.world.entity_to_archetype[ self ] var new_signature = ECS.world._calculate_entity_signature(self ) var comp_types = components.keys() var new_archetype = ECS.world._get_or_create_archetype(new_signature, comp_types) # Only move if we actually need a different archetype if old_archetype != new_archetype: # Remove from old archetype old_archetype.remove_entity(self ) # Add to new archetype new_archetype.add_entity(self ) ECS.world.entity_to_archetype[ self ] = new_archetype # Clean up empty old archetype if old_archetype.is_empty(): old_archetype.add_edges.clear() old_archetype.remove_edges.clear() ECS.world.archetypes.erase(old_archetype.signature) # Emit signals for all removed components for component in removed_components: component_removed.emit(self , component) ## Removes all components from the entity.[br] ## [b]Example:[/b] ## [codeblock]entity.remove_all_components()[/codeblock] func remove_all_components() -> void: for component in components.values(): remove_component(component) ## Retrieves a specific [Component] from the entity.[br] ## [param component] The [Component] class to retrieve.[br] ## Returns the requested [Component] if it exists, otherwise `null`.[br] ## [b]Example:[/b] ## [codeblock]var transform = entity.get_component(Transform)[/codeblock] func get_component(component: Resource) -> Component: return components.get(component.resource_path, null) ## Check to see if an entity has a specific component on it.[br] ## This is useful when you're checking to see if it has a component and not going to use the component itself.[br] ## If you plan on getting and using the component, use [method get_component] instead. func has_component(component: Resource) -> bool: return components.has(component.resource_path) #endregion Components #region Relationships ## Adds a relationship to this entity.[br] ## [param relationship] The [Relationship] to add. func add_relationship(relationship: Relationship) -> void: assert( not relationship._is_query_relationship, "Cannot add query relationships to entities. Query relationships (created with dictionaries) are for matching only, not for storage." ) relationship.source = self relationships.append(relationship) relationship_added.emit(self , relationship) func add_relationships(_relationships: Array): for relationship in _relationships: add_relationship(relationship) ## Removes a relationship from the entity.[br] ## [param relationship] The [Relationship] to remove.[br] ## [param limit] Maximum number of relationships to remove. -1 = all (default), 0 = none, >0 = up to that many.[br] ## [br] ## [b]Examples:[/b] ## [codeblock] ## # Remove all matching relationships (default behavior) ## entity.remove_relationship(Relationship.new(C_Damage.new(), target)) ## ## # Remove only one matching relationship ## entity.remove_relationship(Relationship.new(C_Damage.new(), target), 1) ## ## # Remove up to 3 matching relationships ## entity.remove_relationship(Relationship.new(C_Damage.new(), target), 3) ## ## # Remove no relationships (useful for testing/debugging) ## entity.remove_relationship(Relationship.new(C_Damage.new(), target), 0) ## [/codeblock] func remove_relationship(relationship: Relationship, limit: int = -1) -> void: if limit == 0: return var to_remove = [] var removed_count = 0 var pattern_remove = true if relationships.has(relationship): to_remove.append(relationship) pattern_remove = false if pattern_remove: for rel in relationships: if rel.matches(relationship): to_remove.append(rel) removed_count += 1 # If limit is positive and we've reached it, stop collecting if limit > 0 and removed_count >= limit: break for rel in to_remove: relationships.erase(rel) relationship_removed.emit(self , rel) ## Removes multiple relationships from the entity.[br] ## [param _relationships] Array of [Relationship]s to remove.[br] ## [param limit] Maximum number of relationships to remove per relationship type. -1 = all (default), 0 = none, >0 = up to that many. func remove_relationships(_relationships: Array, limit: int = -1): for relationship in _relationships: remove_relationship(relationship, limit) ## Removes all relationships from the entity. func remove_all_relationships() -> void: var to_remove = relationships.duplicate() for rel in to_remove: relationships.erase(rel) relationship_removed.emit(self , rel) ## Retrieves a specific [Relationship] from the entity. ## [param relationship] The [Relationship] to retrieve. ## [return] The first matching [Relationship] if it exists, otherwise `null` func get_relationship(relationship: Relationship) -> Relationship: var to_remove = [] for rel in relationships: # Check if the relationship is valid if not rel.valid(): to_remove.append(rel) continue if rel.matches(relationship): # Remove invalid relationships before returning for invalid_rel in to_remove: relationships.erase(invalid_rel) relationship_removed.emit(self , invalid_rel) return rel # Remove invalid relationships for rel in to_remove: relationships.erase(rel) relationship_removed.emit(self , rel) return null ## Retrieves [Relationship]s from the entity. ## [param relationship] The [Relationship]s to retrieve. ## [return] Array of all matching [Relationship]s (empty array if none found). func get_relationships(relationship: Relationship) -> Array[Relationship]: var results: Array[Relationship] = [] var to_remove = [] for rel in relationships: # Check if the relationship is valid if not rel.valid(): to_remove.append(rel) continue if rel.matches(relationship): results.append(rel) # Remove invalid relationships for rel in to_remove: relationships.erase(rel) relationship_removed.emit(self , rel) return results ## Checks if the entity has a specific relationship.[br] ## [param relationship] The [Relationship] to check for. func has_relationship(relationship: Relationship) -> bool: return get_relationship(relationship) != null #endregion Relationships #region Lifecycle Methods ## Called after the entity is fully initialized and ready.[br] ## Override this method to perform additional setup after all components have been added. func on_ready() -> void: pass ## Called right before the entity is freed from memory.[br] ## Override this method to perform any necessary cleanup before the entity is destroyed. func on_destroy() -> void: pass ## Called when the entity is disabled.[br] func on_disable() -> void: pass ## Called when the entity is enabled.[br] func on_enable() -> void: pass ## Define the default components in code to use (Instead of in the editor)[br] ## This should return a list of components to add by default when the entity is created func define_components() -> Array: return [] ## INTERNAL: Called when entity.enabled changes to move entity between archetypes func _on_enabled_changed(old_value: bool, new_value: bool) -> void: # Only handle if entity is already in a world if not ECS.world or not ECS.world.entity_to_archetype.has(self ): return # OPTIMIZATION: Update bitset instead of moving between archetypes # This eliminates the need for separate enabled/disabled archetypes var archetype = ECS.world.entity_to_archetype[ self ] archetype.update_entity_enabled_state(self , new_value) # Invalidate query cache since archetypes changed ECS.world.cache_invalidated.emit() #endregion Lifecycle Methods