using System; using System.IO; using System.Reflection; using HybridCLR; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.Networking; using UnityEngine.ResourceManagement.AsyncOperations; using UnityEngine.SceneManagement; /// /// AOT 启动器(必须位于 AOT 程序集,且不能编译期引用任何热更类型)。 /// /// 启动流程: /// BootScene(纯 AOT 空场景) /// → Launcher 在 AfterAssembliesLoaded 阶段加载热更 dll(Game.Hotfix / Game.LegacyScripts) /// → 等 BootScene 加载完(AfterSceneLoad)后,通过 Addressables 加载真正的业务场景 UIScene /// /// 为什么不能直接把 UIScene 放进 Build Settings 当启动场景: /// HybridCLR 官方明确限制——热更脚本不能挂在"随主包的资源"上(含 Build Settings 里的场景)。 /// 因为构建时 IFilterBuildAssemblies 会把热更 dll 从序列化列表里剔除,导致场景里的热更脚本 /// 字段写不进包体,运行时反序列化就报 "The referenced script on this Behaviour is missing!"。 /// 解决办法:热更脚本所在场景必须走 AssetBundle/Addressables 加载。 /// /// 为什么加载 UIScene 要放在 AfterSceneLoad 而不是更早: /// Addressables.LoadSceneAsync(..., Single) 会替换当前场景,如果在 Unity 自身加载第一个场景 /// (BeforeSceneLoad)期间发起,会和引擎的首次场景加载相互冲突。等第一个场景加载完再替换才安全。 /// public static class Launcher { /// 热更 dll 目录(相对 StreamingAssets) private const string HotfixDir = "HybridCLR/Hotfix"; /// AOT 补充元数据 dll 目录(相对 StreamingAssets) private const string AOTMetaDir = "HybridCLR/AOT"; /// 需要热更加载的程序集(与 ProjectSettings/HybridCLRSettings.asset 的 hotUpdateAssemblies 一致) private static readonly string[] HotfixAssemblies = { "Game.Hotfix", "Game.LegacyScripts" }; /// 通过 Addressables 加载的主场景 key(需在 Addressables 里把 UIScene 标记为可寻址) private const string MainSceneKey = "UIScene"; /// 是否已加载完成(多个阶段只加载一次) private static bool _loaded; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] private static void BootAfterAssembliesLoaded() { EnsureLoaded(RuntimeInitializeLoadType.AfterAssembliesLoaded); } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)] private static void BootBeforeSplashScreen() { EnsureLoaded(RuntimeInitializeLoadType.BeforeSplashScreen); } /// 第一个场景(BootScene)加载完成后,再切换到业务主场景 UIScene [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] private static void BootAfterSceneLoad() { // 兜底:若前面的阶段都没能加载(例如被平台时序打断),这里再试一次 if (!_loaded) EnsureLoaded(RuntimeInitializeLoadType.AfterSceneLoad); if (!_loaded) return; LoadMainSceneViaAddressables(); } private static void EnsureLoaded(RuntimeInitializeLoadType phase) { if (_loaded) { Debug.Log($"[Launcher] 已在更早阶段加载完成,{phase} 阶段跳过"); return; } int frame = Time.frameCount; Debug.Log($"[Launcher] 开始加载:阶段={phase},frame={frame}"); bool aotOk = LoadAOTMetadata(); bool hotOk = LoadHotfixAssemblies(); _loaded = aotOk && hotOk; Debug.Log($"[Launcher] 加载完成:阶段={phase},AOT元数据={(aotOk ? "全部OK" : "有缺失")}," + $"热更dll={(hotOk ? "全部OK" : "有失败")},Game.Hotfix 类型总数={HotfixTypeCount()}," + $"GameManager 可解析={HotfixTypeResolvable("GameManager")}"); if (!_loaded) { Debug.LogError("[Launcher] 热更程序集未全部加载成功,游戏将无法正常运行。请检查 StreamingAssets/HybridCLR 下的 dll 是否已拷贝。"); } } /// /// 初始化 Addressables 并加载主场景 UIScene。 /// UIScene 必须事先在 Addressables Groups 窗口里标记为可寻址(key 为 "UIScene")。 /// private static void LoadMainSceneViaAddressables() { Debug.Log("[Launcher] 开始初始化 Addressables ..."); var initHandle = Addressables.InitializeAsync(); initHandle.Completed += handle => { if (handle.Status != AsyncOperationStatus.Succeeded) { Debug.LogError($"[Launcher] Addressables 初始化失败: {handle.OperationException}"); return; } Debug.Log("[Launcher] Addressables 初始化完成,开始加载 UIScene ..."); var sceneHandle = Addressables.LoadSceneAsync(MainSceneKey, LoadSceneMode.Single); sceneHandle.Completed += sceneOp => { if (sceneOp.Status == AsyncOperationStatus.Succeeded) { Debug.Log($"[Launcher] UIScene 加载成功: {sceneOp.Result.Scene.name}"); } else { Debug.LogError($"[Launcher] UIScene 加载失败: {sceneOp.OperationException}。" + $"请确认已在 Addressables Groups 里把 UIScene 标记为可寻址,且 address key 为 '{MainSceneKey}'。"); } }; }; } /// /// 加载 AOT 补充元数据。清单来自 HybridCLR 生成的 AOTGenericReferences.PatchedAOTAssemblyList /// (它由 HybridCLR/Generate/AOTGenericReferences.cs 产出,与本脚本同属 AOT 程序集)。 /// private static bool LoadAOTMetadata() { bool allOk = true; foreach (var dllName in AOTGenericReferences.PatchedAOTAssemblyList) { // dllName 形如 "DOTween.dll",磁盘文件为 "DOTween.dll.bytes" byte[] bytes = ReadStreamingAsset(Path.Combine(AOTMetaDir, dllName + ".bytes")); if (bytes == null) { Debug.LogError($"[Launcher] 缺少 AOT 补充元数据: {dllName}"); allOk = false; continue; } LoadImageErrorCode err = RuntimeApi.LoadMetadataForAOTAssembly(bytes, HomologousImageMode.SuperSet); if (err != LoadImageErrorCode.OK) { Debug.LogError($"[Launcher] AOT 元数据加载失败: {dllName} → {err}"); allOk = false; } } return allOk; } /// 加载热更程序集(Assembly.Load 之后,场景里的热更 MonoBehaviour 才能被实例化) private static bool LoadHotfixAssemblies() { bool allOk = true; foreach (var asmName in HotfixAssemblies) { byte[] bytes = ReadStreamingAsset(Path.Combine(HotfixDir, asmName + ".dll.bytes")); if (bytes == null) { Debug.LogError($"[Launcher] 缺少热更 dll: {asmName}"); allOk = false; continue; } try { Assembly asm = Assembly.Load(bytes); Debug.Log($"[Launcher] 热更程序集已加载: {asm.FullName}"); } catch (System.Exception e) { Debug.LogError($"[Launcher] 热更 dll 加载异常: {asmName} → {e}"); allOk = false; } } return allOk; } /// 诊断用:Game.Hotfix 里已加载的类型总数(-1 表示该程序集没找到) private static int HotfixTypeCount() { var asm = FindAssembly("Game.Hotfix"); if (asm == null) return -1; try { return asm.GetTypes().Length; } catch { return -2; } } /// 诊断用:能否在 Game.Hotfix 里解析到指定类型 private static bool HotfixTypeResolvable(string typeName) { var asm = FindAssembly("Game.Hotfix"); return asm != null && asm.GetType(typeName) != null; } private static Assembly FindAssembly(string name) { foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) if (asm.GetName().Name == name) return asm; return null; } /// /// 读取 StreamingAssets 下的文件(兼容 Android 的 apk 内路径)。 /// 找不到或读取失败返回 null。 /// private static byte[] ReadStreamingAsset(string relativePath) { string fullPath = Path.Combine(Application.streamingAssetsPath, relativePath); // Android / WebGL:StreamingAssets 在 apk/包体内,必须走 UnityWebRequest #if UNITY_ANDROID && !UNITY_EDITOR using (var req = UnityWebRequest.Get(fullPath)) { req.SendWebRequest(); while (!req.isDone) { } // 启动阶段短暂阻塞,可接受 if (req.result != UnityWebRequest.Result.Success) return null; return req.downloadHandler.data; } #else if (!File.Exists(fullPath)) return null; return File.ReadAllBytes(fullPath); #endif } }