482 lines
17 KiB
C#
482 lines
17 KiB
C#
using HybridCLR;
|
||
using Sirenix.Utilities;
|
||
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using System.Reflection;
|
||
using UnityEngine;
|
||
using UnityEngine.AddressableAssets;
|
||
using UnityEngine.ResourceManagement.AsyncOperations;
|
||
using UnityEngine.ResourceManagement.ResourceLocations;
|
||
using UnityEngine.ResourceManagement.ResourceProviders;
|
||
using UnityEngine.SceneManagement;
|
||
|
||
public class GameLauncher : MonoBehaviour
|
||
{
|
||
private class MethodExecutionInfo
|
||
{
|
||
public MethodInfo method;
|
||
public int sequnence;
|
||
|
||
public MethodExecutionInfo(MethodInfo method, int sequnence)
|
||
{
|
||
this.method = method;
|
||
this.sequnence = sequnence;
|
||
}
|
||
}
|
||
|
||
[Serializable]
|
||
private class DownloadContent
|
||
{
|
||
public List<string> catalogs = new();
|
||
}
|
||
|
||
const string START_SCENE_NAME = "Assets/Scenes/StartScene.unity";
|
||
|
||
const string META_DATA_DLL_PATH = "Assets/HotUpdateDlls/MetaDataDll/";
|
||
const string HOT_UPDATE_DLL_PATH = "Assets/HotUpdateDlls/HotUpdateDll/";
|
||
const string PHXH_DLL_NAME = "Phxh.dll";
|
||
const string FRAMEWORK_DLL_NAME = "FrameWork.dll";
|
||
const string GAMEPLAY_DLL_NAME = "GamePlay.dll";
|
||
|
||
//记录在playerPres里的需要下载的catalogs的ID
|
||
const string DOWNLOAD_CATALOGS_ID = "DownloadCatalogs";
|
||
|
||
//最大能容忍的下载卡住时长,一旦超过则重新开始下载
|
||
private const float DOWNLOAD_STUCK_TIME_MAX = 10;
|
||
|
||
private Coroutine _launchCoroutine;
|
||
private byte[] _dllBytes;
|
||
|
||
private Dictionary<string, Assembly> _allHotUpdateAssemblies = new();
|
||
|
||
//version update
|
||
private AsyncOperationHandle _downloadOP;
|
||
private UI_VersionUpdate _versionUpdateUI;
|
||
private Type _versionUpdaterType;
|
||
private float _downloadProgress;
|
||
private float _downloadStuckTime;
|
||
|
||
private List<object> _KeysNeedToDownload = new();
|
||
|
||
//此对象里保存了需要下载的catalog,每次获取新的catalog会将此对象保存到手机上,如果在下载的过程中关闭了游戏,下次打开还能拿到catalog继续下载
|
||
private DownloadContent _downloadContent = new();
|
||
|
||
private bool HasContentToDownload => _downloadContent != null && _downloadContent.catalogs != null &&
|
||
_downloadContent.catalogs.Count > 0;
|
||
|
||
private readonly List<string> _phxhDependencyDlls = new List<string>()
|
||
{
|
||
};
|
||
|
||
private readonly List<string> _frameWorkDependencyDlls = new List<string>()
|
||
{
|
||
//不要随意改顺序!
|
||
};
|
||
|
||
private readonly List<string> _gamePlayDependencyDlls = new List<string>()
|
||
{
|
||
//不要随意改顺序!
|
||
"BehaviorDesigner.Runtime.dll",
|
||
//"BestHTTP.dll",
|
||
"DOTweenCSharp.dll",
|
||
};
|
||
|
||
//所有使用了RuntimeInitializeOnLoadMethod attribute的程序集
|
||
private readonly List<string> _hasRuntimeInitializeOnLoadMethodAssemblies = new List<string>()
|
||
{
|
||
"BehaviorDesigner.Runtime",
|
||
//"BestHTTP",
|
||
"GamePlay",
|
||
};
|
||
|
||
public bool enableHotUpdate = true;
|
||
|
||
private void Start()
|
||
{
|
||
_launchCoroutine = StartCoroutine(Launch());
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
StopCoroutine(_launchCoroutine);
|
||
_launchCoroutine = null;
|
||
}
|
||
|
||
// private void _SafeProtect()
|
||
// {
|
||
// var typeList = new List<Type>();
|
||
// typeList.Add(typeof(BestHTTP.HTTPRequest));
|
||
// typeList.Add(typeof(BestHTTP.HTTPResponse));
|
||
// for (int i = 0; i < typeList.Count; i++)
|
||
// {
|
||
// var type = typeList[i];
|
||
// DebugUtil.LogError("type = {0}", type);
|
||
// }
|
||
// }
|
||
|
||
private IEnumerator Launch()
|
||
{
|
||
// _SafeProtect();
|
||
#if UNITY_EDITOR
|
||
enableHotUpdate = false;
|
||
#endif
|
||
Debug.Log($"Launch Game! enableHotUpdate:{enableHotUpdate}");
|
||
yield return VersionCheck();
|
||
if (HasContentToDownload)
|
||
yield return VersionUpdate();
|
||
if (enableHotUpdate)
|
||
yield return LoadAssemblies();
|
||
yield return EnterGame();
|
||
}
|
||
|
||
private IEnumerator VersionCheck()
|
||
{
|
||
//todo 如果包体版本不一致则提示用户去app store重新下载
|
||
var checkUpdateOP = Addressables.CheckForCatalogUpdates(false);
|
||
yield return checkUpdateOP;
|
||
if (checkUpdateOP.Status == AsyncOperationStatus.Succeeded)
|
||
{
|
||
_downloadContent.catalogs = checkUpdateOP.Result;
|
||
if (HasContentToDownload)
|
||
{
|
||
//如果服务器上的catalog和本地不一致,则覆盖上一次需要下载的被内容
|
||
var jsonStr = JsonUtility.ToJson(_downloadContent);
|
||
Debug.Log($"set download json:{jsonStr}");
|
||
PlayerPrefs.SetString(DOWNLOAD_CATALOGS_ID, jsonStr);
|
||
PlayerPrefs.Save();
|
||
}
|
||
else
|
||
{
|
||
Debug.Log($"try to get last download json");
|
||
if (PlayerPrefs.HasKey(DOWNLOAD_CATALOGS_ID))
|
||
{
|
||
var jsonStr = PlayerPrefs.GetString(DOWNLOAD_CATALOGS_ID);
|
||
Debug.Log($"try to get last download json:{jsonStr}");
|
||
JsonUtility.FromJsonOverwrite(jsonStr, _downloadContent);
|
||
Debug.Log($"catalog count:{_downloadContent.catalogs.Count}");
|
||
}
|
||
}
|
||
|
||
if (HasContentToDownload)
|
||
{
|
||
var updateCatalogOP = Addressables.UpdateCatalogs(_downloadContent.catalogs, false);
|
||
yield return updateCatalogOP;
|
||
if (updateCatalogOP.Status == AsyncOperationStatus.Succeeded)
|
||
{
|
||
_KeysNeedToDownload.Clear();
|
||
foreach (var resourceLocator in updateCatalogOP.Result)
|
||
{
|
||
_KeysNeedToDownload.AddRange(resourceLocator.Keys);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"Update catalog failed!exception:{updateCatalogOP.OperationException.Message}");
|
||
}
|
||
|
||
Addressables.Release(updateCatalogOP);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError($"CheckUpdate failed!exception:{checkUpdateOP.OperationException.Message}");
|
||
}
|
||
|
||
Addressables.Release(checkUpdateOP);
|
||
yield return ReloadAddressableCatalog();
|
||
Debug.Log($"版本检查结束,是否有需要下载的内容:{HasContentToDownload}");
|
||
}
|
||
|
||
private IEnumerator VersionUpdate()
|
||
{
|
||
yield return OpenVersionUpdateUI();
|
||
yield return Download();
|
||
Debug.Log($"版本更新结束!");
|
||
}
|
||
|
||
private IEnumerator LoadAssemblies()
|
||
{
|
||
yield return LoadPhxhDependencyAssemblies();
|
||
yield return LoadPhxhAssembies();
|
||
yield return LoadFrameworkDependencyAssemblies();
|
||
yield return LoadFrameWorkAssemblies();
|
||
yield return LoadMetadataForAOTAssemblies();
|
||
yield return LoadGamePlayDependencyAssemblies();
|
||
yield return LoadGamePlayAssemblies();
|
||
yield return ReloadAddressableCatalog();
|
||
ExecuteRuntimeInitializeOnLoadMethodAttribute();
|
||
Debug.Log("加载程序集结束!");
|
||
yield return null;
|
||
}
|
||
|
||
//补充元数据
|
||
private IEnumerator LoadMetadataForAOTAssemblies()
|
||
{
|
||
var aotAssemblies = AOTGenericReferences.PatchedAOTAssemblyList;
|
||
if (aotAssemblies == null)
|
||
{
|
||
Debug.LogError($"LoadMetadataForAOTAssemblies failed, PatchedAOTAssemblyList is null!");
|
||
yield break;
|
||
}
|
||
|
||
foreach (var aotDllName in aotAssemblies)
|
||
{
|
||
var path = $"{META_DATA_DLL_PATH}{aotDllName}.bytes";
|
||
yield return ReadDllBytes(path);
|
||
if (_dllBytes != null)
|
||
{
|
||
var err = HybridCLR.RuntimeApi.LoadMetadataForAOTAssembly(_dllBytes, HomologousImageMode.SuperSet);
|
||
Debug.Log($"LoadMetadataForAOTAssembly:{aotDllName}. ret:{err}");
|
||
}
|
||
}
|
||
|
||
Debug.Log("补充元数据结束!");
|
||
}
|
||
|
||
private IEnumerator LoadPhxhDependencyAssemblies()
|
||
{
|
||
foreach (var dllName in _phxhDependencyDlls)
|
||
{
|
||
yield return LoadSingleHotUpdateAssembly(dllName);
|
||
}
|
||
|
||
Debug.Log("加载Phxh依赖程序集结束!");
|
||
}
|
||
|
||
private IEnumerator LoadPhxhAssembies()
|
||
{
|
||
yield return LoadSingleHotUpdateAssembly(PHXH_DLL_NAME);
|
||
Debug.Log("加载Phxh程序集结束!");
|
||
}
|
||
|
||
//加载FrameWork依赖的第三方热更序集
|
||
private IEnumerator LoadFrameworkDependencyAssemblies()
|
||
{
|
||
foreach (var dllName in _frameWorkDependencyDlls)
|
||
{
|
||
yield return LoadSingleHotUpdateAssembly(dllName);
|
||
}
|
||
|
||
Debug.Log("加载Framework依赖程序集结束!");
|
||
}
|
||
|
||
//加载Framework程序集
|
||
private IEnumerator LoadFrameWorkAssemblies()
|
||
{
|
||
yield return LoadSingleHotUpdateAssembly(FRAMEWORK_DLL_NAME);
|
||
Debug.Log("加载Framework程序集结束!");
|
||
}
|
||
|
||
//通过加载场景打开版本更新界面
|
||
private IEnumerator OpenVersionUpdateUI()
|
||
{
|
||
_versionUpdateUI = FindObjectOfType<UI_VersionUpdate>(true);
|
||
if (_versionUpdateUI == null)
|
||
{
|
||
Debug.LogError("cant find UI_VersionUpdate");
|
||
}
|
||
|
||
_versionUpdateUI.gameObject.SetActive(true);
|
||
return null;
|
||
}
|
||
|
||
//下载资源
|
||
private IEnumerator Download()
|
||
{
|
||
var downloadSizeOp = Addressables.GetDownloadSizeAsync((IEnumerable)_KeysNeedToDownload);
|
||
yield return downloadSizeOp;
|
||
Debug.Log($"download size:{downloadSizeOp.Result / (1024f * 1024f)}MB");
|
||
|
||
if (downloadSizeOp.Result > 0)
|
||
{
|
||
_downloadStuckTime = 0;
|
||
Addressables.Release(downloadSizeOp);
|
||
//下载经常在100%的时候卡住,参考:https://forum.unity.com/threads/addressables-1-14-2-downloaddependenciesasync-does-not-complete-randomly.966671/
|
||
//倒数第二个回答
|
||
var asyncLoadResources = Addressables.LoadResourceLocationsAsync((IEnumerable)_KeysNeedToDownload,
|
||
Addressables.MergeMode.Union, null);
|
||
IList<IResourceLocation> dependencyLoadList = new List<IResourceLocation>();
|
||
yield return asyncLoadResources;
|
||
foreach (var item in asyncLoadResources.Result)
|
||
{
|
||
dependencyLoadList.Add(item);
|
||
}
|
||
Addressables.Release(asyncLoadResources);
|
||
|
||
_downloadOP =
|
||
Addressables.DownloadDependenciesAsync(dependencyLoadList);
|
||
_versionUpdateUI.SetParams(_downloadOP);
|
||
|
||
while (!_downloadOP.IsDone)
|
||
{
|
||
RefreshDownLoadStatus();
|
||
if (!CheckIfNeedReDownload())
|
||
yield return 0;
|
||
else
|
||
{
|
||
//下载会经常卡在99%,此时还剩下很少的资源要下载,可以直接放弃下载,进入游戏
|
||
Debug.LogError($"下载卡住超过时间上限,放弃下载!");
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (_downloadOP.IsDone && _downloadOP.Status != AsyncOperationStatus.Succeeded)
|
||
{
|
||
Debug.LogError(
|
||
$"Download Update Content Failed! exception:{_downloadOP.OperationException.Message} \r\n {_downloadOP.OperationException.StackTrace}");
|
||
}
|
||
|
||
Addressables.Release(_downloadOP);
|
||
}
|
||
|
||
//清除需要下载的内容
|
||
Debug.Log($"delete key:{DOWNLOAD_CATALOGS_ID}");
|
||
PlayerPrefs.DeleteKey(DOWNLOAD_CATALOGS_ID);
|
||
}
|
||
|
||
private void RefreshDownLoadStatus()
|
||
{
|
||
var currDownloadProgress = _downloadOP.GetDownloadStatus().Percent;
|
||
if (Math.Abs(currDownloadProgress - _downloadProgress) < float.Epsilon)
|
||
_downloadStuckTime += Time.deltaTime;
|
||
else
|
||
_downloadStuckTime = 0;
|
||
_downloadProgress = currDownloadProgress;
|
||
//Debug.Log($"下载进度:{_downloadProgress} 下载卡顿时间:{_downloadStuckTime}");
|
||
}
|
||
|
||
private bool CheckIfNeedReDownload() => _downloadStuckTime >= DOWNLOAD_STUCK_TIME_MAX;
|
||
|
||
//加载GamePlay依赖的第三方程序集
|
||
private IEnumerator LoadGamePlayDependencyAssemblies()
|
||
{
|
||
foreach (var dllName in _gamePlayDependencyDlls)
|
||
{
|
||
yield return LoadSingleHotUpdateAssembly(dllName);
|
||
}
|
||
|
||
Debug.Log("加载GamePlay依赖程序集结束!");
|
||
}
|
||
|
||
//加载GamePlay程序集
|
||
private IEnumerator LoadGamePlayAssemblies()
|
||
{
|
||
yield return LoadSingleHotUpdateAssembly(GAMEPLAY_DLL_NAME);
|
||
Debug.Log("加载GamePlay程序集结束!");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Addressable初始化时热更新代码所对应的ScriptableObject的类型会被识别为System.Object,需要在热更新dll加载完后重新加载一下Addressable的Catalog
|
||
/// https://hybridclr.doc.code-philosophy.com/docs/help/commonerrors
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
private IEnumerator ReloadAddressableCatalog()
|
||
{
|
||
var op = Addressables.LoadContentCatalogAsync($"{Addressables.RuntimePath}/catalog.json");
|
||
yield return op;
|
||
if (op.Status != AsyncOperationStatus.Succeeded)
|
||
{
|
||
Debug.LogError(
|
||
$"load content catalog failed, exception:{op.OperationException.Message} \r\n {op.OperationException.StackTrace}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 反射执行被RuntimeInitializeOnLoadMethod attribute标注的函数,HybirdCLR不支持该attribute
|
||
/// </summary>
|
||
private void ExecuteRuntimeInitializeOnLoadMethodAttribute()
|
||
{
|
||
var runtimeInitializedAttribute = typeof(RuntimeInitializeOnLoadMethodAttribute);
|
||
List<MethodExecutionInfo> runtimeMethods = new();
|
||
foreach (var assemblyName in _hasRuntimeInitializeOnLoadMethodAssemblies)
|
||
{
|
||
var assembly = GetAssembly(assemblyName);
|
||
if (assembly == null)
|
||
{
|
||
Debug.LogError($"找不到使用过RuntimeInitializeOnLoadMethod的assembly,name:{assemblyName}");
|
||
continue;
|
||
}
|
||
|
||
foreach (var type in assembly.GetTypes())
|
||
{
|
||
type.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).ForEach(method =>
|
||
{
|
||
if (!method.IsStatic)
|
||
return;
|
||
var attri = method.GetCustomAttribute(runtimeInitializedAttribute);
|
||
if (attri == null)
|
||
return;
|
||
var sequence = (int)(attri as RuntimeInitializeOnLoadMethodAttribute).loadType;
|
||
var methodInfo = new MethodExecutionInfo(method, sequence);
|
||
runtimeMethods.Add(methodInfo);
|
||
});
|
||
}
|
||
}
|
||
|
||
runtimeMethods.Sort((a, b) => b.sequnence.CompareTo(a.sequnence));
|
||
foreach (var methodInfo in runtimeMethods)
|
||
{
|
||
Debug.Log($"call method methodName:{methodInfo.method.Name} sequnence:{methodInfo.sequnence}");
|
||
methodInfo.method.Invoke(null, null);
|
||
}
|
||
|
||
Debug.Log("调用RuntimeInitializeOnLoadMethod结束!");
|
||
}
|
||
|
||
private IEnumerator ReadDllBytes(string path)
|
||
{
|
||
var op = Addressables.LoadAssetAsync<TextAsset>(path);
|
||
yield return op;
|
||
if (op.Status == AsyncOperationStatus.Succeeded)
|
||
{
|
||
_dllBytes = op.Result.bytes;
|
||
}
|
||
else
|
||
{
|
||
Debug.LogError(
|
||
$"cant load dll,exception:{op.OperationException.Message}\r\n{op.OperationException.StackTrace}");
|
||
_dllBytes = null;
|
||
yield break;
|
||
}
|
||
}
|
||
|
||
private IEnumerator LoadSingleHotUpdateAssembly(string dllName)
|
||
{
|
||
var path = $"{HOT_UPDATE_DLL_PATH}{dllName}.bytes";
|
||
yield return ReadDllBytes(path);
|
||
if (_dllBytes != null)
|
||
{
|
||
var assembly = Assembly.Load(_dllBytes);
|
||
_allHotUpdateAssemblies.Add(assembly.FullName, assembly);
|
||
Debug.Log($"Load Assembly success,assembly Name:{assembly.FullName}");
|
||
// foreach (var type in GetAssembly(dllName).GetTypes())
|
||
// {
|
||
// Debug.Log($"type:{type} in assembly:{GAMEPLAY_DLL_NAME}");
|
||
// }
|
||
}
|
||
|
||
yield return null;
|
||
}
|
||
|
||
private IEnumerator EnterGame()
|
||
{
|
||
var op = Addressables.LoadSceneAsync(START_SCENE_NAME, LoadSceneMode.Single);
|
||
yield return op;
|
||
if (op.Status != AsyncOperationStatus.Succeeded)
|
||
{
|
||
Debug.LogError(
|
||
$"load scene failed,exception:{op.OperationException.Message} \r\n {op.OperationException.StackTrace}");
|
||
yield break;
|
||
}
|
||
}
|
||
|
||
private Assembly GetAssembly(string assemblyName)
|
||
{
|
||
assemblyName = assemblyName.Replace(".dll", "");
|
||
IEnumerable<Assembly> allAssemblies =
|
||
enableHotUpdate ? _allHotUpdateAssemblies.Values : AppDomain.CurrentDomain.GetAssemblies();
|
||
return allAssemblies.First(assembly => assembly.FullName.Contains(assemblyName));
|
||
}
|
||
} |