Unity 3D模型展示框架篇之项目整理
Unity 3D模型展示框架篇之框架运用
Unity 3D模型展示框架篇之自由观察(Cinemachine)
Unity 3D模型展示框架篇之资源打包、加载、热更(Addressable Asset System | 简称AA)
Unity 3D模型展示框架篇之资源打包、加载、热更(二)
Unity 3D模型展示框架篇之ILRuntime快速入门
Unity 3D模型展示框架篇之ILRuntime整合与应用
本项目将整合之前Unity程序基础小框架专栏在Unity3D模型展示项目基础上进行整合,并记录了集成过程中对原脚本的调整过程。增加了Asset Bundle+ILRuntime热更新技术流程。
本文章主要介绍如何在Unity工程中使用Addressable与ILRuntime项目进行整合的过程与使用实例。
ILRuntime
项目为基于C#的平台(例如Unity)提供了一个纯C#实现,快速、方便且可靠的IL运行时,使得能够在不支持JIT的硬件环境(如iOS)能够实现代码
的热更新。
Addressables
是官方推出的可寻址资产系统提供了一种通过"地址"加载资产的简单方法。它通过简化内容包的创建和部署来处理资产管理开销。可寻址资产系统使用异步加载来支持从任何位置加载任何依赖项集合。无论您使用直接引用、传统的资产包还是ResourceFolders进行资产管理,可寻址资产都提供了一种更简单的方法来使您的游戏更加动态。什么是资产?资产是您用来创建游戏或应用程序的内容。资产的常见例子包括预制、纹理、材料、音频剪辑和动画。什么是可寻址资产?将资产设置为“可寻址”允许您使用该资产的唯一地址在任何地方调用它。
简单来说ILRuntime
+Addressables
涵盖了热更新中的代码更新与资源热更新。
ILRuntime详见官方文档
Addressables官方文档
由于Addressables
不支持DLL资源打包,因此需要将DLL文件转化为二进制文件,再通过Addressables
进行打包进行资源热更后,加载时通过DLL文件流初始化ILRuntime
的入口应用程序域AppDomain
。完成对ILRuntime
代码工程的更新。
修改ILRuntime
热更工程的DLL输出目录
修改目录的原因是因为StreamingAssets
在进行Addressables
分组时报错。因此创建Assets/ILRunTimeAndAddressable/AssetsPackage/HotFixDll
、Assets/ILRunTimeAndAddressable/AssetsPackage/HotFixDllToBytes
,HotFixDll
为生成DLL文件目录,HotFixDllToBytes
为DLL转化二进制文件的存放目录。
创建DLL转二进制文件编辑器
在Editor文件夹中创建DllToBytes.cs
DllToBytes.cs
代码如下:
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;
public class DllToBytes
{
public static string normalPath = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDll";
public static string normalPathToSave = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDllToBytes";
[MenuItem("MyMenu/ILRuntime/DllToByte")]
public static void DllToByte()
{
DllToByte(true);
}
[MenuItem("MyMenu/ILRuntime/DllToByteChoose")]
public static void DllToByteChoose()
{
DllToByte(false);
}
private static void DllToByte(bool autoChoosePath)
{
string folderPath, savePath;
if (autoChoosePath)
{
folderPath = normalPath;
}
else
{
folderPath = EditorUtility.OpenFolderPanel("dll所在的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
}
if (string.IsNullOrEmpty(folderPath))
{
return;
}
DirectoryInfo directoryInfo = new DirectoryInfo(folderPath);
FileInfo[] fileInfos = directoryInfo.GetFiles();
List<FileInfo> listDll = new List<FileInfo>();
List<FileInfo> listPdb = new List<FileInfo>();
for (int i = 0; i < fileInfos.Length; i++)
{
if (fileInfos[i].Extension == ".dll")
{
listDll.Add(fileInfos[i]);
}
else if (fileInfos[i].Extension == ".pdb")
{
listPdb.Add(fileInfos[i]);
}
}
if (listDll.Count + listPdb.Count <= 0)
{
Debug.Log("文件夹下没有dll文件");
}
else
{
Debug.Log("路径为:" + folderPath);
}
if (autoChoosePath)
{
savePath = normalPathToSave;
}
else
{
savePath = EditorUtility.OpenFolderPanel("dll要保存的文件夹", Application.dataPath + "/addressable/IlRuntime", string.Empty);
}
Debug.Log("-----开始转换dll文件------");
string path = string.Empty;
for (int i = 0; i < listDll.Count; i++)
{
//$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了
path = $"{savePath}/{Path.GetFileNameWithoutExtension(listDll[i].Name)}_dll_res.bytes";
Debug.Log(path);
BytesToFile(path, FileToBytes(listDll[i]));
}
Debug.Log("------dll文件转换结束---------");
Debug.Log("-----开始转换pdb文件------");
for (int i = 0; i < listPdb.Count; i++)
{
//$ 符号的作用:等同于string.Format(),不用写占位符了,直接拼起来就可以了
path = $"{savePath}/{Path.GetFileNameWithoutExtension(listPdb[i].Name)}_pdb_res.bytes";
BytesToFile(path, FileToBytes(listPdb[i]));
}
Debug.Log("------pdb文件转换结束---------");
AssetDatabase.Refresh();
}
///
/// file转byte
///
///
///
private static byte[] FileToBytes(FileInfo fileInfo)
{
return File.ReadAllBytes(fileInfo.FullName);
}
///
/// byte转文件
///
///
///
private static void BytesToFile(string path, byte[] bytes)
{
Debug.Log($"路径为:{path}\nlength:{bytes.Length}");
File.WriteAllBytes(path, bytes);
}
}
HotFixMgr.cs
增加LoadHotFixAssembly
通过二进制文件加载热更DLL。修改之前加载DLL的路径Application.streamingAssetsPath
改为新改的路径。整体代码如下:
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
public class HotFixMgr : MonoBehaviour
{
public static HotFixMgr instance;
public ILRuntime.Runtime.Enviorment.AppDomain appdomain;
private System.IO.MemoryStream m_fs, m_p;
public static HotFixMgr Instance
{
get
{
if (instance==null)
{
instance=new GameObject("HotFixMgr").AddComponent<HotFixMgr>();
instance.LoadHotFixAssembly();
}
return instance;
}
}
// Start is called before the first frame update
void Start()
{
}
public void LoadHotFixAssembly() {
dllPath = Application.dataPath + "/ILRunTimeAndAddressable/AssetsPackage/HotFixDll";
appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
#if UNITY_ANDROID
WWW www = new WWW(Application.streamingAssetsPath + "/Hotfix.dll");
#else
WWW www = new WWW("file:///" + dllPath + "/HotFix_Project.dll");
#endif
while (!www.isDone)
//yield return null;
System.Threading.Thread.Sleep(100);
if (!string.IsNullOrEmpty(www.error))
Debug.LogError(www.error);
byte[] dll = www.bytes;
www.Dispose();
#if UNITY_ANDROID
www = new WWW(Application.streamingAssetsPath + "/Hotfix.pdb");
#else
www = new WWW("file:///" + dllPath + "/HotFix_Project.pdb");
#endif
while (!www.isDone)
//yield return null;
System.Threading.Thread.Sleep(100);
if (!string.IsNullOrEmpty(www.error))
Debug.LogError(www.error);
byte[] pdb = www.bytes;
System.IO.MemoryStream fs = new MemoryStream(dll);
System.IO.MemoryStream p = new MemoryStream(pdb);
appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
OnILRuntimeInitialized();
}
///
/// 加载dll,pdb
///
///
///
public void LoadHotFixAssembly(byte[] dll, byte[] pdb)
{
m_fs = new MemoryStream(dll);
m_p = new MemoryStream(pdb);
try
{
appdomain.LoadAssembly(m_fs, m_p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
}
catch
{
Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
return;
}
appdomain.DebugService.StartDebugService(56000);
OnILRuntimeInitialized();
}
void OnILRuntimeInitialized()
{
//appdomain.Invoke("Hotfix.Game", "Initialize", null, null);
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
//由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
//下面再举一个这个Demo中没有用到,但是UGUI经常遇到的一个委托,例如UnityAction
appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((action) =>
{
return new UnityEngine.Events.UnityAction(() =>
{
((System.Action)action)();
});
});
}
// Update is called once per frame
void Update()
{
}
}
修改ProLaunch.cs
增加对热更DLL的加载入口
具体代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.AddressableAssets.ResourceLocators;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
///
/// 加载方式
///
public enum LoadMode
{
ByLocalDll,
ByLocalAddressable
}
public class ProLaunch : MonoBehaviour
{
///
/// 显示下载状态和进度
///
public Text UpdateText;
public Text DownText;
public Button btnCheckAndUpdate;
public Button btnUpdate;
public Button btnDown;
public Button btnLogin;
public Slider Slider;//滑动条组件
private List<object> _updateKeys = new List<object>();
//public Transform father;
[Tooltip("dll文件的加载方式")]
public LoadMode loadingMode = LoadMode.ByLocalAddressable;
// Start is called before the first frame update
void Start()
{
//retryBtn.gameObject.SetActive(false);
btnCheckAndUpdate.onClick.AddListener(() =>
{
StartCoroutine(DoUpdateAddressadble());
});
btnUpdate.onClick.AddListener(() =>
{
UpdateCatalog();
});
// 默认自动执行一次更新检测
//StartCoroutine(DoUpdateAddressadble());
StartHotFix();
btnDown.onClick.AddListener(() =>
{
DownLoad();
});
btnLogin.onClick.AddListener(() =>
{
SceneManager.LoadScene(1);
//StartCoroutine(LoadScene("Test2"));
});
}
// Update is called once per frame
void Update()
{
}
///
/// 加载dll
///
///
public async System.Threading.Tasks.Task StartHotFix()
{
//去服务器上下载最新的aa包资源
//下载热更代码
//string m_url=null;
byte[] dll = new byte[] { };
byte[] pdb = new byte[] { };
if (loadingMode == LoadMode.ByLocalDll)
{
//StartCoroutine(CheckHotUpdate(dll,pdb));
HotFixMgr.instance.LoadHotFixAssembly();
}
else if (loadingMode == LoadMode.ByLocalAddressable)
{
TextAsset assetDll = await Addressables.LoadAssetAsync<TextAsset>("HotFix_Project_dll_res").Task;
dll = assetDll.bytes;
TextAsset assetPdb = await Addressables.LoadAssetAsync<TextAsset>("HotFix_Project_pdb_res").Task;
pdb = assetPdb.bytes;
HotFixMgr.instance.LoadHotFixAssembly(dll, pdb);
}
}
public async void UpdateCatalog()
{
//初始化Addressable
var init = Addressables.InitializeAsync();
await init.Task;
//开始连接服务器检查更新
var handle = Addressables.CheckForCatalogUpdates(false);
await handle.Task;
Debug.Log("check catalog status " + handle.Status);
if (handle.Status == AsyncOperationStatus.Succeeded)
{
List<string> catalogs = handle.Result;
if (catalogs != null && catalogs.Count > 0)
{
foreach (var catalog in catalogs)
{
Debug.Log("catalog " + catalog);
}
Debug.Log("download catalog start ");
UpdateText.text = UpdateText.text + "\n下载更新catalog";
var updateHandle = Addressables.UpdateCatalogs(catalogs, false);
await updateHandle.Task;
foreach (var item in updateHandle.Result)
{
Debug.Log("catalog result " + item.LocatorId);
foreach (var key in item.Keys)
{
Debug.Log("catalog key " + key);
}
_updateKeys.AddRange(item.Keys);
}
Debug.Log("download catalog finish " + updateHandle.Status);
UpdateText.text = UpdateText.text + "\n更新catalog完成" + updateHandle.Status;
}
else
{
Debug.Log("dont need update catalogs");
UpdateText.text = "没有需要更新的catalogs信息";
}
}
Addressables.Release(handle);
}
public void DownLoad()
{
StartCoroutine(DownAssetImpl());
}
public IEnumerator DownAssetImpl()
{
var downloadsize = Addressables.GetDownloadSizeAsync(_updateKeys);
yield return downloadsize;
Debug.Log("start download size :" + downloadsize.Result);
UpdateText.text = UpdateText.text + "\n更新文件大小" + downloadsize.Result;
if (downloadsize.Result > 0)
{
var download = Addressables.DownloadDependenciesAsync(_updateKeys, Addressables.MergeMode.Union);
yield return download;
//await download.Task;
Debug.Log("download result type " + download.Result.GetType());
UpdateText.text = UpdateText.text + "\n下载结果类型 " + download.Result.GetType();
foreach (var item in download.Result as List<UnityEngine.ResourceManagement.ResourceProviders.IAssetBundleResource>)
{
var ab = item.GetAssetBundle();
Debug.Log("ab name " + ab.name);
UpdateText.text = UpdateText.text + "\n ab名称 " + ab.name;
foreach (var name in ab.GetAllAssetNames())
{
Debug.Log("asset name " + name);
UpdateText.text = UpdateText.text + "\n asset 名称 " + name;
}
}
Addressables.Release(download);
}
Addressables.Release(downloadsize);
}
IEnumerator LoadScene(string senceName)
{
// 异步加载场景(如果场景资源没有下载,会自动下载),
var handle = Addressables.LoadSceneAsync(senceName);
if (handle.Status == AsyncOperationStatus.Failed)
{
Debug.LogError("场景加载异常: " + handle.OperationException.ToString());
yield break;
}
while (!handle.IsDone)
{
// 进度(0~1)
float percentage = handle.PercentComplete;
Debug.Log("进度: " + percentage);
yield return null;
}
Debug.Log("场景加载完毕");
}
IEnumerator DoUpdateAddressadble()
{
AsyncOperationHandle<IResourceLocator> initHandle = Addressables.InitializeAsync();
yield return initHandle;
// 检测更新
var checkHandle = Addressables.CheckForCatalogUpdates(false);
yield return checkHandle;
if (checkHandle.Status != AsyncOperationStatus.Succeeded)
{
OnError("CheckForCatalogUpdates Error\n" + checkHandle.OperationException.ToString());
yield break;
}
if (checkHandle.Result.Count > 0)
{
var updateHandle = Addressables.UpdateCatalogs(checkHandle.Result, false);
yield return updateHandle;
if (updateHandle.Status != AsyncOperationStatus.Succeeded)
{
OnError("UpdateCatalogs Error\n" + updateHandle.OperationException.ToString());
yield break;
}
// 更新列表迭代器
List<IResourceLocator> locators = updateHandle.Result;
foreach (var locator in locators)
{
List<object> keys = new List<object>();
keys.AddRange(locator.Keys);
// 获取待下载的文件总大小
var sizeHandle = Addressables.GetDownloadSizeAsync(keys);
yield return sizeHandle;
if (sizeHandle.Status != AsyncOperationStatus.Succeeded)
{
OnError("GetDownloadSizeAsync Error\n" + sizeHandle.OperationException.ToString());
yield break;
}
long totalDownloadSize = sizeHandle.Result;
UpdateText.text = UpdateText.text + "\ndownload size : " + totalDownloadSize;
Debug.Log("download size : " + totalDownloadSize);
if (totalDownloadSize > 0)
{
// 下载
var downloadHandle = Addressables.DownloadDependenciesAsync(keys, Addressables.MergeMode.Union, false);
//yield return downloadHandle;
while (!downloadHandle.IsDone)
{
if (downloadHandle.Status == AsyncOperationStatus.Failed)
{
OnError("DownloadDependenciesAsync Error\n" + downloadHandle.OperationException.ToString());
yield break;
}
// 下载进度
float percentage = downloadHandle.PercentComplete;
Debug.Log($"已下载: {percentage}");
DownText.text = $"已下载: {Mathf.Round(percentage * 100)}%";
Slider.value = percentage;
if (percentage >= 0.98f)//如果进度条已经到达90%
{
Slider.value = 1; //那就让进度条的值编变成1
}
yield return null;
}
yield return downloadHandle;
if (downloadHandle.Status == AsyncOperationStatus.Succeeded)
{
Debug.Log("下载完毕!");
DownText.text = DownText.text + " 下载完毕";
}
}
}
}
else
{
UpdateText.text = UpdateText.text + "\n没有检测到更新";
}
// 进入游戏
EnterPro();
}
// 进入游戏
void EnterPro()
{
// TODO
UpdateText.text = UpdateText.text + "\n进入游戏场景";
Debug.Log("进入游戏");
}
private void OnError(string msg)
{
UpdateText.text = UpdateText.text + $"\n{msg}\n请重试! ";
}
}
测试:在预制体LoginUI中新增退出
按钮,并在热更工程中创建绑定按钮单击的注册事件。使用Addressables进行分组,构建更新包进行测试。具体步骤如下:
HotFix.cs
,增加注册按钮单击事件静态方法。代码如下:using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
namespace HotFix_Project
{
public class HotFix
{
void Awake()
{
UnityEngine.Debug.Log("Awake");
}
public static void HelloWorld(string name,int age) {
UnityEngine.Debug.Log("HelloWorld "+name+" "+ age);
}
public string Hello(string name) {
return "hello " + name;
}
public static void bindClick(GameObject btn) {
btn.transform.GetComponent<Button>().onClick.AddListener(()=> {
UnityEngine.Debug.Log("Hot click");
});
}
}
}
login.cs
并调用热更工程中的代码进行注册,在loginUI中挂在此脚本。代码如下:using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Login : MonoBehaviour
{
public GameObject btn;
// Start is called before the first frame update
void Start()
{
string className = "HotFix_Project.HotFix";
string funName = "bindClick";
btn = gameObject.transform.Find("btnClose").gameObject;
HotFixMgr.Instance.appdomain.Invoke(className, funName, null, btn);
}
// Update is called once per frame
void Update()
{
}
}
运行代码,点击退出按钮后:
进行构建并将更新的文件Copy到服务器上。
修改Play Mode Script
如图进行设置
Unity 3D模型展示框架篇效果演示
以上就是今天要讲的内容,本文介绍如何通过ILRuntime+Adressables技术完成热更案例的整合与测试,这也是Unity 3D模型展示框架篇系列文章的最后一篇,在阅读过程中发现存在遗漏或不足欢迎大家留言指正。