Game Development 12 min read

Implementing Lua‑Based Hot‑Update in Unity Without Rebuilding the Game

This article explains a practical approach to implementing hot‑update in Unity games by injecting Lua‑based patches into C# methods using SLua and the NRefactory library, detailing the execution environment, code‑injection process, Lua patch creation, and a reusable MethodInjector class.

ITPUB
ITPUB
ITPUB
Implementing Lua‑Based Hot‑Update in Unity Without Rebuilding the Game

Background

Unity projects often start with pure C# code. When a hot‑update is required, recompiling the whole project is costly. The article presents a technique that enables patching any C# method at runtime by injecting a call to a Lua script, without converting the entire codebase to Lua.

Hot‑Update Definition

Hot‑update is treated as a patching mechanism: if a patch script exists for a method, it is executed; otherwise the original C# implementation runs. Desired properties are the ability to modify code at any location, apply updates at runtime without restarting, reload instantly, and work in both development and production environments.

Implementation Overview

The solution consists of three steps: (1) set up the execution environment, (2) inject code into C# methods, and (3) write Lua patch scripts. Unity runs C# as the main language; Lua is used for patches via the SLua plugin, which provides reflection and script execution.

Step 1 – Execution Environment

SLua is integrated into the Unity project. A helper class PatchScript checks for the existence of a Lua file ( Script/Path.lua) and loads it with luaState.doScript, then calls the returned LuaFunction.

public bool HasPatchScript(string path)
{
    return File.Exists("Script/" + path + ".lua");
}

public void CallScript(string path)
{
    string scriptCode = File.ReadAllText(path);
    var luaFunc = luaState.doScript(scriptCode) as LuaFunction;
    luaFunc.call();
}

Step 2 – C# Code Injection

All C# source files are parsed with ICSharpCode.NRefactory. For each class and method the injector inserts a call to PatchScript.HasPatchScript at the beginning of the method body. If a patch exists, PatchScript.CallScript is invoked and the original method returns early; otherwise execution continues normally.

using (var script = new DocumentScript(document, formattingOptions, options))
{
    CSharpParser parser = new CSharpParser();
    SyntaxTree syntaxTree = parser.Parse(code, srcFilePath);
    foreach (var classDec in syntaxTree.Descendants.OfType<TypeDeclaration>())
    {
        if (classDec.ClassType == ClassType.Class || classDec.ClassType == ClassType.Struct)
        {
            var className = classDec.Name;
            foreach (var method in classDec.Children.OfType<MethodDeclaration>())
            {
                var returnType = method.ReturnType.ToString();
                if (returnType.Contains("IEnumerator") || returnType.Contains("IEnumerable"))
                    continue; // yield not supported

                var methodSegment = script.GetSegment(method);
                var methodOffset = methodSegment.Offset;

                // Optional before‑insertion
                if (_beforeInsert != null)
                {
                    var beforeText = _beforeInsert(className, method.Name, returnType,
                        GetParamNames(method), GetOutParamAssignments(method));
                    if (!string.IsNullOrEmpty(beforeText))
                        script.InsertText(methodOffset, beforeText);
                }

                // Insert after‑code at the start of the method body
                var firstStmt = method.Body.Statements.FirstOrDefault();
                int insertOffset = firstStmt != null ? script.GetSegment(firstStmt).Offset : methodOffset + 1;
                script.InsertText(insertOffset,
                    _afterInsert(className, method.Name, returnType,
                        GetParamNames(method), GetOutParamAssignments(method)));
            }
        }
    }
}

The injection logic is encapsulated in the MethodInjector class. It supports optional pre‑insertion, handling of method parameters, out‑parameter initialization, and conditional compilation symbols.

public class MethodInjector
{
    public delegate string CSharpMethodInjectorDelegate(string className,
        string methodName, string returnType, string[] parameters, string[] outParams);

    private readonly CSharpMethodInjectorDelegate _afterInsert;
    private readonly CSharpMethodInjectorDelegate _beforeInsert;
    private readonly string[] _defineSymbols;

    public MethodInjector(CSharpMethodInjectorDelegate afterInsert,
        CSharpMethodInjectorDelegate beforeInsert = null,
        string[] defineSymbols = null)
    {
        _afterInsert = afterInsert;
        _beforeInsert = beforeInsert;
        _defineSymbols = defineSymbols;
    }

    public void Inject(string srcFilePath, string outputFilePath)
    {
        // Read source, prepend #define directives if any
        // Parse with NRefactory, locate methods, and insert generated code
        // Write the modified source to outputFilePath
        // (Implementation details omitted for brevity)
    }

    // Helper methods GetParamNames, GetOutParamAssignments, etc. are omitted.
}

Step 3 – Lua Patch Script

For each method that needs to be patched, create a Lua file whose name matches the fully‑qualified method name (e.g., Fucker.Fucking.lua). The file returns a Lua function that implements the new behavior.

-- File: Fucker.Fucking.lua
function Func()
    print("I am a patch")
end
return Func

The injected C# code checks PatchScript.HasPatchScript("Fucker.Fucking"). If true, it loads the Lua file, calls the returned function, and returns from the original method, effectively replacing its behavior at runtime.

Limitations and Extensions

The injector skips methods that return IEnumerable or IEnumerator because yield statements are not supported.

Current implementation injects C# source code; a more robust approach would inject IL directly into compiled assemblies.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

LuaUnityHot UpdateCode InjectionMethodInjectorNRefactoryC#
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.