// ============================================================================
|
// YooAssetService.cs — YooAsset 封装服务
|
// 实现 IYooAssetService 和 IYooAssetBridge,替代 AssetBundleUtility
|
// ============================================================================
|
|
using System;
|
using System.Collections.Generic;
|
using System.Threading;
|
using Cysharp.Threading.Tasks;
|
using UnityEngine;
|
using UnityEngine.SceneManagement;
|
using YooAsset;
|
|
namespace ProjSG.Resource
|
{
|
/// <summary>
|
/// YooAsset 资源加载服务单例。
|
/// 封装 YooAsset ResourcePackage 的核心加载能力,提供 UniTask 异步 API。
|
/// 同时实现 IYooAssetBridge 供 Launch 程序集跨程序集调用。
|
/// </summary>
|
public class YooAssetService : Singleton<YooAssetService>, IYooAssetService, IYooAssetBridge
|
{
|
private readonly Dictionary<string, ResourcePackage> _packages = new Dictionary<string, ResourcePackage>();
|
private ResourcePackage _defaultPackage;
|
private IRemoteServices _remoteServices;
|
private bool _isInitialized;
|
private EPlayMode _playMode;
|
|
// ====================================================================
|
// IYooAssetService Properties
|
// ====================================================================
|
|
/// <inheritdoc />
|
public bool IsInitialized => _isInitialized;
|
|
/// <inheritdoc />
|
public EPlayMode PlayMode => _playMode;
|
|
// ====================================================================
|
// IYooAssetBridge Properties
|
// ====================================================================
|
|
bool IYooAssetBridge.IsRegistered => _isInitialized;
|
|
// ====================================================================
|
// Initialization
|
// ====================================================================
|
|
/// <inheritdoc />
|
public async UniTask InitializeAsync(EPlayMode playMode, IRemoteServices remoteServices = null)
|
{
|
if (_isInitialized)
|
{
|
Debug.LogWarning("[YooAssetService] Already initialized.");
|
return;
|
}
|
|
_playMode = playMode;
|
_remoteServices = remoteServices;
|
|
// YooAsset 全局初始化(幂等操作)
|
YooAssets.Initialize();
|
|
// 初始化所有配置中的包裹
|
foreach (var pkgName in YooAssetPackageConfig.AllPackages)
|
{
|
try
|
{
|
// 优先复用 Launch 阶段已创建的包裹
|
var package = YooAssets.TryGetPackage(pkgName);
|
if (package != null)
|
{
|
Debug.Log($"[YooAssetService] Reusing existing package '{pkgName}' from YooAssetInitializer");
|
}
|
else
|
{
|
// 自行创建并初始化(首次启动或该包未在 Launch 阶段创建)
|
package = YooAssets.CreatePackage(pkgName);
|
var initParams = CreateInitParameters(playMode, remoteServices, pkgName);
|
var initOp = package.InitializeAsync(initParams);
|
await initOp.ToUniTask();
|
|
if (initOp.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogWarning($"[YooAssetService] Package '{pkgName}' init failed: {initOp.Error}");
|
continue;
|
}
|
|
Debug.Log($"[YooAssetService] Package '{pkgName}' newly initialized.");
|
}
|
|
_packages[pkgName] = package;
|
|
// 设置默认包
|
if (_defaultPackage == null || pkgName == YooAssetPackageConfig.DefaultPackage)
|
{
|
_defaultPackage = package;
|
YooAssets.SetDefaultPackage(package);
|
}
|
}
|
catch (Exception ex)
|
{
|
// EditorSimulateMode 下包不在 Collector 中会抛异常,跳过
|
Debug.LogWarning($"[YooAssetService] Package '{pkgName}' init exception (skipped): {ex.Message}");
|
}
|
}
|
|
if (_defaultPackage == null)
|
{
|
Debug.LogError("[YooAssetService] No packages initialized successfully!");
|
throw new InvalidOperationException("YooAsset initialization failed: no packages available.");
|
}
|
|
_isInitialized = true;
|
Debug.Log($"[YooAssetService] Initialized {_packages.Count}/{YooAssetPackageConfig.AllPackages.Length} packages with PlayMode={playMode}");
|
}
|
|
/// <summary>
|
/// 初始化指定名称的额外资源包裹。
|
/// </summary>
|
public async UniTask InitializePackageAsync(string packageName, EPlayMode playMode,
|
IRemoteServices remoteServices = null)
|
{
|
if (_packages.ContainsKey(packageName))
|
{
|
Debug.LogWarning($"[YooAssetService] Package '{packageName}' already initialized.");
|
return;
|
}
|
|
// 优先复用已存在的包裹(可能由 Launch 阶段创建)
|
var package = YooAssets.TryGetPackage(packageName);
|
if (package == null)
|
{
|
package = YooAssets.CreatePackage(packageName);
|
var initParams = CreateInitParameters(playMode, remoteServices ?? _remoteServices, packageName);
|
var initOp = package.InitializeAsync(initParams);
|
await initOp.ToUniTask();
|
|
if (initOp.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] Initialize package '{packageName}' failed: {initOp.Error}");
|
throw new InvalidOperationException($"YooAsset package '{packageName}' initialization failed: {initOp.Error}");
|
}
|
}
|
|
_packages[packageName] = package;
|
Debug.Log($"[YooAssetService] Package '{packageName}' initialized.");
|
}
|
|
private InitializeParameters CreateInitParameters(EPlayMode playMode, IRemoteServices remoteServices, string packageName)
|
{
|
switch (playMode)
|
{
|
case EPlayMode.EditorSimulateMode:
|
{
|
#if UNITY_EDITOR
|
var simulateResult = EditorSimulateModeHelper.SimulateBuild(packageName);
|
return new EditorSimulateModeParameters
|
{
|
EditorFileSystemParameters = FileSystemParameters
|
.CreateDefaultEditorFileSystemParameters(simulateResult.PackageRootDirectory)
|
};
|
#else
|
throw new InvalidOperationException("EditorSimulateMode is only available in Unity Editor.");
|
#endif
|
}
|
case EPlayMode.HostPlayMode:
|
{
|
return new HostPlayModeParameters
|
{
|
BuildinFileSystemParameters = FileSystemParameters
|
.CreateDefaultBuildinFileSystemParameters(),
|
CacheFileSystemParameters = FileSystemParameters
|
.CreateDefaultCacheFileSystemParameters(remoteServices)
|
};
|
}
|
case EPlayMode.OfflinePlayMode:
|
{
|
return new OfflinePlayModeParameters
|
{
|
BuildinFileSystemParameters = FileSystemParameters
|
.CreateDefaultBuildinFileSystemParameters()
|
};
|
}
|
case EPlayMode.WebPlayMode:
|
{
|
var webParams = new WebPlayModeParameters();
|
#if UNITY_WEBGL && WEIXINMINIGAME && !UNITY_EDITOR
|
string packageRoot = $"{WeChatWASM.WX.env.USER_DATA_PATH}/__GAME_FILE_CACHE";
|
webParams.WebServerFileSystemParameters = WechatFileSystemCreater
|
.CreateFileSystemParameters(packageRoot, remoteServices);
|
#elif UNITY_WEBGL && DOUYINMINIGAME && !UNITY_EDITOR
|
string packageRoot = TTSDK.TTFileSystem.USER_DATA_PATH + "/__GAME_FILE_CACHE";
|
webParams.WebServerFileSystemParameters = TiktokFileSystemCreater
|
.CreateFileSystemParameters(packageRoot, remoteServices);
|
#else
|
webParams.WebServerFileSystemParameters = FileSystemParameters
|
.CreateDefaultWebServerFileSystemParameters();
|
if (remoteServices != null)
|
{
|
webParams.WebRemoteFileSystemParameters = FileSystemParameters
|
.CreateDefaultWebRemoteFileSystemParameters(remoteServices);
|
}
|
#endif
|
return webParams;
|
}
|
default:
|
throw new ArgumentOutOfRangeException(nameof(playMode), playMode, "Unsupported PlayMode.");
|
}
|
}
|
|
// ====================================================================
|
// Asset Loading
|
// ====================================================================
|
|
/// <summary>
|
/// 资源加载重试配置
|
/// </summary>
|
private const int MAX_RETRY_COUNT = 3;
|
private const int BASE_RETRY_DELAY_MS = 500; // 500ms, 1000ms, 2000ms (exponential)
|
|
/// <summary>
|
/// 带重试的异步操作执行器。
|
/// 使用指数退避策略(500ms → 1000ms → 2000ms)。
|
/// </summary>
|
/// <param name="operation">要执行的异步操作</param>
|
/// <param name="operationName">操作名称(用于日志)</param>
|
/// <param name="ct">取消令牌</param>
|
/// <returns>操作结果</returns>
|
private async UniTask<T> ExecuteWithRetryAsync<T>(
|
Func<UniTask<T>> operation,
|
string operationName,
|
CancellationToken ct = default)
|
{
|
Exception lastException = null;
|
|
for (int attempt = 0; attempt <= MAX_RETRY_COUNT; attempt++)
|
{
|
try
|
{
|
ct.ThrowIfCancellationRequested();
|
return await operation();
|
}
|
catch (OperationCanceledException)
|
{
|
throw; // Don't retry cancellations
|
}
|
catch (Exception ex)
|
{
|
lastException = ex;
|
if (attempt < MAX_RETRY_COUNT)
|
{
|
int delayMs = BASE_RETRY_DELAY_MS * (1 << attempt); // Exponential backoff
|
Debug.LogWarning($"[YooAssetService] {operationName} failed (attempt {attempt + 1}/{MAX_RETRY_COUNT + 1}), retrying in {delayMs}ms: {ex.Message}");
|
await UniTask.Delay(delayMs, cancellationToken: ct);
|
}
|
}
|
}
|
|
Debug.LogError($"[YooAssetService] {operationName} failed after {MAX_RETRY_COUNT + 1} attempts: {lastException?.Message}");
|
return default;
|
}
|
|
private void ThrowIfNotInitialized()
|
{
|
if (!_isInitialized)
|
throw new InvalidOperationException("[YooAssetService] Service not initialized. Call InitializeAsync first.");
|
}
|
|
/// <summary>
|
/// 根据资源路径查找应使用的 ResourcePackage。
|
/// 使用 YooAssetPackageConfig 路由表确定目标包,找不到则回退到默认包。
|
/// </summary>
|
private ResourcePackage FindPackageForAsset(string location)
|
{
|
var packageName = YooAssetPackageConfig.GetPackageForLocation(location);
|
if (_packages.TryGetValue(packageName, out var package))
|
return package;
|
|
// 路由到的包尚未初始化,回退到默认包
|
return _defaultPackage;
|
}
|
|
/// <inheritdoc />
|
public async UniTask<T> LoadAssetAsync<T>(string location, uint priority = 0,
|
CancellationToken ct = default) where T : UnityEngine.Object
|
{
|
ThrowIfNotInitialized();
|
|
if (string.IsNullOrEmpty(location))
|
{
|
Debug.LogError("[YooAssetService] LoadAssetAsync: location is null or empty.");
|
return null;
|
}
|
|
var package = FindPackageForAsset(location);
|
return await ExecuteWithRetryAsync(async () =>
|
{
|
var handle = package.LoadAssetAsync<T>(location, priority);
|
await handle.ToUniTask(cancellationToken: ct);
|
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
throw new InvalidOperationException($"LoadAssetAsync failed for '{location}': {handle.LastError}");
|
}
|
|
return handle.GetAssetObject<T>();
|
}, $"LoadAssetAsync<{typeof(T).Name}>('{location}')", ct);
|
}
|
|
/// <inheritdoc />
|
public async UniTask<UnityEngine.Object> LoadAssetAsync(string location, Type type, uint priority = 0,
|
CancellationToken ct = default)
|
{
|
ThrowIfNotInitialized();
|
|
if (string.IsNullOrEmpty(location))
|
{
|
Debug.LogError("[YooAssetService] LoadAssetAsync: location is null or empty.");
|
return null;
|
}
|
|
var package = FindPackageForAsset(location);
|
return await ExecuteWithRetryAsync(async () =>
|
{
|
var handle = package.LoadAssetAsync(location, type, priority);
|
await handle.ToUniTask(cancellationToken: ct);
|
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
throw new InvalidOperationException($"LoadAssetAsync failed for '{location}': {handle.LastError}");
|
}
|
|
return handle.AssetObject;
|
}, $"LoadAssetAsync('{location}', {type.Name})", ct);
|
}
|
|
/// <summary>
|
/// 同步加载资产(仅在非 WebGL 平台过渡期使用)。
|
/// </summary>
|
[System.Obsolete("Use LoadAssetAsync instead. Sync loading will be removed in US2.")]
|
public T LoadAssetSync<T>(string location) where T : UnityEngine.Object
|
{
|
ThrowIfNotInitialized();
|
|
if (string.IsNullOrEmpty(location))
|
{
|
Debug.LogError("[YooAssetService] LoadAssetSync: location is null or empty.");
|
return null;
|
}
|
|
var package = FindPackageForAsset(location);
|
var handle = package.LoadAssetSync<T>(location);
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] LoadAssetSync failed for '{location}': {handle.LastError}");
|
return null;
|
}
|
|
return handle.GetAssetObject<T>();
|
}
|
|
/// <inheritdoc />
|
public async UniTask<SubAssetsHandle> LoadSubAssetsAsync<T>(string location, uint priority = 0,
|
CancellationToken ct = default) where T : UnityEngine.Object
|
{
|
ThrowIfNotInitialized();
|
|
var package = FindPackageForAsset(location);
|
var handle = package.LoadSubAssetsAsync<T>(location, priority);
|
await handle.ToUniTask();
|
ct.ThrowIfCancellationRequested();
|
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] LoadSubAssetsAsync failed for '{location}': {handle.LastError}");
|
}
|
|
return handle;
|
}
|
|
/// <inheritdoc />
|
public async UniTask<AllAssetsHandle> LoadAllAssetsAsync<T>(string location, uint priority = 0,
|
CancellationToken ct = default) where T : UnityEngine.Object
|
{
|
ThrowIfNotInitialized();
|
|
var package = FindPackageForAsset(location);
|
var handle = package.LoadAllAssetsAsync<T>(location, priority);
|
await handle.ToUniTask();
|
ct.ThrowIfCancellationRequested();
|
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] LoadAllAssetsAsync failed for '{location}': {handle.LastError}");
|
}
|
|
return handle;
|
}
|
|
// ====================================================================
|
// RawFile Loading
|
// ====================================================================
|
|
/// <inheritdoc />
|
public async UniTask<string> LoadRawFileTextAsync(string location, CancellationToken ct = default)
|
{
|
ThrowIfNotInitialized();
|
|
var rawPackage = FindPackageForAsset(location);
|
return await ExecuteWithRetryAsync(async () =>
|
{
|
var handle = rawPackage.LoadRawFileAsync(location);
|
await handle.ToUniTask(cancellationToken: ct);
|
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
throw new InvalidOperationException($"LoadRawFileTextAsync failed for '{location}': {handle.LastError}");
|
}
|
|
return handle.GetRawFileText();
|
}, $"LoadRawFileTextAsync('{location}')", ct);
|
}
|
|
/// <inheritdoc />
|
public async UniTask<byte[]> LoadRawFileBytesAsync(string location, CancellationToken ct = default)
|
{
|
ThrowIfNotInitialized();
|
|
var rawPackage = FindPackageForAsset(location);
|
return await ExecuteWithRetryAsync(async () =>
|
{
|
var handle = rawPackage.LoadRawFileAsync(location);
|
await handle.ToUniTask(cancellationToken: ct);
|
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
throw new InvalidOperationException($"LoadRawFileBytesAsync failed for '{location}': {handle.LastError}");
|
}
|
|
return handle.GetRawFileData();
|
}, $"LoadRawFileBytesAsync('{location}')", ct);
|
}
|
|
// ====================================================================
|
// Scene Loading
|
// ====================================================================
|
|
/// <inheritdoc />
|
public async UniTask<SceneHandle> LoadSceneAsync(string location, LoadSceneMode sceneMode = LoadSceneMode.Single,
|
LocalPhysicsMode physicsMode = LocalPhysicsMode.None, bool suspendLoad = false, uint priority = 0, CancellationToken ct = default)
|
{
|
ThrowIfNotInitialized();
|
|
var package = FindPackageForAsset(location);
|
var handle = package.LoadSceneAsync(location, sceneMode, physicsMode, suspendLoad, priority);
|
await handle.ToUniTask();
|
ct.ThrowIfCancellationRequested();
|
|
if (handle.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] LoadSceneAsync failed for '{location}': {handle.LastError}");
|
}
|
|
return handle;
|
}
|
|
// ====================================================================
|
// Query
|
// ====================================================================
|
|
/// <inheritdoc />
|
public bool CheckLocationValid(string location)
|
{
|
ThrowIfNotInitialized();
|
// 先用路由包检查,找不到则遍历所有包
|
var package = FindPackageForAsset(location);
|
if (package.CheckLocationValid(location))
|
return true;
|
foreach (var kvp in _packages)
|
{
|
if (kvp.Value != package && kvp.Value.CheckLocationValid(location))
|
return true;
|
}
|
return false;
|
}
|
|
/// <inheritdoc />
|
public YooAsset.AssetInfo[] GetAssetInfosByTag(string tag)
|
{
|
ThrowIfNotInitialized();
|
// 从所有包收集指定标签的资源信息
|
var allInfos = new List<YooAsset.AssetInfo>();
|
foreach (var kvp in _packages)
|
{
|
var infos = kvp.Value.GetAssetInfos(tag);
|
if (infos != null && infos.Length > 0)
|
allInfos.AddRange(infos);
|
}
|
return allInfos.ToArray();
|
}
|
|
/// <inheritdoc />
|
public bool IsNeedDownloadFromRemote(string location)
|
{
|
ThrowIfNotInitialized();
|
var package = FindPackageForAsset(location);
|
return package.IsNeedDownloadFromRemote(location);
|
}
|
|
// ====================================================================
|
// Download
|
// ====================================================================
|
|
/// <inheritdoc />
|
public async UniTask DownloadByTagsAsync(string[] tags, int downloadingMaxNumber = 10,
|
int failedTryAgain = 3, IProgress<float> progress = null, CancellationToken ct = default)
|
{
|
ThrowIfNotInitialized();
|
|
foreach (var tag in tags)
|
{
|
// 对所有包按标签创建下载器
|
foreach (var kvp in _packages)
|
{
|
var downloader = kvp.Value.CreateResourceDownloader(tag, downloadingMaxNumber, failedTryAgain);
|
if (downloader.TotalDownloadCount == 0)
|
continue;
|
|
downloader.BeginDownload();
|
while (!downloader.IsDone)
|
{
|
ct.ThrowIfCancellationRequested();
|
progress?.Report(downloader.Progress);
|
await UniTask.Yield();
|
}
|
|
if (downloader.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] Download tag '{tag}' from package '{kvp.Key}' failed: {downloader.Error}");
|
throw new InvalidOperationException($"Resource download failed for tag '{tag}': {downloader.Error}");
|
}
|
}
|
}
|
|
progress?.Report(1f);
|
}
|
|
// ====================================================================
|
// Version Management
|
// ====================================================================
|
|
/// <inheritdoc />
|
public async UniTask<string> RequestPackageVersionAsync(CancellationToken ct = default)
|
{
|
ThrowIfNotInitialized();
|
|
var op = _defaultPackage.RequestPackageVersionAsync();
|
await op.ToUniTask();
|
ct.ThrowIfCancellationRequested();
|
|
if (op.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] RequestPackageVersion failed: {op.Error}");
|
throw new InvalidOperationException($"Request package version failed: {op.Error}");
|
}
|
|
return op.PackageVersion;
|
}
|
|
/// <inheritdoc />
|
public async UniTask UpdatePackageManifestAsync(string packageVersion, CancellationToken ct = default)
|
{
|
ThrowIfNotInitialized();
|
|
var op = _defaultPackage.UpdatePackageManifestAsync(packageVersion);
|
await op.ToUniTask();
|
ct.ThrowIfCancellationRequested();
|
|
if (op.Status != EOperationStatus.Succeed)
|
{
|
Debug.LogError($"[YooAssetService] UpdatePackageManifest failed: {op.Error}");
|
throw new InvalidOperationException($"Update package manifest failed: {op.Error}");
|
}
|
}
|
|
// ====================================================================
|
// Release
|
// ====================================================================
|
|
/// <inheritdoc />
|
public void ReleaseHandle(HandleBase handle)
|
{
|
if (handle == null) return;
|
handle.Release();
|
}
|
|
/// <inheritdoc />
|
public async UniTask UnloadUnusedAssetsAsync()
|
{
|
ThrowIfNotInitialized();
|
// 对所有包执行卸载
|
foreach (var kvp in _packages)
|
{
|
var op = kvp.Value.UnloadUnusedAssetsAsync();
|
await op.ToUniTask();
|
}
|
}
|
|
/// <inheritdoc />
|
public async UniTask UnloadAllAssetsAsync()
|
{
|
ThrowIfNotInitialized();
|
// 对所有包执行卸载
|
foreach (var kvp in _packages)
|
{
|
var op = kvp.Value.UnloadAllAssetsAsync();
|
await op.ToUniTask();
|
}
|
}
|
|
// ====================================================================
|
// IYooAssetBridge Implementation
|
// ====================================================================
|
|
async UniTask<T> IYooAssetBridge.LoadAssetAsync<T>(string location)
|
{
|
return await LoadAssetAsync<T>(location);
|
}
|
|
async UniTask<string> IYooAssetBridge.LoadRawFileTextAsync(string location)
|
{
|
return await LoadRawFileTextAsync(location);
|
}
|
|
async UniTask<byte[]> IYooAssetBridge.LoadRawFileBytesAsync(string location)
|
{
|
return await LoadRawFileBytesAsync(location);
|
}
|
|
async UniTask IYooAssetBridge.PreloadAsync(string[] locations)
|
{
|
// 批量预加载,使用 UniTask.WhenAll 并行
|
var tasks = new List<UniTask>(locations.Length);
|
foreach (var loc in locations)
|
{
|
tasks.Add(LoadAssetAsync<UnityEngine.Object>(loc).AsUniTask());
|
}
|
await UniTask.WhenAll(tasks);
|
}
|
|
T IYooAssetBridge.GetCached<T>(string location)
|
{
|
// 委托给 ResourceCacheManager(US4 已集成)
|
if (ProjSG.Resource.ResourceCacheManager.IsValid())
|
{
|
return ProjSG.Resource.ResourceCacheManager.Instance.GetCached<T>(location);
|
}
|
return null;
|
}
|
|
// ====================================================================
|
// Sync Wrappers (Transitional — removed in US2)
|
// ====================================================================
|
|
/// <summary>
|
/// 同步加载所有同类型资源(过渡期使用)。
|
/// </summary>
|
[System.Obsolete("Use LoadAllAssetsAsync instead. Sync loading will be removed in US2.")]
|
public AllAssetsHandle LoadAllAssetsSync<T>(string location) where T : UnityEngine.Object
|
{
|
ThrowIfNotInitialized();
|
var package = FindPackageForAsset(location);
|
return package.LoadAllAssetsSync<T>(location);
|
}
|
}
|
}
|