14. 调试与数值设计
2026/4/13大约 5 分钟
调试与数值设计
游戏功能做完了,但可能还有很多 bug,数值也不一定平衡。本章教你如何高效地找到问题、调整数值。
使用 Godot 调试工具
Godot 内置了强大的调试工具:
| 工具 | 用途 | 打开方式 |
|---|---|---|
| 输出面板 | 查看代码中的 print() 输出 | 编辑器底部 → 输出 |
| 调试器 | 断点调试、查看变量值 | 运行时 → 编辑器底部 → 调试 |
| 性能分析器 | 查看帧率、内存、函数耗时 | 运行时 → 编辑器底部 → 性能 |
常用调试方法
C
using Godot;
// 1. GD.Print 输出(最简单)
public override void _PhysicsProcess(double delta)
{
GD.Print("玩家位置: ", Position, " 速度: ", Velocity);
}
// 2. GD.PushWarning / GD.PushError(分类输出)
public void TakeDamage(int amount)
{
if (Hp <= 0)
GD.PushWarning("角色 HP 已为负值!当前值: ", Hp);
}
// 3. 断言(条件不满足时报错)
public override void _Ready()
{
Debug.Assert(Hp > 0, "角色初始 HP 必须大于 0");
}GDScript
# 1. print 输出(最简单)
func _physics_process(delta):
print("玩家位置: ", position, " 速度: ", velocity)
# 2. push_warning / push_error(分类输出)
func take_damage(amount):
if hp <= 0:
push_warning("角色 HP 已为负值!当前值: ", hp)
# 3. 断言(条件不满足时报错)
func _ready():
assert(hp > 0, "角色初始 HP 必须大于 0")常见问题排查
| 问题 | 排查方法 |
|---|---|
| 角色穿墙 | 检查碰撞形状是否正确、移动速度是否过快 |
| 动画不切换 | 检查动画名是否拼写正确、是否调用了 play() |
| 音效不播放 | 检查节点路径、音量是否为 0、文件是否导入 |
| 场景切换崩溃 | 检查路径是否正确、目标场景是否存在 |
| 帧率低 | 用性能分析器查看哪些函数耗时最多 |
提示
按 F5 运行当前场景(只运行你正在编辑的),F6 运行主场景。调试时建议用 F5,启动更快。
数值设计——让游戏好玩又平衡
数值设计决定了游戏的"手感"和"难度"。核心原则:数值服务于体验。
基础数值表模板
| 参数 | 初始值 | 说明 |
|---|---|---|
| 玩家生命值 | 100 | 被攻击减少 |
| 玩家攻击力 | 10 | 每次攻击对敌人造成的伤害 |
| 玩家移动速度 | 200 像素/秒 | 太快不好控制,太慢没节奏感 |
| 玩家跳跃高度 | -400 | 负值代表向上 |
| 敌人生命值 | 30 | 决定玩家需要打几下 |
| 敌人移动速度 | 80 像素/秒 | 应该比玩家慢,给玩家反应时间 |
| 敌人攻击力 | 15 | 低于玩家 HP 的 1/5,给容错空间 |
| 金币得分 | 10 | 每个金币的分数 |
| 击杀得分 | 50 | 每消灭一个敌人的分数 |
难度曲线设计
好游戏的难度逐步上升:
难度
↑
│ ★ 第5关 Boss
│ ★
│ ★
│ ★
│ ★
│★ 第1关
└──────────────────→ 关卡- 前 3 关:让玩家熟悉操作,敌人少且弱
- 中间关卡:引入新敌人类型和新机制
- 后期关卡:敌人组合、时间压力、复杂地形
- Boss 关:每 3-5 关一个 Boss 战,给玩家成就感
常见数值错误
- 敌人太强:新手第一关就被秒杀,直接劝退
- 敌人太弱:闭着眼都能过,没有挑战感
- 数值膨胀:后期数值指数增长,前期道具完全没用
- 随机性过大:伤害波动 ±50%,玩家感觉胜负全靠运气
手感优化——从"能玩"到"好玩"
"手感"是玩家操作时的反馈体验,好的手感让玩家觉得操作"流畅"、"有力"。
| 优化点 | 方法 |
|---|---|
| 跳跃手感 | 添加"土狼时间"(Coyote Time):离开平台边缘后 0.1 秒内仍可跳跃 |
| 打击感 | 被击中时加 0.05 秒"卡顿"(Hit Stop),让玩家感到"打中了" |
| 操作缓冲 | 落地前 0.1 秒按跳跃键,落地后自动执行跳跃 |
| 屏幕震动 | 受到攻击或大爆炸时,相机短暂震动 |
土狼时间(Coyote Time)
C
using Godot;
private double _coyoteTime = 0.1;
private double _coyoteTimer = 0.0;
public override void _PhysicsProcess(double delta)
{
if (IsOnFloor())
_coyoteTimer = _coyoteTime;
else
_coyoteTimer -= delta;
if (Input.IsActionJustPressed("jump") && _coyoteTimer > 0)
{
Velocity = new Vector2(Velocity.X, JumpVelocity);
_coyoteTimer = 0;
}
}GDScript
var coyote_time = 0.1
var coyote_timer = 0.0
func _physics_process(delta):
if is_on_floor():
coyote_timer = coyote_time
else:
coyote_timer -= delta
if Input.is_action_just_pressed("jump") and coyote_timer > 0:
velocity.y = JUMP_VELOCITY
coyote_timer = 0打击卡顿(Hit Stop)
C
using Godot;
public async void HitStop(double duration = 0.05)
{
var tree = GetTree();
tree.Paused = true;
await ToSignal(tree.CreateTimer(duration), SceneTreeTimer.SignalName.Timeout);
tree.Paused = false;
}GDScript
func hit_stop(duration := 0.05):
var tree = get_tree()
tree.paused = true
await tree.create_timer(duration).timeout
tree.paused = false屏幕震动
C
using Godot;
public void ScreenShake(double intensity = 5.0, double duration = 0.2)
{
var camera = GetViewport().GetCamera2D();
if (camera != null)
{
var tween = CreateTween();
var originalOffset = camera.Offset;
for (int i = 0; i < 4; i++)
{
camera.Offset = new Vector2(
(float)GD.RandRange(-intensity, intensity),
(float)GD.RandRange(-intensity, intensity)
);
tween.TweenInterval(duration / 4.0);
}
tween.TweenProperty(camera, "offset", originalOffset, 0.05);
}
}GDScript
func screen_shake(intensity := 5.0, duration := 0.2):
var camera = get_viewport().get_camera_2d()
if camera:
var tween = create_tween()
var original_offset = camera.offset
for i in range(4):
camera.offset = Vector2(
randf_range(-intensity, intensity),
randf_range(-intensity, intensity)
)
tween.tween_interval(duration / 4)
tween.tween_property(camera, "offset", original_offset, 0.05)存档系统
让玩家可以保存和加载进度:
C
using Godot;
using System.Collections.Generic;
private const string SavePath = "user://save_game.json";
public void SaveGame()
{
var data = new Dictionary<string, Variant>
{
{ "level", _currentLevel },
{ "score", _score },
{ "hp", _player.Hp },
{ "coins", _player.Coins },
};
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
file.StoreString(Json.Stringify(data));
}
public void LoadGame()
{
if (FileAccess.FileExists(SavePath))
{
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read);
var data = (Dictionary<string, Variant>)Json.ParseString(file.GetAsText());
_currentLevel = (int)data["level"];
_score = (int)data["score"];
}
}GDScript
const SAVE_PATH = "user://save_game.json"
func save_game():
var data = {
"level": current_level,
"score": score,
"hp": player.hp,
"coins": player.coins,
}
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(data))
func load_game():
if FileAccess.file_exists(SAVE_PATH):
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
var data = JSON.parse_string(file.get_as_text())
current_level = data["level"]
score = data["score"]下一章
游戏调试和数值打磨好了,准备发布。
→ 15. 导出发布
