Performance Optimalisatie
Best practices voor optimale game performance.
Performance Doelen
┌─────────────────────────────────────────────────────────────────┐
│ Performance Targets │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Platform FPS Target Max Frame Time │
│ ───────────────────────────────────────────────────── │
│ Desktop 60 FPS 16.67ms │
│ Mobile (High) 60 FPS 16.67ms │
│ Mobile (Low) 30 FPS 33.33ms │
│ Web 60 FPS 16.67ms │
│ │
│ Memory Targets: │
│ ───────────────── │
│ Desktop: < 500 MB │
│ Mobile: < 200 MB │
│ Web: < 300 MB │
│ │
└─────────────────────────────────────────────────────────────────┘
Rendering Optimalisatie
Batching
# Gebruik CanvasGroup voor batching
# Alle kinderen worden als één draw call gerenderd
# scene_tree_example:
# CanvasGroup (batch container)
# ├── Sprite2D (item 1)
# ├── Sprite2D (item 2)
# ├── Sprite2D (item 3)
# └── ... (allemaal in één draw call)
func setup_batched_container() -> CanvasGroup:
var group = CanvasGroup.new()
group.fit_margin = 10 # Extra margin voor effects
return group
Culling
# Visibility notifier voor off-screen objecten
func _ready() -> void:
var notifier = VisibleOnScreenNotifier2D.new()
notifier.screen_entered.connect(_on_visible)
notifier.screen_exited.connect(_on_hidden)
add_child(notifier)
func _on_visible() -> void:
set_process(true)
$AnimatedSprite2D.play()
func _on_hidden() -> void:
set_process(false)
$AnimatedSprite2D.stop()
Texture Atlases
# Gebruik texture atlases voor gerelateerde sprites
# Vermindert texture switches
# Project Settings -> Rendering -> Textures
# - Import Atlas: Enabled
# - Atlas Max Width: 2048
# - Atlas Max Height: 2048
# In code: laad van atlas
var atlas_texture = AtlasTexture.new()
atlas_texture.atlas = preload("res://assets/atlas/items.png")
atlas_texture.region = Rect2(0, 0, 64, 64) # Eerste item
Script Optimalisatie
Object Pooling
# object_pool.gd
class_name ObjectPool extends Node
var _pool: Array = []
var _scene: PackedScene
var _initial_size: int
func _init(scene: PackedScene, initial_size: int = 10) -> void:
_scene = scene
_initial_size = initial_size
func _ready() -> void:
_fill_pool(_initial_size)
func _fill_pool(count: int) -> void:
for i in range(count):
var instance = _scene.instantiate()
instance.set_process(false)
instance.visible = false
_pool.append(instance)
add_child(instance)
func get_object() -> Node:
if _pool.is_empty():
_fill_pool(5) # Expand pool
var obj = _pool.pop_back()
obj.set_process(true)
obj.visible = true
return obj
func return_object(obj: Node) -> void:
obj.set_process(false)
obj.visible = false
_pool.append(obj)
Caching
# Cache veelgebruikte lookups
var _cached_player: Node2D = null
var _cached_inventory: InventoryManager = null
func _ready() -> void:
# Cache bij start
_cached_player = get_tree().get_first_node_in_group("player")
_cached_inventory = get_node("/root/InventoryManager")
# Vermijd in _process:
# BAD:
func _process_bad(delta: float) -> void:
var player = get_tree().get_first_node_in_group("player") # Elke frame!
position = position.move_toward(player.position, speed * delta)
# GOOD:
func _process_good(delta: float) -> void:
if _cached_player:
position = position.move_toward(_cached_player.position, speed * delta)
Signal Optimalisatie
# Gebruik signals i.p.v. polling
# BAD:
func _process(delta: float) -> void:
if inventory.item_count != _last_count:
_update_ui()
_last_count = inventory.item_count
# GOOD:
func _ready() -> void:
inventory.item_changed.connect(_update_ui)
func _update_ui() -> void:
# Alleen aangeroepen wanneer nodig
pass
Delta Time & Physics
# Gebruik _physics_process voor consistente updates
func _physics_process(delta: float) -> void:
# Vast 60 FPS, onafhankelijk van framerate
velocity = direction * speed
position += velocity * delta
# _process alleen voor visuals
func _process(delta: float) -> void:
# Interpoleer visual position naar physics position
sprite.global_position = sprite.global_position.lerp(
actual_position,
10 * delta
)
Memory Management
Resource Loading
# Lazy loading voor grote resources
var _loaded_textures: Dictionary = {}
func get_texture(path: String) -> Texture2D:
if path not in _loaded_textures:
_loaded_textures[path] = load(path)
return _loaded_textures[path]
# Unload wanneer niet meer nodig
func unload_scene_resources(scene_name: String) -> void:
for path in _scene_resources[scene_name]:
if path in _loaded_textures:
_loaded_textures.erase(path)
Node Cleanup
# Proper cleanup
func cleanup_children() -> void:
for child in get_children():
child.queue_free()
# Wacht tot nodes daadwerkelijk freed zijn
await get_tree().process_frame
# Vermijd orphaned nodes
func _exit_tree() -> void:
# Cleanup timers, tweens, etc.
if _active_tween:
_active_tween.kill()
Texture Memory
# Compressie settings in .import files
# Voor desktop: VRAM Compressed (BC/DXT)
# Voor mobile: ETC2
# Mipmaps alleen voor 3D of scaling sprites
# Project Settings -> Rendering -> Textures -> Default Texture Filter
# Check texture memory usage
func log_texture_memory() -> void:
var texture_mem = Performance.get_monitor(Performance.RENDER_TEXTURE_MEM_USED)
print("Texture Memory: %.2f MB" % (texture_mem / 1024.0 / 1024.0))
Profiling
Built-in Profiler
# Enable profiler in editor: Debugger -> Profiler tab
# Custom profiling
func expensive_operation() -> void:
var start = Time.get_ticks_usec()
# ... operation ...
var duration = Time.get_ticks_usec() - start
print("Operation took: %d μs" % duration)
Performance Monitors
func _process(delta: float) -> void:
if OS.is_debug_build():
_update_debug_info()
func _update_debug_info() -> void:
var fps = Performance.get_monitor(Performance.TIME_FPS)
var frame_time = Performance.get_monitor(Performance.TIME_PROCESS)
var physics_time = Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS)
var draw_calls = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
var objects = Performance.get_monitor(Performance.OBJECT_NODE_COUNT)
debug_label.text = """
FPS: %.0f
Frame: %.2f ms
Physics: %.2f ms
Draw Calls: %.0f
Nodes: %.0f
""" % [fps, frame_time * 1000, physics_time * 1000, draw_calls, objects]
Common Bottlenecks
GDScript Pitfalls
# AVOID: String operations in loops
for item in items:
label.text += item.name + ", " # Slow!
# BETTER: Use Array.join
label.text = ", ".join(items.map(func(i): return i.name))
# AVOID: Dictionary/Array lookups in hot paths
func _process(delta: float) -> void:
var data = complex_dictionary["nested"]["path"]["to"]["data"] # Slow!
# BETTER: Cache the reference
var _cached_data = null
func _ready() -> void:
_cached_data = complex_dictionary["nested"]["path"]["to"]["data"]
# AVOID: Creating objects in _process
func _process(delta: float) -> void:
var vec = Vector2(x, y) # Creates new object every frame
# BETTER: Reuse objects
var _temp_vec: Vector2 = Vector2.ZERO
func _process(delta: float) -> void:
_temp_vec.x = x
_temp_vec.y = y
Physics Optimization
# Gebruik collision layers effectief
# Layer 1: Player
# Layer 2: Enemies
# Layer 3: Interactables
# Layer 4: Walls
# Player zoekt alleen naar interactables
# collision_mask = 4 (alleen layer 3)
# Vermijd te veel collision shapes
# Combineer waar mogelijk
# Gebruik simple shapes (Circle > Polygon)
Performance Budget
┌─────────────────────────────────────────────────────────────────┐
│ Frame Budget (16.67ms) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Physics: 3ms (collision, movement) │
│ Game Logic: 4ms (AI, scripts) │
│ Rendering: 6ms (draw calls, shaders) │
│ UI: 2ms (layout, updates) │
│ Buffer: 1.67ms (headroom) │
│ │
│ Total: 16.67ms = 60 FPS │
│ │
└─────────────────────────────────────────────────────────────────┘