RTS_Entity: The Core Unit Host
Overview
RTS_Entity is the central hub that hosts and coordinates all components for a unit in the RTS Entity Controller. It extends CharacterBody3D and acts as the owner/parent that manages component lifecycle, state coordination, and integration with the broader system. An Entity can represent anything from a movable Unit to an immovable structure such as a building.
The Host/Owner Pattern
Rather than components being standalone, they are children of the RTS_Entity node which acts as their host and orchestrator.
Why This Design?
- Centralized State - Entity manages all component state in one place
- Easy Discovery - Components are automatically found and cached
- Physics Support - CharacterBody3D provides physics and collision support
- Spatial Hashing - Entity represents a spatial unit for queries
- Lifecycle Management - Entity emits useful lifecycle events
EntityResource
Basic data, such as the entities name, id or thumbnail should be configured by creating a EntityResource resource, from which the RTS_Entity can read.
Component Discovery & Caching
RTS_Entity automatically discovers components on startup:
@export var selectable: RTS_Selectable
@export var movable: RTS_Movable
@export var health: RTS_HealthComponent
@export var attack: RTS_AttackComponent
@export var defense: RTS_Defense
@export var stunnable: RTS_StunnableComponent
@export var anim_tree: RTS_AnimationTreeComponent
@export var visuals: RTS_VisualComponent
@export var obstacle: NavigationObstacleComponent
In _ready(), the entity calls update_and_fetch_components():
func update_and_fetch_components():
abilities.clear()
abilities_array.clear()
for child in get_children():
if child is RTS_Selectable:
selectable = child
if child is RTS_Movable:
movable = child
if child is RTS_HealthComponent:
health = child
# ... etc for all components
This means:
- No manual setup needed - Just add components as children
- Components are cached - Accessed via properties, not
get_node()calls - Type-safe - Full IDE autocomplete support
Since the script as @tool annotated, this happens during editor time, to avoid race conditions during startup.
Faction System
Entities belong to factions that determine team affiliation:
enum Faction { PLAYER, ENEMY, NEUTRAL }
@export var faction = Faction.PLAYER
Use faction to: - Determine ally/enemy relationships - Set collision layers - Control targeting - Manage rendering/highlighting
func setup_unit(_faction: Faction):
faction = _faction
# Faction affects collision detection and team relationships
Component Coordination
The entity coordinates between components by connecting signals and routing events:
Component State & Animation Sync
if movable:
movable.sm.enter_state.connect(on_movable_enter_state)
on_movable_enter_state(movable.sm.current_state)
When the certain component changes states, the entity updates its own state dictionaries:
var sb : Dictionary[StringName, bool] = {} #state bool
var si : Dictionary[StringName, int] = {} #state integer
At first this might seem redundant and look like unnecessary coupling, however this is done so the AnimationTree a central place to check state for its State Transitions. Unforuntately the advanced expressions in Godots StateMachines can only evaluate state from one script (called the "AdvancedExpressionBaseNode") and the Entity is the perfect candidate to read this state from. Thus you can evaluate any state easily in the AnimationTree's StateMachine, for instance
si["move_state"] = new_state # Which movement state
si["attack_state"] = new_state # Which attack state
sb["is_stunned"] = value # Stunned status
si["weapon_index"] = weapon_index # Current weapon
Ability Management
RTS_Entity automatically collects and manages abilities:
var abilities: Dictionary[String, RTS_Ability] = {}
var abilities_array: Array[RTS_Ability] = []
All RTS_Ability components are discovered and stored by ID:
if child is RTS_Ability:
abilities_array.append(child)
abilities[child.resource.id] = child
Access abilities by name or iterate:
# Get specific ability
var fireball = entity.abilities["fireball"]
# Iterate all
for ability in entity.abilities_array:
ability.use(target)
Spatial Hashing Integration
The entity integrates with spatial hashing for efficient queries:
@export var space_hash: bool = true:
set(value):
space_hash = value
When set to false, the spatial hash system will not include this entity as a "client" in its grid. This can be efficient to set to false for immovable structures or entities that don't require special quering, to reduce workload on the spatial hash system.
Lifecycle Events
The entity emits important lifecycle signals:
signal before_tree_exit(entity: RTS_Entity)
signal end_of_life(entity: RTS_Entity) # Guaranteed exactly once
These allow other systems to track unit creation and destruction.
Screen Visibility
Track when units enter/exit screen for optimization:
visible_on_screen.screen_entered.connect(on_screen_entered)
visible_on_screen.screen_exited.connect(on_screen_exited)
func on_screen_entered():
RTS_EventBus.entity_screen_visible.emit(self, true)
Use this to: - Stop processing off-screen units - Optimize rendering - Control audio playback
Accessing Entity from Components
Every component implements the func fetch_entity() -> RTS_Entity to easily fetch the corresponding entity.
Creating a Custom Entity
RTS_Entity can be extended for unique behaviour, i.e.
class_name HeroUnit extends RTS_Entity
@export var experience: int = 0
but before you do, think twice and reconsider if this additional behavior can not rather be implement using a new component, to keep your entities modular!
Best Practices
1. Always Check Component Existence (since they are optional)
# Good
if entity.health:
entity.health.take_damage(10)
# Avoid
entity.health.take_damage(10) # Crashes if health component missing
2. Use Entity as Central Coordinator
Even though certain components technically depend on each other (for instance RTS_Defense requires a RTS_Health to work an deal damage), it is usually best to query this component from RTS_Entity, to avoid coupling and spaghetti references.
3. Leverage Caching
# Good - Cached reference
var health = entity.health
for i in range(100):
health.current_hp -= 1
# Avoid - Repeated node lookups
for i in range(100):
entity.get_node("RTS_HealthComponent").current_hp -= 1
See Also
- Component System - Creating components
- Getting Started - Setting up units
- Best Practices - Design patterns