从标题到结算:场景切换驱动的游戏循环
godot-game-one 的游戏循环没有传统的 FSM enum——“状态”由场景切换驱动。标题是一个场景,大厅是一个场景,战斗是一个场景,结算是一个场景。场景之间的切换就是状态之间的转移。
graph TD
A["TitleScreen"] -->|"单机"| B["Main 战斗场景"]
A -->|"联机"| C["Lobby 大厅"]
C -->|"房主开始"| B
B -->|"game_running = false"| D["GameOver 结算"]
D -->|"按 R / 点击"| B
Autoload 单例:跨场景的状态容器
场景切换会清空场景树,但 Autoload 单例不会。GameState 作为全局 Autoload,在场景切换间保持存活:
1 | # game_state.gd — Autoload 单例 |
main.gd 进入战斗时调用 GameState.game_running = true,玩家死亡时设为 false,GameOver 界面读取这个值决定显示成功还是失败。
状态切换入口
标题 → 战斗(单机)
1 | # title_screen.gd |
标题 → 大厅 → 战斗(联机)
1 | # lobby.gd |
关键设计:每次切换场景前都先清理所有网络服务(stop_all() + disconnect_network()),避免残留的连接状态污染下一个场景。
战斗内部:main.gd 的子状态
进入 main.tscn 后,战斗场景内部还有隐式的子状态:
| 阶段 | 标志 | 行为 |
|---|---|---|
| 初始化 | _ready() 完成 |
生成玩家、初始化网络同步 |
| 战斗中 | GameState.game_running = true |
敌人波次、Boss 战、掉落物 |
| Boss 战 | current_boss != null |
切换 BGM、调整弹幕密度 |
| 结算 | GameState.game_running = false |
停止敌人生成、显示结算面板 |
1 | # main.gd 战斗结束判定 |
结算 → 重新开始
1 | # game_over_panel 或 HUD 中 |
reload_current_scene() 重新加载 main.tscn,但 GameState.reset_game() 先清零所有状态(分数、等级、击杀数、多人玩家状态等)。如果不 reset,新一局会继承上一局的数据。
为什么不用 State Pattern?
这个项目的”状态”粒度很粗——标题、大厅、战斗、结算,每个都是完整的场景,不是轻量的状态对象。场景切换本身就是最自然的状态转移:
- Godot 原生支持:
change_scene_to_file()自动清理场景树,不需要手动管理生命周期 - 状态隔离天然存在:每个场景的节点互不影响,不会出现旧状态泄漏
- Autoload 做桥接:跨场景的数据(分数、等级、网络状态)全部放在 Autoload 单例里
如果用 State Pattern,反而需要自己管理场景加载/卸载、节点生命周期、资源释放——这些 Godot 已经帮你做了。
场景内部的轻量状态
场景内部的小状态(比如 Boss 的战斗阶段 boss.gd 中的多阶段状态机、精英怪的四阶段设计)用 enum + match 管理。粗粒度用场景切换,细粒度用 enum。两层状态管理各司其职。
状态管理不是越抽象越好。粗粒度的状态用场景切换,细粒度的状态用 enum。匹配问题规模的最简方案就是好方案。