basic ECS spawner

This commit is contained in:
2026-01-15 15:27:48 +01:00
parent 24a781f36a
commit eb737b469c
860 changed files with 58621 additions and 32 deletions

View File

@@ -0,0 +1,724 @@
# GECS Best Practices Guide
> **Write maintainable, performant ECS code**
This guide covers proven patterns and practices for building robust games with GECS. Apply these patterns to keep your code clean, fast, and easy to debug.
## 📋 Prerequisites
- Completed [Getting Started Guide](GETTING_STARTED.md)
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
## 🧱 Component Design Patterns
### Keep Components Pure Data
Components should only hold data, never logic or behavior.
```gdscript
# ✅ Good - Pure data component
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
func _init(max_health: float = 100.0):
maximum = max_health
current = max_health
```
```gdscript
# ❌ Avoid - Logic in components
class_name C_Health
extends Component
@export var current: float = 100.0
@export var maximum: float = 100.0
# This belongs in a system, not a component
func take_damage(amount: float):
current -= amount
if current <= 0:
print("Entity died!")
```
### Use Composition Over Inheritance
Build entities by combining simple components rather than complex inheritance hierarchies.
```gdscript
# ✅ Good - Composable components via define_components() or scene setup
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Input.new()
]
class_name Enemy
extends Entity
func define_components() -> Array:
return [
C_Health.new(50),
C_Transform.new(),
C_AI.new()
]
```
### Design for Configuration
Make components easily configurable through export properties.
```gdscript
# ✅ Good - Configurable component
class_name C_Movement
extends Component
@export var speed: float = 100.0
@export var acceleration: float = 500.0
@export var friction: float = 800.0
@export var max_speed: float = 300.0
@export var can_fly: bool = false
func _init(spd: float = 100.0, can_fly_: bool = false):
speed = spd
can_fly = can_fly_
```
## ⚙️ System Design Patterns
### Single Responsibility Principle
Each system should handle one specific concern.
```gdscript
# ✅ Good - Focused systems
class_name MovementSystem extends System
func query(): return q.with_all([C_Position, C_Velocity])
class_name RenderSystem extends System
func query(): return q.with_all([C_Position, C_Sprite])
class_name HealthSystem extends System
func query(): return q.with_all([C_Health])
```
### Use System Groups for Processing Order
Organize systems into logical groups using scene-based organization. Systems are grouped in scene nodes and processed in the correct order.
```gdscript
# main.gd - Process systems in correct order
func _process(delta):
world.process(delta, "run-first") # Initialization systems
world.process(delta, "input") # Input handling
world.process(delta, "gameplay") # Game logic
world.process(delta, "ui") # UI updates
world.process(delta, "run-last") # Cleanup systems
func _physics_process(delta):
world.process(delta, "physics") # Physics systems
world.process(delta, "debug") # Debug systems
```
### Early Exit for Performance
Return early from system processing when no work is needed.
```gdscript
# ✅ Good - Early exit patterns
class_name HealthRegenerationSystem extends System
func query():
return q.with_all([C_Health]).with_none([C_Dead])
func process(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
var health = entity.get_component(C_Health)
# Early exit if already at max health
if health.current >= health.maximum:
continue
# Apply regeneration
health.current = min(health.current + health.regeneration_rate * delta, health.maximum)
```
## 🏗️ Code Organization Patterns
### GECS Naming Conventions
```gdscript
# ✅ GECS Standard naming patterns:
# Components: C_ComponentName class, c_component_name.gd file
class_name C_Health extends Component # c_health.gd
class_name C_Position extends Component # c_position.gd
# Systems: SystemNameSystem class, s_system_name.gd file
class_name MovementSystem extends System # s_movement.gd
class_name RenderSystem extends System # s_render.gd
# Entities: EntityName class, e_entity_name.gd file
class_name Player extends Entity # e_player.gd
class_name Enemy extends Entity # e_enemy.gd
# Observers: ObserverNameObserver class, o_observer_name.gd file
class_name HealthUIObserver extends Observer # o_health_ui.gd
```
### File Organization
Organize your ECS files by theme for better scalability:
```
project/
├── components/
│ ├── ai/ # AI-related components
│ ├── animation/ # Animation components
│ ├── gameplay/ # Core gameplay components
│ ├── gear/ # Equipment/gear components
│ ├── item/ # Item system components
│ ├── multiplayer/ # Multiplayer-specific
│ ├── relationships/ # Relationship components
│ ├── rendering/ # Visual/rendering
│ └── weapon/ # Weapon system
├── entities/
│ ├── enemies/ # Enemy entities
│ ├── gameplay/ # Core entities
│ ├── items/ # Item entities
│ └── ui/ # UI entities
├── systems/
│ ├── combat/ # Combat systems
│ ├── core/ # Core ECS systems
│ ├── gameplay/ # Gameplay systems
│ ├── input/ # Input systems
│ ├── interaction/ # Interaction systems
│ ├── physics/ # Physics systems
│ └── ui/ # UI systems
└── observers/
└── o_transform.gd # Reactive systems
```
## 🎮 Common Game Patterns
### Player Character Pattern
```gdscript
# e_player.gd
class_name Player
extends Entity
func on_ready():
# Common pattern: sync scene transform to component
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
add_to_group("player")
```
### Enemy Pattern
```gdscript
# e_enemy.gd
class_name Enemy
extends Entity
func on_ready():
# Sync transform and add to enemy group
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
add_to_group("enemies")
```
## 🚀 Performance Best Practices
### Choose the Right Query Method ⭐ NEW!
**Query Performance Ranking** (v5.0.0-rc4+):
```gdscript
# 🏆 FASTEST - Enabled/disabled queries (constant time)
class_name ActiveEntitiesOnly extends System
func query():
return q.enabled(true) # ~0.05ms for any number of entities
# 🥈 EXCELLENT - Component queries (heavily optimized)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]) # ~0.6ms for 10K entities
# 🥉 GOOD - Use with_any strategically
class_name DamageableSystem extends System
func query():
return q.with_any([C_Player, C_Enemy]).with_all([C_Health]) # ~5.6ms for 10K
# 🐌 AVOID - Group queries are slowest
class_name PlayerSystem extends System
func query():
return q.with_group("player") # ~16ms for 10K entities
# Better: q.with_all([C_Player])
```
### Use iterate() for Batch Performance
```gdscript
# ✅ Good - Batch processing with iterate()
class_name TransformSystem
extends System
func query():
# Use iterate() to get component arrays
return q.with_all([C_Transform]).iterate([C_Transform])
func process(entities: Array[Entity], components: Array, delta: float):
# Batch access to components for better performance
var transforms = components[0] # C_Transform array from iterate()
for i in range(entities.size()):
entities[i].global_transform = transforms[i].transform
```
### Use Specific Queries
```gdscript
# ✅ BEST - Combine enabled filter with components
class_name ActivePlayerInputSystem extends System
func query():
return q.with_all([C_Input, C_Movement]).enabled(true)
# Super fast: enabled filtering + component matching
# ✅ GOOD - Specific component query
class_name ProjectileSystem extends System
func query():
return q.with_all([C_Projectile, C_Velocity]) # Fast and specific
# ❌ AVOID - Group-based queries (slow)
class_name PlayerSystem extends System
func query():
return q.with_group("player") # Use q.with_all([C_Player]) instead
# ❌ AVOID - Overly broad queries
class_name UniversalMovementSystem extends System
func query():
return q.with_all([C_Transform]) # Too broad - matches everything
```
## 🎭 Entity Prefabs (Scene Files)
### Using Godot Scenes as Entity Prefabs
The most powerful pattern in GECS is using Godot's scene system (.tscn files) as entity prefabs. This combines ECS data with Godot's visual editor:
```
e_player.tscn Structure:
├── Player (Entity node - extends your e_player.gd class)
│ ├── MeshInstance3D (visual representation)
│ ├── CollisionShape3D (physics collision)
│ ├── AudioStreamPlayer3D (sound effects)
│ └── SkeletonAttachment3D (for equipment)
```
**Benefits of Scene-based Prefabs:**
- **Visual Editing**: Design entities in Godot's 3D editor
- **Component Assignment**: Set up ECS components in the Inspector
- **Godot Integration**: Leverage existing Godot nodes and systems
- **Reusability**: Instantiate the same prefab multiple times
- **Version Control**: Scene files work well with git
**Setting up Entity Prefabs:**
1. **Create scene with Entity as root**: `e_player.tscn` with `Player` entity node.
- Another trick here is to add a CharacterBody3d and then extend that CharacterBody3D with the e_player.gd script this way you get Entity class and CharacterBody3D class data
2. **Add visual/physics children**: Add MeshInstance3D, CollisionShape3D, etc. as children
3. **Configure components in Inspector**: Add components to the `component_resources` array
4. **Save as reusable prefab**: Save the .tscn file for instantiation
5. **Set up on_ready()**: Handle any initialization logic
### Component Assignment in Prefabs
**Method 1: Inspector Assignment (Recommended)**
Set up components directly in the Godot Inspector:
```gdscript
# In e_player.tscn entity root node Inspector:
# Component Resources array:
# - [0] C_Health.new() (max: 100, current: 100)
# - [1] C_Transform.new() (synced with scene transform)
# - [2] C_Input.new() (for player controls)
# - [3] C_LocalPlayer.new() (mark as local player)
```
**Method 2: define_components() (Programmatic)**
```gdscript
# e_player.gd attached to Player.tscn root
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Input.new(),
C_LocalPlayer.new()
]
func on_ready():
# Initialize after components are ready
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
add_to_group("player")
```
**Method 3: Hybrid Approach**
```gdscript
# Core components via Inspector, dynamic components via script
func on_ready():
# Sync scene transform to component
if has_component(C_Transform):
var transform_comp = get_component(C_Transform)
transform_comp.transform = global_transform
# Add conditional components based on game state
if GameState.is_multiplayer:
add_component(C_NetworkSync.new())
if GameState.debug_mode:
add_component(C_DebugInfo.new())
```
### Instantiating Entity Prefabs
**Basic Spawning Pattern:**
```gdscript
# Spawn system or main scene
@export var player_prefab: PackedScene
@export var enemy_prefab: PackedScene
func spawn_player(position: Vector3) -> Entity:
var player = player_prefab.instantiate() as Entity
player.global_position = position
get_tree().current_scene.add_child(player) # Add to scene
ECS.world.add_entity(player) # Register with ECS
return player
func spawn_enemy(position: Vector3) -> Entity:
var enemy = enemy_prefab.instantiate() as Entity
enemy.global_position = position
get_tree().current_scene.add_child(enemy)
ECS.world.add_entity(enemy)
return enemy
```
**Advanced Spawning with SpawnSystem:**
```gdscript
# s_spawner.gd
class_name SpawnerSystem
extends System
func query():
return q.with_all([C_SpawnPoint])
func process(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
var spawn_point = entity.get_component(C_SpawnPoint)
if spawn_point.should_spawn():
var spawned = spawn_point.prefab.instantiate() as Entity
spawned.global_position = entity.global_position
get_tree().current_scene.add_child(spawned)
ECS.world.add_entity(spawned)
spawn_point.mark_spawned()
```
**Prefab Management Best Practices:**
```gdscript
# Organize prefabs in preload statements
const PLAYER_PREFAB = preload("res://entities/gameplay/e_player.tscn")
const ENEMY_PREFAB = preload("res://entities/enemies/e_enemy.tscn")
const WEAPON_PREFAB = preload("res://entities/items/e_weapon.tscn")
# Or use a prefab registry
class_name PrefabRegistry
static var prefabs = {
"player": preload("res://entities/gameplay/e_player.tscn"),
"enemy": preload("res://entities/enemies/e_enemy.tscn"),
"weapon": preload("res://entities/items/e_weapon.tscn")
}
static func spawn(prefab_name: String, position: Vector3) -> Entity:
var prefab = prefabs[prefab_name]
var entity = prefab.instantiate() as Entity
entity.global_position = position
get_tree().current_scene.add_child(entity)
ECS.world.add_entity(entity)
return entity
```
## 🏗️ Main Scene Architecture
### Scene Structure Pattern
Organize your main scene using the proven structure pattern:
```
Main.tscn
├── World (World node)
├── DefaultSystems (Node - instantiated from default_systems.tscn)
│ ├── run-first (Node - SystemGroup)
│ │ ├── VictimInitSystem
│ │ └── EcsStorageLoad
│ ├── input (Node - SystemGroup)
│ │ ├── ItemSystem
│ │ ├── WeaponsSystem
│ │ └── PlayerControlsSystem
│ ├── gameplay (Node - SystemGroup)
│ │ ├── GearSystem
│ │ ├── DeathSystem
│ │ └── EventSystem
│ ├── physics (Node - SystemGroup)
│ │ ├── FrictionSystem
│ │ ├── CharacterBody3DSystem
│ │ └── TransformSystem
│ ├── ui (Node - SystemGroup)
│ │ └── UiVisibilitySystem
│ ├── debug (Node - SystemGroup)
│ │ └── DebugLabel3DSystem
│ └── run-last (Node - SystemGroup)
│ ├── ActionsSystem
│ └── PendingDeleteSystem
├── Level (Node3D - for level geometry)
└── Entities (Node3D - spawned entities go here)
```
### Systems Setup in Main Scene
**Scene-based Systems Setup (Recommended)**
Use scene composition to organize systems. The default_systems.tscn contains all systems organized by execution groups:
```gdscript
# main.gd - Simple main scene setup
extends Node
@onready var world: World = $World
func _ready():
Bootstrap.bootstrap() # Initialize any game-specific setup
ECS.world = world
# Systems are automatically registered via scene composition
```
**Creating a Default Systems Scene:**
1. Create `default_systems.tscn` with system groups as Node children
2. Add individual system scripts as children of each group
3. Instantiate this scene in your main scene
4. Systems are automatically discovered and registered by the World
### Processing Systems by Group
```gdscript
# main.gd - Process systems in correct order
extends Node3D
func _process(delta):
if ECS.world:
ECS.process(delta, "input") # Handle input first
ECS.process(delta, "core") # Core logic
ECS.process(delta, "gameplay") # Game mechanics
ECS.process(delta, "render") # UI/visual updates last
func _physics_process(delta):
if ECS.world:
ECS.process(delta, "physics") # Physics systems
```
## 🛠️ Common Utility Patterns
### Transform Synchronization
Common transform synchronization patterns:
```gdscript
# Sync entity transform TO component (scene → component)
static func sync_transform_to_component(entity: Entity):
if entity.has_component(C_Transform):
var transform_comp = entity.get_component(C_Transform)
transform_comp.transform = entity.global_transform
# Sync component transform TO entity (component → scene)
static func sync_component_to_transform(entity: Entity):
if entity.has_component(C_Transform):
var transform_comp = entity.get_component(C_Transform)
entity.global_transform = transform_comp.transform
# Common usage in entity on_ready()
func on_ready():
sync_transform_to_component(self) # Sync scene position to C_Transform
```
### Component Helpers
Build helpers for common component operations:
```gdscript
# Helper functions you can add to your project
static func add_health_to_entity(entity: Entity, max_health: float):
var health = C_Health.new(max_health)
entity.add_component(health)
return health
static func damage_entity(entity: Entity, amount: float):
if entity.has_component(C_Health):
var health = entity.get_component(C_Health)
health.current = max(0, health.current - amount)
return health.current <= 0 # Return true if entity died
return false
```
## 🎛️ Relationship Management Best Practices
### Limited Removal Patterns
**Use Descriptive Constants:**
```gdscript
# ✅ Good - Clear intent with constants
const WEAK_CLEANSE = 1
const MEDIUM_CLEANSE = 3
const STRONG_CLEANSE = -1 # All
# ✅ Good - Stack-based constants
const SINGLE_STACK = 1
const PARTIAL_STACKS = 3
const ALL_STACKS = -1
func cleanse_debuffs(entity: Entity, power: int):
match power:
1: entity.remove_relationship(Relations.any_debuff(), WEAK_CLEANSE)
2: entity.remove_relationship(Relations.any_debuff(), MEDIUM_CLEANSE)
3: entity.remove_relationship(Relations.any_debuff(), STRONG_CLEANSE)
```
**Validate Before Removal:**
```gdscript
# ✅ Excellent - Safe removal with validation
func safe_partial_heal(entity: Entity, heal_amount: int):
var damage_rels = entity.get_relationships(Relations.any_damage())
if damage_rels.is_empty():
print("Entity has no damage to heal")
return
var to_heal = min(heal_amount, damage_rels.size())
entity.remove_relationship(Relations.any_damage(), to_heal)
print("Healed ", to_heal, " damage effects")
# ✅ Good - Helper function with built-in safety
func remove_poison_stacks(entity: Entity, stacks_to_remove: int):
if stacks_to_remove <= 0:
return
entity.remove_relationship(Relations.poison_effect(), stacks_to_remove)
```
**System Integration Patterns:**
```gdscript
# ✅ Excellent - Integration with game systems
class_name StatusEffectSystem extends System
func process(entities: Array[Entity], components: Array, delta: float):
# Example: process spell casting entities
for entity in entities:
var spell = entity.get_component(C_SpellCaster)
if spell.is_casting_cleanse():
process_cleanse_spell(entity, spell.target, spell.power)
func process_cleanse_spell(caster: Entity, target: Entity, spell_power: int):
# Calculate cleanse strength based on spell power and caster stats
var cleanse_strength = calculate_cleanse_strength(caster, spell_power)
# Apply graduated cleansing based on strength
match cleanse_strength:
1..3: target.remove_relationship(Relations.any_debuff(), 1)
4..6: target.remove_relationship(Relations.any_debuff(), 2)
7..9: target.remove_relationship(Relations.any_debuff(), 3)
_: target.remove_relationship(Relations.any_debuff()) # Remove all
func process_antidote_item(user: Entity, antidote_strength: int):
# Remove poison based on antidote quality
user.remove_relationship(Relations.poison_effect(), antidote_strength)
# Remove poison resistance temporarily to prevent immediate repoison
user.add_relationship(Relations.poison_immunity(), 5.0) # 5 second immunity
class_name InventorySystem extends System
func consume_item_stack(entity: Entity, item_type: Script, count: int):
# Consume specific number of items from inventory
entity.remove_relationship(
Relationship.new(C_HasItem.new(), item_type),
count
)
func use_consumable(entity: Entity, item: Component, quantity: int = 1):
# Use consumable items with quantity
entity.remove_relationship(
Relationship.new(C_HasItem.new(), item),
quantity
)
```
**Performance Optimization:**
```gdscript
# ✅ Good - Cache relationships for multiple operations
func optimize_bulk_removal(entity: Entity):
# Cache the relationship for reuse
var poison_rel = Relations.poison_effect()
var damage_rel = Relations.any_damage()
# Multiple targeted removals
entity.remove_relationship(poison_rel, 2) # Remove 2 poison
entity.remove_relationship(damage_rel, 1) # Remove 1 damage
entity.remove_relationship(poison_rel, 1) # Remove 1 more poison
# ✅ Excellent - Batch removal patterns
func batch_cleanup(entities: Array[Entity]):
var cleanup_rel = Relations.temporary_effect()
for entity in entities:
# Remove up to 3 temporary effects from each entity
entity.remove_relationship(cleanup_rel, 3)
```
## 🎯 Next Steps
Now that you understand best practices:
1. **Apply these patterns** in your projects
2. **Learn advanced topics** in [Core Concepts](CORE_CONCEPTS.md)
3. **Optimize performance** with [Performance Guide](PERFORMANCE_OPTIMIZATION.md)
**Need help?** [Join our Discord](https://discord.gg/eB43XU2tmn) for community discussions and support.
---
_"Good ECS code is like a well-organized toolbox - every component has its place, every system has its purpose, and everything works together smoothly."_

View File

@@ -0,0 +1,182 @@
# Component Queries in GECS
> **Advanced property-based entity filtering**
Component Queries provide a powerful way to filter entities not just based on the presence of components but also on the data within those components. This allows for precise, data-driven entity selection in your game systems.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Basic Queries](CORE_CONCEPTS.md#query-system)
## 🎯 Introduction
In standard ECS queries, you filter entities by which components they have or don't have. Component Queries take this further by letting you filter based on the **values** inside those components.
Instead of just asking "which entities have a HealthComponent?", you can ask "which entities have a HealthComponent with current health less than 20?"
## Using Component Queries with `QueryBuilder`
The `QueryBuilder` class allows you to construct queries to retrieve entities that match certain criteria. With component queries, you can specify conditions on component properties within `with_all` and `with_any` methods.
### Syntax
A component query is a `Dictionary` that maps a component class to a query `Dictionary` specifying property conditions.
```gdscript
{ ComponentClass: { property_name: { operator: value } } }
```
### Supported Operators
- `_eq`: Equal to
- `_ne`: Not equal to
- `_gt`: Greater than
- `_lt`: Less than
- `_gte`: Greater than or equal to
- `_lte`: Less than or equal to
- `_in`: Value is in a list
- `_nin`: Value is not in a list
### Examples
#### 1. Basic Component Query
Retrieve entities where `C_TestC.value` is equal to `25`.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_eq": 25 } } }
]).execute()
```
#### 2. Multiple Conditions on a Single Component
Retrieve entities where `C_TestC.value` is between `20` and `25`.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_gte": 20, "_lte": 25 } } }
]).execute()
```
#### 3. Combining Component Queries and Regular Components
Retrieve entities that have `C_TestD` component and `C_TestC.value` greater than `20`.
```gdscript
var result = QueryBuilder.new(world).with_all([
C_TestD,
{ C_TestC: { "value": { "_gt": 20 } } }
]).execute()
```
#### 4. Using `with_any` with Component Queries
Retrieve entities where `C_TestC.value` is less than `15` **or** `C_TestD.points` is greater than or equal to `100`.
```gdscript
var result = QueryBuilder.new(world).with_any([
{ C_TestC: { "value": { "_lt": 15 } } },
{ C_TestD: { "points": { "_gte": 100 } } }
]).execute()
```
#### 5. Using `_in` and `_nin` Operators
Retrieve entities where `C_TestC.value` is either `10` or `25`.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_in": [10, 25] } } }
]).execute()
```
#### 6. Complex Queries
Retrieve entities where:
- `C_TestC.value` is greater than or equal to `25`, and
- `C_TestD.points` is greater than `75` **or** less than `30`, and
- Excludes entities with `C_TestE` component.
```gdscript
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_gte": 25 } } }
]).with_any([
{ C_TestD: { "points": { "_gt": 75 } } },
{ C_TestD: { "points": { "_lt": 30 } } }
]).with_none([C_TestE]).execute()
```
## Important Notes
- **Component Queries with `with_none`**: Component queries are **not supported** with the `with_none` method. This is because querying properties of components that should not exist on the entity doesn't make logical sense. Use `with_none` to exclude entities that have certain components.
```gdscript
# Correct usage of with_none
var result = QueryBuilder.new(world).with_none([C_Inactive]).execute()
```
- **Empty Queries Match All Instances of the Component**
If you provide an empty query dictionary for a component, it will match all entities that have that component, regardless of its properties.
```gdscript
# This will match all entities that have C_TestC component
var result = QueryBuilder.new(world).with_all([
{ C_TestC: {} }
]).execute()
```
- **Non-existent Properties**
If you query a property that doesn't exist on the component, it will not match any entities.
```gdscript
# Assuming 'non_existent' is not a property of C_TestC
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "non_existent": { "_eq": 10 } } }
]).execute()
# result will be empty
```
## Comprehensive Example
Here's a full example demonstrating several component queries:
```gdscript
# Setting up entities with components
var entity1 = Entity.new()
entity1.add_component(C_TestC.new(25))
entity1.add_component(C_TestD.new(100))
var entity2 = Entity.new()
entity2.add_component(C_TestC.new(10))
entity2.add_component(C_TestD.new(50))
var entity3 = Entity.new()
entity3.add_component(C_TestC.new(25))
entity3.add_component(C_TestD.new(25))
var entity4 = Entity.new()
entity4.add_component(C_TestC.new(30))
world.add_entity(entity1)
world.add_entity(entity2)
world.add_entity(entity3)
world.add_entity(entity4)
# Query: Entities with C_TestC.value == 25 and C_TestD.points > 50
var result = QueryBuilder.new(world).with_all([
{ C_TestC: { "value": { "_eq": 25 } } },
{ C_TestD: { "points": { "_gt": 50 } } }
]).execute()
# result will include entity1
```
## Conclusion
Component Queries extend the querying capabilities of the GECS framework by allowing you to filter entities based on component data. By utilizing the supported operators and combining component queries with traditional component filters, you can precisely target the entities you need for your game's logic.
For more information on how to use the `QueryBuilder`, refer to the `query_builder.gd` documentation and the test cases in `test_query_builder.gd`.

View File

@@ -0,0 +1,699 @@
# 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](GETTING_STARTED.md)
- 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:
```gdscript
# ❌ 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:
1. **Entities** IDs or "slots" for your game objects
2. **Components** Pure data objects that define state (e.g., velocity, health)
3. **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:**
```gdscript
# 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.
```gdscript
# 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:
1. **Initialization** - Entity added to world, components loaded from `component_resources`
2. **define_components()** - Called to add components via code
3. **on_ready()** - Setup initial states, sync transforms
4. **on_destroy()** - Cleanup before removal
5. **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**: `ClassCase` representing the thing they are
- **File names**: `e_entity_name.gd` using snake_case
**Examples:**
```gdscript
# 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:
```gdscript
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.
```gdscript
# 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:**
```gdscript
# ✅ 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:**
```gdscript
# ❌ 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_ComponentName` in ClassCase
- **File names**: `c_component_name.gd` in snake_case
- **Organization**: Group by purpose in folders
**Examples:**
```gdscript
# 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():**
```gdscript
# 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:**
```gdscript
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():**
```gdscript
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:
```gdscript
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:
```gdscript
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**: `SystemNameSystem` in 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 world
- `process(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:
```gdscript
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:**
```gdscript
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:
```gdscript
# 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:
```gdscript
# Find players or enemies (anything controllable)
q.with_any([C_Player, C_Enemy])
```
**with_none** - Entities must NOT have any of these components:
```gdscript
# 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:
```gdscript
# 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
```gdscript
# 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:**
```gdscript
# 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:**
```gdscript
# 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:**
```gdscript
# 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:**
```gdscript
# 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:
```gdscript
# 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:
```gdscript
# ✅ 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:
```gdscript
# ✅ 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:
1. **Apply these patterns** in your own projects
2. **Experiment with relationships** for complex entity interactions
3. **Design component hierarchies** that support your game's needs
4. **Learn optimization techniques** in [Performance Guide](PERFORMANCE_OPTIMIZATION.md)
5. **Master common patterns** in [Best Practices Guide](BEST_PRACTICES.md)
## 📚 Related Documentation
- **[Getting Started](GETTING_STARTED.md)** - Build your first ECS project
- **[Best Practices](BEST_PRACTICES.md)** - Write maintainable ECS code
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Make your games run fast
- **[Troubleshooting](TROUBLESHOOTING.md)** - 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."_

View File

@@ -0,0 +1,357 @@
# Debug Viewer
> **Real-time debugging and visualization for your ECS projects**
The GECS Debug Viewer provides live inspection of entities, components, systems, and relationships while your game is running. Perfect for understanding entity behavior, optimizing system performance, and debugging complex interactions.
## 📋 Prerequisites
- GECS plugin enabled in your project
- Debug mode enabled: `Project > Project Settings > GECS > Debug Mode`
- Game running from the editor (F5 or F6)
## 🎯 Quick Start
### Opening the Debug Viewer
1. **Run your game** from the Godot editor (F5 for current scene, F6 for main scene)
2. **Open the debugger panel** (bottom of editor, usually appears automatically)
3. **Click the "GECS" tab** next to "Debugger", "Errors", and "Profiler"
> 💡 **Debug Mode Required**: If you see an overlay saying "Debug mode is disabled", go to `Project > Project Settings > GECS` and enable "Debug Mode"
## 🔍 Features Overview
The debug viewer is split into two main panels:
### Systems Panel (Right)
Monitor system execution and performance in real-time.
**Features:**
- **System execution time** - See how long each system takes to process (milliseconds)
- **Entity count** - Number of entities processed per system
- **Active/Inactive status** - Toggle systems on/off at runtime
- **Sortable columns** - Click column headers to sort by name, time, or status
- **Performance metrics** - Archetype count, parallel processing info
**Status Bar:**
- Total system count
- Combined execution time
- Most expensive system highlighted
### Entities Panel (Left)
Inspect individual entities and their components.
**Features:**
- **Entity hierarchy** - See all entities in your world
- **Component data** - View component properties in real-time (WIP)
- **Relationships** - Visualize entity connections and associations
- **Search/filter** - Find entities or components by name
## 🎮 Using the Debug Viewer
### Monitoring System Performance
**Sort by execution time:**
1. Click the **"Time (ms)"** column header in the Systems panel
2. Systems are now sorted by performance (slowest first by default)
3. Click again to reverse the sort order
**Identify bottlenecks:**
- Look for systems with high execution times (> 5ms)
- Check the entity count - more entities = more processing
- Consider optimization strategies from [Performance Optimization](PERFORMANCE_OPTIMIZATION.md)
**Example:**
```
Name Time (ms) Status
PhysicsSystem 8.234 ms ACTIVE ← Bottleneck!
RenderSystem 2.156 ms ACTIVE
AISystem 0.892 ms ACTIVE
```
### Toggling Systems On/Off
**Disable a system at runtime:**
1. Locate the system in the Systems panel
2. Click on the **Status** column (shows "ACTIVE" or "INACTIVE")
3. System immediately stops processing entities
4. Click again to re-enable
**Use cases:**
- Test game behavior without specific systems
- Isolate bugs by disabling systems one at a time
- Temporarily disable expensive systems during debugging
- Verify system dependencies
> ⚠️ **Important**: System state resets when you restart the game. This is a debugging tool, not a save/load feature.
### Inspecting Entities
**View entity components:**
1. Expand an entity in the Entities panel
2. See all attached components (e.g., `C_Health`, `C_Transform`)
3. Expand a component to view its properties
4. Values update in real-time as your game runs
**Example entity structure:**
```
Entity #123 : /root/World/Player
├── C_Health
│ ├── current: 87.5
│ └── maximum: 100.0
├── C_Transform
│ └── position: (15.2, 0.0, 23.8)
└── C_Velocity
└── velocity: (2.5, 0.0, 1.3)
```
### Viewing Relationships
Relationships show how entities are connected to each other.
**Relationship types displayed:**
- **Entity → Entity**: `Relationship: C_ChildOf -> Entity /root/World/Parent`
- **Entity → Component**: `Relationship: C_Damaged -> C_FireDamage`
- **Entity → Archetype**: `Relationship: C_Buff -> Archetype Player`
- **Entity → Wildcard**: `Relationship: C_Damage -> Wildcard`
**Expand relationships to see:**
- Relation component properties
- Target component properties (for component relationships)
- Full relationship metadata
> 💡 **Learn More**: See [Relationships](RELATIONSHIPS.md) for details on creating and querying entity relationships
### Using Search and Filters
**Systems panel:**
- Type in the "Filter Systems" box to find systems by name
- Only matching systems remain visible
**Entities panel:**
- Type in the "Filter Entities" box to search
- Searches entity names, component names, and property names
- Useful for finding specific entities in large worlds
### Multi-Monitor Setup
**Pop-out window:**
1. Click **"Pop Out"** button at the top of the debug viewer
2. Debug viewer moves to a separate window
3. Position on second monitor for permanent visibility
4. Click **"Pop In"** to return to the editor tab
**Benefits:**
- Keep debug info visible while editing scenes
- Monitor performance during gameplay
- Track entity changes without switching panels
### Collapse/Expand Controls
**Quick controls:**
- **Collapse All** / **Expand All** - Manage all entities at once
- **Systems Collapse All** / **Systems Expand All** - Manage all systems at once
- Individual items can be collapsed/expanded by clicking
## 🔧 Common Workflows
### Performance Optimization Workflow
1. **Sort systems by execution time** (click "Time (ms)" header)
2. **Identify slowest system** (top of sorted list)
3. **Expand system details** to see entity count and archetype count
4. **Review system implementation** for optimization opportunities
5. **Apply optimizations** from [Performance Optimization](PERFORMANCE_OPTIMIZATION.md)
6. **Re-run and compare** execution times
### Debugging Workflow
1. **Identify the problematic entity** using search/filter
2. **Expand entity** to view all components
3. **Watch component values** update in real-time
4. **Toggle related systems off/on** to isolate the issue
5. **Check relationships** if entity interactions are involved
6. **Fix the issue** in your code
### Testing System Dependencies
1. **Run your game** from the editor
2. **Disable systems one at a time** using the Status column
3. **Observe game behavior** for each disabled system
4. **Document dependencies** you discover
5. **Design systems to be more independent** if needed
## 📊 Understanding System Metrics
When you expand a system in the Systems panel, you'll see detailed metrics:
**Execution Time (ms):**
- Time spent in the system's `process()` function
- Lower is better (aim for < 1ms for most systems)
- Spikes indicate performance issues
**Entity Count:**
- Number of entities that matched the system's query
- High counts + high execution time = optimization needed
- Zero entities may indicate query issues
**Archetype Count:**
- Number of unique component combinations processed
- Higher counts can impact performance
- See [Performance Optimization](PERFORMANCE_OPTIMIZATION.md#archetype-optimization)
**Parallel Processing:**
- `true` if system uses parallel iteration
- `false` for sequential processing
- Parallel systems can process entities faster
**Subsystem Info:**
- For multi-subsystem systems (advanced feature)
- Shows entity count per subsystem
## ⚠️ Troubleshooting
### Debug Viewer Shows "Debug mode is disabled"
**Solution:**
1. Go to `Project > Project Settings`
2. Navigate to `GECS` category
3. Enable "Debug Mode" checkbox
4. Restart your game
> 💡 **Performance Note**: Debug mode adds overhead. Disable it for production builds.
### No Entities/Systems Appearing
**Possible causes:**
1. Game isn't running - Press F5 or F6 to run from editor
2. World not created - Verify `ECS.world` exists in your code
3. Entities/Systems not added to world - Check `world.add_child()` calls
### Component Properties Not Updating
**Solution:**
- Component properties update when they change
- Properties without `@export` won't be visible
- Make sure your systems are modifying component properties correctly
### Systems Not Toggling
**Possible causes:**
1. System has `paused` property set - Check system code
2. Debugger connection lost - Restart the game
3. System is critical - Some systems might ignore toggle requests
## 🎯 Best Practices
### During Development
**Do:**
- Keep debug viewer open while testing gameplay
- Sort systems by time regularly to catch performance regressions
- Use entity search to track specific entities
- Disable systems to test game behavior
**Don't:**
- Leave debug mode enabled in production builds
- Rely on system toggling for game logic (use proper activation patterns)
- Expect perfect frame timing (debug mode adds overhead)
### For Performance Tuning
1. **Baseline first**: Run game without debug viewer, note FPS
2. **Enable debug viewer**: Identify expensive systems
3. **Focus on top 3**: Optimize the slowest systems first
4. **Measure impact**: Re-check execution times after changes
5. **Disable debug mode**: Always profile final builds without debug overhead
## 🚀 Advanced Tips
### Custom Component Serialization
If your component properties aren't showing up properly:
```gdscript
# Mark properties with @export for debug visibility
class_name C_CustomData
extends Component
@export var visible_property: int = 0 # ✅ Shows in debug viewer
var hidden_property: int = 0 # ❌ Won't appear
```
### Relationship Debugging
Use the debug viewer to verify complex relationship queries:
1. **Create test entities** with relationships
2. **Check relationship display** in Entities panel
3. **Verify relationship properties** are correct
4. **Test relationship queries** in your systems
### Performance Profiling Workflow
Combine debug viewer with Godot's profiler:
1. **Debug Viewer**: Identify slow ECS systems
2. **Godot Profiler**: Deep-dive into specific functions
3. **Fix bottlenecks**: Optimize based on both tools
4. **Verify improvements**: Check both metrics improve
## 📚 Related Documentation
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding entities, components, and systems
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Optimize systems identified as bottlenecks
- **[Relationships](RELATIONSHIPS.md)** - Working with entity relationships
- **[Troubleshooting](TROUBLESHOOTING.md)** - Common issues and solutions
## 💡 Summary
The Debug Viewer is your window into the ECS runtime. Use it to:
- 🔍 Monitor system performance and identify bottlenecks
- 🎮 Inspect entities and components in real-time
- 🔗 Visualize relationships between entities
- Toggle systems on/off for debugging
- 📊 Track entity counts and archetype distribution
> **Pro Tip**: Pop out the debug viewer to a second monitor and leave it visible while developing. You'll catch performance issues and bugs much faster!
---
**Next Steps:**
- Learn about [Performance Optimization](PERFORMANCE_OPTIMIZATION.md) to fix bottlenecks you discover
- Explore [Relationships](RELATIONSHIPS.md) to understand entity connections better
- Check [Troubleshooting](TROUBLESHOOTING.md) if you encounter issues

View File

@@ -0,0 +1,341 @@
# Getting Started with GECS
> **Build your first ECS project in 5 minutes**
This guide will walk you through creating a simple player entity with health and transform components using GECS. By the end, you'll understand the core concepts and have a working example.
## 📋 Prerequisites
- Godot 4.x installed
- Basic GDScript knowledge
- 5 minutes of your time
## ⚡ Step 1: Setup (1 minute)
### Install GECS
1. **Download GECS** and place it in your project's `addons/` folder
2. **Enable the plugin**: Go to `Project > Project Settings > Plugins` and enable "GECS"
3. **Verify setup**: The ECS singleton should be automatically added to AutoLoad
> 💡 **Quick Check**: If you see errors, make sure `ECS` appears in `Project > Project Settings > AutoLoad`
## 🎮 Step 2: Your First Entity (2 minutes)
Entities in GECS extend Godot's `Node` class. You have two options for creating entities:
### **Option A: Scene-based Entities** (For spatial properties)
Use this when you need access to `Node3D` or `Node2D` properties like position, rotation, scale, or want to add visual children (sprites, meshes, etc.).
> ⚠️ **Key Point**: `Entity` extends `Node` (not `Node3D` or `Node2D`), so create a scene with the appropriate spatial node type as the root, then attach your entity script to it.
**Steps:**
1. **Create a new scene** in Godot:
- Click `Scene > New Scene` or press `Ctrl+N`
- Select **"Node3D"** as the root node type (for 3D games) or **"Node2D"** (for 2D games)
- Rename the root node to `Player`
2. **Attach the entity script**:
- With the root node selected, click the "Attach Script" button (📄+ icon)
- Save as `e_player.gd`
3. **Save the scene**:
- Save as `e_player.tscn` in your scenes folder
**File: `e_player.gd`**
```gdscript
# e_player.gd
class_name Player
extends Entity
func on_ready():
# Sync the entity's scene position to the Transform component
if has_component(C_Transform):
var c_trs = get_component(C_Transform) as C_Transform
c_trs.position = self.global_position
```
> 💡 **Use case**: Players, enemies, projectiles, or anything that needs a position in your game world.
### **Option B: Code-based Entities** (Pure data containers)
Use this when you DON'T need spatial properties and just want a pure data container (e.g., game managers, abstract systems, timers).
```gdscript
# Just extend Entity directly
class_name GameManager
extends Entity
# No scene needed - instantiate with GameManager.new()
```
> 💡 **Use case**: Game state managers, quest trackers, inventory systems, or any non-spatial game logic.
---
**For this tutorial**, we'll use **Option A** (scene-based) since we want our player to move around the screen with a position.
## 📦 Step 3: Your First Components (1 minute)
Components hold data. Let's create health and transform components:
**File: `c_health.gd`**
```gdscript
# c_health.gd
class_name C_Health
extends Component
@export var current: float = 100.0
@export var maximum: float = 100.0
func _init(max_health: float = 100.0):
maximum = max_health
current = max_health
```
**File: `c_transform.gd`**
```gdscript
# c_transform.gd
class_name C_Transform
extends Component
@export var position: Vector3 = Vector3.ZERO
func _init(pos: Vector3 = Vector3.ZERO):
position = pos
```
**File: `c_velocity.gd`**
```gdscript
# c_velocity.gd
class_name C_Velocity
extends Component
@export var velocity: Vector3 = Vector3.ZERO
func _init(vel: Vector3 = Vector3.ZERO):
velocity = vel
```
> 💡 **Key Principle**: Components only hold data, never logic. Think of them as data containers.
> ⚠️ **Important Note**: Components `_init` function requires that all arguments have a default value or Godot will crash.
## ⚙️ Step 4: Your First System (1 minute)
Systems contain the logic that operates on entities with specific components. This system moves entities across the screen:
**File: `s_movement.gd`**
```gdscript
# s_movement.gd
class_name MovementSystem
extends System
func query():
# Find all entities that have both transform and velocity
return q.with_all([C_Transform, C_Velocity])
func process(entities: Array[Entity], components: Array, delta: float):
# Process each entity in the array
for entity in entities:
var c_trs = entity.get_component(C_Transform) as C_Transform
var c_velocity = entity.get_component(C_Velocity) as C_Velocity
# Move the entity based on its velocity
c_trs.position += c_velocity.velocity * delta
# Update the actual entity position in the scene
entity.global_position = c_trs.position
# Bounce off screen edges (simple example)
if c_trs.position.x > 10 or c_trs.position.x < -10:
c_velocity.velocity.x *= -1
```
> 💡 **System Logic**: Query finds entities with required components, process() runs the movement logic on each entity every frame.
## 🎬 Step 5: See It Work (1 minute)
Now let's put it all together in a main scene:
### Create Main Scene
1. **Create a new scene** with a `Node` as the root
2. **Add a World node** as a child (Add Child Node > search for "World")
3. **Attach this script** to the root node:
**File: `main.gd`**
```gdscript
# main.gd
extends Node
@onready var world: World = $World
func _ready():
ECS.world = world
# Load and instantiate the player entity scene
var player_scene = preload("res://e_player.tscn") # Adjust path as needed
var e_player = player_scene.instantiate() as Player
# Add components to the entity
e_player.add_components([
C_Health.new(100),
C_Transform.new(),
C_Velocity.new(Vector3(2, 0, 0)) # Move right at 2 units/second
])
add_child(e_player) # Add to scene tree
ECS.world.add_entity(e_player) # Add to ECS world
# Create the movement system
var movement_system = MovementSystem.new()
ECS.world.add_system(movement_system)
func _process(delta):
# Process all systems
if ECS.world:
ECS.process(delta)
```
**Run your project!** 🎉 You now have a working ECS setup where the player entity moves across the screen and bounces off the edges! The MovementSystem updates entity positions based on their velocity components.
> 💡 **Scene-based entities**: Notice we load and instantiate the `e_player.tscn` scene instead of calling `Player.new()`. This is required because we need access to spatial properties (position). For entities that don't need spatial properties, `Entity.new()` works fine.
## 🎯 What You Just Built
Congratulations! You've created your first ECS project with:
- **Entity**: Player - a container for components
- **Components**: C_Health, C_Transform, C_Velocity - pure data containers
- **System**: MovementSystem - logic that moves entities based on velocity
- **World**: Container that manages entities and systems
## 📈 Next Steps
Now that you have the basics working, here's how to level up:
### 1. Create Entity Prefabs (Recommended)
Instead of creating entities in code, use Godot's scene system:
1. **Create a new scene** with your Entity class as the root node
2. **Add visual children** (MeshInstance3D, Sprite3D, etc.)
3. **Add components via define_components()** or `component_resources` array in Inspector
4. **Save as .tscn file** (e.g., `e_player.tscn`)
5. **Load and instantiate** in your main scene
```gdscript
# Improved e_player.gd with define_components()
class_name Player
extends Entity
func define_components() -> Array:
return [
C_Health.new(100),
C_Transform.new(),
C_Velocity.new(Vector3(1, 0, 0)) # Move right slowly
]
func on_ready():
# Sync scene position to component
if has_component(C_Transform):
var c_trs = get_component(C_Transform) as C_Transform
c_trs.position = self.global_position
```
### 2. Organize Your Main Scene
Structure your main scene using the proven scene-based pattern:
```
Main.tscn
├── World (World node)
├── DefaultSystems (instantiated from default_systems.tscn)
│ ├── input (SystemGroup)
│ ├── gameplay (SystemGroup)
│ ├── physics (SystemGroup)
│ └── ui (SystemGroup)
├── Level (Node3D for static environment)
└── Entities (Node3D for spawned entities)
```
**Benefits:**
- **Visual organization** in Godot editor
- **Easy system reordering** between groups
- **Reusable system configurations**
### 3. Learn More Patterns
### 🧠 Understand the Concepts
**→ [Core Concepts Guide](CORE_CONCEPTS.md)** - Deep dive into Entities, Components, Systems, and Relationships
### 🔧 Add More Features
Try adding these to your moving player:
- **Input system** - Add C_Input component and system to control movement with arrow keys
- **Multiple entities** - Create more moving objects with different velocities
- **Collision system** - Add C_Collision component and detect when entities hit each other
- **Gravity system** - Add downward velocity to make entities fall
### 📚 Learn Best Practices
**→ [Best Practices Guide](BEST_PRACTICES.md)** - Write maintainable ECS code
### 🔧 Explore Advanced Features
- **[Component Queries](COMPONENT_QUERIES.md)** - Filter by component property values
- **[Relationships](RELATIONSHIPS.md)** - Link entities together for complex interactions
- **[Observers](OBSERVERS.md)** - Reactive systems that respond to changes
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Make your games run fast
## ❓ Having Issues?
### Player not responding?
- Check that `ECS.process(delta)` is called in `_process()`
- Verify components are added to the entity via `define_components()` or Inspector
- Make sure the system is added to the world
- Ensure transform synchronization is called in entity's `on_ready()`
### Can't access position/rotation properties?
- ⚠️ **Entity extends Node, not Node3D**: To access spatial properties, create a scene with `Node3D` (3D) or `Node2D` (2D) as the root node type
- Attach your entity script (that extends `Entity`) to the Node3D/Node2D root
- Load and instantiate the scene file (don't use `.new()` for spatial entities)
- **If you don't need spatial properties**: Using `Entity.new()` is perfectly fine for pure data containers
- See Step 2 for both entity creation approaches
### Errors in console?
- Check that all classes extend the correct base class
- Verify file names match class names
- Ensure GECS plugin is enabled
**Still stuck?** → [Troubleshooting Guide](TROUBLESHOOTING.md)
## 🏆 What's Next?
You're now ready to build amazing games with GECS! The Entity-Component-System pattern will help you:
- **Scale your game** - Add features without breaking existing code
- **Reuse code** - Components and systems work across different entity types
- **Debug easier** - Clear separation between data and logic
- **Optimize performance** - GECS handles efficient querying for you
**Ready to dive deeper?** Start with [Core Concepts](CORE_CONCEPTS.md) to really understand what makes ECS powerful.
**Need help?** [Join our Discord community](https://discord.gg/eB43XU2tmn) for support and discussions.
---
_"The best way to learn ECS is to build with it. Start simple, then add complexity as you understand the patterns."_

View File

@@ -0,0 +1,351 @@
# Observers in GECS
> **Reactive systems that respond to component changes**
Observers provide a reactive programming model where systems automatically respond to component changes, additions, and removals. This allows for decoupled, event-driven game logic.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Systems](CORE_CONCEPTS.md#systems)
- Observers must be added to the World to function
## 🎯 What are Observers?
Observers are specialized systems that watch for changes to specific components and react immediately when those changes occur. Instead of processing entities every frame, observers only trigger when something actually changes.
**Benefits:**
- **Performance** - Only runs when changes occur, not every frame
- **Decoupling** - Components don't need to know what systems depend on them
- **Reactivity** - Immediate response to state changes
- **Clean Logic** - Separate change-handling logic from regular processing
## 🔧 Observer Structure
Observers extend the `Observer` class and implement key methods:
1. **`watch()`** - Specifies which component to monitor for events (**required** - will crash if not overridden)
2. **`match()`** - Defines a query to filter which entities trigger events (optional - defaults to all entities)
3. **Event Handlers** - Handle specific types of changes
```gdscript
# o_transform.gd
class_name TransformObserver
extends Observer
func watch() -> Resource:
return C_Transform # Watch for transform component changes (REQUIRED)
func on_component_added(entity: Entity, component: Resource):
# Sync component transform to entity when added
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
# Sync component transform to entity when changed
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
```
## 🎮 Observer Event Types
### on_component_added()
Triggered when a watched component is added to an entity:
```gdscript
class_name HealthUIObserver
extends Observer
func watch() -> Resource:
return C_Health
func match():
return q.with_all([C_Health]).with_group("player")
func on_component_added(entity: Entity, component: Resource):
# Create health bar when player gains health component
var health = component as C_Health
# Use call_deferred to avoid timing issues during component changes
call_deferred("create_health_bar", entity, health.maximum)
```
### on_component_changed()
Triggered when a watched component's property changes:
```gdscript
class_name HealthBarObserver
extends Observer
func watch() -> Resource:
return C_Health
func match():
return q.with_all([C_Health]).with_group("player")
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
if property == "current":
var health = component as C_Health
# Update health bar display
call_deferred("update_health_bar", entity, health.current, health.maximum)
```
### on_component_removed()
Triggered when a watched component is removed from an entity:
```gdscript
class_name HealthUIObserver
extends Observer
func watch() -> Resource:
return C_Health
func on_component_removed(entity: Entity, component: Resource):
# Clean up health bar when health component is removed
call_deferred("remove_health_bar", entity)
```
## 💡 Common Observer Patterns
### Transform Synchronization
Keep entity scene transforms in sync with Transform components:
```gdscript
# o_transform.gd
class_name TransformObserver
extends Observer
func watch() -> Resource:
return C_Transform
func on_component_added(entity: Entity, component: Resource):
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
var transform_comp = component as C_Transform
entity.global_transform = transform_comp.transform
```
### Status Effect Visuals
Show visual feedback for status effects:
```gdscript
# o_status_effects.gd
class_name StatusEffectObserver
extends Observer
func watch() -> Resource:
return C_StatusEffect
func on_component_added(entity: Entity, component: Resource):
var status = component as C_StatusEffect
call_deferred("add_status_visual", entity, status.effect_type)
func on_component_removed(entity: Entity, component: Resource):
var status = component as C_StatusEffect
call_deferred("remove_status_visual", entity, status.effect_type)
func add_status_visual(entity: Entity, effect_type: String):
match effect_type:
"poison":
# Add poison particle effect
pass
"shield":
# Add shield visual overlay
pass
func remove_status_visual(entity: Entity, effect_type: String):
# Remove corresponding visual effect
pass
```
### Audio Feedback
Trigger sound effects on component changes:
```gdscript
# o_audio_feedback.gd
class_name AudioFeedbackObserver
extends Observer
func watch() -> Resource:
return C_Health
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
if property == "current":
var health_change = new_value - old_value
if health_change < 0:
# Health decreased - play damage sound
call_deferred("play_damage_sound", entity.global_position)
elif health_change > 0:
# Health increased - play heal sound
call_deferred("play_heal_sound", entity.global_position)
```
## 🏗️ Observer Best Practices
### Naming Conventions
**Observer files and classes:**
- **Class names**: `DescriptiveNameObserver` (TransformObserver, HealthUIObserver)
- **File names**: `o_descriptive_name.gd` (o_transform.gd, o_health_ui.gd)
### Use Deferred Calls
Always use `call_deferred()` to defer work and avoid immediate execution during component updates:
```gdscript
# ✅ Good - Defer work for later execution
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
call_deferred("update_ui_element", entity, new_value)
# ❌ Avoid - Immediate execution can cause issues
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
update_ui_element(entity, new_value) # May cause timing issues
```
### Keep Observer Logic Simple
Focus observers on single responsibilities:
```gdscript
# ✅ Good - Single purpose observer
class_name HealthUIObserver
extends Observer
func watch() -> Resource:
return C_Health
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
if property == "current":
call_deferred("update_health_display", entity, new_value)
# ❌ Avoid - Observer doing too much
class_name HealthObserver
extends Observer
func on_component_changed(entity: Entity, component: Resource, property: String, new_value: Variant, old_value: Variant):
# Too many responsibilities in one observer
update_health_display(entity, new_value)
play_damage_sound(entity)
check_achievements(entity)
save_game_state()
```
### Use Specific Queries
Filter which entities trigger observers with `match()`:
```gdscript
# ✅ Good - Specific query
func match():
return q.with_all([C_Health]).with_group("player") # Only player health
# ❌ Avoid - Too broad
func match():
return q.with_all([C_Health]) # ALL entities with health
```
## 🎯 When to Use Observers
**Use Observers for:**
- UI updates based on game state changes
- Audio/visual effects triggered by state changes
- Immediate response to critical state changes (death, level up)
- Synchronization between components and scene nodes
- Event logging and analytics
**Use Regular Systems for:**
- Continuous processing (movement, physics)
- Frame-by-frame updates
- Complex logic that depends on multiple entities
- Performance-critical processing loops
## 🚀 Adding Observers to the World
Observers must be registered with the World to function. There are several ways to do this:
### Manual Registration
```gdscript
# In your scene or main script
func _ready():
var health_observer = HealthUIObserver.new()
ECS.world.add_observer(health_observer)
# Or add multiple observers at once
ECS.world.add_observers([health_observer, transform_observer, audio_observer])
```
### Automatic Scene Tree Registration
Place Observer nodes in your scene under the systems root (default: "Systems" node), and they'll be automatically registered:
```
Main
├── World
├── Systems/ # Observers placed here are auto-registered
│ ├── HealthUIObserver
│ ├── TransformObserver
│ └── AudioFeedbackObserver
└── Entities/
└── Player
```
### Important Notes:
- Observers are initialized with their own QueryBuilder (`observer.q`)
- The `watch()` method is called during registration to validate the component
- Observers must return a valid Component class from `watch()` or they'll crash
## ⚠️ Common Issues & Troubleshooting
### Observer Not Triggering
**Problem**: Observer events never fire
**Solutions**:
- Ensure the observer is added to the World with `add_observer()`
- Check that `watch()` returns the correct component class
- Verify entities match the `match()` query (if defined)
- Component changes must be on properties, not just internal state
### Crash: "You must override the watch() method"
**Problem**: Observer crashes on registration
**Solution**: Override `watch()` method and return a Component class:
```gdscript
func watch() -> Resource:
return C_Health # Must return actual component class
```
### Events Fire for Wrong Entities
**Problem**: Observer triggers for entities you don't want
**Solution**: Use `match()` to filter entities:
```gdscript
func match():
return q.with_all([C_Health]).with_group("player") # Only players
```
### Property Changes Not Detected
**Problem**: Observer doesn't detect component property changes
**Causes**:
- Direct assignment to properties should work automatically
- Internal object modifications (like Array.append()) may not trigger signals
- Manual signal emission required for complex property changes
## 📚 Related Documentation
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding the ECS fundamentals
- **[Systems](CORE_CONCEPTS.md#systems)** - Regular processing systems
- **[Best Practices](BEST_PRACTICES.md)** - Write maintainable ECS code
---
_"Observers turn your ECS from a polling system into a reactive system, making your game respond intelligently to state changes rather than constantly checking for them."_

View File

@@ -0,0 +1,418 @@
# GECS Performance Optimization Guide
> **Make your ECS games run fast and smooth**
This guide shows you how to optimize your GECS-based games for maximum performance. Learn to identify bottlenecks, optimize queries, and design systems that scale.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Best Practices](BEST_PRACTICES.md)
- A working GECS project to optimize
## 🎯 Performance Fundamentals
### The ECS Performance Model
GECS performance depends on three key factors:
1. **Query Efficiency** - How fast you find entities
2. **Component Access** - How quickly you read/write data
3. **System Design** - How well your logic is organized
Most performance gains come from optimizing these in order of impact.
## 🔍 Profiling Your Game
### Monitor Query Cache Performance
Always profile before optimizing. GECS provides query cache statistics for performance monitoring:
```gdscript
# Main.gd
func _process(delta):
ECS.process(delta)
# Print cache performance stats every second
if Engine.get_process_frames() % 60 == 0:
var cache_stats = ECS.world.get_cache_stats()
print("ECS Performance:")
print(" Query cache hits: ", cache_stats.get("hits", 0))
print(" Query cache misses: ", cache_stats.get("misses", 0))
print(" Total entities: ", ECS.world.entities.size())
# Reset stats for next measurement period
ECS.world.reset_cache_stats()
```
### Use Godot's Built-in Profiler
Monitor your game's performance in the Godot editor:
1. **Run your project** in debug mode
2. **Open the Profiler** (Debug → Profiler)
3. **Look for ECS-related spikes** in the frame time
4. **Identify the slowest systems** in your processing groups
## ⚡ Query Optimization
### 1. Choose the Right Query Method ⭐ NEW!
**As of v5.0.0-rc4**, query performance ranking (10,000 entities):
1. **`.enabled(true/false)` queries**: **~0.05ms** 🏆 **(Fastest - Use when possible!)**
2. **`.with_all([Components])` queries**: **~0.6ms** 🥈 **(Excellent for most use cases)**
3. **`.with_any([Components])` queries**: **~5.6ms** 🥉 **(Good for OR-style queries)**
4. **`.with_group("name")` queries**: **~16ms** 🐌 **(Avoid for performance-critical code)**
**Performance Recommendations:**
```gdscript
# 🏆 FASTEST - Use enabled/disabled queries when you only need active entities
class_name ActiveSystemsOnly extends System
func query():
return q.enabled(true) # Constant-time O(1) performance!
# 🥈 EXCELLENT - Component-based queries (heavily optimized cache)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]) # ~0.6ms for 10K entities
# 🥉 GOOD - Use with_any sparingly, split into multiple systems when possible
class_name DamageableSystem extends System
func query():
return q.with_any([C_Player, C_Enemy]).with_all([C_Health])
# 🐌 AVOID - Group queries are the slowest
class_name PlayerSystem extends System
func query():
return q.with_group("player") # Consider using components instead
# Better: q.with_all([C_Player])
```
### 2. Use Proper System Query Pattern
GECS automatically handles query optimization when you follow the standard pattern:
### 2. Use Proper System Query Pattern
GECS automatically handles query optimization when you follow the standard pattern:
```gdscript
# ✅ Good - Standard GECS pattern (automatically optimized)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]).with_none([C_Frozen])
func process(entities: Array[Entity], components: Array, delta: float):
# Process each entity
for entity in entities:
var pos = entity.get_component(C_Position)
var vel = entity.get_component(C_Velocity)
pos.value += vel.value * delta
```
```gdscript
# ❌ Avoid - Manual query building in process methods
func process(entities: Array[Entity], components: Array, delta: float):
# Don't do this - bypasses automatic query optimization
var custom_entities = ECS.world.query.with_all([C_Position]).execute()
# Process custom_entities...
```
### 3. Optimize Query Specificity
More specific queries run faster:
```gdscript
# ✅ Fast - Use enabled filter for active entities only
class_name PlayerInputSystem extends System
func query():
return q.with_all([C_Input, C_Movement]).enabled(true)
# Super fast enabled filtering + component matching
# ✅ Fast - Specific component query
class_name ProjectileSystem extends System
func query():
return q.with_all([C_Projectile, C_Velocity])
# Only matches projectiles - very specific
```
```gdscript
# ❌ Slow - Overly broad query
class_name UniversalSystem extends System
func query():
return q.with_all([C_Position])
# Matches almost everything in the game!
func process(entities: Array[Entity], components: Array, delta: float):
# Now we need expensive type checking in a loop
for entity in entities:
if entity.has_component(C_Player):
# Handle player...
elif entity.has_component(C_Enemy):
# Handle enemy...
# This defeats the purpose of ECS!
```
### 4. Smart Use of with_any Queries
`with_any` queries are **much faster than before** but still slower than `with_all`. Use strategically:
```gdscript
# ✅ Good - with_any for legitimate OR scenarios
class_name DamageSystem extends System
func query():
return q.with_any([C_Player, C_Enemy, C_NPC]).with_all([C_Health])
# When you truly need "any of these types with health"
# ✅ Better - Split when entities have different behavior
class_name PlayerMovementSystem extends System
func query(): return q.with_all([C_Player, C_Movement])
class_name EnemyMovementSystem extends System
func query(): return q.with_all([C_Enemy, C_Movement])
# Split systems = simpler logic + better performance
```
### 5. Avoid Group Queries for Performance-Critical Code
Group queries are now the slowest option. Use component-based queries instead:
```gdscript
# ❌ Slow - Group-based query (~16ms for 10K entities)
class_name PlayerSystem extends System
func query():
return q.with_group("player")
# ✅ Fast - Component-based query (~0.6ms for 10K entities)
class_name PlayerSystem extends System
func query():
return q.with_all([C_Player])
```
## 🧱 Component Design for Performance
### Keep Components Lightweight
Smaller components = faster memory access:
```gdscript
# ✅ Good - Lightweight components
class_name C_Position extends Component
@export var position: Vector2
class_name C_Velocity extends Component
@export var velocity: Vector2
class_name C_Health extends Component
@export var current: float
@export var maximum: float
```
```gdscript
# ❌ Heavy - Bloated component
class_name MegaComponent extends Component
@export var position: Vector2
@export var velocity: Vector2
@export var health: float
@export var mana: float
@export var inventory: Array[Item] = []
@export var abilities: Array[Ability] = []
@export var dialogue_history: Array[String] = []
# Too much data in one place!
```
### Minimize Component Additions/Removals
Adding and removing components requires index updates. Batch component operations when possible:
```gdscript
# ✅ Good - Batch component operations
func setup_new_enemy(entity: Entity):
# Add multiple components in one batch
entity.add_components([
C_Health.new(),
C_Position.new(),
C_Velocity.new(),
C_Enemy.new()
])
# ✅ Good - Single component change when needed
func apply_damage(entity: Entity, damage: float):
var health = entity.get_component(C_Health)
health.current = clamp(health.current - damage, 0, health.maximum)
if health.current <= 0:
entity.add_component(C_Dead.new()) # Single component addition
```
### Choose Between Boolean Properties vs Components Based on Usage
The choice between boolean properties and separate components depends on how frequently states change and how many entities need them.
#### Use Boolean Properties for Frequently-Changing States
When states change often, boolean properties avoid expensive index updates:
```gdscript
# ✅ Good for frequently-changing states (buffs, status effects, etc.)
class_name C_EntityState extends Component
@export var is_stunned: bool = false
@export var is_invisible: bool = false
@export var is_invulnerable: bool = false
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity, C_EntityState])
# All entities that might need states must have this component
func process(entity: Entity, delta: float):
var state = entity.get_component(C_EntityState)
if state.is_stunned:
return # Just a property check - no index updates
# Process movement...
```
**Tradeoffs:**
- ✅ Fast state changes (no index rebuilds)
- ✅ Simple property checks in systems
- ❌ All entities need the state component (memory overhead)
- ❌ Less precise queries (can't easily find "only stunned entities")
#### Use Separate Components for Rare or Permanent States
When states are long-lasting or infrequent, separate components provide precise queries:
```gdscript
# ✅ Good for rare/permanent states (player vs enemy, permanent abilities)
class_name MovementSystem extends System
func query():
return q.with_all([C_Position, C_Velocity]).with_none([C_Paralyzed])
# Precise query - only entities that can move
# Separate systems can target specific states precisely
class_name ParalyzedSystem extends System
func query():
return q.with_all([C_Paralyzed]) # Only paralyzed entities
```
**Tradeoffs:**
- ✅ Memory efficient (only entities with states have components)
- ✅ Precise queries for specific states
- ❌ State changes trigger expensive index updates
- ❌ Complex queries with multiple exclusions
#### Guidelines:
- **High-frequency changes** (every few frames): Use boolean properties
- **Low-frequency changes** (minutes apart): Use separate components
- **Related states** (buffs/debuffs): Group into property components
- **Distinct entity types** (player/enemy): Use separate components
## ⚙️ System Performance Patterns
### Early Exit Strategies
Return early when no processing is needed:
```gdscript
class_name HealthRegenerationSystem extends System
func process(entities: Array[Entity], components: Array, delta: float):
for entity in entities:
var health = entity.get_component(C_Health)
# Early exits for common cases
if health.current >= health.maximum:
continue # Already at full health
if health.regeneration_rate <= 0:
continue # No regeneration configured
# Only do expensive work when needed
health.current = min(health.current + health.regeneration_rate * delta, health.maximum)
```
### Batch Entity Operations
Group entity operations together:
```gdscript
# ✅ Good - Batch creation
func spawn_enemy_wave():
var enemies: Array[Entity] = []
# Create all entities using entity pooling
for i in range(50):
var enemy = ECS.world.create_entity() # Uses entity pool for performance
setup_enemy_components(enemy)
enemies.append(enemy)
# Add all to world at once
ECS.world.add_entities(enemies)
# ✅ Good - Individual removal (batch removal not available)
func cleanup_dead_entities():
var dead_entities = ECS.world.query.with_all([C_Dead]).execute()
for entity in dead_entities:
ECS.world.remove_entity(entity) # Remove individually
```
## 📊 Performance Targets
### Frame Rate Targets
Aim for these processing times per frame:
- **60 FPS target**: ECS processing < 16ms per frame
- **30 FPS target**: ECS processing < 33ms per frame
- **Mobile target**: ECS processing < 8ms per frame
### Entity Scale Guidelines
GECS handles these entity counts well with proper optimization:
- **Small games**: 100-500 entities
- **Medium games**: 500-2000 entities
- **Large games**: 2000-10000 entities
- **Massive games**: 10000+ entities (requires advanced optimization)
## 🎯 Next Steps
1. **Profile your current game** to establish baseline performance
2. **Apply query optimizations** from this guide
3. **Redesign heavy components** into lighter, focused ones
4. **Implement system improvements** like early exits and batching
5. **Consider advanced techniques** like pooling and spatial partitioning for demanding scenarios
## 🔍 Additional Performance Features
### Entity Pooling
GECS includes built-in entity pooling for optimal performance:
```gdscript
# Use the entity pool for frequent entity creation/destruction
var new_entity = ECS.world.create_entity() # Gets from pool when available
```
### Query Cache Statistics
Monitor query performance with built-in cache tracking:
```gdscript
# Get detailed cache performance data
var stats = ECS.world.get_cache_stats()
print("Cache hit rate: ", stats.get("hits", 0) / (stats.get("hits", 0) + stats.get("misses", 1)))
```
**Need more help?** Check the [Troubleshooting Guide](TROUBLESHOOTING.md) for specific performance issues.
---
_"Fast ECS code isn't about clever tricks - it's about designing systems that naturally align with how the framework works best."_

View File

@@ -0,0 +1,216 @@
# GECS Performance Testing Guide
> **Framework-level performance testing for GECS developers**
This document explains how to run and interpret the GECS performance tests. This is primarily for framework developers and contributors who need to ensure GECS maintains high performance.
**For game developers:** See [Performance Optimization Guide](PERFORMANCE_OPTIMIZATION.md) for optimizing your games.
## 📋 Prerequisites
- GECS framework development environment
- gdUnit4 testing framework
- Godot 4.x
- Test system dependencies: `s_performance_test.gd` and `s_complex_performance_test.gd` in tests/systems/
## 🎯 Overview
The GECS performance test suite provides comprehensive benchmarking for all critical ECS operations:
- **Entity Operations**: Creation, destruction, world management
- **Component Operations**: Addition, removal, lookup, indexing
- **Query Performance**: All query types, caching, complex scenarios
- **System Processing**: Single/multiple systems, different scales
- **Array Operations**: Optimized set operations (intersect, union, difference)
- **Integration Tests**: Realistic game scenarios and stress tests
## 🚀 Running Performance Tests
### Prerequisites
Set the `GODOT_BIN` environment variable to your Godot executable:
```bash
# Windows
setx GODOT_BIN "C:\path\to\godot.exe"
# Linux/Mac
export GODOT_BIN="/path/to/godot"
```
### Running Individual Test Suites
```bash
# Entity performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_entities.gd
# Component performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_components.gd
# Query performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_queries.gd
# System performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_systems.gd
# Array operations performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_arrays.gd
# Integration performance tests
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_integration.gd
```
### Running Complete Performance Suite
```bash
# Run all performance tests with comprehensive reporting
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_master.gd
# Quick smoke test to verify basic performance
addons/gdUnit4/runtest.cmd -a res://addons/gecs/tests/performance/performance_test_master.gd::test_performance_smoke_test
```
## 📊 Test Scales
The performance tests use three different scales:
- **SMALL_SCALE**: 100 entities (for fine-grained testing)
- **MEDIUM_SCALE**: 1,000 entities (for typical game scenarios)
- **LARGE_SCALE**: 10,000 entities (for stress testing)
## ⏱️ Performance Thresholds
The tests include automatic performance threshold checking:
### Entity Operations
- Create 100 entities: < 10ms
- Create 1,000 entities: < 50ms
- Add 1,000 entities to world: < 100ms
### Component Operations
- Add components to 100 entities: < 10ms
- Add components to 1,000 entities: < 75ms
- Component lookup in 1,000 entities: < 30ms
### Query Performance
- Simple query on 100 entities: < 5ms
- Simple query on 1,000 entities: < 20ms
- Simple query on 10,000 entities: < 100ms
- Complex queries: < 50ms
### System Processing
- Process 100 entities: < 5ms
- Process 1,000 entities: < 30ms
- Process 10,000 entities: < 150ms
### Game Loop Performance
- Realistic game frame (1,000 entities): < 16ms (60 FPS target)
## 📈 Understanding Results
### Performance Metrics
Each test provides:
- **Average Time**: Mean execution time across multiple runs
- **Min/Max Time**: Best and worst execution times
- **Standard Deviation**: Consistency of performance
- **Operations/Second**: Throughput measurement
- **Time/Operation**: Per-item processing time
### Result Files
Performance results are saved to `res://reports/` with timestamps:
- `entity_performance_results.json`
- `component_performance_results.json`
- `query_performance_results.json`
- `system_performance_results.json`
- `array_performance_results.json`
- `integration_performance_results.json`
- `complete_performance_results_[timestamp].json`
### Interpreting Results
**Good Performance Indicators:**
- High operations/second (>10,000 for simple operations)
- ✅ Low standard deviation (consistent performance)
- ✅ Linear scaling with entity count
- ✅ Query cache hit rates >80%
**Performance Warning Signs:**
- ⚠️ Tests taking >50ms consistently
- ⚠️ Exponential time scaling with entity count
- ⚠️ High standard deviation (inconsistent performance)
- ⚠️ Cache hit rates <50%
## 🔄 Regression Testing
To monitor performance over time:
1. **Establish Baseline**: Run the complete test suite and save results
2. **Regular Testing**: Run tests after significant changes
3. **Compare Results**: Use the master test suite's regression checking
4. **Set Alerts**: Monitor for >20% performance degradation
## 🎯 Optimization Areas
Based on test results, focus optimization efforts on:
1. **Query Performance**: Most critical for gameplay
2. **Component Operations**: High frequency operations
3. **Array Operations**: Core performance building blocks
4. **System Processing**: Frame-rate critical
5. **Memory Usage**: Large-scale scenarios
## ⚠️ Common Issues
### Missing Dependencies
If tests fail with missing class errors, ensure these files exist:
- `addons/gecs/tests/systems/s_performance_test.gd`
- `addons/gecs/tests/systems/s_complex_performance_test.gd`
### gdUnit4 Setup
Beyond setting `GODOT_BIN`, ensure:
- gdUnit4 plugin is enabled in project settings
- All test component classes are properly defined
## 🔧 Custom Performance Tests
To create custom performance tests:
1. Extend `PerformanceTestBase`
2. Use the `benchmark()` method for timing
3. Set appropriate performance thresholds
4. Include in the master test suite
Example:
```gdscript
extends PerformanceTestBase
func test_my_custom_operation():
var my_test = func():
# Your operation here
pass
benchmark("My_Custom_Test", my_test)
assert_performance_threshold("My_Custom_Test", 10.0, "Custom operation too slow")
```
## 📚 Related Documentation
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - User-focused optimization guide
- **[Best Practices](BEST_PRACTICES.md)** - Write performant ECS code
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding the ECS architecture
---
_This performance testing framework ensures GECS maintains high performance as the codebase evolves. It's a critical tool for framework development and optimization efforts._

View File

@@ -0,0 +1,893 @@
# Relationships in GECS
> **Link entities together for complex game interactions**
Relationships allow you to connect entities in meaningful ways, creating dynamic associations that go beyond simple component data. This guide shows you how to use GECS's relationship system to build complex game mechanics.
## 📋 Prerequisites
- Understanding of [Core Concepts](CORE_CONCEPTS.md)
- Familiarity with [Query System](CORE_CONCEPTS.md#query-system)
## 🔗 What are Relationships?
Think of **components** as the data that makes up an entity's state, and **relationships** as the links that connect entities to other entities, components, or types. Relationships can be simple links or carry data about the connection itself.
In GECS, relationships consist of three parts:
- **Source** - Entity that has the relationship (e.g., Bob)
- **Relation** - Component defining the relationship type (e.g., "Likes", "Damaged")
- **Target** - What is being related to: Entity, Component instance, or archetype (e.g., Alice, FireDamage component, Enemy class)
## 🎯 Relationship Types
GECS supports three powerful relationship patterns:
### 1. **Entity Relationships**
Link entities to other entities:
```gdscript
# Bob likes Alice (entity to entity)
e_bob.add_relationship(Relationship.new(C_Likes.new(), e_alice))
```
### 2. **Component Relationships**
Link entities to component instances for type hierarchies:
```gdscript
# Entity has fire damage (entity to component)
entity.add_relationship(Relationship.new(C_Damaged.new(), C_FireDamage.new(50)))
```
### 3. **Archetype Relationships**
Link entities to classes/types:
```gdscript
# Heather likes all food (entity to type)
e_heather.add_relationship(Relationship.new(C_Likes.new(), Food))
```
This creates powerful queries like "find all entities that like Alice", "find all entities with fire damage", or "find all entities damaged by anything".
## 🎯 Core Relationship Concepts
### Relationship Components
Relationships use components to define their type and can carry data:
```gdscript
# c_likes.gd - Simple relationship
class_name C_Likes
extends Component
# c_loves.gd - Another simple relationship
class_name C_Loves
extends Component
# c_eats.gd - Relationship with data
class_name C_Eats
extends Component
@export var quantity: int = 1
func _init(qty: int = 1):
quantity = qty
```
### Creating Relationships
```gdscript
# Create entities
var e_bob = Entity.new()
var e_alice = Entity.new()
var e_heather = Entity.new()
var e_apple = Food.new()
# Add to world
ECS.world.add_entity(e_bob)
ECS.world.add_entity(e_alice)
ECS.world.add_entity(e_heather)
ECS.world.add_entity(e_apple)
# Create 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
# Remove relationships
e_alice.remove_relationship(Relationship.new(C_Loves.new(), e_heather)) # alice no longer loves heather
# Remove with limits (NEW)
e_player.remove_relationship(Relationship.new(C_Poison.new(), null), 1) # Remove only 1 poison stack
e_enemy.remove_relationship(Relationship.new(C_Buff.new(), null), 3) # Remove up to 3 buffs
e_hero.remove_relationship(Relationship.new(C_Damage.new(), null), -1) # Remove all damage (default)
```
## 🔍 Relationship Queries
### Basic Relationship Queries
**Query for Specific Relationships:**
```gdscript
# Any entity that likes alice (type matching)
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), e_alice)])
# Any entity that eats apples (type matching)
ECS.world.query.with_relationship([Relationship.new(C_Eats.new(), e_apple)])
# Any entity that eats 5 or more apples (component query)
ECS.world.query.with_relationship([
Relationship.new({C_Eats: {'quantity': {"_gte": 5}}}, e_apple)
])
# Any entity that likes the Food entity type
ECS.world.query.with_relationship([Relationship.new(C_Likes.new(), Food)])
```
**Exclude Relationships:**
```gdscript
# Entities with any relation toward heather that don't like bob
ECS.world.query
.with_relationship([Relationship.new(ECS.wildcard, e_heather)])
.without_relationship([Relationship.new(C_Likes.new(), e_bob)])
```
### Wildcard Relationships
Use `ECS.wildcard` (or `null`) to query for any relation or target:
```gdscript
# 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)])
ECS.world.query.with_relationship([Relationship.new(C_Likes.new())]) # Omitting target = wildcard
# Any entity with any relation to Enemy entity type
ECS.world.query.with_relationship([Relationship.new(ECS.wildcard, Enemy)])
```
### Component-Based Relationships
Link entities to **component instances** for powerful type hierarchies and data systems:
```gdscript
# Damage system using component targets
class_name C_Damaged extends Component
class_name C_FireDamage extends Component
@export var amount: int = 0
func _init(dmg: int = 0): amount = dmg
class_name C_PoisonDamage extends Component
@export var amount: int = 0
func _init(dmg: int = 0): amount = dmg
# Entity has multiple damage types
entity.add_relationship(Relationship.new(C_Damaged.new(), C_FireDamage.new(50)))
entity.add_relationship(Relationship.new(C_Damaged.new(), C_PoisonDamage.new(25)))
# Query for entities with any damage type (wildcard)
var damaged_entities = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), null)
]).execute()
# Query for entities with fire damage >= 50 using component query
var high_fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), {C_FireDamage: {"amount": {"_gte": 50}}})
]).execute()
# Query for entities with any fire damage (type matching)
var any_fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), C_FireDamage)
]).execute()
```
### Matching Modes
GECS relationships support two matching modes:
#### Type Matching (Default)
Matches relationships by component type, ignoring property values:
```gdscript
# Matches any C_Damaged relationship regardless of amount
entity.has_relationship(Relationship.new(C_Damaged.new(), target))
# Matches any fire damage effect by type
entity.has_relationship(Relationship.new(C_Damaged.new(), C_FireDamage.new()))
# Query for any entities with fire damage (type matching)
var any_fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_Damaged.new(), C_FireDamage)
]).execute()
```
#### Component Query Matching
Match relationships by specific property criteria using dictionaries:
```gdscript
# Match C_Damaged relationships where amount >= 50
var high_damage = ECS.world.query.with_relationship([
Relationship.new({C_Damaged: {'amount': {"_gte": 50}}}, target)
]).execute()
# Match fire damage with specific duration
var lasting_fire = ECS.world.query.with_relationship([
Relationship.new(
C_Damaged.new(),
{C_FireDamage: {'duration': {"_gt": 5.0}}}
)
]).execute()
# Match both relation AND target with queries
var strong_buffs = ECS.world.query.with_relationship([
Relationship.new(
{C_Buff: {'duration': {"_gt": 10}}},
{C_Player: {'level': {"_gte": 5}}}
)
]).execute()
```
**When to Use Each:**
- **Type Matching**: Find entities with "any fire damage", "any buff of this type"
- **Component Queries**: Find entities with exact damage amounts, specific buff durations, or property criteria
### Component Queries in Relationships
Query relationships by specific property values using dictionaries:
```gdscript
# Query by relation property
var heavy_eaters = ECS.world.query.with_relationship([
Relationship.new({C_Eats: {'amount': {"_gte": 5}}}, e_apple)
]).execute()
# Query by target component property
var high_hp_targets = ECS.world.query.with_relationship([
Relationship.new(C_Targeting.new(), {C_Health: {'hp': {"_gte": 100}}})
]).execute()
# Query operators: _eq, _ne, _gt, _lt, _gte, _lte, _in, _nin, func
var special_damage = ECS.world.query.with_relationship([
Relationship.new(
{C_Damage: {'type': {"_in": ["fire", "ice"]}}},
null
)
]).execute()
# Complex multi-property queries
var critical_effects = ECS.world.query.with_relationship([
Relationship.new(
{C_Effect: {
'damage': {"_gt": 20},
'duration': {"_gte": 10.0},
'type': {"_eq": "critical"}
}},
null
)
]).execute()
```
### Reverse Relationships
Find entities that are the **target** of relationships:
```gdscript
# Find entities that are being liked by someone
ECS.world.query.with_reverse_relationship([Relationship.new(C_Likes.new(), ECS.wildcard)])
# Find entities being attacked
ECS.world.query.with_reverse_relationship([Relationship.new(C_IsAttacking.new())])
# Find food being eaten
ECS.world.query.with_reverse_relationship([Relationship.new(C_Eats.new(), ECS.wildcard)])
```
## 🎛️ Limited Relationship Removal
> **Control exactly how many relationships to remove for fine-grained management**
The `remove_relationship()` method now supports a **limit parameter** that allows you to control exactly how many matching relationships to remove. This is essential for stack-based systems, partial healing, inventory management, and fine-grained effect control.
### Basic Syntax
```gdscript
entity.remove_relationship(relationship, limit)
```
**Limit Values:**
- `limit = -1` (default): Remove **all** matching relationships
- `limit = 0`: Remove **no** relationships (useful for testing/validation)
- `limit = 1`: Remove **one** matching relationship
- `limit > 1`: Remove **up to that many** matching relationships
### Core Use Cases
#### 1. **Stack-Based Systems**
Perfect for buff/debuff stacks, damage over time effects, or any system where effects can stack:
```gdscript
# Poison stack system
class_name C_PoisonStack extends Component
@export var damage_per_tick: float = 5.0
# Apply poison stacks
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null))
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null))
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null))
entity.add_relationship(Relationship.new(C_PoisonStack.new(3.0), null)) # 4 poison stacks
# Antidote removes 2 poison stacks
entity.remove_relationship(Relationship.new(C_PoisonStack.new(), null), 2)
# Entity now has 2 poison stacks remaining
# Strong antidote removes all poison
entity.remove_relationship(Relationship.new(C_PoisonStack.new(), null)) # Default: remove all
```
#### 2. **Partial Healing Systems**
Control damage removal for gradual healing or partial repair:
```gdscript
# Multiple damage sources on entity
entity.add_relationship(Relationship.new(C_Damage.new(), C_FireDamage.new(25)))
entity.add_relationship(Relationship.new(C_Damage.new(), C_FireDamage.new(15)))
entity.add_relationship(Relationship.new(C_Damage.new(), C_SlashDamage.new(30)))
entity.add_relationship(Relationship.new(C_Damage.new(), C_PoisonDamage.new(10)))
# Healing potion removes one damage source
entity.remove_relationship(Relationship.new(C_Damage.new(), null), 1)
# Fire resistance removes only fire damage (up to 2 sources)
entity.remove_relationship(Relationship.new(C_Damage.new(), C_FireDamage), 2)
# Full heal removes all damage
entity.remove_relationship(Relationship.new(C_Damage.new(), null)) # All damage gone
```
#### 3. **Inventory and Resource Management**
Handle item stacks, resource consumption, and crafting materials:
```gdscript
# Item stack system
class_name C_HasItem extends Component
class_name C_HealthPotion extends Component
@export var healing_amount: int = 50
# Player has multiple health potions
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
entity.add_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion.new(50)))
# Use one health potion
entity.remove_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion), 1)
# Vendor buys 2 health potions
entity.remove_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion), 2)
# Drop all potions
entity.remove_relationship(Relationship.new(C_HasItem.new(), C_HealthPotion))
```
#### 4. **Buff/Debuff Management**
Fine-grained control over temporary effects:
```gdscript
# Multiple speed buffs from different sources
entity.add_relationship(Relationship.new(C_Buff.new(), C_SpeedBuff.new(1.2, 10.0))) # Boots
entity.add_relationship(Relationship.new(C_Buff.new(), C_SpeedBuff.new(1.5, 5.0))) # Spell
entity.add_relationship(Relationship.new(C_Buff.new(), C_SpeedBuff.new(1.1, 30.0))) # Passive
# Dispel magic removes one buff
entity.remove_relationship(Relationship.new(C_Buff.new(), null), 1)
# Mass dispel removes up to 3 buffs
entity.remove_relationship(Relationship.new(C_Buff.new(), null), 3)
# Purge removes all buffs
entity.remove_relationship(Relationship.new(C_Buff.new(), null))
```
### Advanced Examples
#### Component Query + Limit Combination
Combine component queries with limits for precise control:
```gdscript
# Remove only high-damage effects (damage > 20), up to 2 of them
entity.remove_relationship(
Relationship.new({C_Damage: {"amount": {"_gt": 20}}}, null),
2
)
# Remove poison effects with duration < 5 seconds, limit to 1
entity.remove_relationship(
Relationship.new({C_PoisonEffect: {"duration": {"_lt": 5.0}}}, null),
1
)
# Remove fire damage with specific amount range, up to 3 instances
entity.remove_relationship(
Relationship.new(
C_Damage.new(),
{C_FireDamage: {"amount": {"_gte": 10, "_lte": 50}}}
),
3
)
# Remove all fire damage regardless of amount (no limit, type matching)
entity.remove_relationship(
Relationship.new(C_Damage.new(), C_FireDamage),
-1
)
# Remove buffs with specific multiplier, limit to 2
entity.remove_relationship(
Relationship.new({C_Buff: {"multiplier": {"_gte": 1.5}}}, null),
2
)
```
#### System Integration
Integrate limited removal into your game systems:
```gdscript
class_name HealingSystem extends System
func heal_entity(entity: Entity, healing_power: int):
"""Remove damage based on healing power"""
if healing_power <= 0:
return
# Partial healing - remove damage effects based on healing power
var damage_to_remove = min(healing_power, get_damage_count(entity))
entity.remove_relationship(Relationship.new(C_Damage.new(), null), damage_to_remove)
print("Healed ", damage_to_remove, " damage effects")
func get_damage_count(entity: Entity) -> int:
return entity.get_relationships(Relationship.new(C_Damage.new(), null)).size()
class_name CleanseSystem extends System
func cleanse_entity(entity: Entity, cleanse_strength: int):
"""Remove debuffs based on cleanse strength"""
match cleanse_strength:
1: # Weak cleanse
entity.remove_relationship(Relationship.new(C_Debuff.new(), null), 1)
2: # Medium cleanse
entity.remove_relationship(Relationship.new(C_Debuff.new(), null), 3)
3: # Strong cleanse
entity.remove_relationship(Relationship.new(C_Debuff.new(), null)) # All debuffs
class_name CraftingSystem extends System
func consume_materials(entity: Entity, recipe: Dictionary):
"""Consume specific amounts of crafting materials"""
for material_type in recipe:
var amount_needed = recipe[material_type]
entity.remove_relationship(
Relationship.new(C_HasMaterial.new(), material_type),
amount_needed
)
```
### Error Handling and Validation
The limit parameter provides built-in safeguards:
```gdscript
# Safe operations - won't crash if fewer relationships exist than requested
entity.remove_relationship(Relationship.new(C_Buff.new(), null), 100) # Removes all available, won't error
# Validation operations
entity.remove_relationship(Relationship.new(C_Damage.new(), null), 0) # Removes nothing - useful for testing
# Check before removal
var damage_count = entity.get_relationships(Relationship.new(C_Damage.new(), null)).size()
if damage_count > 0:
entity.remove_relationship(Relationship.new(C_Damage.new(), null), min(3, damage_count))
```
### Performance Considerations
Limited removal is optimized for efficiency:
```gdscript
# ✅ Efficient - stops searching after finding enough matches
entity.remove_relationship(Relationship.new(C_Effect.new(), null), 5)
# ✅ Still efficient - reuses the same removal logic
entity.remove_relationship(Relationship.new(C_Effect.new(), null), -1) # Remove all
# ✅ Most efficient for single removals
entity.remove_relationship(Relationship.new(C_SpecificEffect.new(exact_data), target), 1)
```
### Integration with Multiple Relationships
Works seamlessly with `remove_relationships()` for batch operations:
```gdscript
# Apply limit to multiple relationship types
var relationships_to_remove = [
Relationship.new(C_Buff.new(), null),
Relationship.new(C_Debuff.new(), null),
Relationship.new(C_TemporaryEffect.new(), null)
]
# Remove up to 2 of each type
entity.remove_relationships(relationships_to_remove, 2)
```
## 🎮 Game Examples
### Status Effect System with Component Relationships
This example shows how to build a flexible status effect system using component-based relationships:
```gdscript
# Status effect marker
class_name C_HasEffect extends Component
# Damage type components
class_name C_FireDamage extends Component
@export var damage_per_second: float = 10.0
@export var duration: float = 5.0
func _init(dps: float = 10.0, dur: float = 5.0):
damage_per_second = dps
duration = dur
class_name C_PoisonDamage extends Component
@export var damage_per_tick: float = 5.0
@export var ticks_remaining: int = 10
func _init(dpt: float = 5.0, ticks: int = 10):
damage_per_tick = dpt
ticks_remaining = ticks
# Buff type components
class_name C_SpeedBuff extends Component
@export var multiplier: float = 1.5
@export var duration: float = 10.0
func _init(mult: float = 1.5, dur: float = 10.0):
multiplier = mult
duration = dur
class_name C_StrengthBuff extends Component
@export var bonus_damage: float = 25.0
@export var duration: float = 8.0
func _init(bonus: float = 25.0, dur: float = 8.0):
bonus_damage = bonus
duration = dur
# Apply various effects to entities
func apply_status_effects():
# Player gets fire damage and speed buff
player.add_relationship(Relationship.new(C_HasEffect.new(), C_FireDamage.new(15.0, 8.0)))
player.add_relationship(Relationship.new(C_HasEffect.new(), C_SpeedBuff.new(2.0, 12.0)))
# Enemy gets poison and strength buff
enemy.add_relationship(Relationship.new(C_HasEffect.new(), C_PoisonDamage.new(8.0, 15)))
enemy.add_relationship(Relationship.new(C_HasEffect.new(), C_StrengthBuff.new(30.0, 10.0)))
# Status effect processing system
class_name StatusEffectSystem extends System
func query():
# Get all entities with any status effects
return ECS.world.query.with_relationship([Relationship.new(C_HasEffect.new(), null)])
func process_fire_damage():
# Find entities with any fire damage effect (type matching)
var fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_FireDamage)
]).execute()
for entity in fire_damaged:
# Get the actual fire damage data using type matching
var fire_rel = entity.get_relationship(
Relationship.new(C_HasEffect.new(), C_FireDamage.new())
)
var fire_damage = fire_rel.target as C_FireDamage
# Apply damage
apply_damage(entity, fire_damage.damage_per_second * delta)
# Reduce duration
fire_damage.duration -= delta
if fire_damage.duration <= 0:
entity.remove_relationship(fire_rel)
func process_speed_buffs():
# Find entities with speed buffs using type matching
var speed_buffed = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_SpeedBuff)
]).execute()
for entity in speed_buffed:
# Get actual speed buff data using type matching
var speed_rel = entity.get_relationship(
Relationship.new(C_HasEffect.new(), C_SpeedBuff.new())
)
var speed_buff = speed_rel.target as C_SpeedBuff
# Apply speed modification
apply_speed_modifier(entity, speed_buff.multiplier)
# Handle duration
speed_buff.duration -= delta
if speed_buff.duration <= 0:
entity.remove_relationship(speed_rel)
func remove_all_effects_from_entity(entity: Entity):
# Remove all status effects using wildcard
entity.remove_relationship(Relationship.new(C_HasEffect.new(), null))
func remove_some_effects_from_entity(entity: Entity, count: int):
# Remove a specific number of status effects using limit parameter
entity.remove_relationship(Relationship.new(C_HasEffect.new(), null), count)
func cleanse_one_debuff(entity: Entity):
# Remove just one debuff (useful for cleanse spells)
entity.remove_relationship(Relationship.new(C_Debuff.new(), null), 1)
func dispel_magic(entity: Entity, power: int):
# Dispel magic spell removes buffs based on power level
match power:
1: entity.remove_relationship(Relationship.new(C_HasEffect.new(), C_SpeedBuff), 1) # Weak dispel - 1 speed buff
2: entity.remove_relationship(Relationship.new(C_HasEffect.new(), null), 2) # Medium dispel - 2 any effects
3: entity.remove_relationship(Relationship.new(C_HasEffect.new(), null)) # Strong dispel - all effects
func antidote_healing(entity: Entity, antidote_strength: int):
# Antidote removes poison effects based on strength
entity.remove_relationship(Relationship.new(C_HasEffect.new(), C_PoisonDamage), antidote_strength)
func partial_fire_immunity(entity: Entity):
# Fire immunity spell removes up to 3 fire damage effects
entity.remove_relationship(Relationship.new(C_HasEffect.new(), C_FireDamage), 3)
func get_entities_with_damage_effects():
# Get entities with any damage type effect (fire or poison)
var fire_damaged = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_FireDamage)
]).execute()
var poison_damaged = ECS.world.query.with_relationship([
Relationship.new(C_HasEffect.new(), C_PoisonDamage)
]).execute()
# Combine results
var all_damaged = {}
for entity in fire_damaged:
all_damaged[entity] = true
for entity in poison_damaged:
all_damaged[entity] = true
return all_damaged.keys()
```
### Combat System with Relationships
```gdscript
# Combat relationship components
class_name C_IsAttacking extends Component
@export var damage: float = 10.0
class_name C_IsTargeting extends Component
class_name C_IsAlliedWith extends Component
# Create combat entities
var player = Player.new()
var enemy1 = Enemy.new()
var enemy2 = Enemy.new()
var ally = Ally.new()
# Setup relationships
enemy1.add_relationship(Relationship.new(C_IsAttacking.new(25.0), player))
enemy2.add_relationship(Relationship.new(C_IsTargeting.new(), player))
player.add_relationship(Relationship.new(C_IsAlliedWith.new(), ally))
# Combat system queries
class_name CombatSystem extends System
func get_entities_attacking_player():
var player = get_player_entity()
return ECS.world.query.with_relationship([
Relationship.new(C_IsAttacking.new(), player)
]).execute()
func get_high_damage_attackers():
var player = get_player_entity()
# Find entities attacking player with damage >= 20
return ECS.world.query.with_relationship([
Relationship.new({C_IsAttacking: {'damage': {"_gte": 20.0}}}, player)
]).execute()
func get_player_allies():
var player = get_player_entity()
return ECS.world.query.with_reverse_relationship([
Relationship.new(C_IsAlliedWith.new(), player)
]).execute()
```
### Hierarchical Entity System
```gdscript
# Hierarchy relationship components
class_name C_ParentOf extends Component
class_name C_ChildOf extends Component
class_name C_OwnerOf extends Component
# Create hierarchy
var parent = Entity.new()
var child1 = Entity.new()
var child2 = Entity.new()
var weapon = Weapon.new()
# Setup parent-child relationships
parent.add_relationship(Relationship.new(C_ParentOf.new(), child1))
parent.add_relationship(Relationship.new(C_ParentOf.new(), child2))
child1.add_relationship(Relationship.new(C_ChildOf.new(), parent))
child2.add_relationship(Relationship.new(C_ChildOf.new(), parent))
# Setup ownership
child1.add_relationship(Relationship.new(C_OwnerOf.new(), weapon))
# Hierarchy system queries
class_name HierarchySystem extends System
func get_children_of_entity(entity: Entity):
return ECS.world.query.with_relationship([
Relationship.new(C_ParentOf.new(), entity)
]).execute()
func get_parent_of_entity(entity: Entity):
return ECS.world.query.with_reverse_relationship([
Relationship.new(C_ParentOf.new(), entity)
]).execute()
```
## 🏗️ Relationship Best Practices
### Performance Optimization
**Reuse Relationship Objects:**
```gdscript
# ✅ Good - Reuse for performance
var r_likes_apples = Relationship.new(C_Likes.new(), e_apple)
var r_attacking_players = Relationship.new(C_IsAttacking.new(), Player)
# Use the same relationship object multiple times
entity1.add_relationship(r_attacking_players)
entity2.add_relationship(r_attacking_players)
```
**Static Relationship Factory (Recommended):**
```gdscript
# ✅ Excellent - Organized relationship management
class_name Relationships
static func attacking_players():
return Relationship.new(C_IsAttacking.new(), Player)
static func attacking_anything():
return Relationship.new(C_IsAttacking.new(), ECS.wildcard)
static func chasing_players():
return Relationship.new(C_IsChasing.new(), Player)
static func interacting_with_anything():
return Relationship.new(C_Interacting.new(), ECS.wildcard)
static func equipped_on_anything():
return Relationship.new(C_EquippedOn.new(), ECS.wildcard)
static func any_status_effect():
return Relationship.new(C_HasEffect.new(), null)
static func any_damage_effect():
return Relationship.new(C_Damage.new(), null)
static func any_buff():
return Relationship.new(C_Buff.new(), null)
# Usage in systems:
var attackers = ECS.world.query.with_relationship([Relationships.attacking_players()]).execute()
var chasers = ECS.world.query.with_relationship([Relationships.chasing_anything()]).execute()
# Usage with limits:
entity.remove_relationship(Relationships.any_status_effect(), 1) # Remove one effect
entity.remove_relationship(Relationships.any_damage_effect(), 3) # Remove up to 3 damage effects
entity.remove_relationship(Relationships.any_buff()) # Remove all buffs
```
**Limited Removal Best Practices:**
```gdscript
# ✅ Good - Clear intent with descriptive variables
var WEAK_CLEANSE = 1
var MEDIUM_CLEANSE = 3
var STRONG_CLEANSE = -1 # All
entity.remove_relationship(Relationships.any_debuff(), WEAK_CLEANSE)
# ✅ Good - Helper functions for common operations
func remove_one_poison(entity: Entity):
entity.remove_relationship(Relationship.new(C_Poison.new(), null), 1)
func remove_all_fire_damage(entity: Entity):
entity.remove_relationship(Relationship.new(C_Damage.new(), C_FireDamage))
func partial_heal(entity: Entity, healing_power: int):
entity.remove_relationship(Relationship.new(C_Damage.new(), null), healing_power)
# ✅ Excellent - Validation before removal
func safe_remove_effects(entity: Entity, count: int):
var current_effects = entity.get_relationships(Relationship.new(C_Effect.new(), null)).size()
var to_remove = min(count, current_effects)
if to_remove > 0:
entity.remove_relationship(Relationship.new(C_Effect.new(), null), to_remove)
print("Removed ", to_remove, " effects")
```
### Naming Conventions
**Relationship Components:**
- Use descriptive names that clearly indicate the relationship
- Follow the `C_VerbNoun` pattern when possible
- Examples: `C_Likes`, `C_IsAttacking`, `C_OwnerOf`, `C_MemberOf`
**Relationship Variables:**
- Use `r_` prefix for relationship instances
- Examples: `r_likes_alice`, `r_attacking_player`, `r_parent_of_child`
## 🎯 Next Steps
Now that you understand relationships, component queries, and limited removal:
1. **Design relationship schemas** for your game's entities
2. **Experiment with wildcard queries** for dynamic systems
3. **Use component queries** to filter relationships by property criteria
4. **Implement limited removal** for stack-based and graduated systems
5. **Combine type matching with component queries** for flexible filtering
6. **Optimize with static relationship factories** for better performance
7. **Use limit parameters** for fine-grained control in healing, crafting, and effect systems
8. **Learn advanced patterns** in [Best Practices Guide](BEST_PRACTICES.md)
**Quick Start Checklist for Component Queries:**
- ✅ Try basic component query: `Relationship.new({C_Damage: {'amount': {"_gt": 10}}}, null)`
- ✅ Use query operators: `_eq`, `_ne`, `_gt`, `_lt`, `_gte`, `_lte`, `_in`, `_nin`
- ✅ Query both relation and target properties
- ✅ Combine queries with wildcards for flexible filtering
- ✅ Use type matching for "any component of this type" queries
**Quick Start Checklist for Limited Removal:**
- ✅ Try basic limit syntax: `entity.remove_relationship(rel, 1)`
- ✅ Build a simple stack system (buffs, debuffs, or damage)
- ✅ Create helper functions for common removal patterns
- ✅ Integrate limits into your game systems (healing, cleansing, etc.)
- ✅ Test edge cases (limit > available relationships)
- ✅ Combine component queries with limits for precise control
## 📚 Related Documentation
- **[Core Concepts](CORE_CONCEPTS.md)** - Understanding the ECS fundamentals
- **[Component Queries](COMPONENT_QUERIES.md)** - Advanced property-based filtering
- **[Best Practices](BEST_PRACTICES.md)** - Write maintainable ECS code
- **[Performance Optimization](PERFORMANCE_OPTIMIZATION.md)** - Optimize relationship queries
---
_"Relationships turn a collection of entities into a living, interconnected game world where entities can react to each other in meaningful ways."_

View File

@@ -0,0 +1,218 @@
# GECS Serialization
The GECS framework provides a robust serialization system using Godot's native resource format, enabling persistent game states, save systems, and level data management.
## Quick Start
### Basic Save/Load
```gdscript
# Save entities with persistent components
var query = ECS.world.query.with_all([C_Persistent])
var data = ECS.serialize(query)
ECS.save(data, "user://savegame.tres")
# Load entities back
var entities = ECS.deserialize("user://savegame.tres")
for entity in entities:
ECS.world.add_entity(entity)
```
### Binary Format
```gdscript
# Save as binary for production (smaller files)
ECS.save(data, "user://savegame.tres", true) # Creates .res file
# Load auto-detects format (tries .res first, then .tres)
var entities = ECS.deserialize("user://savegame.tres")
```
## API Reference
### ECS.serialize(query: QueryBuilder) -> GecsData
Converts entities matching a query into serializable data.
**Example:**
```gdscript
# Serialize specific entities
var player_query = ECS.world.query.with_all([C_Player, C_Health])
var save_data = ECS.serialize(player_query)
```
### ECS.save(data: GecsData, filepath: String, binary: bool = false) -> bool
Saves data to disk. Returns `true` on success.
**Parameters:**
- `data`: Serialized entity data
- `filepath`: Save location (use `.tres` extension)
- `binary`: If `true`, saves as `.res` (smaller, faster loading)
### ECS.deserialize(filepath: String) -> Array[Entity]
Loads entities from file. Returns empty array if file doesn't exist.
**Auto-detection:** Tries binary `.res` first, falls back to text `.tres`.
## Component Serialization
Only `@export` variables are serialized:
```gdscript
class_name C_PlayerData
extends Component
@export var health: float = 100.0 # ✅ Saved
@export var inventory: Array[String] # ✅ Saved
@export var position: Vector2 # ✅ Saved
var _cache: Dictionary = {} # ❌ Not saved
```
**Supported types:** All Godot built-ins (int, float, String, Vector2/3, Color, Array, Dictionary, etc.)
## Use Cases
### Save Game System
```gdscript
func save_game(slot: String):
var query = ECS.world.query.with_all([C_Persistent])
var data = ECS.serialize(query)
if ECS.save(data, "user://saves/slot_%s.tres" % slot, true):
print("Game saved!")
func load_game(slot: String):
ECS.world.purge() # Clear current state
var entities = ECS.deserialize("user://saves/slot_%s.tres" % slot)
for entity in entities:
ECS.world.add_entity(entity)
```
### Level Export/Import
```gdscript
func export_level():
var query = ECS.world.query.with_all([C_LevelObject])
var data = ECS.serialize(query)
ECS.save(data, "res://levels/level_01.tres")
func load_level(path: String):
var entities = ECS.deserialize(path)
ECS.world.add_entities(entities)
```
### Selective Serialization
```gdscript
# Save only player data
var player_query = ECS.world.query.with_all([C_Player])
# Save entities in specific area
var area_query = ECS.world.query.with_group("area_1")
# Save entities with specific components
var combat_query = ECS.world.query.with_all([C_Health, C_Weapon])
```
## Data Structure
The system uses two main resource classes:
### GecsData
```gdscript
class_name GecsData
extends Resource
@export var version: String = "0.1"
@export var entities: Array[GecsEntityData] = []
```
### GecsEntityData
```gdscript
class_name GecsEntityData
extends Resource
@export var entity_name: String = ""
@export var scene_path: String = "" # For prefab entities
@export var components: Array[Component] = []
```
## Error Handling
```gdscript
# Serialize never fails (returns empty data if no matches)
var data = ECS.serialize(query)
# Check save success
if not ECS.save(data, filepath):
print("Save failed - check permissions")
# Handle missing files
var entities = ECS.deserialize(filepath)
if entities.is_empty():
print("No data loaded")
```
## Performance
- **Memory:** Creates component copies during serialization
- **Speed:** Binary format ~60% smaller, faster loading than text
- **Scale:** Tested with 100+ entities, sub-second performance
## Binary vs Text Format
**Text (.tres):**
- Human readable
- Editor inspectable
- Version control friendly
- Development debugging
**Binary (.res):**
- Smaller file size
- Faster loading
- Production builds
- Auto-detection on load
## File Structure Example
```tres
[gd_resource type="GecsData" format=3]
[sub_resource type="C_Health" id="1"]
current = 85.0
maximum = 100.0
[sub_resource type="GecsEntityData" id="2"]
entity_name = "Player"
components = [SubResource("1")]
[resource]
version = "0.1"
entities = [SubResource("2")]
```
## Best Practices
1. **Use meaningful filenames:** `player_save.tres`, `level_boss.tres`
2. **Organize by purpose:** `user://saves/`, `res://levels/`
3. **Handle missing components gracefully**
4. **Use binary format for production**
5. **Version your save data for compatibility**
6. **Test with empty query results**
## Limitations
- No entity relationships (planned feature)
- Prefab entities need scene files present
- External resource references need manual handling

View File

@@ -0,0 +1,434 @@
# GECS Troubleshooting Guide
> **Quickly solve common GECS issues**
This guide helps you diagnose and fix the most common problems when working with GECS. Find your issue, apply the solution, and learn how to prevent it.
## 📋 Quick Diagnosis
### My Game Isn't Working At All
**Symptoms**: No entities moving, systems not running, nothing happening
**Quick Check**:
```gdscript
# In your _process() method, ensure you have:
func _process(delta):
if ECS.world:
ECS.world.process(delta) # This line is critical!
```
**Missing this?** → [Systems Not Running](#systems-not-running)
### Entities Aren't Moving/Updating
**Symptoms**: Entities exist but don't respond to systems
**Quick Check**:
1. Are your entities added to the world? `ECS.world.add_entity(entity)`
2. Do your entities have the right components? Check system queries
3. Are your systems properly organized in scene hierarchy? Check default_systems.tscn
**Still broken?** → [Entity Issues](#entity-issues)
### Performance Is Terrible
**Symptoms**: Low FPS, stuttering, slow response
**Quick Check**:
1. Enable profiling: `ECS.world.enable_profiling = true`
2. Check entity count: `print(ECS.world.entity_count)`
3. Look for expensive queries in your systems
**Need optimization?** → [Performance Issues](#performance-issues)
## 🚫 Systems Not Running
### Problem: Systems Never Execute
**Error Messages**:
- No error, but `process()` method never called
- Entities exist but don't change
**Solution**:
```gdscript
# ✅ Ensure this exists in your main scene
func _process(delta):
ECS.process(delta) # This processes all systems
# OR if using system groups:
func _process(delta):
ECS.process(delta, "physics")
ECS.process(delta, "render")
```
**Prevention**: Always call `ECS.process()` in your main game loop.
### Problem: System Query Returns Empty
**Symptoms**: System exists but `process()` never called
**Diagnosis**:
```gdscript
# Add this to your system for debugging
class_name MySystem extends System
func _ready():
print("MySystem query result: ", query().execute().size())
func query():
return q.with_all([C_ComponentA, C_ComponentB])
```
**Common Causes**:
1. **Missing Components**:
```gdscript
# ❌ Problem - Entity missing required component
var entity = Entity.new()
entity.add_component(C_ComponentA.new())
# Missing C_ComponentB!
# ✅ Solution - Add all required components
entity.add_component(C_ComponentA.new())
entity.add_component(C_ComponentB.new())
```
2. **Wrong Component Types**:
```gdscript
# ❌ Problem - Using instance instead of class
func query():
return q.with_all([C_ComponentA.new()]) # Wrong!
# ✅ Solution - Use class reference
func query():
return q.with_all([C_ComponentA]) # Correct!
```
3. **Component Not Added to World**:
```gdscript
# ❌ Problem - Entity not in world
var entity = Entity.new()
entity.add_component(C_ComponentA.new())
# Entity never added to world!
# ✅ Solution - Add entity to world
ECS.world.add_entity(entity)
```
## 🎭 Entity Issues
### Problem: Entity Components Not Found
**Error Messages**:
- `get_component() returned null`
- `Entity does not have component of type...`
**Diagnosis**:
```gdscript
# Debug what components an entity actually has
func debug_entity_components(entity: Entity):
print("Entity components:")
for component_path in entity.components.keys():
print(" ", component_path)
```
**Solution**: Ensure components are added correctly:
```gdscript
# ✅ Correct component addition
var entity = Entity.new()
entity.add_component(C_Health.new(100))
entity.add_component(C_Position.new(Vector2(50, 50)))
# Verify component exists before using
if entity.has_component(C_Health):
var health = entity.get_component(C_Health)
health.current -= 10
```
### Problem: Component Properties Not Updating
**Symptoms**: Setting component properties has no effect
**Common Causes**:
1. **Getting Component Reference Once**:
```gdscript
# ❌ Problem - Stale component reference
var health = entity.get_component(C_Health)
# ... later in code, component gets replaced ...
health.current = 50 # Updates old component!
# ✅ Solution - Get fresh reference each time
entity.get_component(C_Health).current = 50
```
2. **Modifying Wrong Entity**:
```gdscript
# ❌ Problem - Variable confusion
var player = get_player_entity()
var enemy = get_enemy_entity()
# Accidentally modify wrong entity
player.get_component(C_Health).current = 0 # Meant to be enemy!
# ✅ Solution - Use clear variable names
var player_health = player.get_component(C_Health)
var enemy_health = enemy.get_component(C_Health)
enemy_health.current = 0
```
## 💥 Common Errors
### Error: "Cannot access property/method on null instance"
**Full Error**:
```
Invalid get index 'current' (on base: 'null instance')
```
**Cause**: Component doesn't exist on entity
**Solution**:
```gdscript
# ❌ Causes null error
var health = entity.get_component(C_Health)
health.current -= 10 # health is null!
# ✅ Safe component access
if entity.has_component(C_Health):
var health = entity.get_component(C_Health)
health.current -= 10
else:
print("Entity doesn't have C_Health!")
```
### Error: "Class not found"
**Full Error**:
```
Identifier 'ComponentName' not found in current scope
```
**Causes & Solutions**:
1. **Missing class_name**:
```gdscript
# ❌ Problem - No class_name declaration
extends Component
# Script exists but can't be referenced by name
# ✅ Solution - Add class_name
class_name C_Health
extends Component
```
2. **File not saved or loaded**:
- Save your component script files
- Restart Godot if classes still not found
- Check for syntax errors in the component file
3. **Wrong inheritance**:
```gdscript
# ❌ Problem - Wrong base class
class_name C_Health
extends Node # Should be Component!
# ✅ Solution - Correct inheritance
class_name C_Health
extends Component
```
## 🐌 Performance Issues
### Problem: Low FPS / Stuttering
**Diagnosis Steps**:
1. **Enable profiling**:
```gdscript
ECS.world.enable_profiling = true
# Check processing times
func _process(delta):
ECS.process(delta)
print("Frame time: ", get_process_delta_time() * 1000, "ms")
```
2. **Check entity count**:
```gdscript
print("Total entities: ", ECS.world.entity_count)
print("System count: ", ECS.world.get_system_count())
```
**Common Fixes**:
1. **Too Many Entities in Broad Queries**:
```gdscript
# ❌ Problem - Overly broad query
func query():
return q.with_all([C_Position]) # Matches everything!
# ✅ Solution - More specific query
func query():
return q.with_all([C_Position, C_Movable])
```
2. **Expensive Queries Rebuilt Every Frame**:
```gdscript
# ❌ Problem - Rebuilding queries in process
func process(entities: Array[Entity], components: Array, delta: float):
var custom_entities = ECS.world.query.with_all([C_ComponentA]).execute()
# ✅ Solution - Use the system's query() method (automatically cached)
func query():
return q.with_all([C_ComponentA]) # Automatically cached by GECS
func process(entities: Array[Entity], components: Array, delta: float):
# Just process the entities passed in - already filtered by query
for entity in entities:
# Process entity...
```
## 🔧 Integration Issues
### Problem: GECS Conflicts with Godot Features
**Issue**: Using GECS entities with Godot nodes causes problems
**Solution**: Choose your approach consistently:
```gdscript
# ✅ Approach 1 - Pure ECS (recommended for complex games)
# Entities are not nodes, use ECS for everything
var entity = Entity.new() # Not added to scene tree
entity.add_component(C_Position.new())
ECS.world.add_entity(entity)
# ✅ Approach 2 - Hybrid (good for simpler games)
# Entities are nodes, use ECS for specific systems
var entity = Entity.new()
add_child(entity) # Entity is in scene tree
entity.add_component(C_Health.new())
ECS.world.add_entity(entity)
```
**Avoid**: Mixing approaches inconsistently in the same project.
### Problem: GECS Not Working After Scene Changes
**Symptoms**: Systems stop working when changing scenes
**Solution**: Properly reinitialize ECS in new scenes:
```gdscript
# In each main scene script
func _ready():
# Create new world for this scene
var world = World.new()
add_child(world)
ECS.world = world
# Systems are usually managed via scene composition
# See default_systems.tscn for organization
# Create your entities
setup_entities()
```
**Prevention**: Always initialize ECS properly in each scene that uses it.
## 🛠️ Debugging Tools
### Enable Debug Logging
Add to your project settings or main script:
```gdscript
# Enable GECS debug output
ECS.set_debug_level(ECS.DEBUG_VERBOSE)
# This will show:
# - Entity creation/destruction
# - Component additions/removals
# - System processing information
# - Query execution details
```
### Entity Inspector Tool
Create a debug tool to inspect entities at runtime:
```gdscript
# DebugPanel.gd
extends Control
func _on_inspect_button_pressed():
var entities = ECS.world.get_all_entities()
print("=== ENTITY INSPECTOR ===")
for i in range(min(10, entities.size())): # Show first 10
var entity = entities[i]
print("Entity ", i, ":")
print(" Components: ", entity.components.keys())
print(" Groups: ", entity.get_groups())
# Show component values
for comp_path in entity.components.keys():
var comp = entity.components[comp_path]
print(" ", comp_path, ": ", comp)
```
## 📚 Getting More Help
### Community Resources
- **Discord**: [Join our community](https://discord.gg/eB43XU2tmn) for help and discussions
- **GitHub Issues**: [Report bugs](https://github.com/csprance/gecs/issues)
- **Documentation**: [Complete Guide](../DOCUMENTATION.md)
### Before Asking for Help
Include this information in your question:
1. **GECS version** you're using
2. **Godot version** you're using
3. **Minimal code example** that reproduces the issue
4. **Error messages** (full text, not paraphrased)
5. **Expected vs actual behavior**
### Still Stuck?
If this guide doesn't solve your problem:
1. **Check the examples** in [Getting Started](GETTING_STARTED.md)
2. **Review best practices** in [Best Practices](BEST_PRACTICES.md)
3. **Search GitHub issues** for similar problems
4. **Create a minimal reproduction** and ask for help
---
_"Every bug is a learning opportunity. The key is knowing where to look and what questions to ask."_