Files
godot-shader-experiments/addons/gecs/docs/PERFORMANCE_OPTIMIZATION.md
2026-01-15 15:27:48 +01:00

419 lines
13 KiB
Markdown

# 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."_