第4章 创建《酒魂》项目与场景结构
「君子务本,本立而道生。」——《论语·学而》
前三章我们做了三件事:第 1 章装好了工具、做出了第一个「Hello, 酒魂」;第 2 章建立了「完成优先于完美」的方法论;第 3 章的动手任务带你写下了六份设计文档——那是《酒魂》的施工图纸。
先确认一件事: 这六份文档不在书里,而在你自己项目的
docs/文件夹中(00_gdd.md加 5 份维度文档)。如果你当时跳过了第 3 章的动手任务,不必回头重读全章——直接翻到 3.6 节,照着那里的清单把 6 份文件补出来(每份只需几分钟,完整填写范例见附录 E)。本章和后续所有章节都会用到这份图纸,缺了它,后面的很多参数就失去了出处。
从这一章开始,正式开工。本章的核心任务只有一个:为《酒魂》搭建一个稳固、干净、可扩展的项目骨架。 这就像盖房子打地基、铺管道,看不见摸不着,但决定了整栋房子能盖多高。很多新手项目做到一半推倒重来,都是因为一开始结构混乱,后期难以管理。
本章要解决什么问题: 把《酒魂》的工程搭起来,规划好文件夹、场景树、全局管理器和数据层,避免写到后面推倒重来。
本章你将得到: 一个能运行、不报错的《酒魂》工程骨架——包括规范的文件夹结构、主场景骨架、三个全局单例(GameManager / EventBus / DataManager)、一套全书统一的代码规范,以及第一份 JSON 数据文件。项目完成度到达 5%。
4.1 创建项目,规划文件夹结构
4.1.1 打开项目,确认渲染器
第 1 章的 1.6 节,你已经创建了 WineSoul 项目并显示了「Hello, 酒魂!」。本章直接在这个项目上继续开发,不需要新建——那个项目就是我们此后所有章节的家。
如果你当时删掉了它,或者想换个位置重来,按下面的步骤重建一次(图 4-1):
- 打开 Godot 4.7,在项目管理器点击「新建项目」;
- 项目名称:
WineSoul(《酒魂》的英文名,统一用它,避免路径问题); - 项目路径:选一个固定位置,例如
D:\Godot\Projects\WineSoul; - 渲染器:选「兼容」(Compatibility);
- 点击「创建并编辑」。

为什么选「兼容」渲染器,而不是默认的 Forward+? 《酒魂》是一款 2D 像素游戏,用不到 Forward+ 面向 3D 大作的高级图形特性;而「兼容」渲染器基于 OpenGL,对老电脑、集成显卡更友好——还记得 1.2 节说过 4GB 内存的电脑就够吗?选它,你和你的读者、朋友试玩时都不会遇到「电脑带不动」的问题。如果你发现项目设置里渲染器不是「兼容」(新建时手滑选错了),打开「项目 → 项目设置 → 渲染」,把「渲染方法(Rendering Method)」改为
gl_compatibility即可。
4.1.2 规划文件夹:对杂乱无章说不
一个清晰的目录结构,是项目长期维护的基础。想象一下:三个月后,你的工程有上百个文件,如果全堆在一起,找东西将是一场噩梦。
我们按「类型分类」为《酒魂》建立文件夹体系。在 Godot 编辑器左下角的「文件系统」面板中,右键 res:// 根目录,选择「新建 → 文件夹」,依次创建下表中的文件夹(名称全部小写):
| 文件夹 | 用途说明 | 类比 |
|---|---|---|
assets |
所有外部导入的素材资源 | 建材市场买回来的原料 |
├─ assets/sprites |
2D 图片、角色帧动画 | 油漆、壁纸 |
├─ assets/tilesets |
地图用的瓦片集 | 铺地板的木料 |
├─ assets/fonts |
字体文件(.ttf / .otf) | 挂在墙上的字画 |
├─ assets/sounds |
音效与 BGM | 音响设备 |
scenes |
游戏的各个场景(.tscn) | 房子里的各个房间 |
├─ scenes/characters |
玩家、敌人、NPC 场景 | 房间里的人 |
├─ scenes/levels |
关卡 / 地图场景 | 户型图 |
ui |
用户界面场景与脚本 | 电路开关面板 |
scripts |
所有 GDScript 脚本 | 控制电路和管道 |
data |
游戏配置数据(JSON),4.6 节创建 | 预制菜的配方卡 |
resources |
Godot 资源文件(.tres,第 15 章起使用) | 定制家具的图纸 |
addons |
外部导入的 Godot 插件 | 后期添置的智能家电 |
创建完成后,你的文件系统应该长这样(第 3 章动手任务建的 docs/ 就在根目录下——那六份设计文档是我们的图纸,别删;还没建的读者,按本章开头的提示补做即可):
res://
├─ assets/
│ ├─ sprites/
│ ├─ tilesets/
│ ├─ fonts/
│ └─ sounds/
├─ scenes/
│ ├─ characters/
│ └─ levels/
├─ ui/
├─ scripts/
├─ data/
├─ resources/
├─ addons/
└─ docs/ ← 第 3 章的六份设计文档

关键认知:按「类型」分,而不是按「功能」分。 新手常犯的错是为每个功能建一个文件夹(比如「酿造系统」一个文件夹,里面混着脚本、场景、图片)。项目小的时候两者没差别;但《酒魂》做到后期,一个系统会横跨脚本、场景、数据、素材——按类型分类,你和引擎都能更快定位文件,也避免同一个文件夹里鱼龙混杂。
4.1.3 初始化 Git:给工地装上「反悔键」
第 1 章 1.5 节说过,Git 三步循环会在本章带你真正跑通。现在项目有了正式内容,是时候兑现这个承诺了。
-
创建 .gitignore。 在 Godot 的文件系统面板里看不到隐藏文件,我们用文本编辑器(记事本或 VS Code)在项目根目录(
project.godot所在的文件夹)新建一个名为.gitignore的文本文件,内容如下:# Godot 4 自动生成的缓存,不需要备份 .godot/ # Android 导出模板解压目录(第 26 章才会用到,先忽略) /android/没有这个文件的话,Git 会把 Godot 的临时缓存也当成交付物备份,仓库会变得又大又乱。
-
初始化仓库并首次提交。 在项目文件夹空白处右键 →「Open Git Bash here」(1.5.2 节的小技巧),依次执行:
git init git add . git commit -m "第4章:搭建《酒魂》项目骨架" -
在 Gitee 建一个自己的远程仓库。 登录 Gitee → 右上角「+」→「新建仓库」→ 仓库名填
wine-soul(小写),不要勾选「使用 Readme 文件初始化」,点击创建。 -
生成私人令牌(Gitee 不再接受用登录密码推送)。 现在的 Gitee 出于安全考虑,禁止用网页登录密码做 git 推送——你必须用一把「推送专用钥匙」,叫私人令牌。现在生成它:
- 登录 Gitee → 右上角头像 →「设置」→ 左侧「安全设置」→「私人令牌」→「生成新令牌」;
- 描述随便填(如
wine-soul),权限勾选projects(其他保持默认),点提交; - 输入一次 Gitee 登录密码后,页面会显示一串字符(形如
a1b2c3d4...)——这就是令牌,只显示这一次,立刻复制到记事本保存。丢了就只能删掉重新生成。
令牌和登录密码的关系: 登录密码是进「Gitee 网站」的钥匙;私人令牌是让「你电脑上的 Git」进你仓库的钥匙。两者互相独立,网站改密码不影响已生成的令牌。
-
关联远程并推送。 回到 Git Bash,把命令里的
你的用户名换成你的 Gitee 用户名(就是仓库地址里gitee.com/后面的那一段):git remote add origin https://gitee.com/你的用户名/wine-soul.git git push -u origin master第一次推送时会弹出 Gitee 登录窗口:用户名填你的 Gitee 用户名(或绑定的手机号),密码填刚才保存的私人令牌——不是网页登录密码!填对一次之后 Windows 会记住凭据,以后推送不再询问。
没有弹窗? 如果它直接在终端里问「Username for 'https://gitee.com'」和「Password for ...」,填法同上——注意终端里输入密码时屏幕不会显示任何字符,这是正常的安全设计,盲打完直接回车。
分支名小排查: 若报错含
src refspec master字样,说明你的 Git 默认分支叫main而不是master。执行git branch --show-current看一眼实际名字,把推送命令里的master换成它即可(两个名字都是正常的,Gitee 也都支持)。
推送排错速查:
| 报错关键信息 | 原因 | 解决方法 |
|---|---|---|
Authentication failed |
密码栏填了登录密码,或令牌错了/过期 | 密码栏重新填私人令牌;不确定就回 Gitee 删掉旧令牌重新生成 |
403 Forbidden |
令牌没有 projects 权限,或仓库地址里的用户名不是你的 |
重新生成令牌并勾选 projects;核对 remote 地址(git remote -v 查看) |
src refspec master |
本地分支名不是 master | 见上面「分支名小排查」 |
remote origin already exists |
之前关联过别的地址 | 执行 git remote set-url origin 正确的仓库地址 后重试 |
刷新 Gitee 页面,你会看到项目文件已经在线上了。从现在起,每完成一个动手环节,就做一次三步提交——这是第 2 章「小步快跑」方法论在版本控制上的落地。
4.2 《酒魂》的场景树设计
文件夹是项目的静态骨架,场景树(Scene Tree)是项目的动态骨架——它决定游戏运行时,玩家、地图、UI 如何组织和协作。动手之前,先把蓝图想清楚。
4.2.1 先想清楚:为什么「探索」和「战斗」不分成两个场景
《酒魂》是一款 2D 俯视角动作 RPG(第 3 章 3.2.1 的定位),核心体验是「探索中遇敌,拔刀就砍」的无缝动作感。
如果像传统 JRPG 那样「走路一个场景、遇敌切另一个战斗场景」,无缝感就断了。所以《酒魂》的设计原则是:探索即战斗,地图按需动态加载,UI 覆盖在游戏世界之上。
4.2.2 主场景蓝图
下面是《酒魂》主场景的节点结构蓝图(图 4-3)。现在不用全部创建——本节先看懂设计意图,4.4 节只搭出骨架,其余节点在后续章节按需添加:
Main (Node2D) 主场景根节点
├── CurrentLevel (Node2D) 当前关卡容器(动态替换,第 6 章)
│ ├── TileMapLayer 地图瓦片与碰撞(第 6 章)
│ ├── Player 主角,独立场景实例化进来(第 5 章)
│ ├── Enemies (Node2D) 本关敌人容器(第 11 章)
│ └── NPCs (Node2D) 本关 NPC 容器(第 18 章)
└── UI (CanvasLayer) 全局界面层(永远在游戏世界之上)
├── MainHUD (Control) 战斗 HUD:血条、醉意条(第 14 章)
├── PauseMenu (Control) 暂停菜单(初始隐藏)
├── BreweryPanel (Control) 酿造面板(初始隐藏,第 15 章)
└── DialoguePanel (Control) 对话面板(初始隐藏,第 19 章)

设计意图解析:
Main作为根:一个简单的Node2D,整个游戏世界坐标系的原点。CurrentLevel动态关卡容器:这是无缝体验的关键。玩家走到地图边缘(进入酒坊、切换地图)时,我们卸载当前关卡、把新关卡实例化到这个节点下,玩家无感知换图。第 6 章的 GameManager 会实现这套加载逻辑,本章只先把「容器」留出来。UI层独立:CanvasLayer节点让 UI 永远显示在游戏世界上方,不受摄像机移动影响(第 6 章加了跟随摄像机之后你会更深地体会到这一点)。- 酿造与对话是「面板」,不是「场景」:打开酿造界面,不是切换到独立的酿酒场景,而是在 UI 层弹出一个面板、暂停游戏世界;关掉面板立刻恢复。这样动作不中断,丝滑无违和感——这也是 4.3 节要把 UI 面板归到
CanvasLayer下的原因。
4.2.3 为独立玩法预留「叠加位」
第 7 章我们要做投壶——按 3.2 节的设计,它属于独立规则的小游戏,不适合和主线探索混在一起。正确的做法是叠加场景:触发投壶时,暂停当前关卡,把投壶场景动态加载到 Main 下层叠加在画面上,结束后移除、恢复原状。
本章不做任何实现,只在场景树蓝图里预留这个思路。具体怎么叠加、怎么和对话系统联动,第 7 章实现时细化——过早设计细节,是第 2 章 2.1.2 讲过的「完美主义陷阱三:用再想想代替动手做」。
4.3 节点与场景的核心概念
4.2 节的蓝图里出现了 Node2D、CanvasLayer、Control 这些词。这一节把它们一次讲清楚——这是理解 Godot 的地基,值得多花两分钟。
4.3.1 节点:功能的积木块
Godot 的核心哲学是「一切皆节点」。节点是最小的功能单元,每个节点只做一件事。用《酒魂》的节点来认识几个最常用的类型:
| 节点类型 | 擅长什么 | 在《酒魂》里的例子 |
|---|---|---|
Node |
纯逻辑,没有位置、不能显示 | GameManager、EventBus、DataManager(4.4 / 4.7 节) |
Node2D |
2D 世界里的「物体」,有位置、旋转、缩放 | 主场景根 Main、关卡容器 CurrentLevel |
CharacterBody2D |
自带碰撞检测、能被代码控制移动的 2D 身体 | 主角(第 5 章)、敌人(第 11 章) |
Control |
界面元素,有锚点、边距,负责布局 | 血条、醉意条、背包格子(第 9 章起) |
CanvasLayer |
一个独立的「绘制图层」,永远盖在游戏世界上 | UI 层 |
TileMapLayer |
用小方块拼出大地图(第 6 章详讲) | 江南小镇的地面与建筑 |
4.3.2 场景:可以复用的积木组合
单个节点能力有限,把节点组装成树,打包保存成 .tscn 文件,就是「场景」。
场景最有价值的特点是:可以实例化——把一个场景像零件一样塞进另一个场景里反复使用。第 5 章我们会把主角做成独立的 player.tscn,然后把它实例化到 CurrentLevel 下面;将来做敌人,一个敌人场景可以实例化十次,摆出十个敌人。改一处,所有实例都更新——这就是复用的力量。
你其实早就用过场景了:第 1 章的
main.tscn(Control + Label)就是一个场景。第 4.4 节我们会重建它。
4.3.3 信号:节点之间的广播电台
节点之间怎么说话?比如「醉意值变了」,血条需要知道、屏幕特效需要知道——但醉意系统不该直接认识血条(否则每加一个 UI 都要改战斗代码,乱套)。
Godot 的答案是信号(signal):一个节点发出广播(「我变了!」),任何关心的节点都可以订阅这个广播并做出反应。发广播的不需要知道谁在听,听广播的也不需要认识播音的人——这就是解耦,第 2 章 2.2.2 读过的 drunk_stage_changed 信号就是它的雏形。
《酒魂》里所有跨系统的广播,都会走一条公共频率:EventBus(事件总线)。4.4 节我们马上创建它。信号的完整用法(连接、传参、断开)在第 9 章 9.4 节正式深入,今天先会用。
4.4 动手:创建项目根场景,配置自动加载脚本
「纸上得来终觉浅,绝知此事要躬行。」这一节把蓝图落地,是本章最重要的动手环节,请务必跟着做完。
4.4.1 第一步:项目全局设置(视口与像素渲染)
写第一行代码之前,先把两件「全局性」的事定死,否则第 5、6 章会返工:
① 游戏视口与窗口缩放。 《酒魂》按 640×480 的像素分辨率制作(附录 E.3 的美术基准),玩家窗口更大时由引擎放大。打开「项目 → 项目设置 → 显示 → 窗口」,设置:
| 设置项 | 值 | 说明 |
|---|---|---|
| 视口宽度 / 高度(Viewport Width/Height) | 640 / 480 |
游戏画面的逻辑分辨率,美术都按这个尺寸画 |
| 窗口宽度 / 高度覆盖(Window Width/Height Override) | 1280 / 960 |
打开窗口的实际大小,正好放大 2 倍 |
| 拉伸模式(Stretch Mode) | canvas_items |
画面整体放大,像素保持锐利 |
| 拉伸比例(Stretch Aspect) | keep |
保持 4:3,不变形 |
② 像素纹理过滤。 默认情况下 Godot 会对图片做平滑插值,像素画会糊成一片。打开「项目 → 项目设置 → 渲染 → 纹理」,把「画布纹理 → 默认纹理过滤(Default Texture Filter)」改为 Nearest(最近邻)。
为什么现在就做? 这两个设置影响「每一张图、每一个字」的显示效果。第 5 章的主角、第 6 章的地图都是像素素材,等画完才发现糊了再回来改,你要逐个排查一遍——先定死,一劳永逸。这就是 3.1.1 节「好的数据设计应该在写第一行代码之前确定」的工程版。
4.4.2 第二步:搭建根场景骨架
-
新建场景:菜单「场景 → 新建场景」(Ctrl+N),选择根节点类型为 Node2D,双击节点名重命名为
Main; -
保存:Ctrl+S,保存为
main.tscn,放进scenes/文件夹(它会覆盖掉第 1 章的演示场景,成为正式的游戏入口——第 1 章那个「Hello, 酒魂」已完成历史使命); -
搭建骨架:选中
Main,点「+」号依次添加:- 子节点
Node2D,命名CurrentLevel; - 子节点
CanvasLayer,命名UI; - 选中
UI,在其下添加Control,命名MainHUD。
现在的场景树是:
Main (Node2D) ├── CurrentLevel (Node2D) └── UI (CanvasLayer) └── MainHUD (Control) - 子节点
-
设置游戏入口:打开「项目 → 项目设置 → 通用 → 应用 → 运行」,把「主场景」指向
res://scenes/main.tscn。以后每次按 F5,游戏都从这里启动(图 4-5)。

4.4.3 第三步:创建全局单例并注册自动加载
游戏里总有一些东西全局通用:游戏状态、事件总线、配置数据——不管在哪个场景都要能访问。Godot 的「自动加载(Autoload)」机制就是为此而生:注册进列表的节点,游戏一启动就最先创建,且全程唯一、任何脚本都能直接用名字访问。这种「全局唯一、随处可取」的对象,设计模式里叫单例(深挖见附录 A.6)。
《酒魂》需要三个单例:GameManager(流程指挥)、EventBus(广播站)、DataManager(数据仓库,4.7 节创建)。先创建前两个:
- 在
scripts/文件夹右键「新建 → 脚本」,分别创建game_manager.gd和event_bus.gd(模板都选 Node); - 打开菜单「项目 → 项目设置 → 全局 → 自动加载」,在「路径」选择
res://scripts/game_manager.gd,节点名自动填为GameManager,点「添加」;同样方法把event_bus.gd添加为EventBus(图 4-4)。

- 往两个脚本里填入代码。
event_bus.gd —— 全局事件总线(广播站):
extends Node
# 《酒魂》的全局事件总线:所有跨系统的广播都走这里
# 发广播的一方不需要知道谁在听,听广播的一方不需要认识播音的人
# 后续章节会往这里陆续添加信号(战斗、任务、酿造……)
signal drunk_level_changed(new_value: float) # 醉意值变化(第 7 / 12 章使用)
signal item_obtained(item_id: String, amount: int) # 获得物品(第 10 章背包使用)
game_manager.gd —— 游戏流程指挥中心:
extends Node
# 《酒魂》的宏观状态:整个游戏此刻正在做什么
# 注意区分:GameManager 管「游戏在做什么」(宏观流程);
# 第 8 章的主角状态机管「主角正在做什么动作」(微观动作),两者不要混淆
enum GameState { EXPLORE, BATTLE, BREWING, DIALOGUE, PAUSED }
# 当前游戏状态,初始为探索
var current_state: GameState = GameState.EXPLORE
# 全局醉意值缓存(第 7 章投壶正式使用,第 12 章扩展为完整醉意系统)
var player_drunk_level: float = 0.0
func _ready() -> void:
print("[GameManager] 游戏启动,当前状态: ", GameState.keys()[current_state])
# 切换游戏状态。需要改变游戏流程时,调用 GameManager.change_state(GameState.XXX)
func change_state(new_state: GameState) -> void:
if current_state == new_state:
return # 状态没变就不做事,避免重复执行暂停/恢复逻辑
print("[GameManager] 状态变更: ", GameState.keys()[current_state], " -> ", GameState.keys()[new_state])
current_state = new_state
match new_state:
GameState.EXPLORE:
get_tree().paused = false # 探索:游戏世界正常运行
GameState.PAUSED, GameState.BREWING, GameState.DIALOGUE:
get_tree().paused = true # 弹出面板时暂停游戏世界(UI 怎么保持响应,见下方练习)
GameState.BATTLE:
pass # 动作战斗中世界照常运行,敌我同场,不需要暂停
关于发出信号的两种写法: Godot 4 推荐
drunk_level_changed.emit(0.5)这种「信号名 . emit()」的写法;一些老教程里会看到emit_signal("drunk_level_changed", 0.5),两者效果相同。本书统一用前者。
动手练习 4.1:让 UI 层不受暂停影响 上面代码在弹出酿造/对话面板时会暂停游戏世界,但 UI 自己也会被冻结,按钮就点不动了。解决方法:
- 选中场景树中的
UI (CanvasLayer)节点;- 在检查器中找到 Process → Mode 属性;
- 从
Inherit改为When Paused(暂停时仍可处理)。这样游戏世界暂停了,UI 面板依然可以点击。试着按这个练习改完,F5 运行——输出面板应显示「[GameManager] 游戏启动,当前状态: EXPLORE」。
做完这一节,记得 git add . && git commit -m "第4章:根场景与全局单例"。
4.5 GDScript 风格指南(命名 / 缩进 / 注释,全书统一执行)
从本章起,全书代码都遵守这一套规范。它是「完成优先于完美」在代码上的体现:风格统一不是洁癖,而是让你三个月后回看代码、以及让源码仓库对照(1.8 节 Tag 对照法)时,减少无谓的阅读阻力。
| 内容 | 规范 | 正确示例 | 错误示例 |
|---|---|---|---|
| 脚本文件名 | 全小写 + 下划线 | game_manager.gd |
GameManager.gd |
| 节点名 | 大驼峰 | CurrentLevel |
currentLevel |
| 变量 / 函数名 | 小写 + 下划线 | current_state、change_state() |
currentState |
| 常量 | 全大写 + 下划线 | DATA_FILES |
dataFiles |
| 枚举 | 类型大驼峰,成员全大写 | GameState.EXPLORE |
gamestate.explore |
| 内部私有成员 | 下划线前缀 | _load_all_data() |
load_all_data() |
| 信号名 | 名词 + 过去式(表示「某事发生了」) | drunk_level_changed |
changeDrunk |
| 缩进 | Tab(Godot 默认,不要改成空格) | — | 空格与 Tab 混用 |
| 类型标注 | 变量声明尽量带类型 | var drunk_level: float = 0.0 |
var drunk_level = 0 |
注释三条军规:
- 解释「为什么」,而不是「是什么」。 代码本身已经说明它在做什么(第 2 章 2.2.1 节逐行注释阅读法的镜像——作者侧的规矩);读者真正需要的是背后的意图。
- 魔法数字必须注释。
if drunk_level >= 0.8里的 0.8 是什么?不注释的话三个月后你自己都要猜。写成# 0.8 = 醉意阈值,达到后可放酒魂技(附录 E.2)。 - 每个脚本开头一段总注释,说明它是干什么的、挂在哪——就像本章三个脚本的头部那样。
第 2 章的醉意管理器示例,就是按这套规范写的(你可以现在打开对照检查一遍)。从下一章起,你写代码时也请遵守它——规范只有在自己手上坚持,才会变成习惯。
4.6 数据驱动基础:JSON / ConfigFile / Resource 选型
4.6.1 为什么数值不能写死在代码里
先看一个反面教材:
# ❌ 别这么写:数值直接焊死在逻辑里
func drink():
drunk_level = min(1.0, drunk_level + 0.2) # 0.2 是什么?想调快慢要改代码
第 3 章 3.4 节说过:数据的作用,是在规则确定之后,把体验调节到「刚好好玩」。 调节意味着反复试——如果把每个数值都写死在逻辑里,每次调参都要翻代码、改代码、担心改坏逻辑。正确做法是把「会调的数」放进独立的数据文件,代码只负责「读数据、按数据执行」:
# ✅ 推荐写法:数值来自数据文件,调整参数不动代码
drunk_level = min(1.0, drunk_level + DataManager.get_value("drunk", "gain_per_drink"))
这就是数据驱动:逻辑与数据分离。改平衡只碰数据文件,逻辑代码一次都不用动。
4.6.2 三种数据载体,各管一段
Godot 生态里常用的数据载体有三种,《酒魂》的分工如下:
| 载体 | 长什么样 | 适合什么 | 《酒魂》的用途 |
|---|---|---|---|
| JSON | 纯文本,记事本就能改 | 结构化配置表 | 物品、醉意/投壶参数、对话(本章起,data/ 文件夹) |
| ConfigFile | [分组] 键=值 格式 |
少量键值设置 | 游戏设置、存档元信息(第 21 章) |
| Resource(.tres) | Godot 原生资源文件 | 需要在编辑器里可视化编辑、互相引用的复杂资源 | 配方与词缀定义(第 15、16 章,附录 E.1.4 口径) |
为什么先学 JSON? 它与 Godot 无关——是通用的行业格式,你以后用任何引擎都躲不开;它肉眼可读,新手排查错误容易;而且它和第 3 章
docs/里的01_data_design.md设计文档「同源」——设计文档是图纸,JSON 是照着图纸裁下的用料。Resource 更强大但也更「引擎绑定」,等第 15 章用到时再学不迟。
4.6.3 动手:建立第一份 JSON 数据文件
在 Godot 文件系统面板的 data/ 文件夹下右键「新建 → 文本文件」,命名为 items.json,填入以下内容:
{
"herb_zhuye": {
"display_name": "竹叶",
"type": "material",
"base_value": 5
},
"wine_zhuyeqing": {
"display_name": "竹叶青",
"type": "wine",
"quality": "C",
"base_value": 30,
"drunk_gain": 0.2,
"heal": 20
}
}
这份文件登记了两个《酒魂》里的真实物品:材料「竹叶」和酒品「竹叶青」(id 沿用附录 E.1 的命名:英文前缀 + 拼音)。注意三件事:
- JSON 不允许写注释,字段含义靠命名规范和文档维护;
- 最外层用「物品 id」当 key,查询时直接按名字取,一步到位;
display_name、base_value等字段名与附录 E.1.4 的酒品字段表一致——数据文件从第一天起就对齐设计蓝图,后面第 10 章(背包)、第 15 章(配方)扩写时不会返工。
保存 JSON 后,Godot 会提示重新导入文件,点一下即可。如果 JSON 写错了逗号或引号,下一节的 DataManager 会报错告诉你——这是它存在的意义之一。
4.7 DataManager 单例:把所有配置集中加载(参见附录 A.6)
数据文件有了,谁来读它?如果每个系统都自己写一遍「打开文件 → 解析 JSON」,同样的代码会散落各处,改一次路径要找十个地方。
我们的方案:再建一个全局单例 DataManager,游戏启动时把 data/ 下的所有 JSON 一次性读进内存,之后任何系统都只问它要数据。
- 在
scripts/下新建data_manager.gd,填入代码:
extends Node
# 数据文件登记表:以后每新增一份数据文件(第 7 章的投壶参数、
# 第 12 章的醉意参数、第 15 章的配方……),只需在这里加一行
const DATA_FILES := {
"items": "res://data/items.json",
}
# 所有加载好的数据都在这里,格式:_data["items"]["wine_zhuyeqing"] = {...}
var _data: Dictionary = {}
func _ready() -> void:
_load_all_data()
# 游戏启动时,一次性把登记表里的所有 JSON 读进内存
func _load_all_data() -> void:
for key: String in DATA_FILES:
var path: String = DATA_FILES[key]
if not FileAccess.file_exists(path):
push_warning("[DataManager] 找不到数据文件: %s" % path)
continue # 找不到就跳过而不是崩溃,方便后续章节逐步补文件
var file := FileAccess.open(path, FileAccess.READ)
var parsed: Variant = JSON.parse_string(file.get_as_text())
if parsed == null:
push_error("[DataManager] JSON 解析失败(检查逗号、引号是否成对): %s" % path)
continue
_data[key] = parsed
print("[DataManager] 已加载 %s(%d 条)" % [path, _data[key].size()])
# 查询物品:传入物品 id,返回它的数据字典;查不到返回空字典并给出警告
func get_item(item_id: String) -> Dictionary:
if _data.has("items") and _data["items"].has(item_id):
return _data["items"][item_id]
push_warning("[DataManager] 没有找到物品: " + item_id)
return {}
-
和 4.4 节一样,把
data_manager.gd注册进「项目设置 → 全局 → 自动加载」,节点名DataManager; -
验证:在
game_manager.gd的_ready()末尾临时加一行:print("[验证] 竹叶青的数据: ", DataManager.get_item("wine_zhuyeqing"))按 F5 运行,输出面板应该依次打印:DataManager 加载了
items.json(2 条),以及竹叶青的完整数据。验证通过后,这一行可以删掉,也可以留着当每次启动的自检——删不删都行,养成「临时验证代码用完处理掉」的习惯更重要(呼应 2.3 节第三步的提醒)。
代码里的三个「为什么」:
- 为什么启动时一次读完? JSON 文件很小(几十 KB),一次读进内存后,游戏中查询就是纯字典操作,快且简单。将来数据量极大时才需要按需加载——那超出本书范围,属于进阶话题。
- 为什么查不到返回空字典而不是报错崩溃? 数据没配齐是开发常态。返回空值 + 警告,游戏继续跑,你在输出面板里能看到警告再补数据——这就是第 2 章说的「把 Bug 变成提示」。
- 为什么用登记表
DATA_FILES? 加载逻辑只写一遍,新增数据文件只加一行配置——把「会变化的部分」集中到一处,是数据驱动思想在代码结构上的延续。
至此,《酒魂》的三个全局单例各就各位:GameManager 管流程、EventBus 管通信、DataManager 管数据。 后续章节的所有系统,都会站在这三个基座上开发。
4.8 本章产出:《酒魂》的项目骨架 + 数据层(完成度:5%)
本章你得到了什么
- 一个规范的
WineSoul工程:类型分类的文件夹结构、Git 已初始化并推送到你自己的 Gitee 仓库; - 一套定死的全局设置:640×480 视口、canvas_items 拉伸、最近邻过滤——第 5、6 章的像素素材可以直接进场,不会糊;
- 主场景骨架
main.tscn:Main → CurrentLevel + UI的动态关卡架构(蓝图见 4.2,第 6 章续建); - 三个全局单例:
GameManager(流程状态机)、EventBus(全局事件总线)、DataManager(配置数据仓库); - 第一份数据文件
data/items.json与全书统一的 GDScript 风格指南。
| 我们学了什么 | 为什么它重要 |
|---|---|
| 文件夹按「类型」分类 | 项目变大后,人和引擎都能快速定位文件 |
| 场景树分层:动态关卡容器 + UI 层 | 「探索即战斗」的无缝架构,从此不用推倒重来 |
| 自动加载(单例) | 三个基座让所有后续系统有统一的挂靠点 |
| 信号与 EventBus | 系统间解耦的核心机制,第 9 章正式深入 |
| 数据驱动 + DataManager | 调参不改代码,第 10 / 15 / 16 / 18 / 21 / 22 章直接复用 |
| GDScript 风格指南 | 全书代码统一口径,对照源码仓库时零阻力 |
这些设施会被谁复用?
| 本章设施 | 建立位置 | 被后续哪些章节复用 |
|---|---|---|
| EventBus | 4.4 | 第 7 / 11 / 12 / 14 / 20 章 |
| DataManager | 4.7 | 第 10 / 15 / 16 / 18 / 21 / 22 章 |
| GameManager | 4.4 | 第 6 章(关卡加载)、第 7 章、第 21 章(存档) |
| 风格指南 | 4.5 | 全书每一行代码 |
收尾:打上本章的存档点
按 1.8.3 节的约定,给本章打一个 Tag:
git add .
git commit -m "第4章完成:项目骨架 + 数据层"
git push origin master
git tag ch04
git push origin ch04
千里之行,始于足下。 你现在拥有的不是一个空项目,而是一个可以承载全部设计蓝图的专业工程——五个百分点不高,但从这里开始,每前进一步都踩在坚实的地基上。
下一步: 骨架已经立起,该让《酒魂》活起来了。第 5 章我们将创建 player.tscn,用 CharacterBody2D 给主角一个「身体」,让她在江南小镇上迈出第一步。
延伸阅读
以下资源不是必读,卡住或想深入时再看:
- Godot 官方文档(中文):https://docs.godotengine.org/zh_CN/ —— 搜索「节点与场景」「单例(自动加载)」,本章知识点的官方完整版;
- GDScript 官方风格指南:官方文档搜索「GDScript 风格指南」—— 4.5 节规范的完整依据,本书在其基础上做了新手取舍;
- JSON 官方格式说明:https://www.json.org/json-zh.html —— 十分钟能读完的 JSON 格式规范,比任何二手教程都准确。
评论0
暂时没有评论