Initial commit
This commit is contained in:
600
skills/godot-debugging/SKILL.md
Normal file
600
skills/godot-debugging/SKILL.md
Normal file
@@ -0,0 +1,600 @@
|
||||
---
|
||||
name: godot-debugging
|
||||
description: Expert knowledge of Godot debugging, error interpretation, common bugs, and troubleshooting techniques. Use when helping fix Godot errors, crashes, or unexpected behavior.
|
||||
allowed_tools:
|
||||
- mcp__godot__*
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
---
|
||||
|
||||
You are a Godot debugging expert with deep knowledge of common errors, debugging techniques, and troubleshooting strategies.
|
||||
|
||||
# Common Godot Errors and Solutions
|
||||
|
||||
## Parser/Syntax Errors
|
||||
|
||||
### Error: "Parse Error: Expected ..."
|
||||
**Common Causes:**
|
||||
- Missing colons after function definitions, if statements, loops
|
||||
- Incorrect indentation (must use tabs OR spaces consistently)
|
||||
- Missing parentheses in function calls
|
||||
- Unclosed brackets, parentheses, or quotes
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# WRONG
|
||||
func _ready() # Missing colon
|
||||
print("Hello")
|
||||
|
||||
# CORRECT
|
||||
func _ready():
|
||||
print("Hello")
|
||||
|
||||
# WRONG
|
||||
if player_health > 0 # Missing colon
|
||||
player.move()
|
||||
|
||||
# CORRECT
|
||||
if player_health > 0:
|
||||
player.move()
|
||||
```
|
||||
|
||||
### Error: "Identifier not declared in the current scope"
|
||||
**Common Causes:**
|
||||
- Variable used before declaration
|
||||
- Typo in variable/function name
|
||||
- Trying to access variable from wrong scope
|
||||
- Missing @ symbol for onready variables
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# WRONG
|
||||
func _ready():
|
||||
print(my_variable) # Not declared yet
|
||||
|
||||
var my_variable = 10
|
||||
|
||||
# CORRECT
|
||||
var my_variable = 10
|
||||
|
||||
func _ready():
|
||||
print(my_variable)
|
||||
|
||||
# WRONG
|
||||
@onready var sprite = $Sprite2D # Missing @
|
||||
|
||||
# CORRECT
|
||||
@onready var sprite = $Sprite2D
|
||||
```
|
||||
|
||||
### Error: "Invalid get index 'property_name' (on base: 'Type')"
|
||||
**Common Causes:**
|
||||
- Typo in property name
|
||||
- Property doesn't exist on that node type
|
||||
- Node is null (wasn't found in scene tree)
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Check if node exists before accessing
|
||||
if sprite != null:
|
||||
sprite.visible = false
|
||||
else:
|
||||
print("ERROR: Sprite node not found!")
|
||||
|
||||
# Or use optional chaining (Godot 4.2+)
|
||||
# sprite?.visible = false
|
||||
|
||||
# Verify node path
|
||||
@onready var sprite = $Sprite2D # Make sure this path is correct
|
||||
|
||||
func _ready():
|
||||
if sprite == null:
|
||||
print("Sprite not found! Check node path.")
|
||||
```
|
||||
|
||||
## Runtime Errors
|
||||
|
||||
### Error: "Attempt to call function 'func_name' in base 'null instance' on a null instance"
|
||||
**Common Causes:**
|
||||
- Calling method on null reference
|
||||
- Node removed/freed before accessing
|
||||
- @onready variable references non-existent node
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Always check for null before calling methods
|
||||
if player != null and player.has_method("take_damage"):
|
||||
player.take_damage(10)
|
||||
|
||||
# Verify onready variables in _ready()
|
||||
@onready var sprite = $Sprite2D
|
||||
|
||||
func _ready():
|
||||
if sprite == null:
|
||||
push_error("Sprite node not found at path: $Sprite2D")
|
||||
return
|
||||
|
||||
# Check if node is valid before using
|
||||
if is_instance_valid(my_node):
|
||||
my_node.do_something()
|
||||
```
|
||||
|
||||
### Error: "Invalid operands 'Type' and 'null' in operator '...'"
|
||||
**Common Causes:**
|
||||
- Mathematical operation on null value
|
||||
- Comparing null to typed value
|
||||
- Uninitialized variable used in calculation
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Initialize variables with default values
|
||||
var health: int = 100 # Not null
|
||||
var player: Node2D = null
|
||||
|
||||
# Check before operations
|
||||
if player != null:
|
||||
var distance = global_position.distance_to(player.global_position)
|
||||
|
||||
# Use default values
|
||||
var target_position = player.global_position if player else global_position
|
||||
```
|
||||
|
||||
### Error: "Index [number] out of range (size [size])"
|
||||
**Common Causes:**
|
||||
- Accessing array beyond its length
|
||||
- Using wrong index variable
|
||||
- Array size changed but code assumes old size
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Always check array size
|
||||
var items = [1, 2, 3]
|
||||
|
||||
if index < items.size():
|
||||
print(items[index])
|
||||
else:
|
||||
print("Index out of range!")
|
||||
|
||||
# Or use range-based loops
|
||||
for item in items:
|
||||
print(item)
|
||||
|
||||
# Safe array access
|
||||
var value = items[index] if index < items.size() else null
|
||||
```
|
||||
|
||||
## Scene Tree Errors
|
||||
|
||||
### Error: "Node not found: [path]"
|
||||
**Common Causes:**
|
||||
- Incorrect node path in get_node() or $
|
||||
- Node doesn't exist yet (wrong timing)
|
||||
- Node was removed or renamed
|
||||
- Path case sensitivity issues
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Use @onready for scene tree nodes
|
||||
@onready var sprite = $Sprite2D
|
||||
@onready var timer = $Timer
|
||||
|
||||
# Check if node exists
|
||||
func get_player():
|
||||
var player = get_node_or_null("Player")
|
||||
if player == null:
|
||||
print("Player node not found!")
|
||||
return player
|
||||
|
||||
# Use has_node() to check existence
|
||||
if has_node("Sprite2D"):
|
||||
var sprite = $Sprite2D
|
||||
|
||||
# For dynamic paths, use NodePath
|
||||
var sprite = get_node(NodePath("Path/To/Sprite"))
|
||||
```
|
||||
|
||||
### Error: "Can't change state while flushing queries"
|
||||
**Common Causes:**
|
||||
- Modifying physics objects during physics callback
|
||||
- Adding/removing nodes during iteration
|
||||
- Freeing nodes in wrong context
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Use call_deferred for physics changes
|
||||
func _on_body_entered(body):
|
||||
# WRONG
|
||||
# body.queue_free()
|
||||
|
||||
# CORRECT
|
||||
body.call_deferred("queue_free")
|
||||
|
||||
# Use call_deferred for collision shape changes
|
||||
func disable_collision():
|
||||
$CollisionShape2D.call_deferred("set_disabled", true)
|
||||
|
||||
# Defer node additions/removals
|
||||
func spawn_enemy():
|
||||
var enemy = enemy_scene.instantiate()
|
||||
call_deferred("add_child", enemy)
|
||||
```
|
||||
|
||||
## Signal Errors
|
||||
|
||||
### Error: "Attempt to call an invalid function in base 'MethodBind'"
|
||||
**Common Causes:**
|
||||
- Signal connected to non-existent method
|
||||
- Method signature doesn't match signal parameters
|
||||
- Typo in method name
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Verify method exists and signature matches
|
||||
func _ready():
|
||||
# Signal: timeout()
|
||||
$Timer.timeout.connect(_on_timer_timeout)
|
||||
|
||||
func _on_timer_timeout(): # No parameters for timeout signal
|
||||
print("Timer expired")
|
||||
|
||||
# For signals with parameters
|
||||
func _ready():
|
||||
# Signal: body_entered(body: Node2D)
|
||||
$Area2D.body_entered.connect(_on_body_entered)
|
||||
|
||||
func _on_body_entered(body: Node2D): # Must accept body parameter
|
||||
print("Body entered:", body.name)
|
||||
|
||||
# Check if callable is valid
|
||||
var callable = Callable(self, "_on_timer_timeout")
|
||||
if callable.is_valid():
|
||||
$Timer.timeout.connect(callable)
|
||||
```
|
||||
|
||||
### Error: "Signal 'signal_name' is already connected"
|
||||
**Common Causes:**
|
||||
- Connecting same signal multiple times
|
||||
- Not disconnecting before reconnecting
|
||||
- Multiple _ready() calls on singleton
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Check before connecting
|
||||
func _ready():
|
||||
if not $Timer.timeout.is_connected(_on_timer_timeout):
|
||||
$Timer.timeout.connect(_on_timer_timeout)
|
||||
|
||||
# Or disconnect first
|
||||
func reconnect_signal():
|
||||
if $Timer.timeout.is_connected(_on_timer_timeout):
|
||||
$Timer.timeout.disconnect(_on_timer_timeout)
|
||||
$Timer.timeout.connect(_on_timer_timeout)
|
||||
|
||||
# Use CONNECT_ONE_SHOT for single-use connections
|
||||
$Timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)
|
||||
```
|
||||
|
||||
## Resource/File Errors
|
||||
|
||||
### Error: "Cannot load resource at path: 'res://...' (error code)"
|
||||
**Common Causes:**
|
||||
- File doesn't exist at that path
|
||||
- Typo in file path
|
||||
- File extension missing or incorrect
|
||||
- Resource not imported properly
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Check if resource exists
|
||||
var resource_path = "res://sprites/player.png"
|
||||
if ResourceLoader.exists(resource_path):
|
||||
var texture = load(resource_path)
|
||||
else:
|
||||
print("Resource not found:", resource_path)
|
||||
|
||||
# Use preload for resources that definitely exist
|
||||
const PLAYER_SPRITE = preload("res://sprites/player.png")
|
||||
|
||||
# Handle load errors gracefully
|
||||
var scene = load("res://scenes/level.tscn")
|
||||
if scene == null:
|
||||
print("Failed to load scene!")
|
||||
return
|
||||
var instance = scene.instantiate()
|
||||
```
|
||||
|
||||
### Error: "Condition 'texture.is_null()' is true"
|
||||
**Common Causes:**
|
||||
- Loading failed but error not checked
|
||||
- Resource file missing or corrupted
|
||||
- Incorrect resource type
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Always check load result
|
||||
var texture = load("res://textures/sprite.png")
|
||||
if texture == null:
|
||||
print("Failed to load texture! Using placeholder.")
|
||||
texture = PlaceholderTexture2D.new()
|
||||
texture.size = Vector2(32, 32)
|
||||
|
||||
$Sprite2D.texture = texture
|
||||
```
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Lag/Stuttering
|
||||
**Common Causes:**
|
||||
- Too many _process() or _physics_process() calls
|
||||
- Expensive operations in loops
|
||||
- Memory leaks (not freeing nodes)
|
||||
- Too many signals firing per frame
|
||||
|
||||
**Debugging Steps:**
|
||||
1. Use the Godot Profiler (Debug > Profiler)
|
||||
2. Check for hot spots in code
|
||||
3. Look for memory growth over time
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Disable processing when not needed
|
||||
func _ready():
|
||||
set_physics_process(false) # Enable only when needed
|
||||
|
||||
func start_moving():
|
||||
set_physics_process(true)
|
||||
|
||||
# Cache expensive lookups
|
||||
var player: Node2D = null
|
||||
|
||||
func _ready():
|
||||
player = get_node("/root/Main/Player") # Cache once
|
||||
|
||||
func _process(_delta):
|
||||
if player: # Use cached reference
|
||||
look_at(player.global_position)
|
||||
|
||||
# Use timers instead of checking every frame
|
||||
var check_timer: float = 0.0
|
||||
|
||||
func _process(delta):
|
||||
check_timer += delta
|
||||
if check_timer >= 0.5: # Only check twice per second
|
||||
check_timer = 0.0
|
||||
_do_expensive_check()
|
||||
|
||||
# Free unused nodes
|
||||
func remove_enemy(enemy):
|
||||
enemy.queue_free() # Properly free memory
|
||||
```
|
||||
|
||||
## Memory Leaks
|
||||
|
||||
### Error: Memory usage keeps growing
|
||||
**Common Causes:**
|
||||
- Not calling queue_free() on removed nodes
|
||||
- Circular references preventing garbage collection
|
||||
- Creating new objects without freeing old ones
|
||||
|
||||
**Solutions:**
|
||||
```gdscript
|
||||
# Always free nodes you create
|
||||
func spawn_particle():
|
||||
var particle = particle_scene.instantiate()
|
||||
add_child(particle)
|
||||
|
||||
# Free after animation
|
||||
await get_tree().create_timer(2.0).timeout
|
||||
particle.queue_free()
|
||||
|
||||
# Break circular references
|
||||
class_name Enemy
|
||||
|
||||
var target: Node = null
|
||||
|
||||
func _exit_tree():
|
||||
target = null # Clear reference on removal
|
||||
|
||||
# Use object pooling for frequently created/destroyed objects
|
||||
var bullet_pool = []
|
||||
|
||||
func get_bullet():
|
||||
if bullet_pool.is_empty():
|
||||
return bullet_scene.instantiate()
|
||||
return bullet_pool.pop_back()
|
||||
|
||||
func return_bullet(bullet):
|
||||
bullet.visible = false
|
||||
bullet.set_process(false)
|
||||
bullet_pool.append(bullet)
|
||||
```
|
||||
|
||||
# Debugging Techniques
|
||||
|
||||
## Print Debugging
|
||||
|
||||
```gdscript
|
||||
# Basic print
|
||||
print("Value:", variable)
|
||||
|
||||
# Formatted print
|
||||
print("Player health: %d/%d" % [current_health, max_health])
|
||||
|
||||
# Type checking
|
||||
print("Variable type:", typeof(variable))
|
||||
|
||||
# Node inspection
|
||||
print("Node path:", get_path())
|
||||
print("Parent:", get_parent().name if get_parent() else "none")
|
||||
|
||||
# Stack trace
|
||||
print("Current stack:")
|
||||
print_stack()
|
||||
|
||||
# Warning (shows in yellow)
|
||||
push_warning("This is not good!")
|
||||
|
||||
# Error (shows in red)
|
||||
push_error("Something went wrong!")
|
||||
```
|
||||
|
||||
## Breakpoints and Step Debugging
|
||||
|
||||
1. **Set Breakpoints**: Click line number in script editor
|
||||
2. **Run with Debugging**: Press F5 (or play with debugger enabled)
|
||||
3. **When Paused at Breakpoint:**
|
||||
- **Continue** (F12): Resume execution
|
||||
- **Step Over** (F10): Execute current line, skip into functions
|
||||
- **Step Into** (F11): Enter function calls
|
||||
- **Step Out**: Exit current function
|
||||
|
||||
4. **Inspect Variables**: Hover over variables or check debugger panel
|
||||
|
||||
## Remote Debugger
|
||||
|
||||
When game is running:
|
||||
1. Open **Debugger** tab at bottom of editor
|
||||
2. View **Errors** tab for runtime errors
|
||||
3. Check **Profiler** for performance issues
|
||||
4. Use **Network Profiler** for multiplayer issues
|
||||
|
||||
## Assert Statements
|
||||
|
||||
```gdscript
|
||||
# Assert for debugging assumptions
|
||||
assert(player != null, "Player should exist at this point")
|
||||
assert(health >= 0, "Health should never be negative")
|
||||
assert(items.size() > 0, "Items array should not be empty")
|
||||
|
||||
# Asserts only run in debug builds, removed in release
|
||||
```
|
||||
|
||||
## Debug Drawing
|
||||
|
||||
```gdscript
|
||||
# Draw debug info in 2D games
|
||||
func _draw():
|
||||
if OS.is_debug_build():
|
||||
# Draw collision shapes
|
||||
draw_circle(Vector2.ZERO, 50, Color(1, 0, 0, 0.3))
|
||||
|
||||
# Draw raycast
|
||||
draw_line(Vector2.ZERO, Vector2(100, 0), Color.RED, 2.0)
|
||||
|
||||
# Draw text
|
||||
draw_string(ThemeDB.fallback_font, Vector2(0, -60), "Debug Info")
|
||||
```
|
||||
|
||||
## Conditional Debugging
|
||||
|
||||
```gdscript
|
||||
# Debug mode flag
|
||||
var debug_mode = OS.is_debug_build()
|
||||
|
||||
func _process(delta):
|
||||
if debug_mode:
|
||||
# Extra checks only in debug
|
||||
_validate_state()
|
||||
|
||||
func _validate_state():
|
||||
if health < 0:
|
||||
push_error("Health is negative!")
|
||||
if velocity.length() > max_speed * 2:
|
||||
push_warning("Velocity exceeds safe limits!")
|
||||
```
|
||||
|
||||
# Godot 4 Specific Issues
|
||||
|
||||
## Type Annotations
|
||||
|
||||
```gdscript
|
||||
# Godot 4 uses stronger typing
|
||||
var health: int = 100 # Typed
|
||||
var player: CharacterBody2D = null # Typed with class
|
||||
|
||||
# Arrays can be typed
|
||||
var items: Array[Item] = []
|
||||
|
||||
# Dictionary typing
|
||||
var stats: Dictionary = {
|
||||
"health": 100,
|
||||
"mana": 50
|
||||
}
|
||||
|
||||
# Function return types
|
||||
func get_health() -> int:
|
||||
return health
|
||||
```
|
||||
|
||||
## Node Path Changes
|
||||
|
||||
```gdscript
|
||||
# Godot 4 uses different node types
|
||||
# CharacterBody2D instead of KinematicBody2D
|
||||
# Sprite2D instead of Sprite
|
||||
# AnimatedSprite2D instead of AnimatedSprite
|
||||
|
||||
# Update old code:
|
||||
# extends KinematicBody2D # Old
|
||||
extends CharacterBody2D # New
|
||||
|
||||
# move_and_slide(velocity) # Old
|
||||
# velocity is now a property
|
||||
move_and_slide() # New
|
||||
```
|
||||
|
||||
## Common Migration Issues
|
||||
|
||||
```gdscript
|
||||
# Godot 3 -> 4 changes:
|
||||
|
||||
# Physics
|
||||
# Old: move_and_slide(velocity, Vector2.UP)
|
||||
# New:
|
||||
velocity.y += gravity * delta
|
||||
move_and_slide()
|
||||
|
||||
# Signals
|
||||
# Old: connect("timeout", self, "_on_timer_timeout")
|
||||
# New:
|
||||
timeout.connect(_on_timer_timeout)
|
||||
|
||||
# Getting nodes
|
||||
# Old: $Sprite (works for both)
|
||||
# New: $Sprite2D (node type changed)
|
||||
|
||||
# Tile maps
|
||||
# Old: set_cell(x, y, tile_id)
|
||||
# New: set_cell(0, Vector2i(x, y), 0, Vector2i(tile_id, 0))
|
||||
```
|
||||
|
||||
# When to Activate This Skill
|
||||
|
||||
Activate when the user:
|
||||
- Reports an error message
|
||||
- Asks about crashes or unexpected behavior
|
||||
- Needs help understanding error output
|
||||
- Asks "why isn't this working?"
|
||||
- Mentions debugging, errors, or bugs
|
||||
- Shares code that's not working as expected
|
||||
- Asks about performance issues
|
||||
- Reports memory leaks or crashes
|
||||
|
||||
# Debugging Workflow
|
||||
|
||||
1. **Identify the error** - Read error message carefully
|
||||
2. **Locate the source** - Find which file/line is causing it
|
||||
3. **Understand the cause** - Why is this happening?
|
||||
4. **Apply the fix** - Modify code to resolve issue
|
||||
5. **Test the solution** - Verify fix works
|
||||
6. **Explain to user** - Help them understand what went wrong and why
|
||||
|
||||
When helping debug:
|
||||
- Always explain WHY the error occurred
|
||||
- Provide the corrected code
|
||||
- Suggest preventive measures for similar issues
|
||||
- Recommend debugging techniques for future problems
|
||||
Reference in New Issue
Block a user