12 KiB
GameManager 重构计划(待重新评审)
状态:已暂存方案,尚未获得最终认可,后续继续讨论前不要据此扩大实现。
记录日期:2026-07-16
当前结论
目标是把 GameManager 收敛为游戏会话协调器,但本轮方案和实现涉及范围明显大于最初预期。目前不应把现有实现视为最终架构,应先逐项重新评审,再决定保留、简化或撤回哪些改动。
已明确需要重新讨论的问题:
NarrativeFlowController、TalkSceneCatalog、ChapterController的职责是否拆分过度。NarrativeFlowController与ChapterController当前存在双向依赖,边界没有真正理顺。TalkSceneCatalog与现有 Chapter Graph Editor 只做到数据层兼容,没有做到创作工作流兼容。- Chapter Graph Editor 新建、删除章节时不会同步 Catalog;Catalog 的首章、角色、显示状态和顺序还需要单独维护。
- 在决定保留 Catalog 前,需要先确认章节编辑器是否应继续作为唯一内容创作入口。
原始目标
将 GameManager 收敛为唯一的游戏会话协调器:
- 只管理会话状态、命令互斥、开始、读档、转场、暂停和退出。
- 不再持有章节目录、显示设置、应用启动和 Fix 内部清理职责。
- 一次性迁移旧调用方,不长期保留旧 API 或兼容转发层。
- 保留
GameStart的早期语义,增加SessionReady表示场景和交互真正就绪。 - 新格式存档结构及
sceneSoName身份保持不变。 - 旧格式存档停止运行时恢复。
候选组件边界
| 组件 | 候选职责 | 重新评审重点 |
|---|---|---|
AppBootstrapper |
设置、SnapshotRegistry、本地化、AppStart 和启动遮罩 |
是否值得从 GameManager 单独拆出 |
GameManager |
会话状态、互斥、开始、读档、暂停、返回主界面 | 保留为核心目标 |
NarrativeFlowController |
当前章节、进章和出口跳转 | 是否可以进一步简化,是否与 ChapterController 重叠 |
TalkSceneCatalog |
正式章节、恢复兼容章节和显示顺序目录 | 是否保留;若保留,必须与章节编辑器整合 |
SceneLoader |
串行、可验证的 Addressables 场景加载与卸载 | 可以独立保留 |
SceneReadinessService |
收集当前场景 Gate 并等待业务就绪 | 是否需要独立类型或可并入场景流程 |
SaveRestoreOrchestrator |
快照预检、Provider Barrier、Anchor 恢复和结构化结果 | 与 GameManager 的编排边界需复核 |
ChapterController |
解锁数据和章节面板数据 | 应重命名、收紧职责或合并 |
DisplaySettingsController |
窗口模式和分辨率应用 | 是否值得独立拆出 |
候选会话状态
public enum GameSessionPhase
{
MainMenu,
StartingSession,
Transitioning,
Restoring,
Playing,
ReturningToMenu
}
会话只读状态候选字段:
PhaseIsPausedIsDialogueActiveIsInGameIsTransitioningCanPauseIsInputAvailable
需要重新确认:这些派生状态是否与现有输入、UI 和存档判断相比确实降低复杂度,而不是单纯扩大迁移范围。
GameManager 候选公共 API
public GameSessionState State { get; }
public bool TryStartNewGame();
public bool TryStartChapter(TalkSceneSO chapter);
public bool TryRestoreSlot(int slotIndex, Action<RestoreResult> completed = null);
public bool TryRestoreFile(
string path,
RestoreOptions options,
Action<RestoreResult> completed = null);
public void PauseSession();
public void ResumeSession();
public void ReturnToMainMenu();
public void QuitApp();
Yarn 内部候选入口:
internal IEnumerator AdvanceChapterRoutine(string exitName);
internal IEnumerator LoadNarrativeSceneRoutine(string sceneName);
internal IEnumerator UnloadNarrativeSceneRoutine();
旧 API 的删除范围需要在重新评审后逐项确定,避免为了接口整洁产生不必要的大规模迁移。
事件语义候选
保留:
AppStartGameStartGameQuitPauseGameUnPauseGame
候选新增:
SessionReadySessionTeardownStartedSessionFailed
核心规则:
GameStart表示开始进入正式游戏会话。- 切章和 Yarn 内部换场景不重复发送
GameStart。 - 场景、Gate、对话或 Anchor 全部成功后发送
SessionReady。 - GameManager 先更新自身状态,再广播事件。
DialogStart/DialogEnd只更新对话活动状态。- 新会话、失败和退出时原子清理暂停及对话状态。
转场互斥和取消候选规则
- GameManager 只允许一个活动命令。
- 开始、切章、读档进行中,普通请求直接拒绝,不排队。
- 返回主界面允许请求取消当前流程。
- 已经开始的 Addressables 操作不强行中断;底层操作完成后停止后续 Gate、对话或 Anchor。
- Provider 和 SceneLoader 的不可中断单步允许完成,但阶段之间检查取消。
- 所有活动命令必须通过
try/finally释放。
章节系统待决方案
原 Catalog 设想
Catalog 显式登记:
FirstScene- Runtime 章节
- RestoreOnly 章节
- 章节面板显示状态
- 章节面板顺序
- 以
TalkSceneSO.name为键的恢复查询
原校验规则包括:
- 引用和资产名唯一。
- FirstScene 必须属于 Runtime。
- Runtime 出口目标必须在 Runtime Catalog。
- RestoreOnly 不能显示在章节面板。
- Runtime 必须具有 YarnProject 和场景 Key。
当前暴露的问题
现有 Chapter Graph Editor 以指定文件夹中的 TalkSceneSO 为数据源,并直接编辑 exits。Catalog 是另一份显式目录,因此形成两个需要同步的创作入口。
如果继续保留 Catalog,至少需要做到:
- Chapter Graph Editor 新建章节时同步新增 Runtime 条目。
- 删除章节时同步移除 Catalog 引用,并处理其他节点出口。
- 在章节编辑器内设置 FirstScene。
- 在章节编辑器内维护 Runtime/RestoreOnly、是否显示和章节顺序。
- RestoreOnly 使用独立区域,不混入正式剧情图。
- 加载和校验时区分“当前文件夹过滤”与“全局 Catalog 缺失”,避免误报。
- 所有 Catalog 修改支持 Undo。
重新评审时需要在以下方向中选择:
- 保留 Catalog,并让 Chapter Graph Editor 成为唯一维护入口。
- 取消显式 Catalog,继续由首章链和少量恢复补充目录组成运行时数据。
- 由章节编辑器生成只供运行时使用的 Catalog,不要求人工维护 Catalog Inspector。
在该选择确定之前,不继续扩大章节系统改动。
场景加载与 Gate 候选规则
SceneLoader 候选约束:
- 真正等待正在执行的场景操作完成。
- 使用强类型
AsyncOperationHandle<SceneInstance>。 - 校验空 Key、Addressables 状态和 SceneInstance。
- 只有加载成功后更新当前场景名。
try/finally复位状态和 Loading UI。- 加载失败回滚 ResourceSystem 场景生命周期。
- 卸载失败保留真实句柄和场景名。
- 移除 fire-and-forget 场景入口。
Gate 候选规则:
- 场景加载后从对应 Scene 根对象收集
ISceneDialogueGate。 - 不维护跨场景静态注册表。
- 使用 unscaled time,超时 15 秒。
- 无 Gate 时等待稳定一帧后成功。
- Gate 销毁、取消或超时都返回结构化失败。
- 普通进章在加载后等待。
- 读档在 Provider Barrier 后、Anchor 前等待。
- 超时不得强行启动 Yarn。
存档恢复候选流程
恢复拆成:
PrepareRestore:读盘、格式识别、章节/Yarn/节点预检,不修改运行时
ExecuteRestore:变量、场景、章节、Provider、Readiness、Anchor
候选恢复顺序:
获取恢复锁
→ PrepareRestore
→ GameStart(从主界面恢复时)
→ Phase=Restoring
→ 遮黑
→ 清理现有会话(如有)
→ Yarn variables
→ Scene
→ 设置当前章节
→ Providers
→ SceneReadiness
→ RestoreAnchor
→ Postflight
→ Phase=Playing
→ SessionReady
→ 揭示画面
恢复失败时不得继续 Anchor,应安全清理并回到主界面。旧格式运行时恢复应明确返回不支持,不修改 Yarn 变量或自动档。
需要重新评审:恢复编排究竟应由 GameManager 掌握到什么粒度,以及哪些失败清理可以继续留在 Orchestrator 内部。
暂停和返回主界面候选规则
暂停:
更新暂停状态
→ Time.timeScale = 0
→ PauseGame
恢复:
Time.timeScale = 1
→ 更新暂停状态
→ UnPauseGame
返回主界面:
取消当前转场
→ 遮黑
→ SessionTeardownStarted
→ 等待对话停止
→ 卸载场景
→ State=MainMenu
→ GameQuit
→ 错误提示(如有)
→ 揭示主界面
暂停和清理副作用是否全部下沉到 Audio、Dialog UI、Screen Effect、Camera 和 UIManager,需要逐个确认现有监听器是否因此变得更复杂。
应用启动与显示设置候选
候选 AppBootstrapper 职责:
SettingLoader.InitSnapshotRegistry.EnsureInitialized- 启动遮罩
- 等待本地化
- 发送
AppStart - UI 本地化刷新
- 显示主界面
候选 DisplaySettingsController 职责:
- 应用窗口模式、分辨率和全屏状态。
- 等待实际分辨率变化。
- 发送独立显示事件。
- CameraAspectAdapter 改监听显示事件。
这两项都需要按“是否确实降低 GameManager 复杂度”重新判断,不以拆分类数量为目标。
调用方迁移候选范围
- Yarn
next、load_scene、unload_scene。 LineAdvanceInput。UIManager、CursorManager和游戏内 Terminal。SavePointEvaluator。- Snapshot 捕获和恢复。
ChapterController和章节选择 UI。- 所有退出主界面入口。
- 开发存档跳转工具。
重新实施时应分批迁移并保持每批可编译,不再默认一次性扩大所有调用方改动。
候选验收矩阵
- Unity batchmode 编译无错误。
- 章节目录和 Chapter Graph 校验无错误。
- 新格式开发存档逐档冒烟成功。
- 连续点击两次新游戏只接受一次。
- 场景加载中返回主界面不会启动对话,并最终安全卸载。
- 暂停后返回主界面不会残留
timeScale、暂停或对话状态。 - 默认出口和命名出口进入正确章节。
- 非法出口、非法章节和场景加载失败安全回主界面。
- Fix、Subway、Outside 在 Gate Ready 后启动对话。
- Gate 永久不 Ready 时超时失败,不启动 Yarn。
- 读档 Provider 完成后经过 Gate,再恢复 Anchor。
- 读档预检失败不加载目标场景。
- 旧格式存档提示不兼容且不修改运行时。
- Yarn 场景命令不绕过互斥。
- 返回主界面和切场景后没有资源、对话、相机和表现 UI 残留。
下次继续时的建议顺序
- 先决定是否保留
TalkSceneCatalog,以及章节编辑器谁是唯一数据维护入口。 - 再确定
NarrativeFlowController与ChapterController是保留、重命名还是合并。 - 画出最小调用链,限定 GameManager 必须负责和明确不负责的部分。
- 对现有未提交实现逐文件分类:保留、简化、撤回。
- 先处理章节与恢复主链,再考虑 AppBootstrapper、DisplaySettings 和错误 UI 等外围拆分。
- 每批修改后单独编译和冒烟,不再把整份方案视作必须一次完成的整体。
当前工作区说明
当前工作区已经存在一版按原方案实施的未提交改动。该实现通过了当时的 Unity 编译及现有测试,但这不代表架构已获认可。下次继续时应先检查当前 diff,并以本文件记录的待决问题为准,避免直接在现有实现上继续叠加。