19 KiB
GECS Core Concepts Guide
Deep understanding of Entity Component System architecture
This guide explains the fundamental concepts that make GECS powerful. After reading this, you'll understand how to architect games using ECS principles and leverage GECS's unique features.
📋 Prerequisites
- Completed Getting Started Guide
- Basic GDScript knowledge
- Understanding of Godot's node system
🎯 Why ECS?
The Problem with Traditional OOP
Traditional object-oriented approaches often bundle data and behavior together. Over time, this can become unwieldy and force complicated inheritance structures:
# ❌ Traditional OOP problems
class BaseCharacter:
# Lots of shared code
class Player extends BaseCharacter:
# Player-specific code mixed with shared code
class Enemy extends BaseCharacter:
# Enemy-specific code, some overlap with Player
class Boss extends Enemy:
# Even more inheritance complexity
The ECS Solution
ECS keeps data (components) separate from logic (systems), providing clear organization around three core concepts:
- Entities – IDs or "slots" for your game objects
- Components – Pure data objects that define state (e.g., velocity, health)
- Systems – Logic that processes entities with specific components
This pattern simplifies organization, collaboration, and refactoring. Systems only act upon relevant components. Entities can freely change their makeup without breaking the overall design.
🏗️ GECS Architecture
GECS extends standard ECS with Godot-specific features:
- Integration with Godot nodes - Entities can be scenes, Components are resources
- World management - Central coordination of entities and systems
- ECS singleton - Global access point for queries and processing
- Advanced queries - Property-based filtering and relationship support
- Relationship system - Define complex associations between entities
🎭 Entities
Entity Fundamentals
Entities are the core data containers you work with in GECS. They're Godot nodes extending Entity.gd that hold components and relationships.
Creating Entities in Code:
# Create entity class with components
class_name MyEntity extends Entity
func define_components() -> Array:
return [C_Transform.new(), C_Velocity.new(Vector3.UP)]
# Use the entity
var e_my_entity = MyEntity.new()
ECS.world.add_entity(e_my_entity)
Entity Prefabs (Recommended):
Since GECS integrates with Godot, create scenes with Entity root nodes and save as .tscn files. These "prefabs" can include child nodes for visualization while maintaining ECS data organization.
# e_player.gd - Entity prefab
class_name Player
extends Entity
func on_ready():
# Sync transform from scene to component
var c_trs = get_component(C_Transform) as C_Transform
if not c_trs:
return
transform_comp.transform = self.global_transform # This works because the TSCN base type is Node3D and we extend Node3D with Entity (Which itself extends from Node)
Entity Lifecycle
Entities have a managed lifecycle:
- Initialization - Entity added to world, components loaded from
component_resources - define_components() - Called to add components via code
- on_ready() - Setup initial states, sync transforms
- on_destroy() - Cleanup before removal
- on_disable()/on_enable() - Handle enable/disable states
Note: In GECS v5.0+, entity logic should be handled by Systems, not in entity methods. Entities are pure data containers.
Entity Naming Conventions
GECS follows consistent naming patterns throughout the framework:
- Class names:
ClassCaserepresenting the thing they are - File names:
e_entity_name.gdusing snake_case
Examples:
# e_player.gd
class_name Player extends Entity
# e_enemy.gd
class_name Enemy extends Entity
# e_projectile.gd
class_name Projectile extends Entity
# e_pickup_item.gd
class_name PickupItem extends Entity
Entity as Glue Code
Entities can serve as initialization and connection points:
class_name Player
extends Entity
@onready var mesh_instance = $MeshInstance3D
@onready var collision_shape = $CollisionShape3D
func on_ready():
# Connect scene nodes to components
var c_sprite = get_component(C_Sprite)
if c_sprite:
sprite_comp.mesh_instance = mesh_instance
# Sync editor-placed transform to component
var c_trs = get_component(C_Transform)
if c_trs:
transform_comp.transform = self.global_transform
📦 Components
Component Fundamentals
Components are pure data containers - they store state but contain no game logic. They can emit signals for reactive systems.
# c_health.gd - Example component
class_name C_Health
extends Component
signal health_changed
## How much total health this entity has
@export var maximum := 100.0
## The current health value
@export var current := 100.0
func _init(max_health: float = 100.0):
maximum = max_health
current = max_health
Component Design Principles
Data Only:
# ✅ Good - Pure data
class_name C_Health
extends Component
@export var current: float = 100.0
@export var maximum: float = 100.0
@export var regeneration_rate: float = 1.0
No Game Logic:
# ❌ Avoid - Logic in components
class_name C_Health
extends Component
@export var current: float = 100.0
func take_damage(amount: float): # This belongs in a system!
current -= amount
if current <= 0:
print("Entity died!")
Component Naming Conventions
GECS uses a consistent C_ prefix system:
- Class names:
C_ComponentNamein ClassCase - File names:
c_component_name.gdin snake_case - Organization: Group by purpose in folders
Examples:
# c_health.gd
class_name C_Health extends Component
# c_transform.gd
class_name C_Transform extends Component
# c_velocity.gd
class_name C_Velocity extends Component
# c_user_input.gd
class_name C_UserInput extends Component
# c_sprite_renderer.gd
class_name C_SpriteRenderer extends Component
File Organization:
components/
├── gameplay/
│ ├── c_health.gd
│ ├── c_damage.gd
│ └── c_inventory.gd
├── physics/
│ ├── c_transform.gd
│ ├── c_velocity.gd
│ └── c_collision.gd
└── rendering/
├── c_sprite.gd
└── c_mesh.gd
Adding Components
Via Editor (Recommended):
Add to entity's component_resources array in Inspector - these auto-load when entity is added to world.
Via define_components():
# e_player.gd - Define components programmatically
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Input.new()
]
# Via Inspector: Add to component_resources array
# Components automatically loaded when entity added to world
# Dynamic addition (less common):
var entity = Player.new()
entity.add_component(C_StatusEffect.new("poison"))
ECS.world.add_entity(entity)
⚙️ Systems
System Fundamentals
Systems contain game logic and process entities based on component queries. They should be small, atomic, and focused on one responsibility.
Systems have two main parts:
- Query - Defines which entities to process based on components/relationships
- Process - The function that runs on entities
System Types
Entity Processing:
class_name LifetimeSystem
extends System
func query() -> QueryBuilder:
return q.with_all([C_Lifetime])
func process(entities: Array[Entity], components: Array, delta: float):
# Process each entity - all systems use the same signature
for entity in entities:
var c_lifetime = entity.get_component(C_Lifetime) as C_Lifetime
c_lifetime.lifetime -= delta
if c_lifetime.lifetime <= 0:
ECS.world.remove_entity(entity)
Optimized Batch Processing with iterate():
class_name VelocitySystem
extends System
func query() -> QueryBuilder:
# Use iterate() to get component arrays for faster access
return q.with_all([C_Velocity]).iterate([C_Velocity])
func process(entities: Array[Entity], components: Array, delta: float):
# components[0] contains all C_Velocity components
var velocities = components[0]
for i in entities.size():
# Direct array access is faster than get_component()
var position: Vector3 = entities[i].transform.origin
position += velocities[i].velocity * delta
entities[i].transform.origin = position
Sub-Systems
Group related logic into one system file - all subsystems use the unified signature:
class_name DamageSystem
extends System
func sub_systems():
return [
# [query, callable] - all use same unified process signature
[
q
.with_all([C_Health, C_Damage]),
damage_entities
],
[
q
.with_all([C_Health])
.with_none([C_Dead])
.iterate([C_Health]),
regenerate_health
]
]
func damage_entities(entities: Array[Entity], components: Array, delta: float):
# Process entities with damage
for entity in entities:
var c_health = entity.get_component(C_Health)
var c_damage = entity.get_component(C_Damage)
c_health.current -= c_damage.amount
entity.remove_component(c_damage)
if c_health.current <= 0:
entity.add_component(C_Dead.new())
func regenerate_health(entities: Array[Entity], components: Array, delta: float):
# Batch process using component arrays from iterate()
var healths = components[0]
for i in entities.size():
healths[i].current = min(healths[i].current + 1 * delta, healths[i].maximum)
System Dependencies
Control system execution order with dependencies:
class_name RenderSystem
extends System
func deps() -> Dictionary[int, Array]:
return {
Runs.After: [MovementSystem, TransformSystem], # Run after these
Runs.Before: [UISystem] # Run before this
}
# Special case: run after ALL other systems
class_name TransformSystem
extends System
func deps() -> Dictionary[int, Array]:
return {
Runs.After: [ECS.wildcard] # Runs after everything else
}
System Naming Conventions
- Class names:
SystemNameSystemin ClassCase (TransformSystem, PhysicsSystem) - File names:
s_system_name.gd(s_transform.gd, s_physics.gd)
System Lifecycle
Systems follow Godot node lifecycle:
setup()- Initial setup after system is added to worldprocess(entities, components, delta)- Unified method called each frame for matching entities- System groups for organized processing order
🔍 Query System
Query Builder
GECS uses a fluent API for building entity queries:
ECS.world.query
.with_all([C_Health, C_Position]) # Must have all these components
.with_any([C_Player, C_Enemy]) # Must have at least one of these
.with_none([C_Dead, C_Disabled]) # Must not have any of these
.with_relationship([r_attacking_player]) # Must have these relationships
.without_relationship([r_fleeing]) # Must not have these relationships
.with_reverse_relationship([r_parent_of]) # Must be target of these relationships
.iterate([C_Health]) # Fetch these components and add to components array for quick iteration
Query Methods
Basic Query Operations:
var entities = query.execute() # Get matching entities
var filtered = query.matches(entity_list) # Filter existing list
var combined = query.combine(another_query) # Combine queries
Query Types Explained
with_all - Entities must have ALL specified components:
# Find entities that can move and be damaged
q.with_all([C_Position, C_Velocity, C_Health])
with_any - Entities must have AT LEAST ONE of the components:
# Find players or enemies (anything controllable)
q.with_any([C_Player, C_Enemy])
with_none - Entities must NOT have any of these components:
# Find living entities (exclude dead/disabled)
q.with_all([C_Health]).with_none([C_Dead, C_Disabled])
Component Property Queries
Query based on component data values:
# Find entities with low health
q.with_all([{C_Health: {"current": {"_lt": 20}}}])
# Find fast-moving entities
q.with_all([{C_Velocity: {"speed": {"_gt": 100}}}])
# Find entities with specific states
q.with_all([{C_State: {"current_state": {"_eq": "attacking"}}}])
Supported Operators:
_eq- Equal to_ne- Not equal to_gt- Greater than_lt- Less than_gte- Greater than or equal_lte- Less than or equal_in- Value in list_nin- Value not in list
🔗 Relationships
Relationship Fundamentals
Relationships link entities together for complex associations. They consist of:
- Source - Entity that has the relationship
- Relation - Component defining the relationship type
- Target - Entity or type being related to
# Create relationship components
class_name C_Likes extends Component
class_name C_Loves extends Component
class_name C_Eats extends Component
@export var quantity: int = 1
# Create entities
var e_bob = Entity.new()
var e_alice = Entity.new()
var e_heather = Entity.new()
var e_apple = Food.new()
# Add relationships
e_bob.add_relationship(Relationship.new(C_Likes.new(), e_alice)) # bob likes alice
e_alice.add_relationship(Relationship.new(C_Loves.new(), e_heather)) # alice loves heather
e_heather.add_relationship(Relationship.new(C_Likes.new(), Food)) # heather likes food (type)
e_heather.add_relationship(Relationship.new(C_Eats.new(5), e_apple)) # heather eats 5 apples
Relationship Queries
Specific Relationships:
# Any entity that likes alice
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), e_alice)])
# Any entity that eats 5 apples
ECS.world.query.with_relationship([Relationship.new(C_Eats.new(5), e_apple)])
# Any entity that likes the Food type
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), Food)])
Wildcard Relationships:
# Any entity with any relation toward heather
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, e_heather)])
# Any entity that likes anything
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
# Any entity with any relation to Enemy type
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, Enemy)])
Reverse Relationships:
# Find entities that are being liked by someone
ECS.world.query.with_reverse_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
Relationship Best Practices
Reuse Relationship Objects:
# Reuse for performance
var r_likes_apples = Relationship.new(C_Likes.new(), e_apple)
var r_attacking_players = Relationship.new(C_IsAttacking.new(), Player)
# Consider a static relationships class
class_name Relationships
static func attacking_players():
return Relationship.new(C_IsAttacking.new(), Player)
static func chasing_anything():
return Relationship.new(C_IsChasing.new(), ECS.wildcard)
🌍 World Management
World Lifecycle
The World is the central manager for all entities and systems:
# main.gd - Simple scene-based setup
extends Node
@onready var world: World = $World
func _ready():
Bootstrap.bootstrap() # Initialize game-specific setup
ECS.world = world
# Systems are automatically registered via scene composition
# Process systems by groups in order
func _process(delta):
world.process(delta, "run-first") # Initialization
world.process(delta, "input") # Input handling
world.process(delta, "gameplay") # Game logic
world.process(delta, "ui") # UI updates
world.process(delta, "run-last") # Cleanup
func _physics_process(delta):
world.process(delta, "physics") # Physics systems
world.process(delta, "debug") # Debug systems
System Groups and Processing Order
Organize systems using scene-based composition with execution groups:
default_systems.tscn Structure:
├── run-first (SystemGroup)
│ ├── VictimInitSystem
│ └── EcsStorageLoad
├── input (SystemGroup)
│ ├── ItemSystem
│ ├── WeaponsSystem
│ └── PlayerControlsSystem
├── gameplay (SystemGroup)
│ ├── GearSystem
│ ├── DeathSystem
│ └── EventSystem
├── physics (SystemGroup)
│ ├── FrictionSystem
│ ├── CharacterBody3DSystem
│ └── TransformSystem
├── ui (SystemGroup)
│ └── UiVisibilitySystem
├── debug (SystemGroup)
│ └── DebugLabel3DSystem
└── run-last (SystemGroup)
├── ActionsSystem
└── PendingDeleteSystem
Scene Setup Benefits:
- Visual Organization: See system hierarchy in Godot editor
- Easy Reordering: Drag systems between groups
- Inspector Configuration: Set system properties in editor
- Reusable Scenes: Share system configurations between projects
🔄 Data-Driven Architecture
Composition Over Inheritance
Build entities by combining simple components rather than complex inheritance:
# ✅ Composition approach in entity definition
class_name Player extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Movement.new(200.0),
C_Input.new(),
C_Inventory.new()
]
# Same components reused for different entity types
enemy.add_component(C_Health.new(50))
enemy.add_component(C_Movement.new(100.0))
enemy.add_component(C_AI.new())
enemy.add_component(C_Sprite.new("enemy.png"))
Modular System Design
Keep systems small and focused:
# ✅ Focused systems
class_name MovementSystem extends System
# Only handles position updates
class_name CollisionSystem extends System
# Only handles collision detection
class_name HealthSystem extends System
# Only handles health changes
This ensures:
- Easier debugging - Clear separation of concerns
- Better reusability - Systems work across different entity types
- Simplified testing - Each system can be tested independently
- Performance optimization - Systems can be profiled and optimized individually
🎯 Next Steps
Now that you understand GECS's core concepts:
- Apply these patterns in your own projects
- Experiment with relationships for complex entity interactions
- Design component hierarchies that support your game's needs
- Learn optimization techniques in Performance Guide
- Master common patterns in Best Practices Guide
📚 Related Documentation
- Getting Started - Build your first ECS project
- Best Practices - Write maintainable ECS code
- Performance Optimization - Make your games run fast
- Troubleshooting - Solve common issues
"Understanding ECS is about shifting from 'what things are' to 'what things have' and 'what operates on them.' This separation of data and logic is the key to scalable game architecture."