Godot 4游戏入口流程实战:主菜单、新手引导与暂停系统

发布时间:2026/10/3 15:42:57
Godot 4游戏入口流程实战:主菜单、新手引导与暂停系统 Godot 3D 的完整流程走到这里基本就差两块拼图玩家进入游戏看到的第一个画面主页以及教会玩家基础操作的引导流程。这篇是系列第 30 节专门把“游戏引导”和“游戏主页”放在同一套工程里解决。本文基于 Godot 4.x 和 GDScript 2.0 编写不引入任何第三方插件。你可以从这套例子里看到“主菜单 → 教学关卡 → 正式关卡 → 暂停返回主页”的最小闭环是怎么组织的。文中的所有脚本都可以直接复制到你的项目里改着用不需要额外安装资源包。核心特点先摆出来主页使用 Control 节点 信号Signal组织按钮点击、场景切换都在脚本里显式连接新手引导用 3D 场景中的 Area3D 区域触发配合 CanvasLayer 上的提示面板展示引导文案引导完成状态写入本地存档ConfigFile二次进游戏直接跳过引导暂停菜单监听ui_cancel输入支持恢复游戏和返回主页。这篇文章会带你把项目初始化、输入映射、主菜单、全局状态、教学关卡、暂停菜单全部跑通。读完你就能在自己的 Godot 3D 工程里接入一套可扩展的入口与引导流程。1. 核心能力速览能力项说明适用引擎Godot 4.xGDScript 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_cancelEsc是引擎内置动作不需要自己添加。动作名称绑定按键作用move_leftA向左移动move_rightD向右移动move_forwardW向前移动move_backS向后移动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.gdextends 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给一个材质方便区分方向添加JumpPlatformCSGBox3DSize 设为(4, 2, 4)位置Y设为 1勾选Use Collision让玩家能跳上去移动目标和跳跃目标各加一个Area3D下面挂CollisionShape3D形状选BoxShape3D大小约为(2, 2, 2)两个 Area3D 下的MeshInstance3D可以用BoxMesh材质设置为半透明蓝色方便玩家看到目标点。半透明材质的设置方式是在检查器中新建StandardMaterial3D把Transparency设为AlphaAlbedo Color的 Alpha 调低到 0.4 左右。6.2 玩家控制器脚本给Player挂scripts/player_controller.gdextends 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.gdextends 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.gdextends 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 几何体换成导入模型。建议收藏备用做独立游戏原型的时候直接把这套主菜单和引导体系拿过去用。