basic ECS spawner
This commit is contained in:
724
addons/gecs/docs/BEST_PRACTICES.md
Normal file
724
addons/gecs/docs/BEST_PRACTICES.md
Normal 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."_
|
||||
182
addons/gecs/docs/COMPONENT_QUERIES.md
Normal file
182
addons/gecs/docs/COMPONENT_QUERIES.md
Normal 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`.
|
||||
699
addons/gecs/docs/CORE_CONCEPTS.md
Normal file
699
addons/gecs/docs/CORE_CONCEPTS.md
Normal 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."_
|
||||
357
addons/gecs/docs/DEBUG_VIEWER.md
Normal file
357
addons/gecs/docs/DEBUG_VIEWER.md
Normal 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
|
||||
341
addons/gecs/docs/GETTING_STARTED.md
Normal file
341
addons/gecs/docs/GETTING_STARTED.md
Normal 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."_
|
||||
351
addons/gecs/docs/OBSERVERS.md
Normal file
351
addons/gecs/docs/OBSERVERS.md
Normal 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."_
|
||||
418
addons/gecs/docs/PERFORMANCE_OPTIMIZATION.md
Normal file
418
addons/gecs/docs/PERFORMANCE_OPTIMIZATION.md
Normal 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."_
|
||||
216
addons/gecs/docs/PERFORMANCE_TESTING.md
Normal file
216
addons/gecs/docs/PERFORMANCE_TESTING.md
Normal 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._
|
||||
893
addons/gecs/docs/RELATIONSHIPS.md
Normal file
893
addons/gecs/docs/RELATIONSHIPS.md
Normal 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."_
|
||||
218
addons/gecs/docs/SERIALIZATION.md
Normal file
218
addons/gecs/docs/SERIALIZATION.md
Normal 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
|
||||
434
addons/gecs/docs/TROUBLESHOOTING.md
Normal file
434
addons/gecs/docs/TROUBLESHOOTING.md
Normal 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."_
|
||||
Reference in New Issue
Block a user