Unity 热更新的几个方案

刚刚接触到热更新这块,感觉作为一个独立开发者的尊严和荣光都没了.....
以下是几个思路。

1. 脚本存为 TextAsset,和其余所有内容一起放到 Asset Bundle 里。客户端拿到 Asset Bundle 后用 Reflection 创建 type。

Link: http://docs.unity3d.com/Manual/scriptsinassetbundles.html

AssetBundle bundle = www.assetBundle;
// Load the TextAsset object
TextAsset txt = bundle.Load("myBinaryAsText", typeof(TextAsset)) as TextAsset;
// Load the assembly and get a type (class) from it
var assembly = System.Reflection.Assembly.Load(txt.bytes);
var type = assembly.GetType("MyClassDerivedFromMonoBehaviour");
// Instantiate a GameObject and add a component with the loaded class
GameObject go = new GameObject();
go.AddComponent(type);

好处:方便。
坏处:不能用 Prefab 存脚本的 Public 属性值,因为所有的脚本都要在读取 Asset Bundle 后重新挂载到 GameObject 上去。

2. Downloading the Hydra

Link: http://angryant.com/2010/01/05/downloading-the-hydra/
Angry Ant 在 10 年的办法。大体就是预先编译好 DLL,然后客户端去拿 DLL,然后运行时的时候加载。
好处:脚本不用改为 TextAsset
坏处:仍然不能保留 Scripts 在 Scene / Prefab 里面的 Reference,即:如果你预先通过 Asset Bundle 加载了一个场景到内存然后再加载一个包含场景需要脚本的 DLL 到内存(或者顺序相反,反正结果一样),这个时候 LoadScene 的结果是场景所有的 Script 仍然会显示为 Missing。Assembly.Load () 是可以加载到运行时,但是不代表 Unity 能认出这个 type。
相关讨论

3. Runtime Evaluating / Compiling CSharp

这个比较奇技淫巧,而且不支持 AOT,不过 Android 上就无所谓了。
具体有好几种思路:
第一种,用 Mono.CSharp.Run / Mono.CSharp.Evaluate

int cnt = 0;
while (cnt < 2)
{
	// this needs to be run twice
	foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) {
		if (assembly == null) {
			UnityEngine.Debug.Log ("Null Assembly");
			continue;
		}
		UnityEngine.Debug.Log (assembly);
		try {
			Mono.CSharp.Evaluator.ReferenceAssembly (assembly);
		} catch (NullReferenceException e) {
			UnityEngine.Debug.Log ("Bad Assembly");
		}
	}
	cnt++;
}
Mono.CSharp.Evaluator.Run("using UnityEngine;");
Mono.CSharp.Evaluator.Evaluate ("1+2;");

第二种,用 Mono.CSharp.Compile

var evaluator = new Evaluator(
                new CompilerSettings(),
                new Report(new ConsoleReportPrinter()));

            // Make it reference our own assembly so it can use IFoo
            evaluator.ReferenceAssembly(typeof(IFoo).Assembly);

            // Feed it some code
            evaluator.Compile(
                @"
    public class Foo : MonoCompilerDemo.IFoo
    {
        public string Bar(string s) { return s.ToUpper(); }
    }");

            for (; ; )
            {
                string line = Console.ReadLine();
                if (line == null) break;

                object result;
                bool result_set;
                evaluator.Evaluate(line, out result, out result_set);
                if (result_set) Console.WriteLine(result);
            }

参见:http://blog.davidebbo.com/2012/02/quick-fun-with-monos-csharp-compiler-as.html

第三种,用System.CodeDom.Compiler / System.Reflection.Emit

CSharpCodeProvider provider = new CSharpCodeProvider();
			CompilerParameters parameters = new CompilerParameters();

			foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
			{
				//Dbg.Log("refer: {0}", assembly.FullName);
				if( assembly.FullName.Contains("Cecil") || assembly.FullName.Contains("UnityEngine") )
					continue;
				parameters.ReferencedAssemblies.Add(assembly.Location);
			}
			parameters.IncludeDebugInformation = true;
			// Reference to System.Drawing library
//			parameters.ReferencedAssemblies.Add ("System.dll");
//			parameters.ReferencedAssemblies.Add ("System.Core.dll");
			parameters.ReferencedAssemblies.Add (typeof(MonoBehaviour).Assembly.Location);
			//parameters.ReferencedAssemblies.Add("/Applications/Unity/Unity.app/Contents/Frameworks/Managed/UnityEngine.dll");
			// True - memory generation, false - external file generation
			parameters.OutputAssembly = "AutoGen.dll";
			parameters.GenerateInMemory = false;
			// True - exe file generation, false - dll file generation
			parameters.GenerateExecutable = false;

			CompilerResults results = provider.CompileAssemblyFromSource(parameters,scripts);
			if (results.Errors.HasErrors)
			{
				foreach (CompilerError error in results.Errors)
				{
					UnityEngine.Debug.LogError("Error " + error.ErrorNumber + error.ErrorText);
				}
			}

上面是 CodeDom 的例子,不过 Unity 似乎阉割了这个部分,一直报错,我没有在这个方向再往下看。(Edit : GHOST_URL/c-repl-fun/) 至于 CodeDom 和 Emit 的对比,参见:http://stackoverflow.com/questions/2366921/reflection-emit-vs-codedom

以上所有 Runtime 的方法:
好处:可以传 string 执行方法
坏处:不够稳定,以及要自己造一套轮子

4. sLua / uLua / UniLua

好处:可以传 string 执行方法
坏处:效率低(不过可以接受啦),以及不是自己的轮子,有时候不是很好 Debug。