第十二章:错误处理与调试
"调试是发现并修复bug的过程,而最好的调试是首先避免bug的产生。"
编写代码时,错误是不可避免的。本章将介绍GDScript中的错误处理机制和Godot强大的调试工具,帮助你更高效地发现和解决问题。
12.1 错误类型
12.1.1 语法错误
编译时发现,代码无法运行:
# 缺少冒号
func example() # 错误!
pass
# 缩进错误
func test():
print("hello") # 错误!
# 拼写错误
var player_health = 100
print(player_heath) # 错误!未定义变量
12.1.2 运行时错误
代码执行时发生:
# 空引用
var node = get_node("NonExistent") # 可能返回null
node.position = Vector2.ZERO # 错误!null没有position
# 数组越界
var arr = [1, 2, 3]
print(arr[10]) # 错误!索引越界
# 类型错误
var num: int = "hello" # 错误!类型不匹配
# 除零错误
var result = 10 / 0 # 错误!
12.1.3 逻辑错误
代码能运行但结果不正确:
# 逻辑错误示例
func calculate_damage(base: int, multiplier: float) -> int:
return base + multiplier # 应该是乘法!
# 条件错误
if health > 0: # 应该是 <= 0
die()
# 循环错误
for i in range(10):
print(i)
i += 1 # 无效!for循环中i会被覆盖
12.2 错误预防
12.2.1 类型注解
# 使用类型注解捕获错误
func calculate(value: int) -> float:
return value * 1.5
# 类型化数组
var enemies: Array[Enemy] = []
enemies.append("not an enemy") # 错误!类型不匹配
# 可选类型(可为null)
var target: Node = null # 明确表示可能为null
# 严格类型模式
var health: int = 100
health = "full" # 编译错误!
12.2.2 空值检查
# 检查节点是否存在
func safe_get_node(path: String) -> Node:
var node = get_node_or_null(path)
if node == null:
push_warning("节点不存在:" + path)
return node
# 使用前检查
var player = get_node_or_null("Player")
if player:
player.move()
# 使用is_instance_valid检查对象
if is_instance_valid(enemy):
enemy.take_damage(10)
12.2.3 断言(assert)
# 断言用于检查不应该发生的情况
func divide(a: float, b: float) -> float:
assert(b != 0, "除数不能为零")
return a / b
func set_health(value: int) -> void:
assert(value >= 0, "生命值不能为负")
assert(value <= max_health, "生命值超过最大值")
health = value
# 在开发时检查前置条件
func attack(target: Enemy) -> void:
assert(target != null, "目标不能为空")
assert(is_instance_valid(target), "目标已被销毁")
# 执行攻击...
# 注意:发布版本中assert可能被禁用
12.2.4 防御性编程
# 参数验证
func create_enemy(type: String, health: int) -> Enemy:
if type.is_empty():
push_error("敌人类型不能为空")
return null
if health <= 0:
push_warning("生命值应该大于0,使用默认值")
health = 100
# 创建敌人...
return enemy
# 边界检查
func get_item(index: int) -> Item:
if index < 0 or index >= items.size():
push_error("索引越界:%d" % index)
return null
return items[index]
# 状态验证
func start_game() -> void:
if is_game_running:
push_warning("游戏已经在运行")
return
# 开始游戏...
12.3 错误处理
12.3.1 错误报告函数
# 不同级别的错误报告
print("普通信息") # 白色
print_rich("[color=green]成功[/color]") # 富文本
push_warning("警告信息") # 黄色,显示在编辑器
push_error("错误信息") # 红色,显示在编辑器
# printerr直接输出到stderr
printerr("标准错误输出")
12.3.2 Result模式
# 使用字典返回结果和错误
func load_save_file(path: String) -> Dictionary:
if not FileAccess.file_exists(path):
return {"ok": false, "error": "文件不存在"}
var file = FileAccess.open(path, FileAccess.READ)
if file == null:
return {"ok": false, "error": "无法打开文件"}
var content = file.get_as_text()
var data = JSON.parse_string(content)
if data == null:
return {"ok": false, "error": "JSON解析失败"}
return {"ok": true, "data": data}
# 使用
var result = load_save_file("save.json")
if result.ok:
apply_save_data(result.data)
else:
show_error_message(result.error)
12.3.3 可选值模式
# 返回null表示失败
func find_enemy_by_name(name: String) -> Enemy:
for enemy in enemies:
if enemy.name == name:
return enemy
return null # 未找到
# 调用者检查
var enemy = find_enemy_by_name("Boss")
if enemy:
attack(enemy)
else:
print("未找到敌人")
# 使用get方法的默认值
var config = {"volume": 0.8}
var music_volume = config.get("music_volume", 1.0) # 使用默认值
12.4 调试工具
12.4.1 print调试
# 基本打印
print("变量值:", variable)
# 格式化打印
print("位置: (%d, %d)" % [x, y])
print("玩家 %s 的生命值: %d/%d" % [name, health, max_health])
# 打印对象信息
print("节点:", node, " 类型:", node.get_class())
# 打印调用栈
print_stack()
# 打印对象详细信息
print(inst_to_dict(object))
# 条件打印(避免发布版本输出)
if OS.is_debug_build():
print("调试信息:", debug_data)
12.4.2 断点调试
在编辑器中:
- 点击脚本编辑器行号左侧设置断点
- 运行游戏(F5)
- 程序会在断点处暂停
- 使用调试面板查看变量
# 代码中设置断点
func suspicious_function():
var data = calculate_something()
breakpoint # 程序会在这里暂停
process(data)
12.4.3 调试器面板
调试器选项卡:
| 标签 | 功能 |
|---|---|
| 堆栈变量 | 查看当前作用域的变量 |
| 堆栈跟踪 | 查看调用栈 |
| 断点 | 管理所有断点 |
| 错误 | 查看运行时错误 |
调试控制:
- 继续(F5):继续执行
- 单步跳过(F10):执行当前行,不进入函数
- 单步进入(F11):进入函数内部
- 单步跳出(Shift+F11):跳出当前函数
12.4.4 远程调试
# 查看运行时场景树
# 调试 → 远程场景树
# 监控实时变量
# 在监视面板添加表达式
# 实时修改属性
# 在远程检查器中直接修改值
12.5 性能分析
12.5.1 内置分析器
# 打开性能分析器
# 调试 → 监视器
# 关键指标
# - FPS: 帧率
# - Process: _process耗时
# - Physics Process: _physics_process耗时
# - Navigation Process: 导航耗时
# - 内存使用
# - 视频内存
12.5.2 代码计时
# 测量代码执行时间
func benchmark_function():
var start_time = Time.get_ticks_usec()
# 要测量的代码
expensive_operation()
var end_time = Time.get_ticks_usec()
var duration = (end_time - start_time) / 1000.0
print("执行时间:%.2f 毫秒" % duration)
# 简单计时类
class Timer:
var start_time: int
func start():
start_time = Time.get_ticks_usec()
func stop(label: String = "操作"):
var duration = (Time.get_ticks_usec() - start_time) / 1000.0
print("%s 耗时:%.2f ms" % [label, duration])
12.5.3 性能监控
# 获取性能数据
func _process(delta):
# 获取当前FPS
var fps = Engine.get_frames_per_second()
# 获取内存使用
var static_memory = OS.get_static_memory_usage()
var dynamic_memory = Performance.get_monitor(Performance.MEMORY_DYNAMIC)
# 获取渲染信息
var draw_calls = Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
var objects = Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)
if fps < 30:
push_warning("FPS过低:%d" % fps)
12.6 常见错误与解决
12.6.1 空引用错误
# 错误
var node = $NonExistentNode
node.position = Vector2.ZERO # 崩溃!
# 解决方案
var node = get_node_or_null("NonExistentNode")
if node:
node.position = Vector2.ZERO
# 或使用@onready确保节点存在
@onready var player = $Player # 在_ready时获取
# 检查is_instance_valid
func attack_enemy(enemy: Enemy):
if not is_instance_valid(enemy):
return
enemy.take_damage(10)
12.6.2 信号连接错误
# 错误:重复连接
func _ready():
$Button.pressed.connect(_on_pressed)
func _on_pressed():
# 每次点击都会添加一个新连接!
$Button.pressed.connect(_on_pressed) # 错误!
# 解决方案
func _on_pressed():
if not $Button.pressed.is_connected(_on_pressed):
$Button.pressed.connect(_on_pressed)
# 或使用CONNECT_ONE_SHOT
$Button.pressed.connect(_on_pressed, CONNECT_ONE_SHOT)
12.6.3 循环引用
# 错误:循环引用导致内存泄漏
class_name NodeA
var node_b: NodeB
class_name NodeB
var node_a: NodeA
# 解决方案:使用弱引用或手动断开
func _exit_tree():
if node_b:
node_b.node_a = null
node_b = null
12.6.4 场景树修改错误
# 错误:在遍历时修改
for child in get_children():
if should_remove(child):
child.queue_free() # 可能导致问题
# 解决方案1:收集后删除
var to_remove = []
for child in get_children():
if should_remove(child):
to_remove.append(child)
for child in to_remove:
child.queue_free()
# 解决方案2:倒序遍历
var children = get_children()
for i in range(children.size() - 1, -1, -1):
if should_remove(children[i]):
children[i].queue_free()
12.7 调试技巧
12.7.1 日志系统
# logger.gd (Autoload)
extends Node
enum Level { DEBUG, INFO, WARNING, ERROR }
var current_level: Level = Level.DEBUG
var log_to_file: bool = false
var log_file: FileAccess
func _ready():
if log_to_file:
var path = "user://game.log"
log_file = FileAccess.open(path, FileAccess.WRITE)
func debug(message: String, context: String = ""):
_log(Level.DEBUG, message, context)
func info(message: String, context: String = ""):
_log(Level.INFO, message, context)
func warning(message: String, context: String = ""):
_log(Level.WARNING, message, context)
push_warning(message)
func error(message: String, context: String = ""):
_log(Level.ERROR, message, context)
push_error(message)
func _log(level: Level, message: String, context: String):
if level < current_level:
return
var timestamp = Time.get_datetime_string_from_system()
var level_str = Level.keys()[level]
var full_message = "[%s][%s]%s %s" % [
timestamp,
level_str,
" [" + context + "]" if context else "",
message
]
print(full_message)
if log_file:
log_file.store_line(full_message)
log_file.flush()
# 使用
Logger.debug("玩家位置", "Player")
Logger.info("游戏开始")
Logger.warning("生命值较低")
Logger.error("存档损坏")
12.7.2 可视化调试
# 绘制调试信息
extends Node2D
@export var debug_draw: bool = true
func _draw():
if not debug_draw:
return
# 绘制碰撞区域
draw_circle(Vector2.ZERO, attack_range, Color(1, 0, 0, 0.3))
# 绘制方向
draw_line(Vector2.ZERO, velocity.normalized() * 50, Color.GREEN, 2)
# 绘制路径
if path.size() > 1:
for i in range(path.size() - 1):
draw_line(path[i], path[i + 1], Color.YELLOW, 1)
func _process(delta):
if debug_draw:
queue_redraw()
12.7.3 调试命令
# debug_console.gd
extends CanvasLayer
var commands: Dictionary = {}
func _ready():
register_command("god", _cmd_god_mode, "切换无敌模式")
register_command("give", _cmd_give_item, "给予物品")
register_command("tp", _cmd_teleport, "传送到位置")
register_command("spawn", _cmd_spawn, "生成敌人")
func register_command(name: String, callback: Callable, description: String):
commands[name] = {"callback": callback, "description": description}
func execute(input: String):
var parts = input.split(" ")
var cmd = parts[0]
var args = parts.slice(1)
if commands.has(cmd):
commands[cmd].callback.call(args)
else:
print("未知命令:", cmd)
func _cmd_god_mode(args: Array):
var player = get_tree().get_first_node_in_group("player")
if player:
player.invincible = not player.invincible
print("无敌模式:", "开" if player.invincible else "关")
12.8 第二部分总结
恭喜!你已经完成了《Godot游戏开发权威指南》第二部分:GDScript编程。
学习回顾:
| 章节 | 主要内容 |
|---|---|
| 第五章 | GDScript基础语法、关键字、运算符 |
| 第六章 | 变量、数据类型、类型系统 |
| 第七章 | 条件语句、循环、控制流程 |
| 第八章 | 函数定义、参数、返回值、lambda |
| 第九章 | 面向对象编程、类、封装 |
| 第十章 | 继承、多态、抽象类 |
| 第十一章 | 信号系统、事件驱动 |
| 第十二章 | 错误处理、调试工具 |
你已经掌握了:
- ✅ GDScript的完整语法
- ✅ 类型系统和变量管理
- ✅ 控制流程和函数设计
- ✅ 面向对象编程范式
- ✅ 继承和多态的运用
- ✅ 信号系统的使用
- ✅ 调试和错误处理技巧
下一步:
第三部分将进入2D游戏开发实战,你将学习:
- 场景和节点系统
- 精灵和动画
- 物理引擎
- TileMap
- 完整2D游戏开发
上一章:信号系统
下一部分:2D游戏开发