ILRuntime热更新通过Addressables加载DLL
需求
通过热更的方式改变游戏物体的运动状态
Addressables资源加载可以看下之前写的“Unity资源打包Addressable AA包”
1.安装插件
首先需要在项目的Packages/manifest.json中,添加ILRuntime的源信息,在这个文件的dependencies节点前和列表中增加以下内容
{
"scopedRegistries": [
{
"name": "ILRuntime",
"url": "https://registry.npmjs.org",
"scopes": [
"com.ourpalm"
]
}
],
"dependencies": {
"com.ourpalm.ilruntime": "1.6.0",
2.允许非安全代码
Edit > Project Settings > Player中勾选Allow 'unsafe' code
3.创建程序集
ILRuntime热更新简单来说
之前写的代码都打入一个包内了,比如打包的Windows程序下面路径内MyILRuntime_Data\Managed\Assembly-CSharp.dll
现在分成不同的包,并且是在程序运行后再加载需要热更新的程序包。
1.创建两个文件夹
在Assets中创建一个Script文件夹,
Script文件夹中再创建
一个Hotfix文件夹,这里存放热更新的代码
一个DLL文件夹,这里存放上面代码打包的DLL文件
2.创建一个程序集
在Hotfix文件夹中右键 Create > Assembly Definition 创建一个程序集,这里名为Unity.Hotfix。
此时此文件夹中的代码都会被打包到一起。
在Hotfix文件夹中增加一个Hot.cs文件。
\Library\ScriptAssemblies 文件夹中会多出一个Unity.Hotfix.dll文件,此文件夹中的脚本都会被打包到此文件夹中。
3.复制程序集DLL文件到指定文件夹
创建BuildHotfixEditor.cs文件到Assets > Editor 文件夹下,这个路径Editor是必须的。
using UnityEngine;
using UnityEditor;
using System.IO;
//Unity 自带特性 每次编译完脚本后都会自动编译(编译器模式下)
[InitializeOnLoad]
public class BuildHotfixEditor
{
//程序集路径
const string scriptAssemblies = "Library/ScriptAssemblies";
//目标路径
const string codeDir = "Assets/Script/DLL";
//hotfixdll
const string hotfixDll = "Unity.Hotfix.dll";
//Hotfixpdb
const string hotfixPdb = "Unity.Hotfix.pdb";
static BuildHotfixEditor()
{
string fixDll = Path.Combine(codeDir,$"{hotfixDll}.txt");
string fixPdb = Path.Combine(codeDir,$"{hotfixPdb}.txt");
//开始复制
File.Copy(Path.Combine(scriptAssemblies,hotfixDll),fixDll,true);
File.Copy(Path.Combine(scriptAssemblies,hotfixPdb),fixPdb,true);
//刷新
AssetDatabase.Refresh();
Debug.Log("程序集拷贝完成");
}
}
Unity编辑器会把程序集自动保存在\Library\ScriptAssemblies路径下
复制程序集的两个文件"Unity.Hotfix.dll"、"Unity.Hotfix.pdb"到Assets/Script/DLL指定目录下(方便Addressables加载)
复制过程中增加了.txt后缀,这是因为Addressables不支持.dll .pdb作为后缀的资源
整个过程是每次编辑代码后自动完成的
4.配置程序集配置文件
Assembly Definition References
配置此程序集会引用到的其他程序集
允许使用非安全代码
Allow 'unsafe' Code
4.编写测试代码
1.调用静态方法
Hot.cs 位于Assets\Script\Hotfix 热更新代码
可以传入两个参数的静态方法
using UnityEngine;
namespace Unity.Hotfix
{
public class Hot
{
public static void Log(string str,string str1)
{
Debug.Log("Log:" + str+":"+str1);
}
}
}
Hello.cs 位于Assets\Script 正常Unity中的代码
决定了什么时候加载和使用热更新代码
using System.IO;
using ILRuntime.Mono.Cecil.Pdb;
using UnityEngine;
using UnityEngine.AddressableAssets;
public class Hello : MonoBehaviour
{
MemoryStream hotfixdllstream;
MemoryStream hotfixpdbstream;
private UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<TextAsset> handle_dll;
private UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<TextAsset> handle_pdb;
byte[] dll;
bool dllOK = false;
byte[] pdb;
bool pdbOK = false;
void Start()
{
handle_dll = Addressables.LoadAssetAsync<TextAsset>("Assets/Script/DLL/Unity.Hotfix.dll.txt");
handle_dll.Completed += (obj) =>
{
dllOK = true;
dll = obj.Result.bytes;
if (pdbOK)
LoadEnd();
};
handle_pdb = Addressables.LoadAssetAsync<TextAsset>("Assets/Script/DLL/Unity.Hotfix.pdb.txt");
handle_pdb.Completed += (obj) =>
{
pdbOK = true;
pdb = obj.Result.bytes;
if (dllOK)
LoadEnd();
};
}
public void LoadEnd()
{
hotfixdllstream = new MemoryStream(dll);
hotfixpdbstream = new MemoryStream(pdb);
var appDomain = ILRuntimeAppDomainManager.Instance.AppDomain;
appDomain.LoadAssembly(hotfixdllstream, hotfixpdbstream, new PdbReaderProvider());
appDomain.Invoke("Unity.Hotfix.Hot", "Log", null, new string[] { "Hello ILRuntime", "热更新" });
}
}
其中void Start()中都是之前写的“Unity资源打包Addressable AA包”中的内容
通过Addressable加载Hotfix程序集
加载到程序中转换成MemoryStream
通过appDomain.LoadAssembly(hotfixdllstream, hotfixpdbstream, new PdbReaderProvider());方法加载
最后appDomain.Invoke("Unity.Hotfix.Hot", "Log", null, new string[] { "Hello ILRuntime", "热更新" });调用热更中的Log方法
其中需要实例化一个AppDomain类,可以把它作为一个单例。
ILRuntimeAppDomainManager.cs 位于Assets\Script
using System.Collections.Generic;
using ILRuntime.CLR.Method;
using ILRuntime.CLR.TypeSystem;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;
using ILRuntime.Runtime.Stack;
using UnityEngine;
public class ILRuntimeAppDomainManager
{
private static ILRuntimeAppDomainManager _instance;
private AppDomain _appDomain;
// 私有构造函数,确保单例模式
private ILRuntimeAppDomainManager()
{
_appDomain = new AppDomain();
_appDomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());
SetupCLRRedirection();
}
unsafe void SetupCLRRedirection()
{
//这里面的通常应该写在InitializeILRuntime,这里为了演示写这里
var arr = typeof(GameObject).GetMethods();
foreach (var i in arr)
{
if (i.Name == "AddComponent" && i.GetGenericArguments().Length == 1)
{
_appDomain.RegisterCLRMethodRedirection(i, AddComponent);
}
}
}
unsafe static StackObject* AddComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
{
//CLR重定向的说明请看相关文档和教程,这里不多做解释
ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;
var ptr = __esp - 1;
//成员方法的第一个参数为this
GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject;
if (instance == null)
throw new System.NullReferenceException();
__intp.Free(ptr);
var genericArgument = __method.GenericArguments;
//AddComponent应该有且只有1个泛型参数
if (genericArgument != null && genericArgument.Length == 1)
{
var type = genericArgument[0];
object res;
if(type is CLRType)
{
//Unity主工程的类不需要任何特殊处理,直接调用Unity接口
res = instance.AddComponent(type.TypeForCLR);
}
else
{
//热更DLL内的类型比较麻烦。首先我们得自己手动创建实例
var ilInstance = new ILTypeInstance(type as ILType, false);//手动创建实例是因为默认方式会new MonoBehaviour,这在Unity里不允许
//接下来创建Adapter实例
var clrInstance = instance.AddComponent<MonoBehaviourAdapter.Adaptor>();
//unity创建的实例并没有热更DLL里面的实例,所以需要手动赋值
clrInstance.ILInstance = ilInstance;
clrInstance.AppDomain = __domain;
//这个实例默认创建的CLRInstance不是通过AddComponent出来的有效实例,所以得手动替换
ilInstance.CLRInstance = clrInstance;
res = clrInstance.ILInstance;//交给ILRuntime的实例应该为ILInstance
clrInstance.Awake();//因为Unity调用这个方法时还没准备好所以这里补调一次
}
return ILIntepreter.PushObject(ptr, __mStack, res);
}
return __esp;
}
// 获取单例实例的属性
public static ILRuntimeAppDomainManager Instance
{
get
{
if (_instance == null)
{
_instance = new ILRuntimeAppDomainManager();
}
return _instance;
}
}
// 获取AppDomain实例的属性
public AppDomain AppDomain
{
get { return _appDomain; }
}
}
这里本来只需要实现一个单例即可
下面些代码是为热更中实现MonoBehaviour添加的代码
_appDomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());
SetupCLRRedirection();
unsafe void SetupCLRRedirection()
unsafe static StackObject* AddComponent
MonoBehaviourAdapter.cs 位于Assets\Script
using UnityEngine;
using System;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;
using ILRuntime.CLR.Method;
public class MonoBehaviourAdapter : CrossBindingAdaptor
{
public override Type BaseCLRType
{
get
{
return typeof(MonoBehaviour);
}
}
public override Type AdaptorType
{
get
{
return typeof(Adaptor);
}
}
public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
{
return new Adaptor(appdomain, instance);
}
//为了完整实现MonoBehaviour的所有特性,这个Adapter还得扩展,这里只抛砖引玉,只实现了最常用的Awake, Start和Update
public class Adaptor : MonoBehaviour, CrossBindingAdaptorType
{
ILTypeInstance instance;
ILRuntime.Runtime.Enviorment.AppDomain appdomain;
public Adaptor()
{
}
public Adaptor(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
{
this.appdomain = appdomain;
this.instance = instance;
}
public ILTypeInstance ILInstance { get { return instance; } set { instance = value; } }
public ILRuntime.Runtime.Enviorment.AppDomain AppDomain { get { return appdomain; } set { appdomain = value; } }
IMethod mAwakeMethod;
bool mAwakeMethodGot;
public void Awake()
{
//Unity会在ILRuntime准备好这个实例前调用Awake,所以这里暂时先不掉用
if (instance != null)
{
if (!mAwakeMethodGot)
{
mAwakeMethod = instance.Type.GetMethod("Awake", 0);
mAwakeMethodGot = true;
}
if (mAwakeMethod != null)
{
appdomain.Invoke(mAwakeMethod, instance, null);
}
}
}
IMethod mStartMethod;
bool mStartMethodGot;
void Start()
{
if (!mStartMethodGot)
{
mStartMethod = instance.Type.GetMethod("Start", 0);
mStartMethodGot = true;
}
if (mStartMethod != null)
{
appdomain.Invoke(mStartMethod, instance, null);
}
}
IMethod mUpdateMethod;
bool mUpdateMethodGot;
void Update()
{
if (!mUpdateMethodGot)
{
mUpdateMethod = instance.Type.GetMethod("Update", 0);
mUpdateMethodGot = true;
}
if (mUpdateMethod != null)
{
appdomain.Invoke(mUpdateMethod, instance, null);
}
}
public override string ToString()
{
IMethod m = appdomain.ObjectType.GetMethod("ToString", 0);
m = instance.Type.GetVirtualMethod(m);
if (m == null || m is ILMethod)
{
return instance.ToString();
}
else
return instance.Type.FullName;
}
}
}
打包调试
编写好上面的代码后,Hotfix.dll.txt和Hotfix.pdb.txt会自动复制到Assets>Script>DLL文件夹中
Addressables Groups中新创建一个Script分组,记得修改成远程加载
关于远程打包看另外两篇文章 "Unity资源打包Addressable AA包" "Addressables资源打包(AA包)代码中改变远程地址"
运行程序,这里自行查看log。
可以用Log Viewer.unitypackage或SRDebugger1.12.1.unitypackage工具
热更
修改位于Assets\Script\Hotfix 热更新代码Hot.cs
代码的搬运是自动完成的只要在Addressables中Build即可
再将资源复制到服务器路径中即可
2.控制游戏物体移动
首先根据另外两篇文章 "Unity资源打包Addressable AA包" "Addressables资源打包(AA包)代码中改变远程地址"实现游戏物体的远程加载
接着上面的脚本增加两个脚本,上面的脚本中已经增加MonoBehaviourAdapter这样的适配器
ControlMove.cs 放在Assets\Script中并挂载在游戏物体上
using UnityEngine;
using ILRuntime.Runtime.Enviorment;
public class ControlMove : MonoBehaviour
{
AppDomain appDomain;
private void Start()
{
appDomain = ILRuntimeAppDomainManager.Instance.AppDomain;
CallHotfixMethod();
}
public void CallHotfixMethod()
{
appDomain.Invoke("Unity.Hotfix.AddComponent", "ADD", null, gameObject);
}
}
HotMove.cs 放在Assets\Script\Hotfix文件夹中 通过上面的代码动态添加到游戏物体上
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Unity.Hotfix
{
class SomeMonoBehaviour : MonoBehaviour
{
float time;
void Awake()
{
Debug.Log("!! SomeMonoBehaviour.Awake");
}
void Start()
{
Debug.Log("!! SomeMonoBehaviour.Start");
}
void Update()
{
this.transform.Translate(new Vector3(0,1*Time.deltaTime,0));
}
public void Test()
{
Debug.Log("SomeMonoBehaviour");
}
}
public class AddComponent
{
public static void ADD(GameObject go)
{
go.AddComponent<SomeMonoBehaviour>();
}
}
}
修改热更的代码,进行替换可以改变游戏物体的运动方式