10. 打磨与发布
2026/4/14大约 5 分钟
10. 打磨与发布
10.1 什么是"打磨"?
打磨是游戏开发的最后阶段——功能都做完了,但游戏还不够"好玩"。打磨的目标是让每一个细节都能给玩家更好的感受。
横版动作游戏重点打磨的方向:
| 方向 | 具体内容 |
|---|---|
| 手感 | 移动是否流畅?跳跃落地有没有回弹感? |
| 打击感 | 连招节奏、Hit Stop、屏幕震动是否到位 |
| 关卡节奏 | 战斗区域密度是否合理,有没有喘息空间 |
| UI反馈 | 血条变化、倒地救援提示是否清晰 |
| Boss体验 | Boss登场是否震撼,二阶段变化是否明显 |
10.2 关卡节奏设计
一个好关卡不是一直打打打,需要有高潮和喘息:
[入口] → [小怪区域A] → [宝箱/回复] → [小怪+精英] → [Boss前缓冲] → [Boss]
↓ 紧张 ↓ 放松 ↓ 紧张 ↓ 准备 ↓ 高潮关卡结构配置
C
using Godot;
/// <summary>
/// 关卡配置——定义一关的完整结构
/// </summary>
[GlobalClass]
public partial class StageConfig : Resource
{
[Export] public int StageNumber { get; set; } = 1;
[Export] public string StageName { get; set; } = "第一关";
[ExportGroup("战斗区域")]
[Export] public int NormalZoneCount { get; set; } = 3; // 普通战斗区域数量
[Export] public int EliteZoneCount { get; set; } = 1; // 精英区域数量
[Export] public bool HasBossRoom { get; set; } = true;
[ExportGroup("难度")]
[Export] public float EnemyHpScale { get; set; } = 1.0f;
[Export] public float EnemyAttackScale { get; set; } = 1.0f;
[Export] public int BossBaseHp { get; set; } = 500;
[ExportGroup("奖励")]
[Export] public int ExpReward { get; set; } = 100;
[Export] public int GoldReward { get; set; } = 50;
}GDScript
class_name StageConfig
extends Resource
## 关卡配置——定义一关的完整结构
@export var stage_number: int = 1
@export var stage_name: String = "第一关"
@export_group("战斗区域")
@export var normal_zone_count: int = 3
@export var elite_zone_count: int = 1
@export var has_boss_room: bool = true
@export_group("难度")
@export var enemy_hp_scale: float = 1.0
@export var enemy_attack_scale: float = 1.0
@export var boss_base_hp: int = 500
@export_group("奖励")
@export var exp_reward: int = 100
@export var gold_reward: int = 5010.3 存档系统
横版动作游戏的存档比RPG简单——只需要记录已通关数据和最高成绩,不需要记录玩家位置:
C
using Godot;
using System.Text.Json;
/// <summary>
/// 地下城与勇士存档管理器
/// </summary>
public partial class DnfSaveManager : Node
{
public static DnfSaveManager Instance { get; private set; }
private const string SavePath = "user://dnf_save.json";
public class SaveData
{
public int HighestStageCleared { get; set; } = 0; // 最高通关关卡
public int TotalKills { get; set; } = 0; // 击杀总数
public int TotalPlaySeconds { get; set; } = 0; // 总游戏时间(秒)
public int BestClearTimeMs { get; set; } = 0; // 最佳通关时间(毫秒)
}
public override void _EnterTree() => Instance = this;
public override void _ExitTree() { if (Instance == this) Instance = null; }
public void SaveGame(SaveData data)
{
var json = JsonSerializer.Serialize(data,
new JsonSerializerOptions { WriteIndented = true });
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
if (file != null) { file.StoreString(json); file.Close(); }
}
public SaveData LoadGame()
{
if (!FileAccess.FileExists(SavePath)) return new SaveData();
var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read);
if (file == null) return new SaveData();
var json = file.GetAsText(); file.Close();
try { return JsonSerializer.Deserialize<SaveData>(json); }
catch { return new SaveData(); }
}
public bool HasSave() => FileAccess.FileExists(SavePath);
}GDScript
extends Node
class_name DnfSaveManager
## 地下城与勇士存档管理器
static var instance: DnfSaveManager
const SAVE_PATH: String = "user://dnf_save.json"
class SaveData:
var highest_stage_cleared: int = 0 # 最高通关关卡
var total_kills: int = 0 # 击杀总数
var total_play_seconds: int = 0 # 总游戏时间
var best_clear_time_ms: int = 0 # 最佳通关时间
func to_dict() -> Dictionary:
return {
"highest_stage_cleared": highest_stage_cleared,
"total_kills": total_kills,
"total_play_seconds": total_play_seconds,
"best_clear_time_ms": best_clear_time_ms,
}
static func from_dict(d: Dictionary) -> SaveData:
var sd = SaveData.new()
sd.highest_stage_cleared = d.get("highest_stage_cleared", 0)
sd.total_kills = d.get("total_kills", 0)
sd.total_play_seconds = d.get("total_play_seconds", 0)
sd.best_clear_time_ms = d.get("best_clear_time_ms", 0)
return sd
func _enter_tree() -> void: instance = self
func _exit_tree() -> void:
if instance == self: instance = null
func save_game(data: SaveData) -> void:
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file:
file.store_string(JSON.stringify(data.to_dict(), " "))
file.close()
func load_game() -> SaveData:
if not FileAccess.file_exists(SAVE_PATH):
return SaveData.new()
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
if not file:
return SaveData.new()
var json = JSON.new()
if json.parse(file.get_as_text()) != OK:
return SaveData.new()
file.close()
return SaveData.from_dict(json.data)
func has_save() -> bool:
return FileAccess.file_exists(SAVE_PATH)10.4 数值平衡
横版动作游戏的数值比RPG简单,但也需要仔细调整:
各关卡推荐基础数值
| 关卡 | 普通怪血量 | 普通怪攻击 | 精英怪血量 | Boss血量(1人) |
|---|---|---|---|---|
| 第1关 | 50 | 10 | 150 | 500 |
| 第2关 | 80 | 15 | 240 | 800 |
| 第3关 | 120 | 22 | 360 | 1200 |
| 第4关 | 170 | 30 | 510 | 1700 |
| 第5关 | 230 | 40 | 690 | 2300 |
动态难度下的最终数值
上表是1人数值,人数增加后由 DifficultyManager 自动缩放:
最终血量 = 基础血量 × 人数缩放倍率
最终攻击 = 基础攻击 × 人数缩放倍率
示例:第3关 Boss,4人同玩
Boss血量 = 1200 × 1.6 = 1920
Boss攻击 = (根据设计保持不变,不缩放攻击力以免虐杀单人玩家)10.5 发布前检查清单
功能测试
| 检查项 | 说明 |
|---|---|
| 1人通关测试 | 单人能否正常完成全部关卡 |
| 4人同玩测试 | 4人同屏是否有卡顿、碰撞异常 |
| 玩家中途加入 | 第2名玩家在战斗中加入是否正常 |
| 全员倒地 | 全部倒地后是否正确触发游戏结束 |
| Boss阶段变化 | Boss进入第二阶段时特效和行为是否正确 |
| 存档读取 | 通关后存档,重启游戏是否正确读取 |
性能测试
| 检查项 | 目标 |
|---|---|
| 帧率 | 4人同玩时稳定 60 fps |
| 内存 | 通关完整一关后内存无明显增长 |
| 音效 | 多人同时攻击时音效不卡顿 |
发布前清理
☑ 关闭所有 GD.Print() / print() 调试输出
☑ 删除测试用的无敌/秒杀代码
☑ 检查 Export 变量在编辑器中都已正确填写
☑ 测试在无手柄情况下键盘能否正常操作10.6 导出平台
| 平台 | 注意事项 |
|---|---|
| Windows | 最简单,直接导出 .exe |
| macOS | 需要签名证书,使用 Notarization |
| Linux | 测试主流发行版(Ubuntu、Fedora) |
| Android | 需要适配触屏虚拟摇杆,手柄仍然支持 |
| Web | 注意包体大小,音频格式用 .ogg |
10.7 全系列回顾
恭喜完成了地下城与勇士横版动作游戏的全部10章!
| 章节 | 内容 |
|---|---|
| 1. 核心玩法 | 横版清关循环、四职业、动态难度设计 |
| 2. 项目搭建 | 目录结构、4人输入映射、GameManager |
| 3. 角色系统 | PlayerBase 移动、翻滚无敌帧、倒地救援 |
| 4. 战斗系统 | Hitbox/Hurtbox 伤害判定、DifficultyManager |
| 5. 关卡设计 | BattleZone 战斗区域、多玩家摄像机 |
| 6. 敌人AI | 三状态状态机、追击最近玩家 |
| 7. 多人系统 | PlayerManager、动态难度实时响应 |
| 8. Boss战 | BossBase 多阶段、Boss血条HUD |
| 9. 音效打击感 | 连招音效、Hit Stop、屏幕震动 |
| 10. 打磨发布 | 关卡配置、存档、数值平衡、导出 |
从实时战斗到多人联机,你已经掌握了横版动作游戏的核心技术。接下来可以继续扩展:加入更多职业、设计更多关卡,或者挑战实现在线联机模式!
