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。