第十二章:错误处理与调试

第十二章:错误处理与调试

"调试是发现并修复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 断点调试

在编辑器中:

  1. 点击脚本编辑器行号左侧设置断点
  2. 运行游戏(F5)
  3. 程序会在断点处暂停
  4. 使用调试面板查看变量
# 代码中设置断点
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游戏开发

← 返回目录