Files
5.24BuYu/Assets/Boot/Launcher.cs
2026-10-15 03:57:20 +08:00

227 lines
9.4 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 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)期间发起,会和引擎的首次场景加载相互冲突。等第一个场景加载完再替换才安全。
/// </summary>
public static class Launcher
{
/// <summary>热更 dll 目录(相对 StreamingAssets)</summary>
private const string HotfixDir = "HybridCLR/Hotfix";
/// <summary>AOT 补充元数据 dll 目录(相对 StreamingAssets)</summary>
private const string AOTMetaDir = "HybridCLR/AOT";
/// <summary>需要热更加载的程序集(与 ProjectSettings/HybridCLRSettings.asset 的 hotUpdateAssemblies 一致)</summary>
private static readonly string[] HotfixAssemblies = { "Game.Hotfix", "Game.LegacyScripts" };
/// <summary>通过 Addressables 加载的主场景 key(需在 Addressables 里把 UIScene 标记为可寻址)</summary>
private const string MainSceneKey = "UIScene";
/// <summary>是否已加载完成(多个阶段只加载一次)</summary>
private static bool _loaded;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
private static void BootAfterAssembliesLoaded() { EnsureLoaded(RuntimeInitializeLoadType.AfterAssembliesLoaded); }
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSplashScreen)]
private static void BootBeforeSplashScreen() { EnsureLoaded(RuntimeInitializeLoadType.BeforeSplashScreen); }
/// <summary>第一个场景(BootScene)加载完成后,再切换到业务主场景 UIScene</summary>
[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 是否已拷贝。");
}
}
/// <summary>
/// 初始化 Addressables 并加载主场景 UIScene。
/// UIScene 必须事先在 Addressables Groups 窗口里标记为可寻址(key 为 "UIScene")。
/// </summary>
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}'。");
}
};
};
}
/// <summary>
/// 加载 AOT 补充元数据。清单来自 HybridCLR 生成的 AOTGenericReferences.PatchedAOTAssemblyList
/// (它由 HybridCLR/Generate/AOTGenericReferences.cs 产出,与本脚本同属 AOT 程序集)。
/// </summary>
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;
}
/// <summary>加载热更程序集(Assembly.Load 之后,场景里的热更 MonoBehaviour 才能被实例化)</summary>
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;
}
/// <summary>诊断用:Game.Hotfix 里已加载的类型总数(-1 表示该程序集没找到)</summary>
private static int HotfixTypeCount()
{
var asm = FindAssembly("Game.Hotfix");
if (asm == null) return -1;
try { return asm.GetTypes().Length; } catch { return -2; }
}
/// <summary>诊断用:能否在 Game.Hotfix 里解析到指定类型</summary>
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;
}
/// <summary>
/// 读取 StreamingAssets 下的文件(兼容 Android 的 apk 内路径)。
/// 找不到或读取失败返回 null。
/// </summary>
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
}
}