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.
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 FuncThe 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
