GameManager
May 12
游戏到这一步已经能玩了,但缺少游戏状态管理和计分。我们需要一个统一的管理者来掌控全局 — 这就是 GameManager。
为什么需要 GameManager?
想象一下,游戏有 3 种状态:
- READY(准备)— 显示"点击开始"
- PLAYING(进行中)— 小鸟能跳、水管在生成
- GAME_OVER(结束)— 显示分数、可以重开
如果让每个节点自己判断状态,比如小鸟自己看"现在能不能跳"、水管自己看"现在能不能生成",代码会乱成一团 — 改一个地方要同步改很多处。
GameManager 就是所有节点共同遵守的"裁判" — 状态、分数都由它统一保管,其他节点只问它要、不自己存。
创建 GameManager 脚本
新建文件 scripts/autoload/game_manager.gd:
extends Node
# 定义游戏状态
enum GameState { READY, PLAYING, GAME_OVER }
var current_state = GameState.READY
var total_score: int = 0 # 总分
var high_score: int = 0 # 最高分
# 信号:通知其他节点
signal score_changed(new_score)
signal state_changed(new_state)
## 清空分数
func clear_score():
total_score = 0
score_changed.emit(total_score)
## 添加得分
func add_score(amount: int = 1):
total_score += amount
score_changed.emit(total_score)
## 设置当前游戏状态
func set_state(new_state: GameState):
current_state = new_state
state_changed.emit(new_state)
## 游戏结束
func game_over():
set_state(GameState.GAME_OVER)
if high_score < total_score:
high_score = total_score
Autoload — 全局单例
脚本写好了,但现在它只是个 .gd 文件,别的节点没办法访问到它。
Godot 提供了 Autoload(自动加载)机制 — 注册之后,这个脚本/场景全程只有一份,任何节点都能直接通过名字访问,不需要 get_node() 找路径。
注册步骤
路径:项目 → 项目设置 → 全局 → 自动加载

- 路径:选
res://scripts/autoload/game_manager.gd - 节点名称:自动填好
GameManager(也可以手动改) - 点击「添加」
- 确认右侧「启用」是勾选状态
注册之后,任何节点的脚本里都能直接写 GameManager.xxx 来访问它,比如:
GameManager.add_score(1)
GameManager.current_state
无需 preload / load / get_node,超方便。
这段代码做了啥?
enum GameState
enum GameState { READY, PLAYING, GAME_OVER }
enum(枚举)就是给一组"状态"取个好记的名字。本质上 READY=0、PLAYING=1、GAME_OVER=2,但写代码时用名字比写数字清楚得多:
# ❌ 不直观
if current_state == 0:
# ✅ 一眼明白
if current_state == GameManager.GameState.READY:
signal(信号)
signal score_changed(new_score)
signal state_changed(new_state)
信号是 Godot 的「事件通知机制」。GameManager 不需要知道谁在听 — 它只管"广播消息":
- 分数变了 → 喊一声
score_changed,UI 接到了就更新分数显示 - 状态变了 → 喊一声
state_changed,所有关心状态的节点都能响应
这样 GameManager 不用知道 UI 长啥样,UI 也不用主动来问 GameManager。两边解耦,互不干扰 — 信号是 Godot 里非常核心的设计思想。
add_score() 和 score_changed.emit()
func add_score(amount: int = 1):
total_score += amount
score_changed.emit(total_score)
amount: int = 1— 参数默认值是 1,不传就 +1,传 2 就 +2score_changed.emit(total_score)— 发出信号,把最新分数传出去
set_state() 和 game_over()
set_state 是修改状态的唯一入口,每次修改都自动 emit 信号。这样任何状态改变都会被广播,不会漏。
game_over 不仅切换状态,还顺便更新最高分。
把 GameManager 接进 Killzone 和 障碍物
GameManager 创建好了,现在回到 上一章 里我们留的"后续优化预览"代码,把它们正式接入:
死区脚本(killzone.gd)
extends Area2D
func _on_body_entered(body: Node2D) -> void:
if body.name == "Bird" and GameManager.current_state == GameManager.GameState.PLAYING:
GameManager.game_over()
加上 current_state == PLAYING 判断 — 防止 READY 状态(还没开始)和 GAME_OVER 状态(已经结束)时误触发。
障碍物脚本(pillar_pair.gd)
extends Node2D
@export var speed := 200.0
@onready var goal: Area2D = $Goal
func _physics_process(delta: float) -> void:
# 只在游戏进行中才移动
if GameManager.current_state == GameManager.GameState.PLAYING:
position.x -= speed * delta
if position.x < -500:
queue_free()
func _on_goal_body_entered(body: Node2D) -> void:
if body.name == "Bird":
if GameManager.has_method("add_score"):
GameManager.add_score(1)
goal.set_deferred("monitoring", false)
生成器脚本(pillar_spawner.gd)
extends Node2D
@export var pillar_scene: PackedScene
@export var y_range := 250.0
func _on_timer_timeout() -> void:
spawn_pillar()
func spawn_pillar() -> void:
# 只在游戏进行中才生成
if GameManager.current_state == GameManager.GameState.PLAYING:
var new_pillar = pillar_scene.instantiate()
var spawn_pos = global_position
spawn_pos.y += randf_range(-y_range, y_range)
new_pillar.global_position = spawn_pos
get_tree().current_scene.add_child(new_pillar)
测试运行
现在还没有 UI,所以视觉上看不出区别 — 但状态系统已经在跑了。
你可以在 bird.gd 里临时加个调试:
func _physics_process(delta: float) -> void:
if Input.is_action_just_pressed("fly"):
print("当前状态:", GameManager.current_state)
# ...
不过这时候默认是 READY 状态,水管不会生成、撞死也不会触发 — 因为我们还没有"开始游戏"的入口。下一章做 UI 时会加上一个开始按钮,把状态切到 PLAYING,整个流程才会真正跑起来。