谢夏戈 @ xiexiage.com

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() 找路径。

注册步骤

路径:项目 → 项目设置 → 全局 → 自动加载

05-game-manager-全局单列GM

  1. 路径:选 res://scripts/autoload/game_manager.gd
  2. 节点名称:自动填好 GameManager(也可以手动改)
  3. 点击「添加」
  4. 确认右侧「启用」是勾选状态

注册之后,任何节点的脚本里都能直接写 GameManager.xxx 来访问它,比如:

GameManager.add_score(1)
GameManager.current_state

无需 preload / load / get_node,超方便。

这段代码做了啥?

enum GameState

enum GameState { READY, PLAYING, GAME_OVER }

enum(枚举)就是给一组"状态"取个好记的名字。本质上 READY=0PLAYING=1GAME_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 就 +2
  • score_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,整个流程才会真正跑起来。

2023-PRESENT © 谢夏戈