gewuyou 85f7c1707e docs(game): 同步场景与宿主入口配置边界
- 补充 Scene 与 UI 入口对配置系统正式边界页的指引

- 明确 oneOf、anyOf 与非 false additionalProperties 不属于默认采用路径

- 更新 Godot storage 入口对 VS Code 工具辅助层与 raw YAML 回退路径的说明
2026-04-30 13:25:29 +08:00

8.9 KiB
Raw Blame History

title, description
title description
场景系统 说明 GFramework.Game 场景路由的当前入口、项目侧接入职责与扩展边界。

场景系统

GFramework.Game 的场景系统是“路由基类 + 场景契约 + 过渡管线”的组合,不是替你包办注册表、节点树和引擎对象装配的 一体化方案。

框架当前负责的是:

  • 场景栈管理
  • Load -> Enter -> Pause -> Resume -> Exit -> Unload 生命周期顺序
  • 路由守卫与过渡处理器执行时机
  • SceneRouterBase 这一层的默认切换编排

项目或引擎适配层仍然需要自己提供:

  • ISceneFactory
  • ISceneRoot
  • 具体的 ISceneBehavior / IScene
  • 场景键和资源、节点、预制体之间的映射关系

如果你把它理解为“可复用的场景路由底座”而不是“现成的完整场景框架”,后续接法会更贴近源码。

当前公开入口

IScene

业务场景生命周期契约,描述加载、进入、暂停、恢复、退出、卸载这六个阶段。

ISceneBehavior

路由器直接操作的运行时对象。它除了场景生命周期外,还携带:

  • Key
  • Original
  • IsLoaded
  • IsActive
  • IsTransitioning

如果你的引擎对象本身就能承担这些语义,可以直接实现 ISceneBehavior。如果你更想把业务逻辑放在纯 C# 场景类中,也可以由 项目侧行为包装器承载真正的引擎节点,再把业务场景逻辑委托出去。

ISceneRouter

当前公开的路由接口,重点入口是:

  • BindRoot(ISceneRoot root)
  • ReplaceAsync(string sceneKey, ISceneEnterParam? param = null)
  • PushAsync(string sceneKey, ISceneEnterParam? param = null)
  • PopAsync()
  • ClearAsync()
  • Contains(string sceneKey)

SceneRouterBase

GFramework.Game 提供的默认实现基类。它会:

  • OnInit() 中获取 ISceneFactory
  • 通过 SemaphoreSlim 串行化切换
  • 调用守卫、过渡处理器和环绕处理器
  • 维护场景栈与恢复顺序

通常项目不会直接修改框架里的 SceneRouterBase,而是在项目层继承它。

场景栈的真实语义

按当前实现,最常用的三个动作语义如下:

  • ReplaceAsync
    • 清空整个栈,再加载并进入目标场景。
  • PushAsync
    • 先检查守卫,再创建新场景,挂到 ISceneRoot,执行 OnLoadAsync(),暂停当前栈顶,最后让新场景 OnEnterAsync()
  • PopAsync
    • 对栈顶执行离开检查,通过后退出并卸载它,再从 ISceneRoot 移除,然后恢复新的栈顶。

当前还有两个容易混淆的点:

  • SceneRouterBase 默认不允许同一个 sceneKey 在栈中重复存在;内部会先做 Contains(sceneKey) 检查
  • 框架不会替你实现“场景键 -> 具体场景实例”的注册逻辑;这仍然是 ISceneFactory 或项目注册表的职责

最小接入路径

推荐按下面的顺序接入。

推荐目录与文件约定(项目侧)

场景系统的目录结构不由框架强制,但建议把“路由编排、实例创建、引擎挂载、业务场景”分开放置,避免后续把 SceneRouterBase 派生类写成巨型协调器。

Game/Scene/
  GameSceneRouter.cs
  GameSceneFactory.cs
  SceneRoot.cs
  Scenes/
    GameplayScene.cs
    PauseMenuScene.cs
  Params/
    GameplayEnterParam.cs
  Registry/
    SceneRegistry.cs

推荐约定如下:

  • GameSceneRouter.cs:项目侧 router继承 SceneRouterBase,只注册 guard、transition handler 和 around handler
  • GameSceneFactory.cs:实现 ISceneFactory,负责 sceneKey -> ISceneBehavior 的映射与实例创建
  • SceneRoot.cs:实现 ISceneRoot,负责把行为对象对应的引擎节点挂到场景容器并移除
  • Scenes/*:放具体业务场景、行为包装器或引擎节点包装类型
  • Params/*:放实现 ISceneEnterParam 的进入参数,按业务场景拆分
  • Registry/*:如果项目已有场景表或资源表,建议收口在这里,再由 GameSceneFactory 使用

最小 wiring 通常是:

architecture.RegisterUtility<ISceneFactory>(new GameSceneFactory());
architecture.RegisterSystem(new GameSceneRouter());

然后在 SceneRoot 的引擎生命周期就绪点调用 BindRoot(this)。如果项目已有不同的资源目录、节点层级或场景注册表, 保留原结构即可;只要最终能提供 ISceneFactoryISceneRootISceneBehavior,就不需要为了框架重排所有文件。

1. 准备项目自己的 router

using GFramework.Game.Scene;
using LoggingTransitionHandler = GFramework.Game.Scene.Handler.LoggingTransitionHandler;

public sealed class GameSceneRouter : SceneRouterBase
{
    protected override void RegisterHandlers()
    {
        RegisterHandler(new LoggingTransitionHandler());
    }
}

这一步只解决“切换流程怎么跑”,不解决“场景从哪来”。

2. 提供 ISceneFactory

SceneRouterBase 会在初始化阶段通过 GetUtility<ISceneFactory>() 获取工厂,因此项目必须先注册它。

工厂的职责通常是:

  • sceneKey 找到项目自己的注册表、预制体或资源描述
  • 创建或获取 ISceneBehavior
  • 决定行为对象如何包裹引擎节点与业务场景逻辑

如果项目里已经有场景注册表,也建议把它收口在 factory 内部,而不是让文档继续暗示框架自带统一注册中心。

3. 提供 ISceneRoot

ISceneRoot 只做两件事:

  • AddScene(ISceneBehavior scene)
  • RemoveScene(ISceneBehavior scene)

也就是说root 是“挂载/移除容器”,不是路由器本身。项目通常会在自己的 Godot 节点里实现 ISceneRoot,并在 _Ready() 时调用 BindRoot(this)

4. 把 router 和 factory 装进架构

architecture.RegisterUtility<ISceneFactory>(new GameSceneFactory());
architecture.RegisterSystem(new GameSceneRouter());

如果你的项目还需要动画、黑幕或 loading 过渡,可以继续在 RegisterHandlers() 里补自己的处理器。

5. 在 root 就绪后绑定

public sealed class SceneRoot : Node2D, ISceneRoot
{
    [GetSystem] private ISceneRouter _sceneRouter = null!;

    public override void _Ready()
    {
        __InjectContextBindings_Generated();
        _sceneRouter.BindRoot(this);
    }

    public void AddScene(ISceneBehavior scene)
    {
        // 项目侧决定如何把 scene.Original 挂进引擎节点树
    }

    public void RemoveScene(ISceneBehavior scene)
    {
        // 项目侧决定如何移除并释放引擎对象
    }
}

6. 从业务代码发起导航

await sceneRouter.ReplaceAsync(
    "Gameplay",
    new GameplayEnterParam
    {
        Seed = "new-game"
    });

await sceneRouter.PushAsync("PauseMenu");
await sceneRouter.PopAsync();

扩展点

路由守卫

如果你要在进入或离开场景前做业务检查,实现 ISceneRouteGuard

  • CanEnterAsync(string sceneKey, ISceneEnterParam? param)
  • CanLeaveAsync(string sceneKey)

适合放:

  • 未保存进度拦截
  • 场景解锁条件检查
  • 新手引导流程限制

过渡处理器

SceneRouterBase 公开了:

  • RegisterHandler(ISceneTransitionHandler handler, SceneTransitionHandlerOptions? options = null)
  • RegisterAroundHandler(ISceneAroundTransitionHandler handler, SceneTransitionHandlerOptions? options = null)

适合放:

  • 日志
  • 黑幕、淡入淡出或 loading 动画
  • 切场前后的指标采集

如果你的项目已经有复杂引擎过渡逻辑,优先把这些逻辑放进 handler而不是把 SceneRouterBase 派生类本身做成巨型协调器。

与旧写法的边界

下面这些说法不再适合作为默认接入指导:

  • “框架会帮你直接注册和发现所有场景类型”
  • “只要写一个 IScene 就能自动接入所有引擎对象”
  • “场景系统本身自带统一注册表和完整项目结构”

当前更准确的理解是:

  • 框架提供通用场景切换编排
  • 项目提供 factory、root、资源映射和具体引擎装配
  • 文档中的最小示例应优先说明职责边界,而不是继续堆叠大而全教程

配置系统边界提示

如果你的场景路由接线同时依赖 AI-First 配置系统,本页只负责说明场景宿主、路由和生命周期接法,不负责定义配置 schema 的正式支持边界。涉及 YAML 配置契约、组合关键字或编辑器辅助能力时,请回到 Game 配置系统 作为正式说明页。

默认采用路径之外的场景包括:

  • oneOf / anyOf
  • falseadditionalProperties
  • 依赖开放对象形状、形状合并或更复杂嵌套数组的 schema shape

这类复杂 shape 不应从场景接线页推断支持范围。VS Code 工具只是辅助编辑与预览层;如果遇到这些情况,应直接回到 raw YAML 和 schema 本体设计处理。

推荐阅读

  1. Game 模块总览
  2. UI 系统
  3. Game 抽象层说明
  4. API 参考