文档
Unity热更新之ILRuntime_暗夜__的博客-CSDN博客_ilruntime
任务
1.接入ILruntime
2.接入Ilriuntime的Protobuf
代码
首先定义好自己的热更布局,(比如我喜欢把热更脚本正常放在Unity里面,设置好你的程序集的引用就完全没问题)
比如:
编辑器playersetting别忘记改了,
在 Unity 中使用 .NET 4.x | Microsoft Docs
简单了解一下:简单说就是4.xAPI更多,但是个别api不是所有平台都支持的
定义在热更的初始脚本:
using System;
using UnityEngine;
namespace HotFix
{
public class HotFixStart
{
public static void Start()
{
Debug.Log("Start---------------------------------");
}
}
}
外部调用热更的管理脚本:
找到你自己的热更dll(随便你怎么弄放在哪最终把这个dll拿到我们的项目就行),把dll改成后缀名txt或者是bytes的都可以方便我们读取该dll(这里放在StreamingAssets下为了方便测试,想要动态热更的话用你自己的资源加载工具加载即可比如Addressable)
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public class Startup : MonoBehaviour
{
private ILRuntime.Runtime.Enviorment.AppDomain appdomain;
void Start()
{
StartCoroutine(LoadILRuntime());
}
IEnumerator LoadILRuntime()
{
appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
UnityWebRequest webRequest = UnityWebRequest.Get(StreamingAssetsPath("HotFix.dll.txt"));
yield return webRequest.SendWebRequest();
if (webRequest.result != UnityWebRequest.Result.Success)
{
yield break;
}
byte[] dll = webRequest.downloadHandler.data;
webRequest.Dispose();
webRequest = UnityWebRequest.Get(StreamingAssetsPath("HotFix.pdb.txt"));
yield return webRequest.SendWebRequest();
if (webRequest.result != UnityWebRequest.Result.Success)
{
yield break;
}
byte[] pdb = webRequest.downloadHandler.data;
webRequest.Dispose();
appdomain.LoadAssembly(new MemoryStream(dll), new MemoryStream(pdb), new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
OnILRuntimeInitialized();
}
void OnILRuntimeInitialized()
{
appdomain.Invoke("HotFix.AppMain", "Start", null, null);
}
public string StreamingAssetsPath(string fileName)
{
string path = Application.streamingAssetsPath + "/" + fileName;
return path;
}
}
理论上来说其实已经可以了只要你能跑起来
跨域继承脚本代理:
using ILRuntime.CLR.TypeSystem;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
/// <summary>
/// 在GameObject上挂载的真正的脚本对象MonoProxy
/// 运行时,会把逻辑执行转交给绑定的对应热更脚本对象ScriptObject
/// </summary>
public class MonoProxy : MonoBehaviour
{
/// <summary>
/// 当前这个MonoProxy对象映射的热更脚本的类型字符串
/// </summary>
public string ScriptName;
/// <summary>
/// 映射的热更脚本的类型的对象
/// </summary>
public object ScriptObject;
/// <summary>
/// 将本MonoProxy对象和一个热更脚本绑定在一起
/// </summary>
/// <param name="scriptName"></param>
public void Bind(string scriptName)
{
ScriptName = "HotFix." + scriptName;
ScriptObject = Startup.appdomain.Instantiate(ScriptName);
IType scriptIType = Startup.appdomain.LoadedTypes[ScriptName];
FieldInfo goField = scriptIType.ReflectionType.GetField("gameObject");
goField.SetValue(ScriptObject, gameObject);
Startup.appdomain.Invoke(ScriptName, "Awake", ScriptObject, null);
}
void Start()
{
Startup.appdomain.Invoke(ScriptName, "Start", ScriptObject, null);
}
void Update()
{
Startup.appdomain.Invoke(ScriptName, "Update", ScriptObject, null);
}
private void OnDestroy()
{
Startup.appdomain.Invoke(ScriptName, "OnDestroy", ScriptObject, null);
}
}
热更项目中继承的伪Mono脚本:
using System.Collections;
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace HotFix
{
public class MonoBehaviourEX
{
public GameObject gameObject;
public Transform transform => gameObject.transform;
public virtual void Awake() { }
public virtual void Start() { }
public virtual void Update() { }
public virtual void OnDestroy() { }
public void StartCoroutineEx(IEnumerator coroutine)
{
gameObject.GetComponent<MonoProxy>().StartCoroutine(coroutine);
}
}
}
热更中扩展辅助脚本挂载:(为了符合自己的习惯喜欢用AddComponent罢了)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace HotFix
{
public static class GameObjectEx
{
/// <summary>
/// 给GameObject类型,扩展一个新函数Bind,就是给GameObject绑定一个热更的脚本对象
/// </summary>
/// <param name="gameObject"></param>
/// <param name="scriptName"></param>
public static void Bind(this GameObject gameObject, string scriptName)
{
MonoProxy monoProxy = gameObject.AddComponent<MonoProxy>();
monoProxy.Bind(scriptName);
}
public static T Bind<T>(this GameObject gameObject)
{
MonoProxy monoProxy = gameObject.AddComponent<MonoProxy>();
string scriptName = typeof(T).Name;
Debug.Log(scriptName);
return (T)monoProxy.Bind(scriptName);
}
public static T AddComponent<T>(this GameObject gameObject) where T : MonoBehaviourEX
{
MonoProxy monoProxy = gameObject.AddComponent<MonoProxy>();
string scriptName = typeof(T).Name;
Debug.Log(scriptName);
return (T)monoProxy.Bind(scriptName);
}
}
}
协程适配器:
using ILRuntime.CLR.Method;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CoroutineAdapter : CrossBindingAdaptor
{
public override Type BaseCLRType
{
get
{
return null;
}
}
public override Type[] BaseCLRTypes
{
get
{
//跨域继承只能有1个Adapter,因此应该尽量避免一个类同时实现多个外部接口,对于coroutine来说是IEnumerator<object>,IEnumerator和IDisposable,
//ILRuntime虽然支持,但是一定要小心这种用法,使用不当很容易造成不可预期的问题
//日常开发如果需要实现多个DLL外部接口,请在Unity这边先做一个基类实现那些个接口,然后继承那个基类
return new Type[] { typeof(IEnumerator<object>), typeof(IEnumerator), typeof(IDisposable) };
}
}
public override Type AdaptorType
{
get
{
return typeof(Adaptor);
}
}
public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
{
return new Adaptor(appdomain, instance);
}
/// <summary>
/// Coroutine生成的类实现了IEnumerator<System.Object>, IEnumerator, IDisposable 所以都要实现
/// 这个可以通过reflector之类的IL反编译软件得知
/// </summary>
internal class Adaptor : IEnumerator<System.Object>, IEnumerator, IDisposable, 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; } }
IMethod mCurrentMethod;
bool mCurrentMethodGot;
public object Current
{
get
{
if (!mCurrentMethodGot)
{
mCurrentMethod = instance.Type.GetMethod("get_Current", 0);
if (mCurrentMethod == null)
{
//这里写System.Collections.IEnumerator.get_Current而不是直接get_Current是因为coroutine生成的类是显式实现这个接口的,通过Reflector等反编译软件可得知
//为了兼容其他只实现了单一Current属性的,所以上面先直接取了get_Current
mCurrentMethod = instance.Type.GetMethod("System.Collections.IEnumerator.get_Current", 0);
}
mCurrentMethodGot = true;
}
if (mCurrentMethod != null)
{
object res = appdomain.Invoke(mCurrentMethod, instance, null);
return res;
}
else
{
return null;
}
}
}
IMethod mDisposeMethod;
bool mDisposeMethodGot;
public void Dispose()
{
if (!mDisposeMethodGot)
{
mDisposeMethod = instance.Type.GetMethod("Dispose", 0);
if (mDisposeMethod == null)
{
mDisposeMethod = instance.Type.GetMethod("System.IDisposable.Dispose", 0);
}
mDisposeMethodGot = true;
}
if (mDisposeMethod != null)
{
appdomain.Invoke(mDisposeMethod, instance, null);
}
}
IMethod mMoveNextMethod;
bool mMoveNextMethodGot;
public bool MoveNext()
{
if (!mMoveNextMethodGot)
{
mMoveNextMethod = instance.Type.GetMethod("MoveNext", 0);
mMoveNextMethodGot = true;
}
if (mMoveNextMethod != null)
{
return (bool)appdomain.Invoke(mMoveNextMethod, instance, null);
}
else
{
return false;
}
}
IMethod mResetMethod;
bool mResetMethodGot;
public void Reset()
{
if (!mResetMethodGot)
{
mResetMethod = instance.Type.GetMethod("Reset", 0);
mResetMethodGot = true;
}
if (mResetMethod != null)
{
appdomain.Invoke(mResetMethod, 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;
}
}
}
在热更新中加入Protobuf
protobuf-net: Protobuf-net for ILRuntime 可以使用repeat 支持proto2 & proto3 只支持unity2017 .net 4.6
导入之后找到Editor文件夹 修改你自己的参数
#if UNITY_2018_1_OR_NEWER
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using ProtoBuf;
using Google.Protobuf.Reflection;
using ProtoBuf.Reflection;
using System.IO;
public class Protogen
{
private static string ProtoPath = Application.dataPath + "/../../ProtoFiles";
private static string ExePath = Application.dataPath + "/../../Protoc/protoc-3.5.1-win32/bin/";
/// <summary>
/// 忽略这里,这是给我自己服务器配的地址
/// </summary>
private static string outPath = Application.dataPath + "/../../Server/proto/";
[MenuItem("CustomTools/Generate Protocs")]
public static void GenerateProtobufCS()
{
Generate(ProtoPath, new string[] { "NetMessage.proto" },
//这个是你的输出地址
Application.dataPath + "/ILRunimeFrameWork/Hotfix/ProtoCode");
}
[MenuItem("CustomTools/Generate ServerProtocs")]
public static void Generateser()
{
RunExeByProcess(outPath);
}
static void Generate(string inpath, string[] inprotos, string outpath)
{
var set = new FileDescriptorSet();
set.AddImportPath(inpath);
foreach (var inproto in inprotos)
{
set.Add(inproto, true);
}
set.Process();
var errors = set.GetErrors();
CSharpCodeGenerator.ClearTypeNames();
var files = CSharpCodeGenerator.Default.Generate(set);
int idx = 1;
foreach (var file in files)
{
EditorUtility.DisplayProgressBar("Generate", file.Name, idx / (1.0f * inprotos.Length));
var path = Path.Combine(outpath, file.Name);
File.WriteAllText(path, file.Text);
Debug.Log($"generated: {path}");
}
EditorUtility.ClearProgressBar();
AssetDatabase.Refresh();
}
//private string savePath = System.Environment.CurrentDirectory + @"\文件夹名";
public static void RunExeByProcess(string arguments)
{
//开启新线程
System.Diagnostics.Process process = new System.Diagnostics.Process();
//调用的exe名称
process.StartInfo.FileName = "GenForServer.bat";
process.StartInfo.WorkingDirectory = ExePath;
//传递参数
process.StartInfo.Arguments = arguments;
process.Start();
process.WaitForExit();//停顿,当外部程序退出后才能继续执行
if (process.ExitCode == 0)//程序退出
{
Debug.Log("执行成功");
}
}
}
#endif
这时候生成的代码还是不能用的:一个是没在Ilruntime的环境里注册,另一个是还需要一个辅助类(他文档里写了看看就会了):
ilruntime注册:
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
using SimpleJSON;
using System;
public class AppStart : MonoBehaviour
{
public static ILRuntime.Runtime.Enviorment.AppDomain appdomain;
void Start()
{
StartCoroutine(LoadILRuntime());
}
IEnumerator LoadILRuntime()
{
appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
/*UnityWebRequest webRequest = UnityWebRequest.Get(StreamingAssetsPath("Hotfix.dll.txt"));
yield return webRequest.SendWebRequest();
byte[] dll = webRequest.downloadHandler.data;
webRequest.Dispose();
webRequest = UnityWebRequest.Get(StreamingAssetsPath("Hotfix.pdb.txt"));
yield return webRequest.SendWebRequest();
byte[] pdb = webRequest.downloadHandler.data;
webRequest.Dispose();*/
byte[] dll = AssetLoader.Instance.CreateAsset<TextAsset>("Launch", "Assets/GAssets/Launch/Scriptdll/Hotfix.dll.bytes", gameObject).bytes;
byte[] pdb = AssetLoader.Instance.CreateAsset<TextAsset>("Launch", "Assets/GAssets/Launch/Scriptdll/Hotfix.pdb.bytes", gameObject).bytes;
yield return null;
appdomain.LoadAssembly(new MemoryStream(dll), new MemoryStream(pdb), new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
appdomain.RegisterCrossBindingAdaptor(new CoroutineAdapter());
appdomain.DelegateManager.RegisterFunctionDelegate<System.String, JSONNode>();
appdomain.DelegateManager.RegisterMethodDelegate<global::NetPacket>();
appdomain.DelegateManager.RegisterMethodDelegate<System.Int32>();
appdomain.DelegateManager.RegisterMethodDelegate<cfg.BagItem>();
appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((act) =>
{
return new UnityEngine.Events.UnityAction(() =>
{
((Action)act)();
});
});
// 注册Protobuf相关
ProtoBuf.PType.RegisterFunctionCreateInstance(PType_CreateInstance);
ProtoBuf.PType.RegisterFunctionGetRealType(PType_GetRealType);
//appdomain.RegisterCLRMethodRedirection(0, new Bright.Config.BeanBase.GetTypeId());
OnILRuntimeInitialized();
}
public static object PType_CreateInstance(string typeName)
{
return appdomain.Instantiate(typeName);
}
public static Type PType_GetRealType(object o)
{
Type type = o.GetType();
if (type.FullName == "ILRuntime.Runtime.Intepreter.ILTypeInstance")
{
var ilo = o as ILRuntime.Runtime.Intepreter.ILTypeInstance;
type = ProtoBuf.PType.FindType(ilo.Type.FullName);
}
return type;
}
void OnILRuntimeInitialized()
{
appdomain.Invoke("HotFix.HotfixMain", "Start", null, null);
}
public string StreamingAssetsPath(string fileName)
{
string path = Application.streamingAssetsPath + "/" + fileName;
return path;
}
}
hotfix程序集加一个辅助类
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Helper
{
public sealed class ProtoHelper
{
public static byte[] EncodeWithName(object p)
{
var type = p.GetType();
var name = type.FullName;
using (var ms = new MemoryStream())
{
Serializer.Serialize(ms, p);
//var cos = new Google.Protobuf.CodedOutputStream(ms);
//((IMessage)p).Encode(cos); //非反射方式
//cos.Flush();
var nbs = Encoding.UTF8.GetBytes(name);
int nblen = nbs.Length;
if (nblen > 255)
{
throw new Exception("PB:name->" + name + " is To Long " + nblen + " > 255");
}
var buffer = new byte[ms.Length + nbs.Length + 1];
buffer[0] = (byte)((nblen >> 0) & 0xFF);
Buffer.BlockCopy(nbs, 0, buffer, 1, nblen);
ms.Position = 0;
ms.Read(buffer, 1 + nblen, (int)ms.Length);
return buffer;
}
}
public static object DecodeWithName(byte[] b, out string name)
{
var bytesLen = b[0];
name = Encoding.UTF8.GetString(b, 1, bytesLen);
using (var ms = new MemoryStream(b, 1 + bytesLen, b.Length - 1 - bytesLen))
{
Type T = Type.GetType(name);
if (name.Contains(".")) { name = name.Substring(name.LastIndexOf('.') + 1); }
return Serializer.Deserialize(T, ms);
//var o = Activator.CreateInstance(T);
//((IMessage)o).Decode(new Google.Protobuf.CodedInputStream(ms)); //非反射方式
//return o;
}
}
}
public interface IProtoRecv
{
void OnRecv(string name, object o);
}
public interface IMessage
{
void Encode(Google.Protobuf.CodedOutputStream writer);
void Decode(Google.Protobuf.CodedInputStream reader);
}
}
最终在你热更入口调用该函数比如(这个函数就是你生成的protobuf文件里面自动有个函数):
基本完成:该有的功能都有,和你正常在Unity开发应该差不多。。。