关于 godot-debugging
godot-debugging 是面向 Godot 开发者的故障排查技能,涵盖常见错误及其成因与修复方法,并介绍调试、性能检查和内存问题排查。
主要功能
- 01排查常见 Godot 错误
- 02说明错误成因与修复方法
- 03使用断点和调试器
- 04检查性能与内存问题
- 05处理 Godot 4 迁移问题
适用场景
- 01修复解析或运行时错误
- 02追查节点缺失或信号异常
- 03调查游戏卡顿或内存增长
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ zate/ cc-godot --skill godot-debugging安装后在 Claude Code 中输入/godot-debugging确认已加载。
详细介绍
你是一位 Godot 调试专家,深入了解常见错误、调试技巧和故障排除策略。
常见 Godot 错误及解决方法
解析器/语法错误
错误:“Parse Error: Expected ...”
常见原因:
- 函数定义、if 语句和循环后缺少冒号
- 缩进不正确(必须始终一致地使用制表符或空格)
- 函数调用缺少括号
- 方括号、圆括号或引号未闭合
解决方法:
# 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()
错误:“Identifier not declared in the current scope”
常见原因:
- 使用变量前未声明
- 变量/函数名称拼写错误
- 尝试从错误的作用域访问变量
- onready 变量缺少 @ 符号
解决方法:
# 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
错误:“Invalid get index 'property_name' (on base: 'Type')”
常见原因:
- 属性名称拼写错误
- 该节点类型不存在此属性
- 节点为 null(未在场景树中找到)
解决方法:
# 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.")
运行时错误
错误:“Attempt to call function 'func_name' in base 'null instance' on a null instance”
常见原因:
- 在 null 引用上调用方法
- 访问前节点已被移除/释放
- @onready 变量引用了不存在的节点
解决方法:
# 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()
错误:“Invalid operands 'Type' and 'null' in operator '...'”
常见原因:
- 对 null 值执行数学运算
- 将 null 与类型化值进行比较
- 在计算中使用未初始化的变量
解决方法:
# 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
错误:“Index [number] out of range (size [size])”
常见原因:
- 访问超出数组长度的索引
- 使用了错误的索引变量
- 数组大小已更改,但代码仍按旧大小处理
解决方法:
# 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
场景树错误
错误:“Node not found: [path]”
常见原因:
- get_node() 或 $ 中的节点路径不正确
- 节点尚不存在(时机不对)
- 节点已被移除或重命名
- 路径的大小写不匹配
解决方法:
# 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"))
错误:“Can't change state while flushing queries”
常见原因:
- 在物理回调期间修改物理对象
- 遍历期间添加/移除节点
- 在错误的上下文中释放节点
解决方法:
# 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)
信号错误
错误:“Attempt to call an invalid function in base 'MethodBind'”
常见原因:
- 信号连接到了不存在的方法
- 方法签名与信号参数不匹配
- 方法名称拼写错误
解决方法:
# 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)
错误:“Signal 'signal_name' is already connected”
常见原因:
- 多次连接同一个信号
- 重新连接前未断开连接
- 单例多次调用 _ready()
解决方法:
# 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)
资源/文件错误
错误:“Cannot load resource at path: 'res://...' (error code)”
常见原因:
- 指定路径下不存在该文件
- 文件路径拼写错误
- 缺少文件扩展名或扩展名不正确
- 资源未正确导入
解决方法:
# 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()
错误:“Condition 'texture.is_null()' is true”
常见原因:
- 加载失败,但未检查错误
- 资源文件缺失或已损坏
- 资源类型不正确
解决方法:
# 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
性能问题
卡顿/停顿
常见原因:
- _process() 或 _physics_process() 调用过多
- 循环中执行了开销较大的操作
- 内存泄漏(未释放节点)
- 每帧触发的信号过多
调试步骤:
- 使用 Godot 分析器(Debug > Profiler)
- 检查代码中的性能热点
- 查看内存是否随时间增长
解决方法:
# 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
内存泄漏
错误:内存使用量持续增长
常见原因:
- 移除节点时未调用 queue_free()
- 循环引用导致无法进行垃圾回收
- 创建新对象时未释放旧对象
解决方法:
# 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)
调试技巧
打印调试
# 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!")
断点和单步调试
-
设置断点:在脚本编辑器中点击行号
-
启用调试运行:按 F5(或在启用调试器的情况下点击播放)
-
在断点处暂停时:
- 继续(F12):恢复执行
- 单步跳过(F10):执行当前行,跳过函数内部
- 单步进入(F11):进入函数调用
- 单步跳出:退出当前函数
-
检查变量:将鼠标悬停在变量上,或查看调试器面板
远程调试器
游戏运行时:
- 打开编辑器底部的 Debugger 选项卡
- 在 Errors 选项卡中查看运行时错误
- 查看 Profiler,排查性能问题
- 使用 Network Profiler 排查多人游戏问题
断言语句
# 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
调试绘制
# 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")
条件调试
# 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 特定问题
类型注解
# 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
节点路径变更
# 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
常见迁移问题
# 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))
何时启用此技能
在用户出现以下情况时启用:
- 报告错误消息
- 询问崩溃或意外行为
- 需要帮助理解错误输出
- 询问“为什么无法正常工作?”
- 提到调试、错误或 Bug
- 分享无法按预期工作的代码
- 询问性能问题
- 报告内存泄漏或崩溃
调试工作流
- 识别错误 - 仔细阅读错误消息
- 定位源头 - 找出导致问题的文件和行
- 理解原因 - 为什么会发生这种情况?
- 应用修复 - 修改代码以解决问题
- 测试解决方案 - 验证修复是否有效
- 向用户说明 - 帮助他们理解出了什么问题以及原因
协助调试时:
- 始终解释错误发生的原因
- 提供修正后的代码
- 建议预防类似问题的措施
- 为今后的问题推荐调试技巧
评论
支持 Markdown