Godot 3D 的完整流程走到这里,基本就差两块拼图:玩家进入游戏看到的第一个画面(主页),以及教会玩家基础操作的引导流程。这篇是系列第 30 节,专门把“游戏引导”和“游戏主页”放在同一套工程里解决。
本文基于 Godot 4.x 和 GDScript 2.0 编写,不引入任何第三方插件。你可以从这套例子里看到“主菜单 → 教学关卡 → 正式关卡 → 暂停返回主页”的最小闭环是怎么组织的。文中的所有脚本都可以直接复制到你的项目里改着用,不需要额外安装资源包。
核心特点先摆出来:
- 主页使用 Control 节点 + 信号(Signal)组织,按钮点击、场景切换都在脚本里显式连接;
- 新手引导用 3D 场景中的 Area3D 区域触发,配合 CanvasLayer 上的提示面板展示引导文案;
- 引导完成状态写入本地存档(ConfigFile),二次进游戏直接跳过引导;
- 暂停菜单监听
ui_cancel输入,支持恢复游戏和返回主页。
这篇文章会带你把项目初始化、输入映射、主菜单、全局状态、教学关卡、暂停菜单全部跑通。读完你就能在自己的 Godot 3D 工程里接入一套可扩展的入口与引导流程。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 适用引擎 | Godot 4.x,GDScript 2.0 |
| 项目类型 | 3D 游戏入口流程 |
| 核心功能 | 主菜单、新手引导、本地存档、暂停菜单 |
| 运行方式 | Godot 编辑器 F5 直接运行,或导出桌面 / 移动端 |
| 外部依赖 | 无第三方插件,全部使用引擎内置节点 |
| 预计代码量 | 5 个 GDScript 脚本,核心逻辑约 250 行 |
| 技术难度 | 入门到初级,适合学习场景切换与 UI 交互 |
| 扩展点 | 替换正式美术资源、增加多语言、接入手柄输入、扩展引导分支 |
这套方案的优势是结构简单:主页场景只管入口,全局单例只管存档,教学关卡只管引导,互相之间通过场景切换和信号协作。后面要加设置菜单、成就系统、剧情演出,都能顺着这套结构继续扩展。
2. 适用场景与使用边界
这套“引导 + 主页”方案最适合以下场景:
- 独立游戏原型,需要快速搭出“点开始 → 学操作 → 进关卡”的流程;
- 3D 教学项目,用来演示 Godot 的场景切换、Area3D 区域检测、CanvasLayer UI 分层;
- 中小型游戏的主菜单,需要一个干净、稳定、随时能换皮肤的入口界面。
它能解决的问题也很明确:玩家进入游戏的第一个画面;玩家对移动、跳跃等操作一无所知时的引导提示;引导完成后的状态保存;游戏进行中按 Esc 暂停并返回主页。
不过要注意边界:
- 这套引导是线性流程,一次只推进一个步骤。如果要做多分支、多条件并行的复杂引导,需要改成“任务系统”,本文的 TutorialDirector 只是最小实现。
- 本地存档只适合保存本机进度。如果游戏有排行榜、云存档需求,要走服务端方案。
- 引导文案、字体、模型、图标素材都要确认授权。Godot 4 默认字体对中文支持有限,需要在项目里导入开源中文字体(例如思源黑体、文泉驿)并设置给 UI 控件。
3. 环境准备与项目初始化
开始写脚本之前,先准备环境。
3.1 安装 Godot 4.x
从 Godot 官网下载 Godot 4.x 稳定版,标准版即可,不需要 .NET 版(本文不用 C#)。下载后直接解压运行,项目管理器会弹出。
3.2 创建 3D 项目
打开项目管理器,点击“新建”,项目名称填GameDemo,渲染器选择Forward+(桌面平台默认),路径任选一个本地目录。创建完成后,Godot 会自动生成一个包含Node3D根节点的场景。
我在工程里习惯按这个目录结构组织资源:
res:// ├── scenes/ │ ├── main_menu.tscn │ ├── tutorial_level.tscn │ └── main_level.tscn ├── scripts/ │ ├── game_state.gd │ ├── main_menu.gd │ ├── player_controller.gd │ ├── tutorial_overlay.gd │ ├── tutorial_director.gd │ └── pause_menu.gd └── assets/ ├── fonts/ └── models/在 Godot 编辑器的文件系统面板里建好这些目录,后续场景文件一律放scenes/,脚本放scripts/。
3.3 配置输入映射
Godot 4 的输入映射在“项目设置 → 输入映射”中配置。我们需要给玩家控制器添加五个动作:WASD 移动和空格跳跃。ui_cancel(Esc)是引擎内置动作,不需要自己添加。
| 动作名称 | 绑定按键 | 作用 |
|---|---|---|
| move_left | A | 向左移动 |
| move_right | D | 向右移动 |
| move_forward | W | 向前移动 |
| move_back | S | 向后移动 |
| jump | 空格 | 跳跃 |
配置方法:
- 打开“项目 → 项目设置”;
- 切到“输入映射”标签;
- 在左侧输入框输入
move_left,点“添加”; - 展开刚添加的动作,点“+”添加按键,弹出窗口中按下 A 键;
- 按同样方法添加
move_right、move_forward、move_back、jump。
3.4 设置 Autoload 单例
全局状态GameState需要用 Autoload 注册,这样任何场景都能访问它,而不用手动传递引用。
操作路径:“项目 → 项目设置 → 全局 → AutoLoad”。
点击路径输入框,填入:
res://scripts/game_state.gd点击“添加”,节点名称保持默认的GameState即可。这个脚本接下来会在第 5 节创建。
4. 游戏主页场景制作
主页(主菜单)是一个 Control 场景,包含标题、开始按钮、设置按钮、退出按钮。这个场景只负责一个事情:告诉玩家“游戏入口在这,点这个继续”。
4.1 创建 MainMenu 场景
在scenes/目录下新建场景,根节点类型选Control,命名为MainMenu,保存为main_menu.tscn。
场景树结构如下:
MainMenu (Control) ├── CanvasLayer │ └── Control (全屏) │ ├── ColorRect (背景) │ └── CenterContainer │ └── VBoxContainer │ ├── Label (游戏标题) │ ├── StartButton │ ├── SettingsButton │ └── QuitButton按下面的步骤在编辑器里搭:
- 在根节点
MainMenu下添加CanvasLayer; - 在
CanvasLayer下添加Control,把它的锚点铺满全屏(Anchors Preset 选 Full Rect); - 在
Control下添加ColorRect做背景,同样铺满全屏,颜色选深色; - 添加
CenterContainer,铺满全屏; - 在
CenterContainer下添加VBoxContainer,设置 Separation 为 20; - 在
VBoxContainer下依次添加Label和三个Button。
标题 Label 的文本填你的游戏名,字号在检查器里调到 48 左右。按钮文本分别设为“开始游戏”“设置”“退出”。
4.2 主菜单脚本
给根节点MainMenu挂脚本scripts/main_menu.gd。这个脚本的作用是连接三个按钮的信号,并决定“开始游戏”是进入教学关卡还是直接进入正式关卡。
extends Control @onready var start_button: Button = %StartButton @onready var settings_button: Button = %SettingsButton @onready var quit_button: Button = %QuitButton func _ready() -> void: start_button.pressed.connect(_on_start_pressed) settings_button.pressed.connect(_on_settings_pressed) quit_button.pressed.connect(_on_quit_pressed) if GameState.has_seen_tutorial(): start_button.text = "继续游戏" func _on_start_pressed() -> void: if GameState.has_seen_tutorial(): get_tree().change_scene_to_file("res://scenes/main_level.tscn") else: get_tree().change_scene_to_file("res://scenes/tutorial_level.tscn") func _on_settings_pressed() -> void: # 设置面板可以在这里打开一个自定义弹窗,本文先留占位 print("打开设置面板") func _on_quit_pressed() -> void: get_tree().quit()注意几个细节:
- 按钮节点用了
%StartButton这种唯一名称语法,需要在检查器里给每个按钮开启“Editable Children”并确认节点名和代码一致; - 如果不想用
%唯一名称,可以改成$CanvasLayer/Control/CenterContainer/VBoxContainer/StartButton这种完整路径; - 如果
GameState没有注册为全局单例,脚本第一行GameState.has_seen_tutorial()就会直接报错,所以第 3 节的 Autoload 配置必须提前完成。
到这里,按 F5 运行项目,你应该能看到一个简单的主菜单,点击“退出”会退出游戏;“开始游戏”暂时因为tutorial_level.tscn和main_level.tscn还不存在而报错,这是正常的,我们在第 6 节把教学场景建出来。
5. 全局状态与本地存档
主菜单里的“是否看过引导”这个判断,来自全局单例GameState。它负责两件事:在内存中保存引导完成标记;把标记写入本地配置文件,让游戏重启后依然有效。
Godot 的user://路径对应系统为当前应用分配的存档目录。桌面平台上,Windows 通常在AppData/Roaming/Godot/app_userdata/项目名/下,Linux 和 macOS 也有对应的用户数据目录。在编辑器里按 F5 运行时会直接映射到本地,导出后也一样,不需要处理绝对路径。
创建scripts/game_state.gd:
extends Node const SAVE_PATH := "user://game_state.cfg" var tutorial_completed := false func _ready() -> void: _load_state() func has_seen_tutorial() -> bool: return tutorial_completed func complete_tutorial() -> void: tutorial_completed = true _save_state() func _load_state() -> void: var config := ConfigFile.new() var err := config.load(SAVE_PATH) if err == OK: tutorial_completed = config.get_value("game", "tutorial_completed", false) func _save_state() -> void: var config := ConfigFile.new() config.set_value("game", "tutorial_completed", tutorial_completed) var err := config.save(SAVE_PATH) if err != OK: push_warning("无法保存游戏状态,错误码:%s" % err)这段逻辑的核心是:
_ready()在游戏启动时读取存档;has_seen_tutorial()供主菜单判断走哪条路;complete_tutorial()在引导完成时调用;ConfigFile是 Godot 内置的键值对配置文件,读写格式类似 INI。
以后要增加音量、画质、语言等设置项,都在这个单例里加,保存结构统一用config.set_value(section, key, value)。
6. 新手引导场景实现
新手引导我做成一个简短的教学关卡:玩家出生在一片地面上,先移动到蓝色光圈,再跳上高台,到达第二个光圈后引导完成,自动进入正式关卡。
6.1 创建 TutorialLevel 场景
新建场景,根节点类型选Node3D,命名为TutorialLevel,保存为tutorial_level.tscn。
场景树如下:
TutorialLevel (Node3D) ├── WorldEnvironment ├── DirectionalLight3D ├── Player (CharacterBody3D) │ ├── CollisionShape3D │ └── MeshInstance3D ├── Ground (CSGBox3D) ├── MoveTargetArea (Area3D) │ ├── CollisionShape3D │ └── MeshInstance3D ├── JumpPlatform (CSGBox3D) ├── JumpTargetArea (Area3D) │ ├── CollisionShape3D │ └── MeshInstance3D ├── TutorialOverlay (CanvasLayer) │ └── Control │ └── PanelContainer │ └── VBoxContainer │ ├── TitleLabel │ ├── ContentLabel │ └── SkipButton └── TutorialDirector (Node)在编辑器里按顺序操作:
- 添加
WorldEnvironment,新建一个Environment资源,背景颜色可以设成浅蓝或灰色; - 添加
DirectionalLight3D,调整方向,保证场景有光照; - 添加
Ground,在检查器里调整Size为(20, 1, 20),勾选Use Collision; - 添加
Player,类型选CharacterBody3D,给它挂第 6.2 节的玩家脚本; - 在
Player下添加CollisionShape3D,形状选CapsuleShape3D,调整半径和高度; - 在
Player下添加MeshInstance3D,网格选CapsuleMesh或BoxMesh,给一个材质方便区分方向; - 添加
JumpPlatform,CSGBox3D,Size 设为(4, 2, 4),位置Y设为 1,勾选Use Collision,让玩家能跳上去; - 移动目标和跳跃目标各加一个
Area3D,下面挂CollisionShape3D(形状选BoxShape3D),大小约为(2, 2, 2); - 两个 Area3D 下的
MeshInstance3D可以用BoxMesh,材质设置为半透明蓝色,方便玩家看到目标点。
半透明材质的设置方式是在检查器中新建StandardMaterial3D,把Transparency设为Alpha,Albedo Color的 Alpha 调低到 0.4 左右。
6.2 玩家控制器脚本
给Player挂scripts/player_controller.gd:
extends CharacterBody3D @export var speed := 5.0 @export var jump_velocity := 4.5 var gravity := ProjectSettings.get_setting("physics/3d/default_gravity") func _physics_process(delta: float) -> void: if not is_on_floor(): velocity.y -= gravity * delta var input := Input.get_vector("move_left", "move_right", "move_forward", "move_back") var direction := (transform.basis * Vector3(input.x, 0, input.y)).normalized() if direction: velocity.x = direction.x * speed velocity.z = direction.z * speed else: velocity.x = move_toward(velocity.x, 0.0, speed) velocity.z = move_toward(velocity.z, 0.0, speed) if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = jump_velocity move_and_slide()说明:
Input.get_vector接收四个动作名,分别对应负 X、正 X、负 Y、正 Y;- 这里把 2D 输入向量映射到 3D 的 XZ 平面,
move_forward对应 Z 轴负方向; transform.basis * Vector3(...)保证方向跟随角色朝向,如果以后做摄像机跟随,这个设计会更有用。
6.3 引导提示面板
TutorialOverlay是 CanvasLayer,负责显示引导标题、正文和跳过按钮。挂载scripts/tutorial_overlay.gd:
extends CanvasLayer @onready var panel := $Control/PanelContainer @onready var title_label := $Control/PanelContainer/VBoxContainer/TitleLabel @onready var content_label := $Control/PanelContainer/VBoxContainer/ContentLabel @onready var skip_button := $Control/PanelContainer/VBoxContainer/SkipButton func _ready() -> void: panel.hide() skip_button.pressed.connect(_on_skip_pressed) func show_hint(title: String, content: String) -> void: title_label.text = title content_label.text = content panel.show() func hide_hint() -> void: panel.hide() func _on_skip_pressed() -> void: GameState.complete_tutorial() get_tree().change_scene_to_file("res://scenes/main_level.tscn")注意,这里的跳过按钮直接调用全局GameState.complete_tutorial(),所以玩家无论在哪个引导步骤选择“跳过”,下次进入主菜单都会显示“继续游戏”,并跳过教学关卡。
6.4 引导导演脚本
TutorialDirector是引导的大脑,挂在场景末端的Node上。它配置了两个区域触发步骤:走到 MoveTargetArea → 跳上 JumpTargetArea → 完成引导。
extends Node signal tutorial_finished @export var overlay: CanvasLayer @export var player: CharacterBody3D @export var move_target_area: Area3D @export var jump_target_area: Area3D enum Step { MOVE, JUMP, DONE } var current_step := Step.MOVE func _ready() -> void: assert(overlay != null, "overlay 未设置") assert(player != null, "player 未设置") assert(move_target_area != null, "move_target_area 未设置") assert(jump_target_area != null, "jump_target_area 未设置") overlay.show_hint("移动引导", "使用 WASD 控制角色,走到蓝色光圈中。") move_target_area.body_entered.connect(_on_move_area_body_entered) jump_target_area.body_entered.connect(_on_jump_area_body_entered) func _on_move_area_body_entered(body: Node3D) -> void: if body == player and current_step == Step.MOVE: current_step = Step.JUMP overlay.show_hint("跳跃引导", "按空格键跳跃,跳到高台上。") move_target_area.queue_free() func _on_jump_area_body_entered(body: Node3D) -> void: if body == player and current_step == Step.JUMP: current_step = Step.DONE overlay.show_hint("引导完成", "你已经掌握基础操作,即将进入正式关卡。") await get_tree().create_timer(1.5).timeout _finish() func _finish() -> void: tutorial_finished.emit() GameState.complete_tutorial() get_tree().change_scene_to_file("res://scenes/main_level.tscn")脚本里几个关键点:
signal tutorial_finished可以留给外部监听;body_entered信号只在Area3D检测到物理刚体或CharacterBody3D进入时触发;move_target_area.queue_free()在第一步完成后销毁光圈,防止重复触发;await get_tree().create_timer(1.5).timeout让完成提示显示 1.5 秒再切场景;- 场景切换前先调用
GameState.complete_tutorial()保存状态。
把TutorialDirector节点的overlay、player、move_target_area、jump_target_area四个导出变量在检查器中拖拽赋值。这一步不赋值,运行时会直接断言报错。
6.5 正式关卡占位
第 4 节和第 6 节的change_scene_to_file都指向res://scenes/main_level.tscn。这个场景不需要复杂内容,可以在scenes/下新建一个简单 3D 场景,放一块地面、一个灯光和一个Label3D,显示“正式关卡”。
如果没有这个场景,点击“开始游戏”或完成引导时会报错:
Failed loading resource res://scenes/main_level.tscn所以一定要先建一个占位场景。
7. 暂停菜单与返回主页
游戏进行中需要支持暂停和返回主页。暂停菜单我单独做成一个CanvasLayer场景,放在scenes/pause_menu.tscn。
7.1 场景结构
PauseMenu (CanvasLayer) └── Control └── PanelContainer └── VBoxContainer ├── Label (暂停) ├── ResumeButton ├── MenuButton └── QuitButton将PauseMenu作为子节点加到main_level.tscn和tutorial_level.tscn中。
7.2 暂停脚本
给根节点挂scripts/pause_menu.gd:
extends CanvasLayer var is_paused := false @onready var pause_panel := $Control/PanelContainer @onready var resume_button := $Control/PanelContainer/VBoxContainer/ResumeButton @onready var menu_button := $Control/PanelContainer/VBoxContainer/MenuButton @onready var quit_button := $Control/PanelContainer/VBoxContainer/QuitButton func _ready() -> void: process_mode = Node.PROCESS_MODE_ALWAYS pause_panel.hide() resume_button.pressed.connect(_on_resume_pressed) menu_button.pressed.connect(_on_menu_pressed) quit_button.pressed.connect(_on_quit_pressed) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): _toggle_pause() func _toggle_pause() -> void: is_paused = not is_paused get_tree().paused = is_paused pause_panel.visible = is_paused func _on_resume_pressed() -> void: _toggle_pause() func _on_menu_pressed() -> void: get_tree().paused = false get_tree().change_scene_to_file("res://scenes/main_menu.tscn") func _on_quit_pressed() -> void: get_tree().paused = false get_tree().quit()这段脚本的关键是:
process_mode = Node.PROCESS_MODE_ALWAYS,保证场景树暂停时,暂停菜单依然能接收输入和按钮点击;ui_cancel默认绑定 Esc 键,不需要额外配置输入映射;- 返回主页前必须把
get_tree().paused重置为false,否则新场景一进来就是暂停状态。
8. 功能测试与效果验证
场景搭完后,按 F5 从main_menu.tscn运行,按下面的清单验证流程。
8.1 主菜单验证
- 预期:看到深色背景、游戏标题、“开始游戏”“设置”“退出”三个按钮。
- 点击“退出”,游戏进程结束。
- 如果按钮没有反应,检查脚本中按钮节点的路径和名称是否一致。
8.2 教学引导验证
- 首次运行主菜单,点击“开始游戏”,进入
tutorial_level.tscn。 - 界面右上角或底部出现引导面板,文案是“移动引导”。
- 用 WASD 控制角色走到蓝色光圈,光圈消失,引导面板切换为“跳跃引导”。
- 按空格跳上高台,进入第二个光圈,引导面板显示“引导完成”。
- 1.5 秒后自动切到
main_level.tscn。
8.3 存档验证
- 完成引导后回到主菜单,点击“开始游戏”,按钮文本应显示“继续游戏”,直接进入
main_level.tscn。 - 重启游戏,主菜单“开始游戏”依然显示“继续游戏”。
- 如果想重新测试引导流程,删除存档文件
user://game_state.cfg,或者临时改GameState里的tutorial_completed := false。
8.4 暂停菜单验证
- 进入正式关卡后按 Esc,游戏暂停,暂停面板显示。
- 点“恢复游戏”或者再次按 Esc,游戏继续。
- 点“返回主页”,回到主菜单。
- 如果按 Esc 没反应,检查
PauseMenu是否被添加到当前场景,以及脚本process_mode是否设置。
8.5 预期结果汇总
| 测试项 | 操作 | 预期结果 |
|---|---|---|
| 主菜单显示 | F5 运行 | 标题和按钮正常渲染 |
| 首次开始游戏 | 点击开始 | 进入教学场景 |
| 移动引导 | WASD 走到光圈 | 提示切换为跳跃引导 |
| 跳跃引导 | 空格跳上高台 | 显示引导完成 |
| 存档生效 | 重新进入主菜单 | 开始按钮变为继续游戏 |
| 暂停 | 按 Esc | 游戏暂停,面板显示 |
| 返回主页 | 点击返回主页 | 回到主菜单且暂停重置 |
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
运行后报错Failed loading resource | 场景路径写错 | 检查change_scene_to_file里的路径 | 确认main_level.tscn或tutorial_level.tscn存在 |
| 按钮点击无反应 | 信号未连接,或节点路径错误 | 在脚本加断点或print输出 | 用%按钮名唯一名称,或使用完整节点路径 |
| 按 WASD 角色不动 | 输入映射没有配置 | 打开项目设置查看动作列表 | 按第 3.3 节添加动作 |
| 按空格不能跳 | jump动作缺失 | 检查输入映射 | 添加空格键到jump动作 |
| 引导面板一直显示 | TutorialDirector 的导出变量未赋值 | 看运行日志是否触发assert | 拖拽赋值overlay、player、target_area |
| 角色直接穿过平台 | CSGBox 未启用碰撞 | 检查Use Collision属性 | 勾选Use Collision,改用 StaticBody3D 更稳 |
| 引导完成后场景不切换 | tutorial_finished信号未触发或路径错误 | 查看_finish是否执行 | 确认main_level.tscn路径,确认complete_tutorial()在切换前调用 |
| 中文文本显示为方块 | Godot 默认字体不支持中文 | 检查 UI 控件字体设置 | 导入开源中文字体,在主题中配置 |
| 暂停后无法恢复 | 暂停菜单节点被暂停 | 检查process_mode | 设置Node.PROCESS_MODE_ALWAYS |
| 返回主页后游戏仍暂停 | 切换场景前未重置暂停 | 检查get_tree().paused | 切换前设为false |
| 存档无法保存 | user://路径不可写 | 查看输出警告 | 检查系统用户目录权限,或补充错误处理 |
10. 最佳实践与使用建议
把这套流程放进真实项目之前,有几个工程化建议值得先做。
10.1 节点命名与引用
Godot 4 推荐用%唯一名称引用 UI 节点,而不是长长的路径。打开节点检查器,在目标节点的右键菜单里选择“设置为唯一名称”,脚本里用%StartButton引用。这样即使调整 UI 层级,脚本不用改。
10.2 引导文案集中管理
把引导文案拆成一个Array[Dictionary],集中放在GameState或单独的资源文件里。修改文案时不用打开场景,风险更小。
const TUTORIAL_STEPS := [ { "title": "移动引导", "content": "使用 WASD 控制角色,走到蓝色光圈中。" }, { "title": "跳跃引导", "content": "按空格键跳跃,跳到高台上。" } ]这个数组可以直接替换TutorialDirector中的硬编码文案,后续要加新步骤,只要增加一个字典。
10.3 UI 布局注意安全区域
引导面板不一定非要在屏幕中间。如果想在正式项目中长期使用,建议把面板放在底部或右上角,避免遮挡玩家视野。带刘海屏的手机导出时,还要为 Safe Area 留出边距。
10.4 存档版本化
game_state.cfg目前只有一个字段。以后添加存档内容时要加一个version字段,读取时做版本判断,避免老存档升级后字段缺失导致崩溃。
config.set_value("game", "version", 1)10.5 测试时手动重置状态
开发过程中会反复测试引导,每次都要删存档很麻烦。可以在GameState里增加一个调试快捷键,按住 Ctrl 加 Q 删除存档并重启:
func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("delete_save_debug"): DirAccess.remove_absolute(SAVE_PATH) get_tree().reload_current_scene()调试功能记得在导出前移除。
11. 总结与下一步
到这里,你的 Godot 3D 工程已经具备一套完整的入口流程:主菜单、全局存档、教学引导、暂停菜单。从代码量来看,这只用了不到十个脚本文件,却覆盖了绝大多数独立游戏起步阶段需要的基础结构。
这套流程最值得验证的是“引导完成状态”和“场景切换”的配合:主菜单根据存档状态决定进入教学还是正式关卡,引导结束又把状态写回存档,下一次进入主菜单自动改变按钮文本。理解了这个循环,你就能把它扩展到更多入口场景,比如章节选择、难度选择、成就奖励。
如果你正在做一个 3D 项目,下一步可以做的扩展包括:
- 把
TutorialDirector的线性步骤改成通用任务系统,支持并发目标和条件分支; - 用
AnimationPlayer给引导提示加入场、退场动画,让引导更有节奏; - 引导面板接入 Gamepad 输入,兼容手柄玩家;
- 替换正式美术资源,把占位用的 CSG 几何体换成导入模型。
建议收藏备用,做独立游戏原型的时候直接把这套主菜单和引导体系拿过去用。