前言

​ 先吐槽一下ET自带的场景切换组件实在是太简陋了。。因此自己根据项目需要对整个流程进行了重构,不得不说ETTask单线程异步任务是真的好用,免去了命令模式的编写。

场景切换流程

场景切换部分___.drawio

  • 通过SceneChangeData实现数据流在场景转换流程中的传递
    image-20220302173956097

    image-20220302174040596

    image-20220302175922416

具体函数

  • InitLoadingUI:
    实现LoadingUI淡入效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private static async ETTask InitLoadingUI(this SceneChangeComponent self, SceneChangeData data)
    {
    //LoadingUI淡入
    UIComponent.Instance.ShowWindow(WindowID.WindowID_Loading);
    DlgLoading dlgLoading=UIComponent.Instance.GetDlgLogic<DlgLoading>();
    if (dlgLoading != null)
    {
    data.dlgLoading = dlgLoading; //存入数据流中
    dlgLoading.SetLoadingProgress(0f); //使得进度条为0
    CanvasGroup canvasGroup = dlgLoading.View.uiTransform.GetComponent<CanvasGroup>();
    canvasGroup.alpha = 0f;
    await UIEffectHelper.FadeIn(canvasGroup, 1f, 1f);
    }
    }
  • HandleExitSceneFunc:
    根据退出的场景类型实现函数分发:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public static async ETTask HandleExitSceneFunc(this SceneChangeComponent self, SceneChangeData data,Scene zoneScene, float progress)
    {
    string lastSceneName = data.lastSceneName;
    if (SceneConst.sceneName2Type.ContainsKey(lastSceneName))
    {
    GameSceneType sceneType = SceneConst.sceneName2Type[lastSceneName];
    if (self.allEvents.TryGetValue(sceneType, out ASceneChangeEvent sceneEvent))
    {
    await sceneEvent.OnExitScene(zoneScene);
    }
    }

    await self.HandleProgress(data, progress);
    }

    不同场景的具体类型在Sceneconst中进行定义:
    image-20220302174621970
    ASceneChangeEvent抽象类:
    image-20220302174733356+

  • HandleUnloadFunc:
    处理场景资源卸载,采用计数方式,因为ETTask的本质为单线程,所以无需考虑多线程加锁。当所有资源卸载完毕后进入下一阶段

    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
    26
    27
    28
    29
    30
    31
    32
    public static async ETTask HandleUnloadFunc(this SceneChangeComponent self, SceneChangeData data, float progress)
    {
    string lastSceneName = data.lastSceneName;
    if (SceneConst.sceneName2Type.ContainsKey(lastSceneName))
    {
    GameSceneType sceneType = SceneConst.sceneName2Type[lastSceneName];
    if (self.allEvents.TryGetValue(sceneType, out ASceneChangeEvent sceneEvent))
    {
    List<string> unloadNames = sceneEvent.GetUnloadResource();
    int count = unloadNames?.Count??0;
    int unloadedCount = 0;
    for (int i = 0; i < count; i++)
    {
    string assetBundleName = $"{unloadNames[i]}.unity3d";
    self.UnloadResources(assetBundleName, () =>
    {
    ++unloadedCount;
    }).Coroutine();

    }

    while (unloadedCount < count)
    {
    await TimerComponent.Instance.WaitFrameAsync();
    }
    }
    }

    //广播场景卸载消息
    EventMessageComponent.Instance.SendMessage(Message.Create(MessageEventNames.UNLOAD_SCENE_MSG, self, data.lastSceneName));
    await self.HandleProgress(data, progress);
    }
  • HandlePreloadFunc:
    场景资源预加载,同样采用计数方式,获取对应场景类型中需要预加载的AB资源列表,分别进行异步加载,全部加载后进入下一阶段

    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
    26
    27
    28
    29
    30
    public static async ETTask HandlePreloadFunc(this SceneChangeComponent self, SceneChangeData data, float progress)
    {
    string nextSceneName = data.nextSceneName;
    if (SceneConst.sceneName2Type.ContainsKey(nextSceneName))
    {
    GameSceneType sceneType = SceneConst.sceneName2Type[nextSceneName];
    if (self.allEvents.TryGetValue(sceneType, out ASceneChangeEvent sceneEvent))
    {
    List<string> preloadNames = sceneEvent.GetPreloadResource();
    int count = preloadNames?.Count ?? 0;
    int loadedCount = 0;
    for (int i = 0; i < count; i++)
    {
    string assetbundleName = $"{preloadNames[i]}.unity3d";
    self.PreloadResources(assetbundleName, () =>
    {
    ++loadedCount; //本质还是单线程协程,不需要加锁
    }).Coroutine();
    }

    while (loadedCount < count)
    {
    await TimerComponent.Instance.WaitFrameAsync();
    }
    }
    }

    EventMessageComponent.Instance.SendMessage(Message.Create(MessageEventNames.LOAD_SCENE_MSG,self,data.nextSceneName));
    await self.HandleProgress(data, progress);
    }
  • LoadingMap:
    这一阶段加载对应场景AB包后通过SceneManager.LoadSceneAsync进行场景的异步加载,加载完成后会发布ChangeSceneFinish事件,可以通过分发执行不同具体场景名的具体函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private static async ETTask LoadingMap(this SceneChangeComponent self, SceneChangeData data,float progress,Scene zoneScene)
    {
    self.tcs = ETTask.Create(true);
    // 加载对应的场景AB包
    await ResourcesComponent.Instance.LoadBundleAsync($"{data.nextSceneName}.unity3d"); //加载对应场景AB包
    // 加载map
    self.loadMapOperation = SceneManager.LoadSceneAsync(data.nextSceneName);
    await self.tcs; //在Update中进行SetResult
    await self.HandleProgress(data, progress);

    await ResourcesComponent.Instance.UnloadBundleAsync($"{data.nextSceneName}.unity3d",false); //卸载对应场景AB包
    //广播场景加载消息
    await Game.EventSystem.PublishAsync(new EventType.ChangeSceneFinish(){sceneName = data.nextSceneName,zoneScene = zoneScene});
    }

    根据具体函数名的函数分发:
    image-20220302175628741

  • HandleEnterSceneFunc
    与HandleExitSceneFunc同理,处理对应场景类型的Enter函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public static async ETTask HandleEnterSceneFunc(this SceneChangeComponent self, SceneChangeData data,Scene zoneScene,float progress)
    {
    string nextSceneName = data.nextSceneName;
    if (SceneConst.sceneName2Type.ContainsKey(nextSceneName))
    {
    GameSceneType sceneType = SceneConst.sceneName2Type[nextSceneName];
    //找到对应分发函数
    if (self.allEvents.TryGetValue(sceneType, out ASceneChangeEvent sceneEvent))
    {
    await sceneEvent.OnEnterScene(zoneScene);
    }
    }

    await self.HandleProgress(data, progress);
    }
  • DisposeLoadingUI
    销毁对应的loadingUI界面,场景转换流程结束

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    private static async ETTask DisposeLoadingUI(this SceneChangeComponent self, SceneChangeData data)
    {
    if (data.dlgLoading != null)
    {
    //LoadingUI淡出
    data.dlgLoading.SetLoadingProgress(1f); //使得进度条充满
    CanvasGroup canvasGroup = data.dlgLoading.View.uiTransform.GetComponent<CanvasGroup>();
    canvasGroup.alpha = 1f;
    await UIEffectHelper.FadeOut(canvasGroup, 0f, 1f, null, () =>
    {
    UIComponent.Instance.CloseWindow<DlgLoading>();
    });
    }
    }

场景Additive加载流程

  • 采取AB包方式进行加载,先进行场景AB包的异步加载,随后执行SceneManager.LoadSceneAsync的场景异步加载
  • 在场景加载完毕后异步卸载场景AB包

具体函数

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
26
27
28
//MARKER:场景Addtive异步加载
public static async ETTask LoadSceneAddtive(this SceneChangeComponent self, string sceneName,Scene zoneScene)
{
await ResourcesComponent.Instance.LoadBundleAsync($"{sceneName}.unity3d");

self.tcs = ETTask.Create(true);
self.loadMapOperation = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
self.loadMapOperation.allowSceneActivation = true;


await self.tcs;

await ResourcesComponent.Instance.UnloadBundleAsync($"{sceneName}.unity3d",false);
//加载光照信息数据
ResourcesComponent resourcesComponent=Game.Scene.GetComponent<ResourcesComponent>();
await resourcesComponent.LoadBundleAsync("scenedata.unity3d");
LightMapPrefab prefab=resourcesComponent.GetAsset("scenedata.unity3d", $"{sceneName}_Data") as LightMapPrefab;
if (prefab != null)
{
prefab.LoadLightMap();
}
await resourcesComponent.UnloadBundleAsync("scenedata.unity3d",false);



Game.EventSystem.Publish(new EventType.AddtiveSceneFinish() { sceneName = sceneName, zoneScene = zoneScene });

}

碰到问题以及解决办法

碰到的问题主要在于用additive加载的场景并没有保留其对应的全局光照信息以及光照贴图,因此我对Unity的Editor进行了拓展,用LightMapPrefab的ScriptableObject存储全局光照信息,并在Scene/Save LightMapData中进行了选项拓展,在Additive加载完成后进行全局光照等信息的恢复。

  • LightMapPrefab类:
    主要是对光照信息、天空盒、全局光照颜色/模式等进行存储

    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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    public class LightMapPrefab:ScriptableObject
    {
    [SerializeField]
    public LightmapsMode mode;
    [SerializeField]
    public Texture2D[] lightmapColor, lightmapDir;

    [SerializeField]
    public Material skyBoxMat;

    [SerializeField]
    public Color ambientColor;

    [SerializeField]
    public AmbientMode ambientMode;

    public LightMapPrefab()
    {

    }

    public void SaveLightMap()
    {
    this.mode = LightmapSettings.lightmapsMode;
    this.lightmapColor = null;
    this.lightmapDir = null;

    if (LightmapSettings.lightmaps != null && LightmapSettings.lightmaps.Length > 0)
    {
    int l = LightmapSettings.lightmaps.Length;
    this.lightmapColor = new Texture2D[l];
    this.lightmapDir = new Texture2D[l];
    for (int i = 0; i < l; i++)
    {
    this.lightmapColor[i] = LightmapSettings.lightmaps[i].lightmapColor;
    this.lightmapDir[i] = LightmapSettings.lightmaps[i].lightmapDir;
    }
    }

    this.skyBoxMat = RenderSettings.skybox;
    this.ambientColor = RenderSettings.ambientLight;
    this.ambientMode = RenderSettings.ambientMode;
    }

    public void LoadLightMap()
    {
    LightmapSettings.lightmapsMode = this.mode;


    int l1 = (this.lightmapDir == null)? 0 : this.lightmapDir.Length;
    int l2 = (this.lightmapColor == null)? 0 : this.lightmapColor.Length;

    int l = Mathf.Max(l1, l2);
    LightmapData[] lightmaps = new LightmapData[l];
    for (int i = 0; i < l; i++)
    {
    lightmaps[i] = new LightmapData();
    if (i < l1)
    {
    lightmaps[i].lightmapDir = this.lightmapDir[i];
    }

    if (i < l2)
    {
    lightmaps[i].lightmapColor = this.lightmapColor[i];
    }
    }


    LightmapSettings.lightmaps = lightmaps;

    RenderSettings.skybox = this.skyBoxMat;
    RenderSettings.ambientMode = this.ambientMode;
    RenderSettings.ambientLight = this.ambientColor;

    }
    }
  • Scene/Save LightMapData 拓展

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static class SceneEditorExpand
    {
    public static string mapDataSavePath = "Assets/Bundles/SceneData";
    public static string assetBundleName = "scenedata.unity3d";

    [MenuItem("Scene/Save LightMapData")]
    static public void SaveSceneMapData()
    {
    LightMapPrefab lightMapPrefab = ScriptableObject.CreateInstance<LightMapPrefab>();
    lightMapPrefab.SaveLightMap();

    string path=mapDataSavePath+$"/{SceneManager.GetActiveScene().name}_Data.asset";
    AssetDatabase.CreateAsset(lightMapPrefab,path);
    AssetDatabase.SaveAssets();

    AssetImporter importer = AssetImporter.GetAtPath(path);
    importer.assetBundleName = assetBundleName;

    AssetDatabase.Refresh();
    }
    }

具体调用方式

​ 动态在zoneScene中加入SceneChangeComponent组件异步执行场景转换流程,结束后销毁组件

image-20220302180142669

拓展思路

若想将思路用在非ETTask的OOP模式中,可以通过类似于状态机的CommandList将整个场景的切换流程贯穿起来,通过Create(),Execute(),Finish(),Cancel()等生命周期去走CommandList中的所有Command,数据流存放在CommandList中即可。