// ============================================================================ // 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 { /// /// YooAsset 资源加载服务单例。 /// 封装 YooAsset ResourcePackage 的核心加载能力,提供 UniTask 异步 API。 /// 同时实现 IYooAssetBridge 供 Launch 程序集跨程序集调用。 /// public class YooAssetService : Singleton, IYooAssetService, IYooAssetBridge { private readonly Dictionary _packages = new Dictionary(); private ResourcePackage _defaultPackage; private IRemoteServices _remoteServices; private bool _isInitialized; private EPlayMode _playMode; // ==================================================================== // IYooAssetService Properties // ==================================================================== /// public bool IsInitialized => _isInitialized; /// public EPlayMode PlayMode => _playMode; // ==================================================================== // IYooAssetBridge Properties // ==================================================================== bool IYooAssetBridge.IsRegistered => _isInitialized; // ==================================================================== // Initialization // ==================================================================== /// 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}"); } /// /// 初始化指定名称的额外资源包裹。 /// 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 // ==================================================================== /// /// 资源加载重试配置 /// private const int MAX_RETRY_COUNT = 3; private const int BASE_RETRY_DELAY_MS = 500; // 500ms, 1000ms, 2000ms (exponential) /// /// 带重试的异步操作执行器。 /// 使用指数退避策略(500ms → 1000ms → 2000ms)。 /// /// 要执行的异步操作 /// 操作名称(用于日志) /// 取消令牌 /// 操作结果 private async UniTask ExecuteWithRetryAsync( Func> 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."); } /// /// 根据资源路径查找应使用的 ResourcePackage。 /// 使用 YooAssetPackageConfig 路由表确定目标包,找不到则回退到默认包。 /// private ResourcePackage FindPackageForAsset(string location) { var packageName = YooAssetPackageConfig.GetPackageForLocation(location); if (_packages.TryGetValue(packageName, out var package)) return package; // 路由到的包尚未初始化,回退到默认包 return _defaultPackage; } /// public async UniTask LoadAssetAsync(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(location, priority); await handle.ToUniTask(cancellationToken: ct); if (handle.Status != EOperationStatus.Succeed) { throw new InvalidOperationException($"LoadAssetAsync failed for '{location}': {handle.LastError}"); } return handle.GetAssetObject(); }, $"LoadAssetAsync<{typeof(T).Name}>('{location}')", ct); } /// public async UniTask 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); } /// /// 同步加载资产(仅在非 WebGL 平台过渡期使用)。 /// [System.Obsolete("Use LoadAssetAsync instead. Sync loading will be removed in US2.")] public T LoadAssetSync(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(location); if (handle.Status != EOperationStatus.Succeed) { Debug.LogError($"[YooAssetService] LoadAssetSync failed for '{location}': {handle.LastError}"); return null; } return handle.GetAssetObject(); } /// public async UniTask LoadSubAssetsAsync(string location, uint priority = 0, CancellationToken ct = default) where T : UnityEngine.Object { ThrowIfNotInitialized(); var package = FindPackageForAsset(location); var handle = package.LoadSubAssetsAsync(location, priority); await handle.ToUniTask(); ct.ThrowIfCancellationRequested(); if (handle.Status != EOperationStatus.Succeed) { Debug.LogError($"[YooAssetService] LoadSubAssetsAsync failed for '{location}': {handle.LastError}"); } return handle; } /// public async UniTask LoadAllAssetsAsync(string location, uint priority = 0, CancellationToken ct = default) where T : UnityEngine.Object { ThrowIfNotInitialized(); var package = FindPackageForAsset(location); var handle = package.LoadAllAssetsAsync(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 // ==================================================================== /// public async UniTask 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); } /// public async UniTask 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 // ==================================================================== /// public async UniTask 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 // ==================================================================== /// 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; } /// public YooAsset.AssetInfo[] GetAssetInfosByTag(string tag) { ThrowIfNotInitialized(); // 从所有包收集指定标签的资源信息 var allInfos = new List(); foreach (var kvp in _packages) { var infos = kvp.Value.GetAssetInfos(tag); if (infos != null && infos.Length > 0) allInfos.AddRange(infos); } return allInfos.ToArray(); } /// public bool IsNeedDownloadFromRemote(string location) { ThrowIfNotInitialized(); var package = FindPackageForAsset(location); return package.IsNeedDownloadFromRemote(location); } // ==================================================================== // Download // ==================================================================== /// public async UniTask DownloadByTagsAsync(string[] tags, int downloadingMaxNumber = 10, int failedTryAgain = 3, IProgress 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 // ==================================================================== /// public async UniTask 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; } /// 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 // ==================================================================== /// public void ReleaseHandle(HandleBase handle) { if (handle == null) return; handle.Release(); } /// public async UniTask UnloadUnusedAssetsAsync() { ThrowIfNotInitialized(); // 对所有包执行卸载 foreach (var kvp in _packages) { var op = kvp.Value.UnloadUnusedAssetsAsync(); await op.ToUniTask(); } } /// public async UniTask UnloadAllAssetsAsync() { ThrowIfNotInitialized(); // 对所有包执行卸载 foreach (var kvp in _packages) { var op = kvp.Value.UnloadAllAssetsAsync(); await op.ToUniTask(); } } // ==================================================================== // IYooAssetBridge Implementation // ==================================================================== async UniTask IYooAssetBridge.LoadAssetAsync(string location) { return await LoadAssetAsync(location); } async UniTask IYooAssetBridge.LoadRawFileTextAsync(string location) { return await LoadRawFileTextAsync(location); } async UniTask IYooAssetBridge.LoadRawFileBytesAsync(string location) { return await LoadRawFileBytesAsync(location); } async UniTask IYooAssetBridge.PreloadAsync(string[] locations) { // 批量预加载,使用 UniTask.WhenAll 并行 var tasks = new List(locations.Length); foreach (var loc in locations) { tasks.Add(LoadAssetAsync(loc).AsUniTask()); } await UniTask.WhenAll(tasks); } T IYooAssetBridge.GetCached(string location) { // 委托给 ResourceCacheManager(US4 已集成) if (ProjSG.Resource.ResourceCacheManager.IsValid()) { return ProjSG.Resource.ResourceCacheManager.Instance.GetCached(location); } return null; } // ==================================================================== // Sync Wrappers (Transitional — removed in US2) // ==================================================================== /// /// 同步加载所有同类型资源(过渡期使用)。 /// [System.Obsolete("Use LoadAllAssetsAsync instead. Sync loading will be removed in US2.")] public AllAssetsHandle LoadAllAssetsSync(string location) where T : UnityEngine.Object { ThrowIfNotInitialized(); var package = FindPackageForAsset(location); return package.LoadAllAssetsSync(location); } } }