Integrating ILRuntime for Hot Code Updates in Unity ET6
ILRuntime is a pure C# IL runtime implementation designed for C#-based platforms like Unity. It provides a fast, convenient, and reliable way to execute IL code, enabling hot code updates on devices that do not support JIT compilation, such as iOS. The official guide can be found at: https://ourpalm.github.io/ILRuntime/public/v1/guide/endex.html.
ET is an open-source dual-end framework for game development, using Unity3D to the client and C# .NET Core for the server. It is a distributed game server framework that emphasizes development efficiency and performance. Key features include shared logic code between client and server, robust hot update mechanisms, support for reliable UDP, TCP, and WebSocket protocols, and server-side 3D recast pathfinding. The repository is at: https://github.com/egametang/ET.git.
Integrating ILRuntime into ET
1. BuildAssemblieEditor.cs
This script compiles the code into a DLL and PDB, copies them to the Unity project, and assigns AssetBundle labels.
public static class BuildAssemblieEditor
{
private const string CodeDir = "Assets/Bundles/Code/";
[MenuItem("Tools/BuildCode _F5")]
public static void BuildCode()
{
BuildMuteAssembly("Code", new[]
{
"Codes/Model/",
"Codes/ModelView/",
"Codes/Hotfix/",
"Codes/HotfixView/"
}, Array.Empty<string>());
AfterCompiling();
AssetDatabase.Refresh();
}
private static void BuildMuteAssembly(string assemblyName, string[] codeDirectories, string[] additionalReferences)
{
var scripts = new List<string>();
foreach (var dir in codeDirectories)
{
var dirInfo = new DirectoryInfo(dir);
var files = dirInfo.GetFiles("*.cs", SearchOption.AllDirectories);
scripts.AddRange(files.Select(f => f.FullName));
}
var dllPath = Path.Combine(Define.BuildOutputDir, $"{assemblyName}.dll");
var pdbPath = Path.Combine(Define.BuildOutputDir, $"{assemblyName}.pdb");
File.Delete(dllPath);
File.Delete(pdbPath);
Directory.CreateDirectory(Define.BuildOutputDir);
var builder = new AssemblyBuilder(dllPath, scripts.ToArray())
{
compilerOptions = { ApiCompatibilityLevel = PlayerSettings.GetApiCompatibilityLevel(BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget)) },
additionalReferences = additionalReferences,
flags = AssemblyBuilderFlags.DevelopmentBuild,
referencesOptions = ReferencesOptions.UseEngineModules,
buildTarget = EditorUserBuildSettings.activeBuildTarget,
buildTargetGroup = BuildPipeline.GetBuildTargetGroup(EditorUserBuildSettings.activeBuildTarget)
};
builder.buildStarted += path => Debug.Log($"Build started: {path}");
builder.buildFinished += (path, messages) =>
{
var errors = messages.Count(m => m.type == CompilerMessageType.Error);
var warnings = messages.Count(m => m.type == CompilerMessageType.Warning);
Debug.Log($"Warnings: {warnings}, Errors: {errors}");
if (errors > 0)
foreach (var msg in messages.Where(m => m.type == CompilerMessageType.Error))
Debug.LogError(msg.message);
};
if (!builder.Build())
Debug.LogError($"Build failed: {builder.assemblyPath}");
}
private static void AfterCompiling()
{
while (EditorApplication.isCompiling)
{
Debug.Log("Waiting for compilation...");
Thread.Sleep(1000);
}
Directory.CreateDirectory(CodeDir);
File.Copy(Path.Combine(Define.BuildOutputDir, "Code.dll"), Path.Combine(CodeDir, "Code.dll.bytes"), true);
File.Copy(Path.Combine(Define.BuildOutputDir, "Code.pdb"), Path.Combine(CodeDir, "Code.pdb.bytes"), true);
AssetDatabase.Refresh();
AssetImporter.GetAtPath("Assets/Bundles/Code/Code.dll.bytes").assetBundleName = "Code.unity3d";
AssetImporter.GetAtPath("Assets/Bundles/Code/Code.pdb.bytes").assetBundleName = "Code.unity3d";
AssetDatabase.Refresh();
Debug.Log("Build completed and copied to bundles.");
}
}
2. CodeLoader.cs
This initializes the ILRuntime domain and launches the hot update entry point.
case Define.CodeMode_ILRuntime:
{
var bundleAssets = AssetsBundleHelper.LoadBundle("code.unity3d");
var dllBytes = ((TextAsset)bundleAssets["Code.dll"]).bytes;
var pdbBytes = ((TextAsset)bundleAssets["Code.pdb"]).bytes;
var domain = new AppDomain();
using (var dllStream = new MemoryStream(dllBytes))
using (var pdbStream = new MemoryStream(pdbBytes))
{
domain.LoadAssembly(dllStream, pdbStream, new PdbReaderProvider());
}
ILHelper.InitILRuntime(domain);
allTypes = domain.LoadedTypes.Values.Select(t => t.ReflectionType).ToArray();
var startMethod = new ILStaticMethod(domain, "ET.Entry", "Start", 0);
startMethod.Run();
break;
}
3. ILHelper.cs
Registers redirect functions, delegates, adapters, and CLR bindings.
public static class ILHelper
{
public static List<Type> RegisteredTypes = new List<Type>();
public static void InitILRuntime(AppDomain domain)
{
// Register types for CLR binding
RegisteredTypes.AddRange(new[]
{
typeof(Dictionary<int, ILTypeInstance>),
typeof(Dictionary<int, int>),
typeof(Dictionary<object, object>),
typeof(Dictionary<int, object>),
typeof(Dictionary<long, object>),
typeof(Dictionary<long, int>),
typeof(Dictionary<int, long>),
typeof(Dictionary<string, long>),
typeof(Dictionary<string, int>),
typeof(Dictionary<string, object>),
typeof(List<ILTypeInstance>),
typeof(List<int>),
typeof(List<long>),
typeof(List<string>),
typeof(List<object>),
typeof(ListComponent<ILTypeInstance>),
typeof(ETTask<int>),
typeof(ETTask<long>),
typeof(ETTask<string>),
typeof(ETTask<object>),
typeof(ETTask<AssetBundle>),
typeof(ETTask<UnityEngine.Object[]>),
typeof(ListComponent<ETTask>),
typeof(ListComponent<Vector3>)
});
// Register method delegates
domain.DelegateManager.RegisterMethodDelegate<List<object>>();
domain.DelegateManager.RegisterMethodDelegate<object>();
domain.DelegateManager.RegisterMethodDelegate<bool>();
domain.DelegateManager.RegisterMethodDelegate<string>();
domain.DelegateManager.RegisterMethodDelegate<float>();
domain.DelegateManager.RegisterMethodDelegate<long, int>();
domain.DelegateManager.RegisterMethodDelegate<long, MemoryStream>();
domain.DelegateManager.RegisterMethodDelegate<long, IPEndPoint>();
domain.DelegateManager.RegisterMethodDelegate<ILTypeInstance>();
domain.DelegateManager.RegisterMethodDelegate<AsyncOperation>();
// Register function delegates
domain.DelegateManager.RegisterFunctionDelegate<UnityAction>();
domain.DelegateManager.RegisterFunctionDelegate<object, ETTask>();
domain.DelegateManager.RegisterFunctionDelegate<ILTypeInstance, bool>();
domain.DelegateManager.RegisterFunctionDelegate<KeyValuePair<string, int>, string>();
domain.DelegateManager.RegisterFunctionDelegate<KeyValuePair<int, int>, bool>();
domain.DelegateManager.RegisterFunctionDelegate<KeyValuePair<string, int>, int>();
domain.DelegateManager.RegisterFunctionDelegate<List<int>, int>();
domain.DelegateManager.RegisterFunctionDelegate<List<int>, bool>();
domain.DelegateManager.RegisterFunctionDelegate<int, bool>();
domain.DelegateManager.RegisterFunctionDelegate<int, int, int>();
domain.DelegateManager.RegisterFunctionDelegate<KeyValuePair<int, List<int>>, bool>();
domain.DelegateManager.RegisterFunctionDelegate<KeyValuePair<int, int>, KeyValuePair<int, int>, int>();
// Register delegate converters
domain.DelegateManager.RegisterDelegateConvertor<UnityAction>(act =>
{
return new UnityAction(() => ((Action)act)());
});
domain.DelegateManager.RegisterDelegateConvertor<Comparison<KeyValuePair<int, int>>>(act =>
{
return new Comparison<KeyValuePair<int, int>>((x, y) =>
((Func<KeyValuePair<int, int>, KeyValuePair<int, int>, int>)act)(x, y));
});
// Register cross-binding adapters
RegisterAdapters(domain);
// Register CLR redirections for Json and Protobuf
LitJson.JsonMapper.RegisterILRuntimeCLRRedirection(domain);
PType.RegisterILRuntimeCLRRedirection(domain);
// Initialize CLR bindings
CLRBindings.Initialize(domain);
}
private static void RegisterAdapters(AppDomain domain)
{
domain.RegisterCrossBindingAdaptor(new IAsyncStateMachineClassInheritanceAdaptor());
}
}