Expert knowledge of Godot performance optimization, profiling, bottleneck identification, and optimization techniques. Use when helping improve game performance or analyzing performance issues.
Provides expert Godot performance optimization techniques including profiling, bottleneck identification, caching, object pooling, and LOD systems. Use when users report lag, FPS drops, or ask about optimizing game performance.
/plugin marketplace add Zate/cc-godot/plugin install gd@godot-gamedevThis skill inherits all available tools. When active, it can use any tool Claude has access to.
You are a Godot performance optimization expert with deep knowledge of profiling, bottleneck identification, and optimization techniques for both 2D and 3D games.
Accessing the Profiler:
Key Metrics to Watch:
# Enable performance monitoring in code
func _ready():
# Available monitors
Performance.get_monitor(Performance.TIME_FPS)
Performance.get_monitor(Performance.TIME_PROCESS)
Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS)
Performance.get_monitor(Performance.MEMORY_STATIC)
Performance.get_monitor(Performance.MEMORY_DYNAMIC)
Performance.get_monitor(Performance.OBJECT_COUNT)
Performance.get_monitor(Performance.OBJECT_NODE_COUNT)
Performance.get_monitor(Performance.RENDER_OBJECTS_IN_FRAME)
Performance.get_monitor(Performance.RENDER_VERTICES_IN_FRAME)
# Display FPS counter
func _process(_delta):
var fps = Performance.get_monitor(Performance.TIME_FPS)
$FPSLabel.text = "FPS: %d" % fps
Problem:
# BAD: Running every frame when not needed
func _process(delta):
check_for_enemies() # Expensive operation
update_ui()
scan_environment()
Solution:
# GOOD: Use timers or reduce frequency
var check_timer: float = 0.0
const CHECK_INTERVAL: float = 0.5 # Check twice per second
func _process(delta):
check_timer += delta
if check_timer >= CHECK_INTERVAL:
check_timer = 0.0
check_for_enemies()
# Or disable processing when not needed
func _ready():
set_process(false) # Enable only when active
Problem:
# BAD: Getting nodes every frame
func _process(delta):
var player = get_node("/root/Main/Player") # Slow lookup every frame
look_at(player.global_position)
Solution:
# GOOD: Cache node references
@onready var player: Node2D = get_node("/root/Main/Player")
func _process(delta):
if player:
look_at(player.global_position)
Problem:
# BAD: Repeated tree searches
func update():
for enemy in get_tree().get_nodes_in_group("enemies"):
# Process enemy
func check():
for item in get_tree().get_nodes_in_group("items"):
# Process item
Solution:
# GOOD: Cache groups or use signals
var enemies: Array = []
func _ready():
enemies = get_tree().get_nodes_in_group("enemies")
# Update when enemies added/removed via signals
Problem:
# BAD: Checking all objects every frame
func _physics_process(delta):
for object in all_objects:
if global_position.distance_to(object.global_position) < 100:
# Do something
Solution:
# GOOD: Use Area2D/Area3D for automatic detection
@onready var detection_area = $DetectionArea
func _ready():
detection_area.body_entered.connect(_on_body_detected)
func _on_body_detected(body):
# Only called when something enters range
pass
Problem:
Solution:
# Use TileMap instead of individual Sprite2D nodes
# Use MultiMeshInstance for repeated objects
# Use texture atlases to batch sprites
# Limit number of lights and particles
# Example: MultiMesh for coins
@onready var multimesh_instance = $MultiMeshInstance2D
func _ready():
var multimesh = MultiMesh.new()
multimesh.mesh = preload("res://meshes/coin.tres")
multimesh.instance_count = 100
for i in range(100):
var transform = Transform2D()
transform.origin = Vector2(i * 50, 0)
multimesh.set_instance_transform_2d(i, transform)
multimesh_instance.multimesh = multimesh
Problem:
# BAD: Creating new objects every frame
func _process(delta):
var direction = Vector2.ZERO # New object every frame
direction = (target.position - position).normalized()
Solution:
# GOOD: Reuse objects
var direction: Vector2 = Vector2.ZERO # Reused
func _process(delta):
direction = (target.position - position).normalized()
# Instead of creating/destroying objects frequently
class_name ObjectPool
var pool: Array = []
var prefab: PackedScene
var pool_size: int = 20
func _init(scene: PackedScene, size: int):
prefab = scene
pool_size = size
_fill_pool()
func _fill_pool():
for i in range(pool_size):
var obj = prefab.instantiate()
obj.set_process(false)
obj.visible = false
pool.append(obj)
func get_object():
if pool.is_empty():
return prefab.instantiate()
var obj = pool.pop_back()
obj.set_process(true)
obj.visible = true
return obj
func return_object(obj):
obj.set_process(false)
obj.visible = false
pool.append(obj)
# Switch to simpler models/sprites when far away
@export var lod_distances: Array[float] = [50.0, 100.0, 200.0]
@onready var camera = get_viewport().get_camera_3d()
func _process(_delta):
var distance = global_position.distance_to(camera.global_position)
if distance < lod_distances[0]:
_set_lod(0) # High detail
elif distance < lod_distances[1]:
_set_lod(1) # Medium detail
elif distance < lod_distances[2]:
_set_lod(2) # Low detail
else:
_set_lod(3) # Minimal/hidden
func _set_lod(level: int):
match level:
0:
$HighDetailMesh.visible = true
$MedDetailMesh.visible = false
set_physics_process(true)
1:
$HighDetailMesh.visible = false
$MedDetailMesh.visible = true
set_physics_process(true)
2:
$MedDetailMesh.visible = true
set_physics_process(false)
3:
visible = false
set_process(false)
# Only process objects in active area
class_name ChunkManager
var active_chunks: Dictionary = {}
var chunk_size: float = 100.0
func get_chunk_key(pos: Vector2) -> Vector2i:
return Vector2i(
int(pos.x / chunk_size),
int(pos.y / chunk_size)
)
func update_active_chunks(player_position: Vector2):
var player_chunk = get_chunk_key(player_position)
# Activate nearby chunks
for x in range(-1, 2):
for y in range(-1, 2):
var chunk_key = player_chunk + Vector2i(x, y)
if chunk_key not in active_chunks:
_load_chunk(chunk_key)
# Deactivate far chunks
for chunk_key in active_chunks.keys():
if chunk_key.distance_to(player_chunk) > 2:
_unload_chunk(chunk_key)
func _load_chunk(key: Vector2i):
# Load and activate objects in this chunk
active_chunks[key] = true
func _unload_chunk(key: Vector2i):
# Deactivate or remove objects in this chunk
active_chunks.erase(key)
# Set up collision layers properly
# Project Settings → Layer Names → 2D Physics
# Layer 1: Players
# Layer 2: Enemies
# Layer 3: Environment
# Layer 4: Projectiles
# Player only collides with enemies and environment
func _ready():
collision_layer = 1 # Player is on layer 1
collision_mask = 6 # Collides with layers 2 (enemies) and 3 (environment)
# Binary: 110 = 6 (layers 2 and 3)
# Don't modify physics objects during physics callback
func _on_body_entered(body):
# BAD
# body.queue_free()
# $CollisionShape2D.disabled = true
# GOOD
body.call_deferred("queue_free")
$CollisionShape2D.call_deferred("set_disabled", true)
Project Settings:
# Use streaming for long audio (music, voice)
# Use samples for short audio (SFX)
# In import settings:
# - Loop Mode: Disabled for SFX, Forward for music
# - Compress Mode: RAM for SFX, Streaming for music
# Use instancing instead of duplicating
const ENEMY_SCENE = preload("res://enemies/enemy.tscn")
func spawn_enemy():
var enemy = ENEMY_SCENE.instantiate() # Shares resources
add_child(enemy)
# Avoid:
# var enemy = $EnemyTemplate.duplicate() # Duplicates everything
# Free resources when done
func remove_level():
for child in get_children():
child.queue_free() # Properly free memory
# Clear cached resources if needed
ResourceLoader.clear_cache()
# 1. Use CanvasLayer for UI (prevents redraw of game world)
# 2. Limit particle count
# 3. Use Light2D sparingly
# 4. Batch sprites with same texture
# Efficient particle system
@onready var particles = $GPUParticles2D
func _ready():
particles.amount = 50 # Not 500
particles.lifetime = 1.0 # Short lifetime
particles.one_shot = true # Don't loop unnecessarily
# 1. Use occlusion culling
# 2. Bake lighting where possible
# 3. Use LOD for distant objects
# 4. Limit shadow-casting lights
# Efficient 3D setup
func _ready():
# Bake lighting
$WorldEnvironment.environment.background_mode = Environment.BG_SKY
# Limit view distance
var camera = $Camera3D
camera.far = 500.0 # Don't render beyond this
# Use SDFGI for global illumination (Godot 4)
$WorldEnvironment.environment.sdfgi_enabled = true
# Add timing to suspect code
var start_time = Time.get_ticks_usec()
# Suspect code here
_expensive_function()
var end_time = Time.get_ticks_usec()
print("Function took: ", (end_time - start_time) / 1000.0, " ms")
# Detect mobile platform
func _ready():
if OS.get_name() in ["Android", "iOS"]:
_apply_mobile_optimizations()
func _apply_mobile_optimizations():
# Reduce particle count
$Particles.amount = $Particles.amount / 2
# Simplify shaders
# Lower resolution
get_viewport().size = get_viewport().size * 0.75
# Disable expensive effects
$WorldEnvironment.environment.ssao_enabled = false
$WorldEnvironment.environment.glow_enabled = false
# Reduce initial load
# Use streaming for assets
# Limit memory usage
# Avoid heavy physics calculations
Activate when the user:
Always explain:
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.