// Copyright (c) 2025 EFramework Innovation. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using EFramework.Unity.Utility;
namespace EFramework.Unity.Asset
{
public partial class XAsset
{
///
/// XAsset.Scene 提供了 Unity 场景的加载与卸载,支持自动处理依赖资源的生命周期。
///
///
///
/// 功能特性
/// - 支持场景的加载与卸载
/// - 自动处理资源依赖关系
///
/// 使用手册
///
/// 1. 同步加载
/// - 功能说明:同步加载场景,在 `Bundle` 模式下会自动加载场景对应的资源包
/// - 函数参数:
/// - `nameOrPath`:场景名称或完整路径(`string` 类型)
/// - `loadMode`:场景加载模式(`LoadSceneMode` 类型),默认为 `Single`
/// - 使用示例:
///
/// XAsset.Scene.Load("Scenes/TestScene", LoadSceneMode.Single);
///
///
/// 2. 异步加载
/// - 功能说明:异步加载场景,适合加载大型场景,避免加载过程阻塞主线程
/// - 函数参数:
/// - `nameOrPath`:场景名称或完整路径(`string` 类型)
/// - `callback`:场景加载完成后的回调函数(`Action` 类型)
/// - `loadMode`:场景加载模式(`LoadSceneMode` 类型),默认为 `Single`
/// - 函数返回:用于跟踪加载进度的 `Handler` 对象
/// - 使用示例:
///
/// XAsset.Scene.LoadAsync("Scenes/TestScene", () =>
/// {
/// Debug.Log("场景加载完成");
/// });
///
///
/// 3. 卸载场景
/// - 功能说明:卸载指定场景,在 `Bundle` 模式下会同时卸载场景对应的资源包
/// - 函数参数:
/// - `nameOrPath`:场景名称或完整路径(`string` 类型)
/// - 使用示例:
///
/// XAsset.Scene.Unload("Scenes/TestScene");
///
///
/// 更多信息请参考模块文档。
///
public partial class Scene
{
///
/// Task 是场景加载的任务,用于跟踪异步加载过程中的场景状态。
///
internal class Task
{
///
/// Name 是场景的名称,用于标识和查找场景。
///
internal string Name;
///
/// Request 是 Unity 场景加载的异步操作对象。
///
internal AsyncOperation Request;
}
///
/// Loading 记录当前正在加载的场景,用于防止重复加载同一场景。
///
internal static readonly Dictionary Loading = new();
///
/// Loaded 记录已加载的场景。
///
internal static readonly List Loaded = new();
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoadMethod]
#else
[UnityEngine.RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#endif
///
/// OnInitialize 是资源系统的初始化方法,在编辑器或运行时自动调用,监听场景加载和卸载的回调,自动处理场景依赖资源的卸载。
///
internal static void OnInitialize()
{
#if UNITY_EDITOR
if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) return;
#endif
SceneManager.sceneLoaded += (scene, mode) =>
{
if (Constants.BundleMode)
{
if (!Loaded.Contains(scene.name)) Loaded.Add(scene.name);
}
};
SceneManager.sceneUnloaded += scene =>
{
if (Constants.BundleMode)
{
if (Loaded.Contains(scene.name))
{
Unload(scene.name);
Loaded.Remove(scene.name);
}
}
};
}
///
/// Load 同步加载场景。在 Bundle 模式下会自动加载场景对应的资源包。
///
/// 场景名称或完整路径,支持从 Assets 目录下的相对路径加载
/// 场景加载模式:Single 会卸载当前场景,Additive 则保留当前场景
public static void Load(string nameOrPath, LoadSceneMode loadMode = LoadSceneMode.Single)
{
var sceneName = nameOrPath.Contains("/") ? Path.GetFileNameWithoutExtension(nameOrPath) : nameOrPath;
try { Event.Notify(EventType.OnPreLoadScene, sceneName); }
catch (Exception e) { XLog.Panic(e); }
try
{
if (Constants.BundleMode && Bundle.Manifest)
{
string bundleName;
if (nameOrPath.Contains("/"))
{
if (nameOrPath.StartsWith("Assets/")) nameOrPath = nameOrPath["Assets/".Length..];
if (!nameOrPath.EndsWith(".unity")) nameOrPath += ".unity";
bundleName = Constants.GetName(nameOrPath);
}
else bundleName = Constants.GetName($"Scenes/{sceneName}.unity");
var bundleInfo = Bundle.Load(bundleName);
if (bundleInfo == null) XLog.Error("XAsset.Scene.Load: can not load scene caused by nil scene bundle file.");
else
{
bundleInfo.Retain(Constants.DebugMode ? $"[Scene.Load: {nameOrPath}]" : "");
SceneManager.LoadScene(sceneName, loadMode);
}
}
else SceneManager.LoadScene(sceneName, loadMode);
}
catch (Exception e) { throw e; }
finally
{
try { Event.Notify(EventType.OnPostLoadScene, sceneName); }
catch (Exception e) { XLog.Panic(e); }
}
}
///
/// LoadAsync 异步加载场景。适用于加载大型场景,避免加载过程阻塞主线程。
///
/// 场景名称或完整路径
/// 场景加载完成后的回调函数
/// 场景加载模式:Single 或 Additive
/// 用于跟踪加载进度的Handler对象
public static Handler LoadAsync(string nameOrPath, Action callback = null, LoadSceneMode loadMode = LoadSceneMode.Single)
{
var handler = new Handler();
if (callback != null) handler.OnPostload += callback;
XLoom.RunCoroutine(LoadAsync(nameOrPath, handler, loadMode));
return handler;
}
///
/// LoadAsync 是场景异步加载的内部实现。
/// 处理场景加载的具体流程:
/// 1. 在Bundle模式下先加载场景资源包
/// 2. 通过SceneManager加载场景
/// 3. 处理加载回调和事件通知
///
/// 场景名称或路径。
/// 场景加载处理程序。
/// 加载模式(单场景或多场景)。
/// 协程。
internal static IEnumerator LoadAsync(string nameOrPath, Handler handler, LoadSceneMode loadMode = LoadSceneMode.Single)
{
var sceneName = nameOrPath.Contains("/") ? Path.GetFileNameWithoutExtension(nameOrPath) : nameOrPath;
try { Event.Notify(EventType.OnPreLoadScene, sceneName); }
catch (Exception e) { XLog.Panic(e); }
if (Constants.BundleMode && Bundle.Manifest)
{
handler.totalCount++; // Load任务
string bundleName;
if (nameOrPath.Contains("/"))
{
if (nameOrPath.StartsWith("Assets/")) nameOrPath = nameOrPath["Assets/".Length..];
if (!nameOrPath.EndsWith(".unity")) nameOrPath += ".unity";
bundleName = Constants.GetName(nameOrPath);
}
else bundleName = Constants.GetName($"Scenes/{sceneName}.unity");
yield return XLoom.RunCoroutine(Bundle.LoadAsync(bundleName, handler));
var bundleInfo = Bundle.Find(bundleName);
if (bundleInfo != null)
{
if (!Loading.TryGetValue(sceneName, out var task))
{
bundleInfo.Retain(Constants.DebugMode ? $"[Scene.LoadAsync: {nameOrPath}]" : "");
var request = SceneManager.LoadSceneAsync(sceneName, loadMode);
task = new Task() { Name = sceneName, Request = request };
handler.Request = request;
handler.InvokePreload();
Loading.Add(sceneName, task);
yield return new WaitUntil(() => task.Request.isDone);
Loading.Remove(sceneName);
handler.doneCount++;
handler.InvokePostload();
}
else
{
handler.Request = task.Request;
handler.InvokePreload();
yield return new WaitUntil(() => task.Request.isDone);
handler.doneCount++;
handler.InvokePostload();
}
}
else
{
XLog.Error("XAsset.Scene.LoadAsync: async load error: {0}", sceneName);
// 加载错误时仍旧回调,业务层可根据 handler.Error 判断是否加载成功
handler.Error = true;
handler.InvokePostload();
}
}
else
{
if (!Loading.TryGetValue(sceneName, out var task))
{
var request = SceneManager.LoadSceneAsync(sceneName, loadMode);
if (request != null)
{
task = new Task() { Name = sceneName, Request = request };
handler.Request = request;
handler.InvokePreload();
Loading.Add(sceneName, task);
yield return new WaitUntil(() => task.Request.isDone);
Loading.Remove(sceneName);
handler.InvokePostload();
}
else
{
// 等待下一帧进行错误处理,避免业务层未获取到 handler 的实例
yield return null;
// 加载错误时仍旧回调,业务层可根据 handler.Error 判断是否加载成功
handler.Error = true;
handler.InvokePostload();
}
}
else
{
handler.Request = task.Request;
handler.InvokePreload();
yield return new WaitUntil(() => task.Request.isDone);
handler.InvokePostload();
}
}
try { Event.Notify(EventType.OnPostLoadScene, sceneName); }
catch (Exception e) { XLog.Panic(e); }
}
///
/// Unload 卸载指定场景。在 Bundle 模式下会同时卸载场景对应的资源包。
///
/// 要卸载的场景名称或路径
public static void Unload(string nameOrPath)
{
var sceneName = nameOrPath.Contains("/") ? Path.GetFileNameWithoutExtension(nameOrPath) : nameOrPath;
if (Constants.BundleMode && Bundle.Manifest)
{
string bundleName;
if (nameOrPath.Contains("/"))
{
if (nameOrPath.StartsWith("Assets/")) nameOrPath = nameOrPath["Assets/".Length..];
if (!nameOrPath.EndsWith(".unity")) nameOrPath += ".unity";
bundleName = Constants.GetName(nameOrPath);
}
else bundleName = Constants.GetName($"Scenes/{sceneName}.unity");
var bundleInfo = Bundle.Find(bundleName);
bundleInfo?.Release(Constants.DebugMode ? $"[Scene.Unload: {nameOrPath}]" : "");
}
}
///
/// IsLoading 检查场景的加载状态。
///
/// 场景名称或路径
/// 是否正在加载
public static bool IsLoading(string nameOrPath)
{
if (string.IsNullOrEmpty(nameOrPath)) return false;
else
{
var sceneName = nameOrPath.Contains("/") ? Path.GetFileNameWithoutExtension(nameOrPath) : nameOrPath;
return Loading.ContainsKey(sceneName);
}
}
}
}
}