419 lines
13 KiB
Markdown
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."_
|