「工欲善其事,必先利其器。」 ------《论语·卫灵公》
从这一章开始,我们打开 Godot,真正动手了。
但在写第一行代码之前,有必要先理解 Godot 的核心哲学。Godot 和 Unity 的底层思维方式不同,如果你带着 Unity 的思维硬套过来,会处处碰壁。理解了「节点与场景」的组合思想,后面所有的代码才会水到渠成。
4.1 节点与场景:Godot 的基石
Godot 最核心的概念就两个:节点(Node)和场景(Scene)。理解了这两个概念,就理解了 Godot 80% 的设计哲学。
4.1.1 节点是积木
节点是 Godot 里最基本的单位。每一个节点都是一个有特定功能的对象:有的能显示图片(Sprite2D),有的能处理物理碰撞(CharacterBody2D),有的能播放声音(AudioStreamPlayer),有的只是一个空容器(Node2D)。
Godot 自带了几百种节点类型,你的游戏对象就是由这些节点组合而成的。
把节点想象成乐高积木:每块积木有自己的形状和功能,你通过拼接积木来构建任何你想要的东西。你不需要「发明」积木,你需要「知道哪块积木能用」,然后把它们组合起来。------作者
以《酒魂》的玩家角色为例,它由以下节点组合而成:
Player ★ (CharacterBody2D) ← 处理物理移动、碰撞
├── Sprite2D ← 显示角色图片
├── AnimationPlayer ← 播放走路、攻击动画
├── CollisionShape2D ← 定义碰撞体形状
├── DrunkSystem ← 我们自己写的醉意值管理器
├── AttackRange ← 投壶的攻击判定区域
│ └── CollisionShape2D
└── AudioStreamPlayer ← 播放角色音效
每个节点各司其职,通过组合产生一个完整的玩家角色。这就是 Godot 的「组合模式」------你不是在「继承一个万能的角色基类」,而是在「把专门的积木拼在一起」。
4.1.2 场景是可复用的节点树
一组节点组合在一起,保存成一个文件(.tscn),就是一个场景。场景可以被实例化------也就是说,你可以在游戏里创建出它的多个副本,每个副本独立运行。
💡场景 = 可复用的对象模板 你把「玩家」的所有节点组合成一个场景(Player.tscn)。之后在地图场景里,你只需要拖入 Player.tscn,Godot 就会创建出完整的玩家对象。同样的道理,你可以有一个 Enemy.tscn,在关卡里放置多个敌人;有一个 BrewingTank.tscn,在酒坊里放置多个酿酒缸。场景是复用的单位。
场景树是 Godot 的核心数据结构------游戏运行时,所有场景嵌套在一起,形成一棵大树。根节点是游戏的入口,子节点是游戏里的所有对象:
Root(游戏根节点)
├── GameMap.tscn(当前地图场景)
│ ├── TileMap(地形)
│ ├── Player.tscn(玩家角色)
│ ├── Enemy.tscn × 3(三个敌人,各自独立)
│ └── BrewingShop.tscn(酒坊场景)
├── UI.tscn(界面层,始终覆盖在游戏上方)
│ ├── HealthBar
│ └── DrunkMeter
└── GameManager(全局游戏管理器,单例)
4.1.3 Unity 用户注意:关键区别
| Unity 的思维 | Godot 的对应思维 |
|---|---|
| GameObject + Component | 节点的组合(直接把功能节点挂为子节点) |
| Prefab(预制体) Scene(场景)------功能完全相同 | |
| Instantiate() 实例化预制体 | 场景里的 instantiate() 方法 |
| Transform 组件(所有对象都有) | Node2D 节点(2D 对象的位置/旋转/缩放) |
| MonoBehaviour + Start/Update | extends Node + _ready()/_process() |
| Static/Singleton 全局变量 | Autoload(自动加载的单例场景) |
| ScriptableObject(数据资产) | Resource 资源类 |
4.2 信号系统:观察者模式的 Godot 实现
如果说「节点与场景」是 Godot 的骨架,那「信号」就是神经系统。理解信号,是写出解耦、可维护代码的关键。
4.2.1 为什么需要信号
假设玩家的醉意值变化了,需要通知 UI 更新血条显示。最直觉的写法是:
❌ 不推荐的写法
javascript
# 直觉写法:在 DrunkSystem 里直接引用 UI 节点
func drink(amount: float) -> void:
drunk_level = min(1.0, drunk_level + amount)
# 直接找到 UI 节点,更新它
get_node("/root/UI/DrunkMeter").update(drunk_level) # ❌ 强耦合
这样写的问题:DrunkSystem 必须「知道」UI 节点的具体路径。一旦 UI 结构变了,DrunkSystem 也得改。两个系统被死死绑在一起------这叫强耦合,是大型项目最头疼的问题来源。
信号的解决方式:DrunkSystem 不需要知道谁关心它的变化,它只管「发出信号」;任何想响应这个变化的节点,自己去连接这个信号。
✅ 推荐的写法:用信号解耦
javascript
# DrunkSystem.gd ------ 只管发信号,不关心谁来接收
signal drunk_level_changed(new_level: float)
func drink(amount: float) -> void:
drunk_level = min(1.0, drunk_level + amount)
emit_signal("drunk_level_changed", drunk_level) # ✅ 解耦
# ──────────────────────────────────────────────────
# DrunkMeter.gd(UI 节点)------ 自己决定要不要监听
func _ready() -> void:
# 找到 DrunkSystem,连接它的信号
var drunk_sys = get_node("/root/Player/DrunkSystem")
drunk_sys.drunk_level_changed.connect(_on_drunk_level_changed)
func _on_drunk_level_changed(new_level: float) -> void:
# 收到信号,更新 UI
progress_bar.value = new_level * 100
4.2.2 信号的三种使用方式
Godot 里连接信号有三种方式,各有适用场景:
| 方式 | 适用场景 & 示例 |
|---|---|
| 编辑器可视化连接 | 简单的 UI 事件(按钮点击、滑块拖动)。在编辑器的「节点」面板里用鼠标连接,无需代码 |
| 代码中 .connect() | 需要动态连接,或者连接对象在运行时才存在。如:敌人生成后才连接死亡信号 |
| 代码中 @warning_ignore + 直接调用 | 同一节点内部的逻辑联动,简单情况可直接调用函数,不必经过信号 |
4.2.3 《酒魂》里的信号设计原则
在《酒魂》里,我们会遵循一条信号设计原则:
📐信号设计原则 「数据变化」用信号通知外部;「行为触发」用函数调用内部。简单说:当一个系统的状态发生了变化(醉意值变了、物品被拾取了、敌人死了),用信号通知关心它的人。当一个系统需要做一件事(喝酒、攻击、播放音效),直接调用那个系统的函数。
4.3 场景树与节点路径
场景树是运行时所有节点的层级结构。理解如何在代码里引用和访问其他节点,是写 GDScript 的基本功。
4.3.1 获取节点的四种方式
获取节点的四种方式
javascript
extends Node2D
# 方式 1:@onready ------ 最推荐,场景加载完成后自动赋值
# $ 是 get_node() 的简写
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim: AnimationPlayer = $AnimationPlayer
@onready var drunk_sys = $DrunkSystem
# 方式 2:get_node() ------ 通过路径获取,适合路径不固定的情况
func _ready() -> void:
var ui = get_node("/root/UI") # 绝对路径(从根节点开始)
var sibling = get_node("../OtherNode") # ../ 代表父节点
var child = get_node("ChildNode/GrandChild") # 相对路径
# 方式 3:get_tree().get_nodes_in_group() ------ 获取同组的所有节点
func attack_all_enemies() -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
enemy.take_damage(10)
# 方式 4:直接赋值 ------ 通过 @export 在编辑器里拖拽指定
@export var brewing_tank: Node # 在检查器里拖入目标节点
4.3.2 @onready 为什么这么常用
@onready 是 Godot 4 里最常用的变量修饰符之一。它的作用是:等场景树加载完成(_ready() 执行之前),自动执行等号右边的赋值。
如果你在 var 声明时直接写 Sprite2D,在脚本加载的瞬间场景树可能还没准备好,Sprite2D,在脚本加载的瞬间场景树可能还没准备好,Sprite2D,在脚本加载的瞬间场景树可能还没准备好,Sprite2D 会是 null。@onready 保证了赋值时机正确。
⚠️最常见的节点引用错误 错误信息:Invalid get index 'xxx' on base 'null instance' 原因:你访问了一个 @onready 变量,但忘记加 @onready,或者节点路径写错了。 检查方法:在 _ready() 里加一行 print(sprite),看输出是节点对象还是 null。
4.4 资源系统:数据与资源的统一管理
Godot 里的「资源(Resource)」是一种特殊的数据容器------它可以被保存为 .tres 或 .res 文件,在多个节点之间共享,也可以作为场景属性在检查器里直接编辑。
4.4.1 Resource vs 普通变量的区别
| 普通变量(写在脚本里) | Resource(独立文件) |
|---|---|
| 数据只存在于当前脚本实例里 | 数据可以被多个场景、多个节点共享 |
| 无法在编辑器里直观编辑 | 在检查器里像填表一样直接编辑字段 |
| 修改需要改代码 | 修改数据不需要动代码,设计师可直接修改 |
| 不能序列化保存为文件 | 可以保存为 .tres 文件,随项目管理 |
4.4.2 定义《酒魂》的物品资源
在《酒魂》里,每一种材料、每一种酒品都定义为一个 Resource 类。这让我们可以在编辑器里直接创建和编辑游戏数据,而不需要手动修改数据库或 JSON 文件。
res://resources/items/item.gd
javascript
# res://resources/items/item.gd
# 物品基类 Resource
class_name Item
extends Resource
## 物品的唯一 ID,用于存档和数据库查询
@export var item_id: String = ""
## 显示名称
@export var display_name: String = ""
## 物品描述
@export_multiline var description: String = ""
## 物品图标(拖入 Texture2D)
@export var icon: Texture2D
## 物品类型(材料、酒品、装备......)
@export_enum("material","liquor","equipment","consumable") var item_type: int = 0
## 稀有度 1-5
@export_range(1, 5) var rarity: int = 1
定义好 Item 类之后,在编辑器里右键 → 新建资源 → 选择 Item,就能像填表一样直接创建一个「茯苓」材料,填入名称、描述、图标,保存为 fuling.tres。
💡Resource 的最大优势 设计数据和写代码完全分离。数值策划或设计师可以直接在编辑器里增删改物品,不需要改任何代码。这在项目中后期极大地提升了迭代效率。
4.5 编辑器速览与工作区定制
打开 Godot,你会看到五个主要区域。每个区域有明确的职责,理解它们能帮你不再在「面板迷宫」里迷路。
| 面板区域 | 功能 & 常用操作 |
|---|---|
| 场景面板(左上) | 显示当前场景的节点树。在这里添加/删除/重命名节点,调整节点层级 |
| 文件系统(左下) | 项目所有文件的树形结构。拖入这里的文件可直接在场景中使用 |
| 视口(中央) | 游戏场景的可视化编辑区。可以直接拖拽节点位置,看到实时效果 |
| 检查器(右上) | 选中节点后显示其所有属性,可直接编辑(位置、大小、脚本变量) |
| 节点面板(右下) | 显示选中节点的所有信号,可以在这里用鼠标连接信号 |
| 底部:输出面板 | print() 的输出和警告/错误信息都在这里 |
| 底部:调试面板 | 断点调试时显示调用栈和变量值 |

4.5.1 十个最高频快捷键
| 快捷键 | 功能 |
|---|---|
| F5 | 运行项目(从默认场景开始) |
| F6 | 运行当前场景 |
| Ctrl+S | 保存当前场景 |
| Ctrl+Z / Ctrl+Y | 撤销 / 重做 |
| Ctrl+D | 复制选中节点 |
| Delete | 删除选中节点 |
| F | 将视口对准选中节点(Frame) |
| Ctrl+Shift+O | 搜索打开文件 |
| Ctrl+F | 在脚本编辑器里搜索 |
| Alt+Up/Down | 在脚本编辑器里上下移动当前行 |
4.6 第一个项目:创建《酒魂》骨架
理论讲完,动手时间。我们从零开始创建《酒魂》的项目骨架------这个骨架将贯穿本书所有后续章节。
4.6.1 创建项目与目录结构
- 打开 Godot,点击「新建项目」
- 项目名称填入「JiuHun」,选择一个路径(建议 D:/Projects/JiuHun/)
- 渲染器选择「兼容模式(Compatibility)」------对移动端和低配电脑友好
- 点击「创建并编辑」

创建项目后,在文件系统里建立以下目录结构(右键文件夹 → 新建文件夹):
res:// (项目根目录)
├── scenes/ ← 所有场景文件
│ ├── player/
│ ├── enemies/
│ ├── maps/
│ ├── ui/
│ └── systems/ ← 游戏系统场景(酿造、存档等)
├── scripts/ ← 独立的 GDScript 文件(工具类、数据类)
├── resources/ ← Resource 资源文件
│ ├── items/ ← 物品数据 (.tres)
│ ├── recipes/ ← 配方数据
│ └── characters/ ← 角色数据
├── assets/ ← 美术资产
│ ├── sprites/
│ ├── audio/
│ └── fonts/
└── addons/ ← 第三方插件(SQLite 等)

4.6.2 项目基础设置
在「项目 → 项目设置」里做几个关键配置:
- 显示 → 窗口 → Viewport Width 设为 1920,Height 设为 1080(全高清基准分辨率)
- 显示 → 窗口 → 拉伸模式设为「canvas_items」,拉伸比例设为「expand」(自适应分辨率)
- 渲染 → 默认纹理过滤设为「Nearest」(像素风格游戏;写实风则选 Linear)
- 应用 → 启动 → 主场景留空,后面创建 Main.tscn 后再设置


project.godot 关键配置(参考,不需手动编辑)
javascript
# 在项目设置里设置完成后,project.godot 文件会自动更新
# 以下是关键配置项的手动等效内容(了解即可,不需要手动编辑)
[display]
window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
[rendering]
textures/canvas_textures/default_texture_filter=0 # 0 = Nearest
4.6.3 创建第一个场景:GameManager
GameManager 是《酒魂》的全局管理器,它以 Autoload(自动加载)的方式存在,在游戏运行的整个生命周期里始终存在。
-
在 scenes/systems/ 里创建新场景,根节点类型选 Node,命名为 GameManager
-
保存为 GameManager.tscn

-
右键 GameManager 节点 → 添加脚本 → 保存为 scripts/game_manager.gd


-
在项目设置 → 自动加载 → 添加 GameManager.tscn,别名设为 GameManager

scripts/game_manager.gd
javascript
# scripts/game_manager.gd
# 全局游戏管理器,通过 Autoload 自动加载
# 任何节点都可以通过 GameManager.xxx 访问这里的变量和方法
extends Node
# 游戏状态枚举
enum GameState {
MAIN_MENU, # 主菜单
PLAYING, # 游戏中
PAUSED, # 暂停
GAME_OVER, # 游戏结束
LOADING, # 加载中
}
# 当前游戏状态
var current_state: GameState = GameState.MAIN_MENU
# 信号:游戏状态变化时通知全局
signal state_changed(new_state: GameState)
func change_state(new_state: GameState) -> void:
current_state = new_state
emit_signal("state_changed", new_state)
print("[GameManager] 状态变为: ", GameState.keys()[new_state])
func _ready() -> void:
print("[GameManager] 初始化完成")

4.7 版本控制:Git 与 Gitee 配置
游戏项目比普通代码项目更容易出现「一个改动毁掉所有东西」的情况------场景文件被误删、资源被覆盖、数值改乱了又不记得原来是什么。Git 是你的安全网。
4.7.1 初始化 Git 仓库
- 打开终端(Windows 用 Git Bash,macOS/Linux 用 Terminal),进入项目目录
- 执行以下命令初始化仓库并完成第一次提交:
初始化 Git 仓库
javascript
# 进入项目目录
cd D:/Projects/JiuHun
# 初始化 Git 仓库
git init
# 创建 .gitignore 文件(Godot 项目专用)
# 告诉 Git 忽略哪些文件(编译缓存、导出文件等)
echo '.godot/' >> .gitignore
echo '*.import' >> .gitignore
echo 'export/' >> .gitignore
# 添加所有文件
git add .
# 第一次提交
git commit -m "初始化《酒魂》项目骨架"
4.7.2 连接到 Gitee 远程仓库
- 在 gitee.com 注册账号,创建新仓库(名称:JiuHun,设为私有)
- 复制仓库的 HTTPS 地址,在终端执行:
连接远程仓库
javascript
# 添加远程仓库(替换为你的 Gitee 仓库地址)
git remote add origin https://gitee.com/你的用户名/JiuHun.git
# 推送到远程仓库
git push -u origin main
# 以后每次提交后,用这个命令同步到远程
git push
💡好的 commit 习惯 每完成一个功能就 commit 一次,描述要具体: ✅ git commit -m "完成玩家移动脚本,支持八方向移动" ✅ git commit -m "添加醉意值系统,完成 DrunkSystem.gd" ❌ git
commit -m "更新了一些东西" 具体的提交信息让你在出问题时能快速找到引入问题的那次提交。
本章小结
本章建立了开发《酒魂》的技术基础。核心概念回顾:
- 节点是积木,场景是可复用的节点树------这是 Godot 一切的基础
- 信号是神经系统------系统之间通过信号解耦通信,而不是直接引用
- @onready 保证节点引用时机,是最常用的修饰符
- Resource 让数据和代码分离,设计师可以直接在编辑器里修改游戏数据
- 项目结构要从第一天就规划好,后期重构代价极高
- Git 是安全网,每完成一个功能就提交一次
🚀下一章 第 5 章深入 GDScript语言:变量、函数、控制流、信号实战、节点引用------并亲手构建一个可交互的「酿酒缸」节点,把本章所有概念串联起来。
延伸阅读
- Godot 官方文档「节点与场景」(docs.godotengine.org)------本章内容的权威参考
- Godot 官方文档「信号」------信号的所有使用方式和最佳实践
- GDQuest「Godot 4 入门教程」(YouTube)------视频形式的编辑器上手,和本章内容互补
- 《游戏编程模式》第 1 章「命令模式」之前的序言------解释为什么「解耦」如此重要