| Assets/Editor/Tool/ClientPackage.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Launch/Launch.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Launch/Manager/LocalResManager.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Launch/Manager/YooAssetInitializer.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| Assets/Launch/UI/LaunchWins/LaunchExWin.cs | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
Assets/Editor/Tool/ClientPackage.cs
@@ -133,93 +133,117 @@ } } // ========== 使用 YooAsset 打包资源 ========== // 全量打包所有 Package(含 HybridCLR),自动拷贝到 StreamingAssets/yoo/{PackageName}/ Debug.Log("[ClientPackage] 开始 YooAsset 全量打包所有 Package..."); if (!YooAssetBuildTool.BuildAllPackagesCore(incremental: false)) { Debug.LogError("[ClientPackage] YooAsset 资源打包失败,中止构建!"); EditorUtility.DisplayDialog("打包失败", "YooAsset 资源打包失败,请查看 Console 日志。", "确定"); return; } Debug.Log("[ClientPackage] YooAsset 资源打包完成。"); // 打包成功后,将完整 StreamingAssets/yoo 拷贝到 Output Path(裁剪前执行,确保拿到全量资源) if (!string.IsNullOrEmpty(_assetBundlePath)) { YooAssetBuildTool.CopyStreamingAssetsToOutputPath(_assetBundlePath); } // YooAsset StreamingAssets 根目录 string yooRoot = AssetBundleBuilderHelper.GetStreamingAssetsRoot(); // ---- NullAsset(小包):不随包携带 StreamingAssets 资源,全部从 CDN 按需下载 ---- if (smallPackages.Count > 0) if (!Directory.Exists(yooRoot)) { YooAssetBuildTool.ClearStreamingAssetsYooDirectory(); Debug.Log("[ClientPackage] NullAsset: 已清空 StreamingAssets/yoo"); // 写入随包 Package 列表,NullAsset 为空列表 YooAssetBuildTool.WriteBuildinPackageList(); for (int i = 0; i < smallPackages.Count; i++) { #if UNITY_ANDROID BuildApk(_sdkPath, _output, smallPackages[i], _buildIndex, _development, _assetBundlePath); #elif UNITY_IOS BuildIpa(_sdkPath, smallPackages[i], _buildIndex, _replace); #endif } // 恢复完整 StreamingAssets 供后续使用 RestoreYooStreamingAssets(); Debug.LogError($"[ClientPackage] StreamingAssets/yoo 目录不存在: {yooRoot}"); EditorUtility.DisplayDialog("打包失败", "未找到 StreamingAssets/yoo,请先准备好资源后再执行 BuildApk。", "确定"); return; } // ---- HalfAsset(中包):剔除部分 Package ---- if (halfPackages.Count > 0) if (!ValidateLaunchRequiredPackages(yooRoot)) { var removePackages = CreateHalfAssetRemovePackages(); EditorUtility.DisplayDialog("打包失败", "StreamingAssets/yoo 缺少必需包(Builtin/Dll),请先准备完整资源。", "确定"); return; } foreach (var pkgName in removePackages) string yooBackupPath = CreateYooStreamingAssetsBackup(yooRoot); if (string.IsNullOrEmpty(yooBackupPath)) { EditorUtility.DisplayDialog("打包失败", "备份 StreamingAssets/yoo 失败,请查看 Console 日志。", "确定"); return; } Debug.Log("[ClientPackage] 跳过 YooAsset 资源打包,直接使用现有 StreamingAssets/yoo 构建。"); // 将完整 StreamingAssets/yoo 拷贝到 Output Path(裁剪前执行,确保拿到全量资源) if (!string.IsNullOrEmpty(_assetBundlePath)) { if (!YooAssetBuildTool.CopyStreamingAssetsToOutputPath(_assetBundlePath)) { string pkgDir = Path.Combine(yooRoot, pkgName); if (Directory.Exists(pkgDir)) Debug.LogError($"[ClientPackage] 拷贝 StreamingAssets/yoo 到输出路径失败: {_assetBundlePath}"); return; } } try { // ---- NullAsset(小包):不随包携带 StreamingAssets 资源,全部从 CDN 按需下载 ---- if (smallPackages.Count > 0) { YooAssetBuildTool.ClearStreamingAssetsYooDirectory(); Debug.Log("[ClientPackage] NullAsset: 已清空 StreamingAssets/yoo"); // 写入随包 Package 列表,NullAsset 为空列表 YooAssetBuildTool.WriteBuildinPackageList(); for (int i = 0; i < smallPackages.Count; i++) { Directory.Delete(pkgDir, true); Debug.Log($"[ClientPackage] HalfAsset: 已剔除 Package '{pkgName}'"); #if UNITY_ANDROID BuildApk(_sdkPath, _output, smallPackages[i], _buildIndex, _development, _assetBundlePath); #elif UNITY_IOS BuildIpa(_sdkPath, smallPackages[i], _buildIndex, _replace); #endif } // 恢复完整 StreamingAssets 供后续使用 if (!RestoreYooStreamingAssetsFromBackup(yooRoot, yooBackupPath)) return; } // ---- HalfAsset(中包):剔除部分 Package ---- if (halfPackages.Count > 0) { var removePackages = CreateHalfAssetRemovePackages(); foreach (var pkgName in removePackages) { string pkgDir = Path.Combine(yooRoot, pkgName); if (Directory.Exists(pkgDir)) { Directory.Delete(pkgDir, true); Debug.Log($"[ClientPackage] HalfAsset: 已剔除 Package '{pkgName}'"); } } // 写入剔除后的 Buildin 包列表 YooAssetBuildTool.WriteBuildinPackageList(); for (int i = 0; i < halfPackages.Count; i++) { #if UNITY_ANDROID BuildApk(_sdkPath, _output, halfPackages[i], _buildIndex, _development, _assetBundlePath); #elif UNITY_IOS BuildIpa(_sdkPath, halfPackages[i], _buildIndex, _replace); #endif } // 恢复完整 StreamingAssets 供后续使用 if (!RestoreYooStreamingAssetsFromBackup(yooRoot, yooBackupPath)) return; } // ---- FullAsset(大包):保留所有 Package ---- if (bigPackages.Count > 0) { // 全量 StreamingAssets,写入完整 Buildin 包列表 YooAssetBuildTool.WriteBuildinPackageList(); for (int i = 0; i < bigPackages.Count; i++) { #if UNITY_ANDROID BuildApk(_sdkPath, _output, bigPackages[i], _buildIndex, _development, _assetBundlePath); #elif UNITY_IOS BuildIpa(_sdkPath, bigPackages[i], _buildIndex, _replace); #endif } } // 写入剔除后的 Buildin 包列表 YooAssetBuildTool.WriteBuildinPackageList(); for (int i = 0; i < halfPackages.Count; i++) { #if UNITY_ANDROID BuildApk(_sdkPath, _output, halfPackages[i], _buildIndex, _development, _assetBundlePath); #elif UNITY_IOS BuildIpa(_sdkPath, halfPackages[i], _buildIndex, _replace); #endif } // 恢复完整 StreamingAssets 供后续使用 RestoreYooStreamingAssets(); } // ---- FullAsset(大包):保留所有 Package ---- if (bigPackages.Count > 0) finally { // 全量 StreamingAssets,写入完整 Buildin 包列表 YooAssetBuildTool.WriteBuildinPackageList(); for (int i = 0; i < bigPackages.Count; i++) { #if UNITY_ANDROID BuildApk(_sdkPath, _output, bigPackages[i], _buildIndex, _development, _assetBundlePath); #elif UNITY_IOS BuildIpa(_sdkPath, bigPackages[i], _buildIndex, _replace); #endif } DeleteDirectoryIfExists(yooBackupPath); } } @@ -781,6 +805,84 @@ YooAssetBuildTool.RestoreBuildOutputToStreamingAssets(); } private static string CreateYooStreamingAssetsBackup(string yooRoot) { try { string backupRoot = Path.Combine(Path.GetTempPath(), $"ProjSG_YooBackup_{Guid.NewGuid():N}"); if (Directory.Exists(backupRoot)) Directory.Delete(backupRoot, true); FileExtersion.DirectoryCopy(yooRoot, backupRoot); Debug.Log($"[ClientPackage] 已备份 StreamingAssets/yoo: {backupRoot}"); return backupRoot; } catch (Exception ex) { Debug.LogError($"[ClientPackage] 备份 StreamingAssets/yoo 失败: {ex}"); return null; } } private static bool RestoreYooStreamingAssetsFromBackup(string yooRoot, string backupRoot) { if (string.IsNullOrEmpty(backupRoot) || !Directory.Exists(backupRoot)) { Debug.LogError($"[ClientPackage] StreamingAssets/yoo 备份不存在: {backupRoot}"); return false; } try { if (Directory.Exists(yooRoot)) Directory.Delete(yooRoot, true); FileExtersion.DirectoryCopy(backupRoot, yooRoot); Debug.Log("[ClientPackage] 已从临时备份恢复 StreamingAssets/yoo"); return true; } catch (Exception ex) { Debug.LogError($"[ClientPackage] 从备份恢复 StreamingAssets/yoo 失败: {ex}"); return false; } } private static bool ValidateLaunchRequiredPackages(string yooRoot) { var missing = new List<string>(); for (int i = 0; i < LaunchRequiredYooPackages.Length; i++) { string packageDir = Path.Combine(yooRoot, LaunchRequiredYooPackages[i]); if (!Directory.Exists(packageDir)) missing.Add(LaunchRequiredYooPackages[i]); } if (missing.Count > 0) { Debug.LogError($"[ClientPackage] StreamingAssets/yoo 缺少必需包: {string.Join(", ", missing)}"); return false; } return true; } private static void DeleteDirectoryIfExists(string path) { if (string.IsNullOrEmpty(path)) return; try { if (Directory.Exists(path)) Directory.Delete(path, true); } catch (Exception ex) { Debug.LogError($"[ClientPackage] 删除临时目录失败: {path}, error={ex}"); } } private static List<string> CreateHalfAssetRemovePackages() { var removePackages = new List<string> { "Battle", "Hero", "Audio", "Video", "UIEffect" }; Assets/Launch/Launch.cs
@@ -59,13 +59,20 @@ private const string PRE_SCENE_RES_PACKAGE_NAME = "PreSceneRes"; private const string LAUNCH_STATIC_WIN_LOCATION = "Assets/ResourcesOut/PreSceneRes/LaunchStaticWin.prefab"; private const int PRE_SCENE_RES_VERIFY_MAX_CONCURRENCY = 8; // 上次更新成功的 PreSceneRes 版本号(方案 A:自存版本号,下次启动本地激活,不联网)。 private const string PRE_SCENE_RES_VERSION_KEY = "yoo_ver_PreSceneRes"; private static void LogTiming(string phase, float startTime) { Debug.LogError($"[Launch][Timing] {phase} cost={(Time.realtimeSinceStartup - startTime):F3}s"); } private static Launch m_Instance; private void Awake() { float startTime = Time.realtimeSinceStartup; if (m_Instance != null) { Debug.LogError("Launch Instance is not null"); @@ -89,12 +96,16 @@ #endif OpenLaunchStaticWin().Forget(); LogTiming("Awake", startTime); } private async UniTaskVoid OpenLaunchStaticWin() { if (isOpeningLaunchStaticWin || launchStaticWin != null) return; float startTime = Time.realtimeSinceStartup; float phaseStart = startTime; isOpeningLaunchStaticWin = true; try @@ -111,9 +122,13 @@ Debug.LogError($"[Launch] LaunchStaticWin prefab not found: {LAUNCH_STATIC_WIN_LOCATION}"); return; } LogTiming("OpenLaunchStaticWin.Editor.LoadPrefab", phaseStart); float editorInstantiateStart = Time.realtimeSinceStartup; launchStaticWin = Instantiate(prefab); launchStaticWin.name = "LaunchStaticWin"; launchStaticWin.AddComponent<DontDestroyOnLoad>(); LogTiming("OpenLaunchStaticWin.Editor.Instantiate", editorInstantiateStart); return; } #endif @@ -125,7 +140,7 @@ if (YooAssetInitializer.Instance.State == YooAssetInitializer.InitState.InitFailed || YooAssetInitializer.Instance.State == YooAssetInitializer.InitState.VersionFailed) { Debug.LogWarning($"[Launch] Skip LaunchStaticWin because YooAsset state={YooAssetInitializer.Instance.State}"); Debug.LogError($"[Launch] Skip LaunchStaticWin because YooAsset state={YooAssetInitializer.Instance.State}"); return; } @@ -134,11 +149,13 @@ await UniTask.Yield(); } LogTiming("OpenLaunchStaticWin.WebGL.WaitReady", phaseStart); phaseStart = Time.realtimeSinceStartup; var webPackage = YooAssetInitializer.Instance.GetPackage(PRE_SCENE_RES_PACKAGE_NAME); if (webPackage == null) { Debug.LogWarning($"[Launch] Package '{PRE_SCENE_RES_PACKAGE_NAME}' not initialized. Skip LaunchStaticWin."); Debug.LogError($"[Launch] Package '{PRE_SCENE_RES_PACKAGE_NAME}' not initialized. Skip LaunchStaticWin."); return; } @@ -150,79 +167,133 @@ return; } prefab = webHandle.GetAssetObject<GameObject>(); LogTiming("OpenLaunchStaticWin.WebGL.LoadPrefab", phaseStart); #else // Android/iOS/PC(含 Editor AB 模式):本地秒开,独立初始化 PreSceneRes,不等网络。 YooAssets.Initialize(); var package = YooAssets.TryGetPackage(PRE_SCENE_RES_PACKAGE_NAME) ?? YooAssets.CreatePackage(PRE_SCENE_RES_PACKAGE_NAME); // 快路径:若本地已缓存 savedVer 的 manifest/hash,则无需再触发 buildin 版本读取与复制。 string savedVer = LocalSave.GetString(PRE_SCENE_RES_VERSION_KEY); bool hasSavedManifest = !string.IsNullOrEmpty(savedVer) && IsPreSceneResManifestCached(savedVer); UniTask<string> buildinVerTask = default; if (!hasSavedManifest) { // 只有在需要 buildin 回退时才读取,避免常规热更后启动的额外开销。 buildinVerTask = ReadBuildinPreSceneResVersionAsync().Preserve(); } if (package.InitializeStatus != EOperationStatus.Succeed) { // 始终用 HostPlayMode(随包 buildin + 缓存)。 // cdnUrl 在 Awake 阶段通常还没拿到,但 CacheFileSystem 初始化不需要它, // 远程地址由 DynamicCdnRemoteServices 在请求版本/下载时实时读取。 var remoteServices = new DynamicCdnRemoteServices(PRE_SCENE_RES_PACKAGE_NAME); var initParams = new HostPlayModeParameters { BuildinFileSystemParameters = FileSystemParameters.CreateDefaultBuildinFileSystemParameters(), CacheFileSystemParameters = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices) }; var initOp = package.InitializeAsync(initParams); await initOp.ToUniTask(); if (initOp.Status != EOperationStatus.Succeed) { Debug.LogError($"[Launch] Init PreSceneRes failed: {initOp.Error}"); if (!await InitializePreSceneResPackageAsync(package, hasSavedManifest)) return; } } LogTiming("OpenLaunchStaticWin.Native.InitPackage", phaseStart); phaseStart = Time.realtimeSinceStartup; // 本地激活 manifest(不联网):取「上次更新保存的版本」与「包内 buildin 版本」中较新的一个。 // 版本号格式 yyyy-MM-dd-HHmm 字典序可排序; // 这样重新打包带入更新的 buildin 资源时,不会被旧的 LocalSave 版本号覆盖。 string savedVer = LocalSave.GetString(PRE_SCENE_RES_VERSION_KEY); string buildinVer = await ReadBuildinPreSceneResVersionAsync(); string buildinVer = null; string localVer; if (string.IsNullOrEmpty(savedVer)) localVer = buildinVer; else if (string.IsNullOrEmpty(buildinVer)) if (hasSavedManifest) { // 快路径:缓存版本存在时直接激活,避免额外等待读取 buildin 版本。 localVer = savedVer; } else localVer = string.CompareOrdinal(savedVer, buildinVer) >= 0 ? savedVer : buildinVer; { buildinVer = await buildinVerTask; localVer = buildinVer; } if (!string.IsNullOrEmpty(savedVer) && !hasSavedManifest) { Debug.LogError($"[Launch] Saved PreSceneRes manifest missing in cache, ignore savedVer={savedVer}, use buildinVer={buildinVer}."); } if (string.IsNullOrEmpty(localVer)) { Debug.LogError("[Launch] PreSceneRes local version unavailable."); return; } Debug.Log($"[Launch] PreSceneRes localVer={localVer} (saved={savedVer}, buildin={buildinVer})"); Debug.LogError($"[Launch] PreSceneRes localVer={localVer} (saved={savedVer}, buildin={buildinVer})"); LogTiming("OpenLaunchStaticWin.Native.PickVersion", phaseStart); phaseStart = Time.realtimeSinceStartup; var localManifestOp = package.UpdatePackageManifestAsync(localVer); await localManifestOp.ToUniTask(); if (localManifestOp.Status != EOperationStatus.Succeed) // 快路径:当前已激活目标版本时,无需再次加载 manifest。 if (package.PackageValid) { // 选中的版本激活失败(如缓存被清且无网络),回退到包内 buildin 版本。 if (!string.IsNullOrEmpty(buildinVer) && buildinVer != localVer) string activeVer = package.GetPackageVersion(); if (!string.IsNullOrEmpty(activeVer) && activeVer == localVer) { Debug.LogWarning($"[Launch] Activate manifest v{localVer} failed, fallback buildin v{buildinVer}: {localManifestOp.Error}"); localVer = buildinVer; localManifestOp = package.UpdatePackageManifestAsync(localVer); await localManifestOp.ToUniTask(); var fastHandle = package.LoadAssetSync<GameObject>(LAUNCH_STATIC_WIN_LOCATION); if (fastHandle.Status == EOperationStatus.Succeed) { prefab = fastHandle.GetAssetObject<GameObject>(); } else { Debug.LogError($"[Launch] Fast-path load LaunchStaticWin failed: {fastHandle.LastError}"); } LogTiming("OpenLaunchStaticWin.Native.FastPathLoadPrefab", phaseStart); phaseStart = Time.realtimeSinceStartup; } } if (prefab == null) { var localManifestOp = package.UpdatePackageManifestAsync(localVer); await localManifestOp.ToUniTask(); if (localManifestOp.Status != EOperationStatus.Succeed) { Debug.LogError($"[Launch] Activate PreSceneRes manifest v{localVer} failed: {localManifestOp.Error}"); // 缓存快路径可能遇到异常缓存;重建为完整 Host 文件系统后再重试一次 saved 版本。 if (hasSavedManifest) { Debug.LogError($"[Launch] Activate saved manifest v{localVer} failed, retry full host init: {localManifestOp.Error}"); if (!await ReinitializePreSceneResPackageWithBuildinAsync(package)) return; localManifestOp = package.UpdatePackageManifestAsync(localVer); await localManifestOp.ToUniTask(); } // 选中的版本激活失败(如缓存被清且无网络),回退到包内 buildin 版本。 if (localManifestOp.Status != EOperationStatus.Succeed && string.IsNullOrEmpty(buildinVer)) { if (!hasSavedManifest) buildinVer = await buildinVerTask; else buildinVer = await ReadBuildinPreSceneResVersionAsync(); } if (localManifestOp.Status != EOperationStatus.Succeed && !string.IsNullOrEmpty(buildinVer) && buildinVer != localVer) { Debug.LogError($"[Launch] Activate manifest v{localVer} failed, fallback buildin v{buildinVer}: {localManifestOp.Error}"); localVer = buildinVer; localManifestOp = package.UpdatePackageManifestAsync(localVer); await localManifestOp.ToUniTask(); } if (localManifestOp.Status != EOperationStatus.Succeed) { Debug.LogError($"[Launch] Activate PreSceneRes manifest v{localVer} failed: {localManifestOp.Error}"); return; } } LogTiming("OpenLaunchStaticWin.Native.ActivateManifest", phaseStart); phaseStart = Time.realtimeSinceStartup; var handle = package.LoadAssetSync<GameObject>(LAUNCH_STATIC_WIN_LOCATION); if (handle.Status != EOperationStatus.Succeed) { Debug.LogError($"[Launch] Load LaunchStaticWin failed: {handle.LastError}"); return; } prefab = handle.GetAssetObject<GameObject>(); LogTiming("OpenLaunchStaticWin.Native.LoadPrefab", phaseStart); } var handle = package.LoadAssetSync<GameObject>(LAUNCH_STATIC_WIN_LOCATION); if (handle.Status != EOperationStatus.Succeed) { Debug.LogError($"[Launch] Load LaunchStaticWin failed: {handle.LastError}"); return; } prefab = handle.GetAssetObject<GameObject>(); // 后台更新(不阻塞秒开):等 cdnUrl 就绪后请求最新版本、更新清单、加载一次触发下载落地, // 并保存新版本号,供下次启动直接用最新资源。 @@ -236,9 +307,11 @@ return; } float instantiateStart = Time.realtimeSinceStartup; launchStaticWin = Instantiate(prefab); launchStaticWin.name = "LaunchStaticWin"; launchStaticWin.AddComponent<DontDestroyOnLoad>(); LogTiming("OpenLaunchStaticWin.Instantiate", instantiateStart); } catch (Exception ex) { @@ -246,8 +319,87 @@ } finally { LogTiming("OpenLaunchStaticWin", startTime); isOpeningLaunchStaticWin = false; } } private async UniTask<bool> InitializePreSceneResPackageAsync(ResourcePackage package, bool hasSavedManifest) { // cdnUrl 在 Awake 阶段通常还没拿到,但 CacheFileSystem 初始化不依赖 cdnUrl。 // 远程地址由 DynamicCdnRemoteServices 在请求版本/下载时实时读取。 if (hasSavedManifest) { var cacheOnlyParams = new HostPlayModeParameters { CacheFileSystemParameters = CreatePreSceneResCacheFileSystemParameters() }; var cacheOnlyOp = package.InitializeAsync(cacheOnlyParams); await cacheOnlyOp.ToUniTask(); if (cacheOnlyOp.Status == EOperationStatus.Succeed) { Debug.LogError("[Launch] Init PreSceneRes with cache-only fast path succeeded."); return true; } Debug.LogError($"[Launch] Init PreSceneRes cache-only failed, fallback full host init: {cacheOnlyOp.Error}"); } var fullHostParams = new HostPlayModeParameters { BuildinFileSystemParameters = CreatePreSceneResBuildinFileSystemParameters(copyBuildinManifest: true), CacheFileSystemParameters = CreatePreSceneResCacheFileSystemParameters() }; var initOp = package.InitializeAsync(fullHostParams); await initOp.ToUniTask(); if (initOp.Status != EOperationStatus.Succeed) { Debug.LogError($"[Launch] Init PreSceneRes failed: {initOp.Error}"); return false; } return true; } private async UniTask<bool> ReinitializePreSceneResPackageWithBuildinAsync(ResourcePackage package) { var destroyOp = package.DestroyAsync(); await destroyOp.ToUniTask(); if (destroyOp.Status != EOperationStatus.Succeed) { Debug.LogError($"[Launch] Destroy PreSceneRes package before reinit failed: {destroyOp.Error}"); return false; } return await InitializePreSceneResPackageAsync(package, hasSavedManifest: false); } private FileSystemParameters CreatePreSceneResBuildinFileSystemParameters(bool copyBuildinManifest) { var buildinFsParams = FileSystemParameters.CreateDefaultBuildinFileSystemParameters(); buildinFsParams.AddParameter(FileSystemParametersDefine.COPY_BUILDIN_PACKAGE_MANIFEST, copyBuildinManifest); // 启动期优先速度:只做存在性校验。 buildinFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_LEVEL, EFileVerifyLevel.Low); // 控制并发可降低移动端线程调度开销,缩短初始化尾部抖动。 buildinFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_MAX_CONCURRENCY, PRE_SCENE_RES_VERIFY_MAX_CONCURRENCY); // 启动期跳过覆盖安装自动清理,避免首次启动额外磁盘清理开销。 buildinFsParams.AddParameter(FileSystemParametersDefine.INSTALL_CLEAR_MODE, EOverwriteInstallClearMode.None); return buildinFsParams; } private FileSystemParameters CreatePreSceneResCacheFileSystemParameters() { var remoteServices = new DynamicCdnRemoteServices(PRE_SCENE_RES_PACKAGE_NAME); var cacheFsParams = FileSystemParameters.CreateDefaultCacheFileSystemParameters(remoteServices); // 启动期优先速度:只做存在性校验。 cacheFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_LEVEL, EFileVerifyLevel.Low); // 控制并发可降低移动端线程调度开销,缩短初始化尾部抖动。 cacheFsParams.AddParameter(FileSystemParametersDefine.FILE_VERIFY_MAX_CONCURRENCY, PRE_SCENE_RES_VERIFY_MAX_CONCURRENCY); // 启动期跳过覆盖安装自动清理,避免首次启动额外磁盘清理开销。 cacheFsParams.AddParameter(FileSystemParametersDefine.INSTALL_CLEAR_MODE, EOverwriteInstallClearMode.None); return cacheFsParams; } #if !UNITY_WEBGL || UNITY_EDITOR @@ -257,11 +409,21 @@ /// </summary> private async UniTask<string> ReadBuildinPreSceneResVersionAsync() { float startTime = Time.realtimeSinceStartup; try { string folder = YooAssetSettingsData.GetDefaultYooFolderName(); string fileName = YooAssetSettingsData.GetPackageVersionFileName(PRE_SCENE_RES_PACKAGE_NAME); string filePath = $"{Application.streamingAssetsPath}/{folder}/{PRE_SCENE_RES_PACKAGE_NAME}/{fileName}"; string filePath = Path.Combine(Application.streamingAssetsPath, folder, PRE_SCENE_RES_PACKAGE_NAME, fileName); #if !UNITY_ANDROID || UNITY_EDITOR // 非 Android 平台优先直接读本地文件,减少 UnityWebRequest 开销。 if (File.Exists(filePath)) { return File.ReadAllText(filePath).Trim(); } #endif // Android 上 streamingAssetsPath 已是 jar:file://... 形式,可直接用于 UnityWebRequest; // 其它平台为本地文件路径,需补 file:// 前缀。 string url; @@ -278,7 +440,7 @@ await req.SendWebRequest().ToUniTask(); if (req.result != UnityEngine.Networking.UnityWebRequest.Result.Success) { Debug.LogWarning($"[Launch] Read buildin PreSceneRes version failed: {req.error}"); Debug.LogError($"[Launch] Read buildin PreSceneRes version failed: {req.error}"); return null; } return req.downloadHandler.text.Trim(); @@ -289,13 +451,61 @@ Debug.LogError($"[Launch] ReadBuildinPreSceneResVersionAsync exception: {ex}"); return null; } finally { LogTiming("ReadBuildinPreSceneResVersionAsync", startTime); } } /// <summary> /// 检查指定版本的 PreSceneRes manifest/hash 是否已落地到本地缓存。 /// 仅当本地真实存在时才允许启动阶段激活该版本,避免在 cdnUrl 未就绪时触发远程请求。 /// </summary> private bool IsPreSceneResManifestCached(string packageVersion) { if (string.IsNullOrEmpty(packageVersion)) return false; try { string cacheRoot = GetYooDefaultCacheRoot(); string manifestRoot = Path.Combine(cacheRoot, PRE_SCENE_RES_PACKAGE_NAME, "ManifestFiles"); string hashPath = Path.Combine(manifestRoot, YooAssetSettingsData.GetPackageHashFileName(PRE_SCENE_RES_PACKAGE_NAME, packageVersion)); string manifestPath = Path.Combine(manifestRoot, YooAssetSettingsData.GetManifestBinaryFileName(PRE_SCENE_RES_PACKAGE_NAME, packageVersion)); return File.Exists(hashPath) && File.Exists(manifestPath); } catch (Exception ex) { Debug.LogError($"[Launch] Check PreSceneRes cache manifest failed: {ex.Message}"); return false; } } /// <summary> /// 对齐 YooAsset 默认缓存根目录规则,供启动阶段做本地 manifest 存在性判断。 /// </summary> private string GetYooDefaultCacheRoot() { string yooFolder = YooAssetSettingsData.GetDefaultYooFolderName(); #if UNITY_EDITOR string projectPath = Path.GetDirectoryName(Application.dataPath); return string.IsNullOrEmpty(yooFolder) ? projectPath : Path.Combine(projectPath, yooFolder); #elif UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX return string.IsNullOrEmpty(yooFolder) ? Application.dataPath : Path.Combine(Application.dataPath, yooFolder); #elif UNITY_STANDALONE_OSX return string.IsNullOrEmpty(yooFolder) ? Application.persistentDataPath : Path.Combine(Application.persistentDataPath, yooFolder); #else return string.IsNullOrEmpty(yooFolder) ? Application.persistentDataPath : Path.Combine(Application.persistentDataPath, yooFolder); #endif } #endif // 启动入口 async void Start() { Debug.Log("Launch Start"); float startTime = Time.realtimeSinceStartup; Debug.LogError("[Launch] Start"); #if UNITY_WEBGL await VersionConfigEx.Get(); #endif @@ -311,6 +521,7 @@ InitSetting(); // 1. 打开加载界面 LogTiming("Start", startTime); } /// <summary> @@ -319,6 +530,7 @@ /// </summary> public async UniTask InitYooAssetEarlyAsync() { float startTime = Time.realtimeSinceStartup; try { YooAsset.IRemoteServices remoteServices = null; @@ -362,7 +574,7 @@ : WebGLRemoteConfig.CreateRemoteServices(); if (!string.IsNullOrEmpty(webCdnUrl)) remoteCdnBaseUrl = webCdnUrl; Debug.Log($"[Launch] WebGL remoteCdnBaseUrl={remoteCdnBaseUrl ?? WebGLRemoteConfig.ActiveServerURL}"); Debug.LogError($"[Launch] WebGL remoteCdnBaseUrl={remoteCdnBaseUrl ?? WebGLRemoteConfig.ActiveServerURL}"); #else // 非 WebGL 正式包: // 先读 VersionConfigEx(Resources.Load,不依赖 YooAsset), @@ -380,11 +592,11 @@ else { playMode = EPlayMode.OfflinePlayMode; Debug.Log("[Launch] No cdnUrl configured, using OfflinePlayMode"); Debug.LogError("[Launch] No cdnUrl configured, using OfflinePlayMode"); } #endif Debug.Log($"[Launch] cdnUrl={remoteCdnBaseUrl+ LocalResManager.fixPath}, PlayMode={playMode}"); Debug.LogError($"[Launch] cdnUrl={remoteCdnBaseUrl+ LocalResManager.fixPath}, PlayMode={playMode}"); await YooAssetInitializer.Instance.InitializeAsync(playMode, remoteServices, remoteCdnBaseUrl + LocalResManager.fixPath); @@ -401,16 +613,21 @@ // HostPlayMode: 不预下载全部资源。YooAsset CacheFileSystem 在 LoadAssetAsync 时自动按需从 CDN 下载。 Debug.Log("[Launch] YooAsset early init completed. State: " + YooAssetInitializer.Instance.State); Debug.LogError("[Launch] YooAsset early init completed. State: " + YooAssetInitializer.Instance.State); } catch (Exception ex) { Debug.LogError($"[Launch] YooAsset early init exception: {ex}"); } finally { LogTiming("InitYooAssetEarlyAsync", startTime); } } private void InitSetting() { float startTime = Time.realtimeSinceStartup; System.Globalization.CultureInfo culture = System.Globalization.CultureInfo.CreateSpecificCulture("en-US"); System.Globalization.CultureInfo.CurrentCulture = culture; System.Globalization.CultureInfo.CurrentUICulture = culture; @@ -424,16 +641,21 @@ // 任何都要去请求版本信息 LocalResManager.step = LocalResManager.LoadDllStep.RequestVersion; LogTiming("InitSetting", startTime); } private void InitPlugins() { float startTime = Time.realtimeSinceStartup; DOTween.Init(); LogTiming("InitPlugins", startTime); } private void SDKInit() { float startTime = Time.realtimeSinceStartup; AotSdkUtility.Instance.Init(); LogTiming("SDKInit", startTime); } /// <summary> @@ -449,6 +671,7 @@ if (launchExWin != null || isOpeningLaunchUI) return; float startTime = Time.realtimeSinceStartup; isOpeningLaunchUI = true; try { @@ -462,6 +685,7 @@ finally { isOpeningLaunchUI = false; LogTiming("ShowLaunchUIAsync", startTime); } } @@ -471,6 +695,7 @@ /// </summary> private void LoadMetadataForAOTAssemblies() { float startTime = Time.realtimeSinceStartup; /// 注意,补充元数据是给 AOT dll 补充元数据,而不是给热更新 dll 补充元数据。 /// 热更新 dll 不缺元数据,不需要补充,如果调用 LoadMetadataForAOTAssembly 会返回错误。 /// @@ -481,8 +706,9 @@ continue; // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码 LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(ReadBytesFromStreamingAssets(aotDllName), mode); Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}"); Debug.LogError($"LoadMetadataForAOTAssembly:{aotDllName}. mode:{mode} ret:{err}"); } LogTiming("LoadMetadataForAOTAssemblies", startTime); } public static byte[] ReadBytesFromStreamingAssets(string dllName) @@ -492,6 +718,7 @@ private void StartGame() { float startTime = Time.realtimeSinceStartup; #if !UNITY_EDITOR LoadMetadataForAOTAssemblies(); _hotUpdateAss = Assembly.Load(ReadBytesFromStreamingAssets("Main.dll.bytes")); @@ -515,7 +742,8 @@ Debug.LogError("无法找到get_Instance方法"); } LocalResManager.Instance.RecordLauchEvent(3); Debug.Log("进入游戏流程"); Debug.LogError("进入游戏流程"); LogTiming("StartGame", startTime); } private void DestroySingleton() @@ -615,6 +843,7 @@ /// </summary> private async UniTask ReadDllBytes(Action callback) { float startTime = Time.realtimeSinceStartup; var dllPackage = YooAssets.TryGetPackage("Dll"); if (dllPackage == null) { @@ -646,12 +875,12 @@ downloadBytes = downloader != null ? downloader.TotalDownloadBytes : 0; needRemote = downloadCount > 0 || downloadBytes > 0; downloadStartTime = Time.realtimeSinceStartup; Debug.Log($"[YooAssetDownload] Start on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}"); Debug.LogError($"[YooAssetDownload] Start on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}"); } } catch (Exception ex) { Debug.LogWarning($"[YooAssetDownload] Check DLL download failed. location='{location}', error={ex.Message}"); Debug.LogError($"[YooAssetDownload] Check DLL download failed. location='{location}', error={ex.Message}"); } var handle = dllPackage.LoadAssetAsync<TextAsset>(location); @@ -671,15 +900,16 @@ if (needRemote) { float cost = Time.realtimeSinceStartup - downloadStartTime; Debug.Log($"[YooAssetDownload] Finish on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}, cost={cost:F2}s"); Debug.LogError($"[YooAssetDownload] Finish on-demand source=Launch.ReadDllBytes, package={dllPackage.PackageName}, location='{location}', files={downloadCount}, size={FormatBytes(downloadBytes)}, cost={cost:F2}s"); } s_assetDatas[dllName] = ((TextAsset)handle.AssetObject).bytes; Debug.Log($"[Launch] Loaded DLL: {dllName} size:{s_assetDatas[dllName].Length}"); Debug.LogError($"[Launch] Loaded DLL: {dllName} size:{s_assetDatas[dllName].Length}"); } DllLoadProgress = (float)(i + 1) / total; } LogTiming("ReadDllBytes", startTime); callback?.Invoke(); } @@ -726,14 +956,14 @@ } } Debug.Log($"[LocalFileRemoteServices] 索引了 {_index.Count} 个文件,根目录: {localPath}"); Debug.LogError($"[LocalFileRemoteServices] 索引了 {_index.Count} 个文件,根目录: {localPath}"); } public string GetRemoteMainURL(string fileName) { if (_index.TryGetValue(fileName, out var uri)) return uri; Debug.LogWarning($"[LocalFileRemoteServices] 文件未找到: {fileName}"); Debug.LogError($"[LocalFileRemoteServices] 文件未找到: {fileName}"); return $"file:///NOT_FOUND/{fileName}"; } Assets/Launch/Manager/LocalResManager.cs
@@ -20,6 +20,11 @@ public class LocalResManager : Singleton<LocalResManager> { private static void LogTiming(string phase, float startTime) { Debug.LogError($"[LocalResManager][Timing] {phase} cost={(Time.realtimeSinceStartup - startTime):F3}s"); } //不下载时本地安卓测试路径 public string assetBundlesPath = Application.dataPath + "/../AssetBundles/Android/"; public string StreamingAssetPath @@ -84,7 +89,7 @@ set { m_step = value; Debug.Log("LoadDllStep:" + m_step); Debug.LogError("LoadDllStep:" + m_step); } } @@ -184,6 +189,7 @@ // BuiltIn 路径下的资源(Sprites/Prefabs)应传入 DefaultPackage(= Builtin 包) private async UniTask<T> LoadAssetWithRetryAsync<T>(string location, ResourcePackage package = null) where T : UnityEngine.Object { float startTime = Time.realtimeSinceStartup; package ??= YooAssetInitializer.Instance.DefaultPackage; Exception lastEx = null; for (int attempt = 0; attempt <= YOO_MAX_RETRY; attempt++) @@ -216,12 +222,13 @@ if (attempt < YOO_MAX_RETRY) { int delayMs = YOO_BASE_DELAY_MS * (1 << attempt); Debug.LogWarning($"[LocalResManager] Load '{location}' failed (attempt {attempt + 1}/{YOO_MAX_RETRY + 1}), retry in {delayMs}ms: {ex.Message}"); Debug.LogError($"[LocalResManager] Load '{location}' failed (attempt {attempt + 1}/{YOO_MAX_RETRY + 1}), retry in {delayMs}ms: {ex.Message}"); await UniTask.Delay(delayMs); } } } Debug.LogError($"[LocalResManager] Load '{location}' failed after {YOO_MAX_RETRY + 1} attempts: {lastEx?.Message}"); LogTiming($"LoadAssetWithRetryAsync<{typeof(T).Name}>('{location}')", startTime); return null; } @@ -251,12 +258,12 @@ if (context.Enabled) { Debug.Log($"[YooAssetDownload] Start on-demand source={source}, package={context.PackageName}, location='{location}', files={context.Count}, size={FormatBytes(context.Bytes)}"); Debug.LogError($"[YooAssetDownload] Start on-demand source={source}, package={context.PackageName}, location='{location}', files={context.Count}, size={FormatBytes(context.Bytes)}"); } } catch (Exception ex) { Debug.LogWarning($"[YooAssetDownload] Check on-demand download failed. source={source}, package={context.PackageName}, location='{location}', error={ex.Message}"); Debug.LogError($"[YooAssetDownload] Check on-demand download failed. source={source}, package={context.PackageName}, location='{location}', error={ex.Message}"); } return context; @@ -270,7 +277,7 @@ float cost = Time.realtimeSinceStartup - context.StartTime; if (succeed) { Debug.Log($"[YooAssetDownload] Finish on-demand source={context.Source}, package={context.PackageName}, location='{context.Location}', files={context.Count}, size={FormatBytes(context.Bytes)}, cost={cost:F2}s"); Debug.LogError($"[YooAssetDownload] Finish on-demand source={context.Source}, package={context.PackageName}, location='{context.Location}', files={context.Count}, size={FormatBytes(context.Bytes)}, cost={cost:F2}s"); } else { @@ -319,12 +326,13 @@ locationValid = $"check-error:{ex.Message}"; } Debug.Log($"[LocalResManager][Diag] BeforeLoad attempt={attempt + 1}, location='{location}', package='{package.PackageName}', initStatus={package.InitializeStatus}, packageValid={package.PackageValid}, manifestVersion={manifestVersion}, assets={assetTotalCount}, bundles={bundleTotalCount}, locationValid={locationValid}, state={YooAssetInitializer.Instance.State}, lastError={YooAssetInitializer.Instance.LastError}"); Debug.LogError($"[LocalResManager][Diag] BeforeLoad attempt={attempt + 1}, location='{location}', package='{package.PackageName}', initStatus={package.InitializeStatus}, packageValid={package.PackageValid}, manifestVersion={manifestVersion}, assets={assetTotalCount}, bundles={bundleTotalCount}, locationValid={locationValid}, state={YooAssetInitializer.Instance.State}, lastError={YooAssetInitializer.Instance.LastError}"); } #endif public void Init() { float startTime = Time.realtimeSinceStartup; if (LocalSave.GetString("#@#BrancH") != string.Empty) { int tmpbranch; @@ -347,12 +355,13 @@ #endif Clock.Init(); Debug.Log("LocalResManager.Init"); Debug.LogError("LocalResManager.Init"); LogTiming("Init", startTime); } public void Release() { Debug.Log("提前ResManager.Release资源"); Debug.LogError("提前ResManager.Release资源"); } #if UNITY_WEBGL @@ -375,7 +384,7 @@ urlIndex++; versionUrl = url; Debug.Log("http地址:versionUrl " + url); Debug.LogError("http地址:versionUrl " + url); HttpRequest.Instance.RequestHttpGet(url, HttpRequest.defaultHttpContentType, 1, OnVersionCheckResult); } catch (Exception ex) @@ -386,6 +395,7 @@ #else public void RequestVersionCheck() { float startTime = Time.realtimeSinceStartup; var versionConfig = VersionConfigEx.Get(); var tables = new Dictionary<string, string>(); tables["channel"] = versionConfig.appId; @@ -401,8 +411,9 @@ urlIndex++; versionUrl = url; Debug.Log("http地址:versionUrl " + url); Debug.LogError("http地址:versionUrl " + url); HttpRequest.Instance.RequestHttpGet(url, HttpRequest.defaultHttpContentType, 1, OnVersionCheckResult); LogTiming("RequestVersionCheck", startTime); } #endif @@ -412,6 +423,7 @@ /// </summary> private void OnVersionCheckResult(bool _ok, string _result) { float startTime = Time.realtimeSinceStartup; if (!_ok) { RetryVersionCheck($"[LocalResManager] 版本接口请求失败,url={versionUrl},error={_result}"); @@ -432,10 +444,12 @@ // InitialFunction 是启动期功能开关和语言配置;CDN 失败时会回落到包内表,避免启动卡死。 LoadInitialFunctionAndStartAsync(BuildInitialFunctionUrl()).Forget(); LogTiming("OnVersionCheckResult", startTime); } private bool TryUpdateVersionInfo(string rawResult) { float startTime = Time.realtimeSinceStartup; versionUrlResult = NormalizeVersionJson(rawResult); if (string.IsNullOrEmpty(versionUrlResult)) { @@ -446,6 +460,7 @@ try { versionInfo = JsonMapper.ToObject<VersionInfo>(versionUrlResult); LogTiming("TryUpdateVersionInfo", startTime); return true; } catch (Exception ex) @@ -459,10 +474,11 @@ private void TryApplyCdnUrlFromVersionInfo() { float startTime = Time.realtimeSinceStartup; var versionConfig = VersionConfigEx.config; if (versionInfo == null || versionConfig == null) { Debug.LogWarning("[LocalResManager] VersionInfo 或 VersionConfigEx 为空,将使用本地/离线资源模式。"); Debug.LogError("[LocalResManager] VersionInfo 或 VersionConfigEx 为空,将使用本地/离线资源模式。"); return; } @@ -471,15 +487,16 @@ string resourceUrl = versionInfo.GetResourcesURL(versionConfig.branch); if (string.IsNullOrEmpty(resourceUrl)) { Debug.LogWarning($"[LocalResManager] resource_url 为空,将使用本地/离线资源模式。branch={versionConfig.branch}"); Debug.LogError($"[LocalResManager] resource_url 为空,将使用本地/离线资源模式。branch={versionConfig.branch}"); return; } versionConfig.cdnUrl = resourceUrl.TrimEnd('/'); LogTiming("TryApplyCdnUrlFromVersionInfo", startTime); } catch (Exception urlEx) { Debug.LogWarning($"[LocalResManager] 获取 resource_url 失败,将使用本地/离线资源模式: {urlEx.Message}"); Debug.LogError($"[LocalResManager] 获取 resource_url 失败,将使用本地/离线资源模式: {urlEx.Message}"); } } @@ -496,6 +513,7 @@ private async UniTaskVoid LoadInitialFunctionAndStartAsync(string initFuncUrl) { float startTime = Time.realtimeSinceStartup; //默认要保证保底测试正确,后续可以在编辑器下增加个开关,来决定是否从CDN读取配置测试 //打包时会默认拷贝InitialFunction.txt到 Resources/Config/InitialFunction.txt #if UNITY_EDITOR @@ -508,6 +526,7 @@ } #endif await InitializeYooAssetAndEnterReadBytesAsync(); LogTiming("LoadInitialFunctionAndStartAsync", startTime); } private const int INIT_FUNC_MAX_RETRY = 5; @@ -515,9 +534,10 @@ private async UniTask<bool> TryLoadInitialFunctionFromCdnAsync(string initFuncUrl) { float startTime = Time.realtimeSinceStartup; if (string.IsNullOrEmpty(initFuncUrl)) { Debug.LogWarning("[LocalResManager] cdnUrl 为空,跳过 CDN InitialFunction.txt,尝试读取包内配置。"); Debug.LogError("[LocalResManager] cdnUrl 为空,跳过 CDN InitialFunction.txt,尝试读取包内配置。"); return false; } @@ -525,20 +545,20 @@ { if (i > 0) { Debug.LogWarning($"[LocalResManager] 重试加载 InitialFunction.txt 第 {i}/{INIT_FUNC_MAX_RETRY} 次..."); Debug.LogError($"[LocalResManager] 重试加载 InitialFunction.txt 第 {i}/{INIT_FUNC_MAX_RETRY} 次..."); await UniTask.Delay(TimeSpan.FromSeconds(INIT_FUNC_RETRY_DELAY_SEC)); } var response = await HttpRequest.Instance.UnityWebRequestGetAsync(initFuncUrl, HttpRequest.defaultHttpContentType, 10); if (!response.ok) { Debug.LogWarning($"[LocalResManager] 从 CDN 加载 InitialFunction.txt 失败(第 {i + 1} 次),url={initFuncUrl},error={response.message}"); Debug.LogError($"[LocalResManager] 从 CDN 加载 InitialFunction.txt 失败(第 {i + 1} 次),url={initFuncUrl},error={response.message}"); continue; } if (string.IsNullOrEmpty(response.message)) { Debug.LogWarning($"[LocalResManager] CDN InitialFunction.txt 内容为空(第 {i + 1} 次),url={initFuncUrl}"); Debug.LogError($"[LocalResManager] CDN InitialFunction.txt 内容为空(第 {i + 1} 次),url={initFuncUrl}"); continue; } @@ -547,7 +567,8 @@ InitialFunctionConfigEx.Init(response.message); // 需要根据languagefix 重新把cdn地址转向正确的多语言版本的cdn地址 InitDefaultLanguage(); Debug.Log($"[LocalResManager] 成功加载 CDN InitialFunction.txt(第 {i + 1} 次尝试)"); Debug.LogError($"[LocalResManager] 成功加载 CDN InitialFunction.txt(第 {i + 1} 次尝试)"); LogTiming("TryLoadInitialFunctionFromCdnAsync", startTime); return true; } catch (Exception ex) @@ -563,19 +584,21 @@ private async UniTask<bool> TryLoadInitialFunctionFromResourcesAsync() { float startTime = Time.realtimeSinceStartup; try { TextAsset textAsset = await Resources.LoadAsync<TextAsset>("Config/InitialFunction") as TextAsset; if (textAsset == null || string.IsNullOrEmpty(textAsset.text)) { Debug.LogWarning("[LocalResManager] 包内 InitialFunction.txt 不存在或内容为空,将继续启动。"); Debug.LogError("[LocalResManager] 包内 InitialFunction.txt 不存在或内容为空,将继续启动。"); return false; } InitialFunctionConfigEx.Init(textAsset.text); // 需要根据languagefix 重新把cdn地址转向正确的多语言版本的cdn地址 InitDefaultLanguage(); Debug.Log("[LocalResManager] 已使用包内 InitialFunction.txt 初始化启动配置。"); Debug.LogError("[LocalResManager] 已使用包内 InitialFunction.txt 初始化启动配置。"); LogTiming("TryLoadInitialFunctionFromResourcesAsync", startTime); return true; } catch (Exception ex) @@ -587,6 +610,7 @@ private async UniTask InitializeYooAssetAndEnterReadBytesAsync() { float startTime = Time.realtimeSinceStartup; if (Launch.Instance == null) { Debug.LogError("[LocalResManager] Launch.Instance 为空,无法继续启动流程。"); @@ -603,11 +627,12 @@ // YooAsset 的 Manifest 就绪后,才可以显示加载界面并进入 DLL 读取阶段。 Launch.Instance.ShowLaunchUI(); step = LoadDllStep.ReadBytes; LogTiming("InitializeYooAssetAndEnterReadBytesAsync", startTime); } private void RetryVersionCheck(string message) { Debug.LogWarning(message + " 将在1秒后重试..."); Debug.LogError(message + " 将在1秒后重试..."); Clock.AlarmAt(DateTime.Now + TimeSpan.FromSeconds(1), RequestVersionCheck); } @@ -623,7 +648,7 @@ if (normalized == "{}" || normalized.Equals("null", StringComparison.OrdinalIgnoreCase)) { Debug.LogWarning("[LocalResManager] 服务端返回空数据({}或null),该渠道可能未在服务端配置版本信息"); Debug.LogError("[LocalResManager] 服务端返回空数据({}或null),该渠道可能未在服务端配置版本信息"); return string.Empty; } @@ -798,17 +823,17 @@ if (languageStartDict == null || VersionConfigEx.config == null || !languageStartDict.ContainsKey(VersionConfigEx.config.appId)) { //检查有没多语言 Debug.Log("当前渠道未开启多语言:" + (VersionConfigEx.config != null ? VersionConfigEx.config.appId : "null")); Debug.LogError("当前渠道未开启多语言:" + (VersionConfigEx.config != null ? VersionConfigEx.config.appId : "null")); return; } Debug.LogFormat("系统语言:{0} {1}", Application.systemLanguage, config.Numerical1); Debug.LogError(string.Format("系统语言:{0} {1}", Application.systemLanguage, config.Numerical1)); var id = LocalSave.GetString("LANGUAGE_ID1"); if (!string.IsNullOrEmpty(id)) { //玩家已经选择过语言,不做处理 Debug.Log("当前选择语言:" + id); Debug.LogError("当前选择语言:" + id); return; } @@ -842,7 +867,7 @@ } Id = id; Debug.LogFormat("系统语言:{0} 设置为{1}", Application.systemLanguage, Id); Debug.LogError(string.Format("系统语言:{0} 设置为{1}", Application.systemLanguage, Id)); } #region 事件汇报 Assets/Launch/Manager/YooAssetInitializer.cs
@@ -445,6 +445,23 @@ } loadHandle.Release(); #if !UNITY_EDITOR // 后台清理旧版本缓存,降低下次启动时 CacheFileSystem 的扫描与校验成本。 var clearBundleOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedBundleFiles); await clearBundleOp.ToUniTask(); if (clearBundleOp.Status != EOperationStatus.Succeed) { Debug.LogWarning($"[YooAssetInitializer] UpdateAndWarmup clear unused bundles failed: {clearBundleOp.Error}"); } var clearManifestOp = package.ClearCacheFilesAsync(EFileClearMode.ClearUnusedManifestFiles); await clearManifestOp.ToUniTask(); if (clearManifestOp.Status != EOperationStatus.Succeed) { Debug.LogWarning($"[YooAssetInitializer] UpdateAndWarmup clear unused manifests failed: {clearManifestOp.Error}"); } #endif LocalSave.SetString(versionKey, latest); Debug.Log($"[YooAssetInitializer] Package '{package.PackageName}' updated to {latest}, will be used on next launch."); } Assets/Launch/UI/LaunchWins/LaunchExWin.cs
@@ -1,5 +1,6 @@ using System.Collections; using System.Collections.Generic; using System; using UnityEngine; using UnityEngine.UI; using Cysharp.Threading.Tasks; @@ -43,6 +44,11 @@ } } private static void LogTiming(string phase, float startTime) { Debug.LogError($"[LaunchExWin][Timing] {phase} cost={(Time.realtimeSinceStartup - startTime):F3}s"); } void Awake() { launchExWin = this; @@ -57,77 +63,43 @@ /// </summary> public async UniTask InitSpritesAsync() { Debug.LogError($"<color=yellow>[YLTEST][LaunchExWin] 开始初始化精灵</color>"); var languageShowDict = JsonMapper.ToObject<Dictionary<string, string>>(InitialFunctionConfigEx.Get("Language").Numerical1); var id = string.IsNullOrEmpty(LocalResManager.Id) ? "zh" : LocalResManager.Id; // 默认中文 sliderText = languageShowDict[id]; var sprites = await UniTask.WhenAll( LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH1"), LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH2"), LocalResManager.Instance.LoadSpriteAsync("LoadingBottom"), LocalResManager.Instance.LoadSpriteAsync("LoadingSlider") ); if (this == null) return; // destroyed check after await Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] 精灵加载完成 - LoadingBottom={sprites.Item3 != null}, LoadingSlider={sprites.Item4 != null}</color>"); imagebg1.sprite = sprites.Item1; imagebg2.sprite = sprites.Item1; imageCircle.sprite = sprites.Item2; imagebg3.sprite = sprites.Item3; imageloding.sprite = sprites.Item4; // 诊断日志:检查进度条组件状态 DiagnoseProgressBarComponents(); if (sprites.Item3 != null && sprites.Item4 != null) float startTime = Time.realtimeSinceStartup; try { SetProgressBarImagesVisible(true); Debug.LogError($"<color=green>[YLTEST][LaunchExWin] 进度条精灵设置成功并显示</color>"); } else { Debug.LogError($"<color=red>[YLTEST][LaunchExWin] 进度条精灵加载失败 LoadingBottom={sprites.Item3 != null}, LoadingSlider={sprites.Item4 != null}</color>"); } } var languageShowDict = JsonMapper.ToObject<Dictionary<string, string>>(InitialFunctionConfigEx.Get("Language").Numerical1); var id = string.IsNullOrEmpty(LocalResManager.Id) ? "zh" : LocalResManager.Id; // 默认中文 sliderText = languageShowDict[id]; var sprites = await UniTask.WhenAll( LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH1"), LocalResManager.Instance.LoadSpriteAsync("TY_TB_JH2"), LocalResManager.Instance.LoadSpriteAsync("LoadingBottom"), LocalResManager.Instance.LoadSpriteAsync("LoadingSlider") ); private void DiagnoseProgressBarComponents() { Debug.LogError($"<color=yellow>[YLTEST][LaunchExWin] 检查进度条组件状态</color>"); if (imagebg3 != null) { Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] imagebg3 (Background) 存在: {imagebg3.name}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Sprite: {(imagebg3.sprite != null ? imagebg3.sprite.name : "NULL")}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Color: {imagebg3.color}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Material: {(imagebg3.material != null ? imagebg3.material.name : "NULL")}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Shader: {(imagebg3.material != null && imagebg3.material.shader != null ? imagebg3.material.shader.name : "NULL")}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Enabled: {imagebg3.enabled}</color>"); } else { Debug.LogError($"<color=red>[YLTEST][LaunchExWin] imagebg3 is NULL</color>"); } if (this == null) return; // destroyed check after await if (imageloding != null) { Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] imageloding (Fill) 存在: {imageloding.name}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Sprite: {(imageloding.sprite != null ? imageloding.sprite.name : "NULL")}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Color: {imageloding.color}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Material: {(imageloding.material != null ? imageloding.material.name : "NULL")}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Shader: {(imageloding.material != null && imageloding.material.shader != null ? imageloding.material.shader.name : "NULL")}</color>"); Debug.LogError($"<color=cyan>[YLTEST][LaunchExWin] - Enabled: {imageloding.enabled}</color>"); if (imageloding.sprite == null) imagebg1.sprite = sprites.Item1; imagebg2.sprite = sprites.Item1; imageCircle.sprite = sprites.Item2; imagebg3.sprite = sprites.Item3; imageloding.sprite = sprites.Item4; if (sprites.Item3 != null && sprites.Item4 != null) { Debug.LogError($"<color=orange>[YLTEST][LaunchExWin] 问题: imageloding Sprite 为 NULL,这会导致显示白色</color>"); SetProgressBarImagesVisible(true); } else { Debug.LogError($"[LaunchExWin] 进度条精灵加载失败 LoadingBottom={sprites.Item3 != null}, LoadingSlider={sprites.Item4 != null}"); } } else catch (Exception ex) { Debug.LogError($"<color=red>[YLTEST][LaunchExWin] imageloding is NULL</color>"); Debug.LogError($"[LaunchExWin] InitSpritesAsync exception: {ex}"); } finally { LogTiming("InitSpritesAsync", startTime); } } @@ -266,8 +238,16 @@ public static async UniTask<GameObject> OpenWindow() { GameObject window = GameObject.Instantiate(await LocalResManager.Instance.LoadBuiltInPrefabAsync("LaunchExWin")); // window.transform.localScale = Vector3.zero; // 移除这行,让 LaunchExWin 正常显示直到 LaunchWin 打开 return window; float startTime = Time.realtimeSinceStartup; try { GameObject window = GameObject.Instantiate(await LocalResManager.Instance.LoadBuiltInPrefabAsync("LaunchExWin")); // window.transform.localScale = Vector3.zero; // 移除这行,让 LaunchExWin 正常显示直到 LaunchWin 打开 return window; } finally { LogTiming("OpenWindow", startTime); } } }