场景流程

异步场景加载,支持加载画面、取消与叠加多场景。

一个可等待的场景转换 API · 自动加载画面、进度事件、协作式取消 · 内建叠加多场景分层 · 无操作替代:NullSceneService

CGS 通过 ISceneService 在场景之间移动你的游戏——每一次"菜单到玩法再回到菜单"的转换都用这一个可等待的 API。你调用 await _scene.LoadAsync("Gameplay"),其余由服务协调:自动显示你的加载画面、把它在屏幕上保持足够长的时间以避免一帧闪烁、为你的进度条发布进度事件,并支持协作式取消(玩家按下 Esc,加载停止)。

同一个服务还处理叠加场景(additive scene)——加载到当前场景之上而不是替换它的场景,这正是叠一层 UI 场景或流式载入相邻区域的方式。Single 模式与叠加加载是同一个服务的两个表面;没有额外的设置。

功能概述

  • 每次转换一个异步调用。 LoadAsync 返回一个可 awaitTask<SceneLoadResult>。成功与失败以结果对象返回;取消与超时抛出 OperationCanceledException
  • 自动加载画面。 在选项中传入一个面板,服务会在加载前压入、加载后弹出,并带可配置的最短显示时间。
  • 进度事件。 服务在事件总线上发布加载开始/进度/完成/取消事件——正是淡出、淡入音乐的合适位置。
  • 叠加分层。 在基础场景之上加载和卸载额外场景,检视已加载场景的完整列表,并选择 Unity 把哪一个当作活动场景。

获取服务

解析一次并缓存引用(绝不要在 Update 里):

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class MenuController : MonoBehaviour
{
    private ISceneService _scene;

    private void Awake()
    {
        _scene = ServiceLocator.Resolve<ISceneService>();
    }
}

加载场景(Single 模式)

Single 模式加载会替换当前场景——标准的主菜单到玩法的转换。

Task<SceneLoadResult> LoadAsync(string sceneName,
                                SceneLoadOptions opts = default,
                                CancellationToken ct = default)

按名称加载场景。场景必须已在 Build Settings 中注册(File > Build Settings > Scenes in Build)。成功或出错时返回 SceneLoadResult;取消或超时抛出 OperationCanceledException

Task<SceneLoadResult> ReloadCurrentAsync(SceneLoadOptions opts = default,
                                         CancellationToken ct = default)

重新加载当前活动场景——方便检查点重生和调试重载。

选项(SceneLoadOptions

字段作用
LoadingScreenPanel可选面板(UI 框架IPanel),服务在加载开始时压入、结束时弹出。
MinDisplayDuration加载画面保持可见的最短秒数。防止加载在几帧内就完成时的突兀闪烁。
LoadTimeoutSeconds单次调用的超时覆盖。0(默认)使用服务级设置。到期时加载抛出内含 TimeoutExceptionOperationCanceledException

结果(SceneLoadResult

属性含义
Succeeded场景正常激活时为 true
ErrorSucceededfalse 时捕获到的异常(例如,场景不在 Build Settings 中)。成功时为 null
SceneName本次加载的目标场景。
TotalDuration端到端的加载耗时,TimeSpan 类型。

加载阶段与查询

一次加载会经过一系列你随时可以观察的阶段:

string ActiveSceneName { get; } // Active scene name; empty before the first load completes
bool IsLoading { get; }         // true while any scene operation is in flight
LoadPhase CurrentPhase { get; } // Idle, Unloading, Loading, MinDisplayHold, Activating, Active

发布的事件

通过事件总线订阅:

事件时机典型用途
SceneLoadStarted(FromSceneName, ToSceneName)加载开始淡出旧音乐
SceneLoadProgress(SceneName, Progress, Phase)进度更新,Progress 取值 0..1驱动进度条
SceneLoadCompleted(FromSceneName, ToSceneName, TotalDuration)加载完成淡入新音乐
SceneLoadCanceled(FromSceneName, AttemptedSceneName, PhaseAtCancel, ElapsedBeforeCancel, Cause)加载被取消或超时干净地返回菜单

叠加场景

叠加加载把一个场景加入到已加载的场景中,而不是替换它们。用于玩法之上的常驻 UI 场景、流式载入的相邻区域,或一个共享的"managers"场景。

Task<SceneLoadResult> LoadAdditiveAsync(string sceneName,
                                        SceneLoadOptions opts = default,
                                        CancellationToken ct = default)

在基础场景之上叠加加载一个场景。活动场景不会改变——ActiveSceneName 仍然报告基础场景。加载一个已经叠加加载的场景是安全的无操作:服务记录一条警告并返回一个已完成的任务(没有引用计数——每个场景最多加载一次)。

Task UnloadAdditiveAsync(string sceneName, CancellationToken ct = default)

卸载一个叠加加载的场景。传入基础场景的名字,或一个未加载的名字,都是带警告的安全无操作——基础场景永远无法用这种方式移除(要替换它请用 LoadAsync)。

IReadOnlyList<string> ActiveSceneList { get; }

所有已加载的场景:基础场景在前,然后是按加载顺序排列的叠加场景。每次读取都返回一份新的快照,你可以随意保留或修改。

bool SetActiveScene(string sceneName)

告诉 Unity 哪个已加载的场景是"活动"场景——接收新实例化对象并决定光照设置的那个场景。该场景必须已出现在 ActiveSceneList 中;未加载时返回 false(带警告,不抛异常)。

值得了解的行为:

  • 叠加加载跳过加载画面。 对叠加操作,LoadingScreenPanelMinDisplayDuration 会被忽略(若你设置了它们会有一条日志消息)——叠加加载本应轻量、无缝。
  • 同样的事件照发。 叠加加载和卸载发布与 Single 模式加载相同的 SceneLoadStarted / SceneLoadProgress / SceneLoadCompleted 事件。区分方法:叠加操作期间 ActiveSceneName 不变。
  • Single 模式加载会清空叠加列表。 LoadAsync 完成时,Unity 已卸载之前的所有场景——服务把 ActiveSceneList 重置为只含新的基础场景。
  • 仍然是同一时间一个操作。 叠加加载和卸载与 Single 模式加载共享同一个进行中门闩:IsLoadingtrue 时,启动另一个操作会被拒绝。

完整示例

using System;
using System.Threading;
using System.Threading.Tasks;
using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class GameFlow : MonoBehaviour
{
    private ISceneService _scene;
    private IPanel _loadingScreen;
    private CancellationTokenSource _menuCts;

    private void Awake()
    {
        _scene = ServiceLocator.Resolve<ISceneService>();
        _loadingScreen = GetComponent<IPanel>(); // Your loading screen panel
    }

    // --- Single-mode: menu -> gameplay with a loading screen ---

    public async void OnPlayButtonClicked()
    {
        _menuCts = new CancellationTokenSource();
        try
        {
            var opts = new SceneLoadOptions
            {
                LoadingScreenPanel = _loadingScreen,
                MinDisplayDuration = 1.0f // At least 1 second on screen
            };
            var result = await _scene.LoadAsync("Gameplay", opts, _menuCts.Token);
            if (!result.Succeeded)
                Debug.LogError($"Scene load failed: {result.Error.Message}");
        }
        catch (OperationCanceledException)
        {
            Debug.Log("Scene load was canceled (player hit Esc)");
        }
    }

    public void OnEscapePressed()
    {
        _menuCts?.Cancel(); // Cancel the in-flight load
    }

    // --- Additive: layer a HUD scene over gameplay ---

    public async Task ShowHudAsync()
    {
        await _scene.LoadAdditiveAsync("HudOverlay");

        Debug.Log(_scene.ActiveSceneName);                    // "Gameplay" — unchanged
        Debug.Log(string.Join(", ", _scene.ActiveSceneList)); // "Gameplay, HudOverlay"
    }

    public async Task HideHudAsync()
    {
        await _scene.UnloadAdditiveAsync("HudOverlay");
    }
}

关闭此服务

ServiceLocator.Replace<ISceneService>(new NullSceneService());

换成 null 实现后,每个加载调用都返回一个已完成的成功任务——没有实际加载、没有事件、没有加载面板。适用于无头测试或对玩法流程做 mock。它仍然尊重取消令牌(一个已取消的令牌产生一个已取消的任务)。

常见陷阱

  • 仅限主线程。 只在主线程上调用加载方法和读取属性。Debug 构建中服务会检查这一点。

  • 同一时间一个场景操作。 IsLoadingtrue 时不能启动另一次加载——先 await 第一次,或取消它。这对 Single 模式加载、叠加加载与叠加卸载一体适用。

  • 取消是协作式的。 一旦新场景的 Awake 调用开始(LoadPhase.Activating),取消就会被忽略,加载会完成——Unity 无法在场景激活中途安全中止。取消在 LoadingMinDisplayHold 阶段会被兑现。

  • MinDisplayDuration 走 UI 时钟。 最短显示等待使用时间服务的 UI 时钟,它在玩法暂停时仍继续流动。如果你在加载期间暂停 UI 时钟,这段等待也会随之暂停。

  • 场景必须在 Build Settings 中。 未注册的名字会以 SceneLoadResult 失败,其 Error 解释了问题所在——检查结果,不要假定成功。

  • IL2CPP + 代码裁剪。 如果你的项目用 IL2CPP 构建,并且你的加载面板或事件订阅者被裁剪掉了,请把这些加入项目的 link.xml

    <type fullname="CommonGameSystem.Core.SceneLoadResult" preserve="all"/>
    <type fullname="System.Threading.Tasks.Task`1[[CommonGameSystem.Core.SceneLoadResult]]" preserve="all"/>
    

相关页面

  • 启动引导 —— 服务如何自动启动
  • UI 框架 —— 用作加载画面的面板类型
  • 事件总线 —— 订阅四个场景事件
  • 时间 —— 为最短显示保持计时的 UI 时钟