Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Integrating ILRuntime for Hot Code Updates in Unity ET6

Tech May 15 1

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());
    }
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.