godot-game-one 的游戏循环没有传统的 FSM enum——“状态”由场景切换驱动。标题是一个场景,大厅是一个场景,战斗是一个场景,结算是一个场景。场景之间的切换就是状态之间的转移。

Autoload 单例:跨场景的状态容器

场景切换会清空场景树,但 Autoload 单例不会。GameState 作为全局 Autoload,在场景切换间保持存活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# game_state.gd — Autoload 单例
const START_HEALTH: int = 50

var game_running: bool = false
var score: int = 0
var level: int = 1
var current_health: int = START_HEALTH
var max_health: int = START_HEALTH

func reset_game() -> void:
score = 0
level = 1
kills = 0
total_kills = 0
death_message = ""
game_running = true
kills_for_next_level = 10
last_elite_threshold = 0
elite_encounter_count = 0
post_elite_multiplier = 1.0
boss_encounter_count = 0
last_boss_level = 0
_player_states.clear()
_player_scores.clear()
# ... 更多多人状态重置

main.gd 进入战斗时调用 GameState.game_running = true,玩家死亡时设为 false,GameOver 界面读取这个值决定显示成功还是失败。

状态切换入口

标题 → 战斗(单机)

1
2
3
4
5
6
# title_screen.gd
func _start_game() -> void:
NetworkDiscovery.stop_all()
HarmonyBridge.stop_all()
NetworkManager.disconnect_network()
get_tree().change_scene_to_file("res://scenes/main.tscn")

标题 → 大厅 → 战斗(联机)

1
2
3
4
5
6
# lobby.gd
@rpc("authority", "reliable", "call_local")
func _rpc_start_game() -> void:
_stop_discovery_services()
game_started = true
get_tree().change_scene_to_file("res://scenes/main.tscn")

关键设计:每次切换场景前都先清理所有网络服务(stop_all() + disconnect_network()),避免残留的连接状态污染下一个场景。

战斗内部:main.gd 的子状态

进入 main.tscn 后,战斗场景内部还有隐式的子状态:

阶段 标志 行为
初始化 _ready() 完成 生成玩家、初始化网络同步
战斗中 GameState.game_running = true 敌人波次、Boss 战、掉落物
Boss 战 current_boss != null 切换 BGM、调整弹幕密度
结算 GameState.game_running = false 停止敌人生成、显示结算面板
1
2
3
4
5
6
# main.gd 战斗结束判定
func _on_player_died() -> void:
GameState.game_running = false
# 显示 GameOver UI
game_over_panel.visible = true
game_over_label.text = "得分: %d" % GameState.score

结算 → 重新开始

1
2
3
4
5
# game_over_panel 或 HUD 中
func _unhandled_input(event: InputEvent) -> void:
if not GameState.game_running and event.is_action_pressed("ui_accept"):
GameState.reset_game()
get_tree().reload_current_scene()

reload_current_scene() 重新加载 main.tscn,但 GameState.reset_game() 先清零所有状态(分数、等级、击杀数、多人玩家状态等)。如果不 reset,新一局会继承上一局的数据。

为什么不用 State Pattern?

这个项目的”状态”粒度很粗——标题、大厅、战斗、结算,每个都是完整的场景,不是轻量的状态对象。场景切换本身就是最自然的状态转移:

  1. Godot 原生支持:change_scene_to_file() 自动清理场景树,不需要手动管理生命周期
  2. 状态隔离天然存在:每个场景的节点互不影响,不会出现旧状态泄漏
  3. Autoload 做桥接:跨场景的数据(分数、等级、网络状态)全部放在 Autoload 单例里

如果用 State Pattern,反而需要自己管理场景加载/卸载、节点生命周期、资源释放——这些 Godot 已经帮你做了。

场景内部的轻量状态

场景内部的小状态(比如 Boss 的战斗阶段 boss.gd 中的多阶段状态机、精英怪的四阶段设计)用 enum + match 管理。粗粒度用场景切换,细粒度用 enum。两层状态管理各司其职。

状态管理不是越抽象越好。粗粒度的状态用场景切换,细粒度的状态用 enum。匹配问题规模的最简方案就是好方案。