510 lines
18 KiB
GDScript
510 lines
18 KiB
GDScript
## 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
|