众所周知,在Unity中每次我们修改C#代码后,Unity都要转一会菊花,而转菊花的目的就是把新的代码重新编译成dll。
而且在Unity中其实有一个运行时编译dll的选项,在preferences内,但是这个选项仍然不能完全满足我们的需求,因为他还是要转菊花,而且如果脚本修改了当前使用的物体,极大概率会报错999+的~
1.CodeDomProvider与动态编译
CodeDomProvider可以帮我们动态编译dll,而这个类使用起来也很简单:这里提供了一个简单的例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// 设定编译参数,DLL代表需要引入的Assemblies CompilerParameters cplist = new CompilerParameters(); cplist.GenerateExecutable = false; // 生成后直接加载到内存中 cplist.GenerateInMemory = true; // 添加其他程序集的引用 cplist.ReferencedAssemblies.Add("System.dll"); cplist.ReferencedAssemblies.Add("System.XML.dll"); cplist.ReferencedAssemblies.Add("System.Data.dll"); // 编译代理类,C# CSharp都可以 CodeDomProvider provider1 = CodeDomProvider.CreateProvider("CSharp"); // 文件数组,我只需要一个file string[] sources = new string[1]; sources[0]=@"d:\TableModel.cs"; // 开始编译咯!从文件编译!因为上面我们传的文件嘛 CompilerResults cr = provider1.CompileAssemblyFromFile(cplist, sources); if (true == cr.Errors.HasErrors) { System.Text.StringBuilder sb = new System.Text.StringBuilder(); foreach (System.CodeDom.Compiler.CompilerError ce in cr.Errors) { sb.Append(ce.ToString()); sb.Append(System.Environment.NewLine); } throw new Exception(sb.ToString()); } // 获得类的Type, 如果要生成实例,需要调用assembly.CreateInstance System.Reflection.Assembly assembly = cr.CompiledAssembly; Type t = assembly.GetType(@namespace + "." + "TableModel", true, true); |
从上面的代码中我们可以看出来,整个的编译流程其实只涉及三个类型:CodeDomProvider、CompilerParameters和CompilerResults。其中:
- CodeDomProvider为编译代理类,其实就是这个类型负责编译~
- CompilerParameters为编译时需要的参数,包括了这个类型需要依赖哪些其他程序集,是否加载到内存等。
- CompilerResults为最终编译后的结果
2.Unity与动态编译
上面动态编译的大概流程我们已经了解了,那么在Unity内如何进行动态编译呢?毕竟上面在编译时已经知道新的程序集依赖了哪些程序集。
不过早就有大佬已经实现了:Unity动态编译
效果:
而且偷完代码发现可以直接用~不过还是踩到了几个坑:
1 2 3 4 5 6 7 8 9 10 11 12 |
if (_compileParams == null) { _compileParams = new CompilerParameters(); // Add ALL of the assembly references foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a=>!a.IsDynamic)) { _compileParams.ReferencedAssemblies.Add(assembly.Location); } _compileParams.GenerateExecutable = false; _compileParams.GenerateInMemory = true; } |
在获取依赖的程序集时:AppDomain.CurrentDomain.GetAssemblies()
,这里获取到的程序集包含了其他的动态程序集,而动态程序集是无法依赖其他动态程序集的,否则会报错:The invoked member is not supported in a dynamic module.
所以在源代码中用Linq清理了一下动态程序集~
此外源代码中将程序集生成到了temp文件夹下,并没有直接加载到内存中。所以还要后面loaddll,但是我嫌麻烦直接加到内存了~_compileParams.GenerateInMemory = true;
完整代码:
DynamicCodeHelper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
using Microsoft.CSharp; using System; using System.CodeDom.Compiler; using System.Linq; using System.Reflection; using System.Text; using UnityEngine; public class DynamicCodeHelper { private CSharpCodeProvider _provider; private CSharpCodeProvider Provider { get { if (_provider == null) { _provider = new CSharpCodeProvider(); } return _provider; } } private CompilerParameters _compileParams; private CompilerParameters CompileParams { get { if (_compileParams == null) { _compileParams = new CompilerParameters(); // Add ALL of the assembly references foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies().Where(a=>!a.IsDynamic)) { _compileParams.ReferencedAssemblies.Add(assembly.Location); } _compileParams.GenerateExecutable = false; _compileParams.GenerateInMemory = true; } _compileParams.OutputAssembly = DynamicCodeWindow.OUTPUT_DLL_DIR + "/DynamicCodeTemp" + Time.realtimeSinceStartup + ".dll"; return _compileParams; } } public void ExcuteDynamicCode(string codeStr, bool isUseTextAsAllContent) { if (codeStr == null) codeStr = ""; string generatedCode; if (isUseTextAsAllContent) { generatedCode = codeStr; } else { generatedCode = GenerateCode(codeStr); } Debug.Log("[DynamicCode] Compile Start: " + generatedCode); CompilerResults compileResults = Provider.CompileAssemblyFromSource(CompileParams, generatedCode); if (compileResults.Errors.HasErrors) { Debug.LogError("[DynamicCode] 编译错误!"); var msg = new StringBuilder(); foreach (CompilerError error in compileResults.Errors) { msg.AppendFormat("Error ({0}): {1}\n", error.ErrorNumber, error.ErrorText); } throw new Exception(msg.ToString()); } // 通过反射,调用DynamicCode的实例 //AppDomain a = AppDomain.CreateDomain(AppDomain.CurrentDomain.FriendlyName); Assembly objAssembly = compileResults.CompiledAssembly; DynamicCodeWindow.ColorDebug("[DynamicCode] Gen Dll FullName: " + objAssembly.FullName); DynamicCodeWindow.ColorDebug("[DynamicCode] Gen Dll Location: " + objAssembly.Location); DynamicCodeWindow.ColorDebug("[DynamicCode] PathToAssembly: " + compileResults.PathToAssembly); object objDynamicCode = objAssembly.CreateInstance("DynamicCode"); MethodInfo objMI = objDynamicCode.GetType().GetMethod("CodeExecute"); objMI.Invoke(objDynamicCode, null); } private string GenerateCode(string methodCode) { StringBuilder sb = new StringBuilder(); sb.Append(@"using System; using UnityEngine; public class DynamicCode { public void CodeExecute() { "); sb.Append(methodCode); sb.Append("}}"); string code = sb.ToString(); return code; } } |
DynamicCodeWindow:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
#if UNITY_EDITOR_WIN using UnityEditor; using UnityEngine; /// <summary> /// 字符串编译成DLL载入,只在编辑器中使用 /// </summary> public class DynamicCodeWindow : EditorWindow { // 生成在 ..\Client\Client\Temp\DynamicCode\DynamicCodeTemp.dll public const string OUTPUT_DLL_DIR = @"Temp\DynamicCode"; [MenuItem("TestTool/DynamicRun")] private static void Open() { GetWindow<DynamicCodeWindow>(); } private static DynamicCodeHelper _instance; private static DynamicCodeHelper Helper { get { if (_instance == null) { _instance = new DynamicCodeHelper(); } return _instance; } } private bool isUseTextAsAllContent; private string content = @"Debug.Log(""Hello"");"; private void OnGUI() { isUseTextAsAllContent = EditorGUILayout.ToggleLeft("完全使用文本作为编译内容(手动添加using等)", isUseTextAsAllContent); content = EditorGUILayout.TextArea(content, GUILayout.Height(200)); if (GUILayout.Button("执行代码")) { Run(content, isUseTextAsAllContent); } if (GUILayout.Button("重置内容")) { if (isUseTextAsAllContent) { content = @"using System; using UnityEngine; public class DynamicCode { public void CodeExecute() { Debug.Log(""Hello""); } }"; } else { content = @"Debug.Log(""Hello"");"; } } if (GUILayout.Button("新建/打开缓存目录")) { if (!System.IO.Directory.Exists(OUTPUT_DLL_DIR)) { System.IO.Directory.CreateDirectory(OUTPUT_DLL_DIR); } System.Diagnostics.Process.Start(OUTPUT_DLL_DIR); } } private static void Run(string code, bool isUseTextAsAllContent) { ColorDebug("[DynamicCode] Start......"); string codetmp = code; Helper.ExcuteDynamicCode(codetmp, isUseTextAsAllContent); ColorDebug("[DynamicCode] End......"); } public static void ColorDebug(string content) { Debug.Log(string.Format("<color=#ff8400>{0}</color>", content)); } } #endif |
3.动态编译+Unity Hooker
其实动态编译到这已经差不多可以结束了,但是我们之前还研究过一下下UnityHooker。那么我们能不能把这两个东西结合一下呢?
我们调试的时候经常会有逻辑十分复杂,但是又不知道是什么逻辑导致的一些比如GameObject不知道被谁给激活/隐藏了。那么我们的动态编译与Hooker就可以大显身手了!
就像上图一样,有一段莫名其妙的逻辑一直在开关某个预制体(当然是我随便写的!),但是我们又一时之间找不到对应逻辑在哪,或者说想在SetActive的时候插入一段逻辑。那么只要把我们在一边写好的Hook逻辑(当然是编辑器里写好的复制过来的)逻辑动态编译进来,就可以动态的添加代码,调试代码了!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/* * 对 GameObject.SetActive 进行hook的测试用例 */ using System; using System.Reflection; using UnityEngine; using Hook; //[InitializeOnLoad] public class DynamicCode { private static MethodHook _hook; public void CodeExecute() { if (_hook == null) { Type type = typeof(GameObject).Assembly.GetType("UnityEngine.GameObject"); MethodInfo miTarget = type.GetMethod("SetActive", BindingFlags.Instance | BindingFlags.Public); type = typeof(DynamicCode); MethodInfo miReplacement = type.GetMethod("SetActiveNew", BindingFlags.Static | BindingFlags.NonPublic); MethodInfo miProxy = type.GetMethod("SetActiveProxy", BindingFlags.Static | BindingFlags.NonPublic); _hook = new MethodHook(miTarget, miReplacement, miProxy); Debug.Log("Hooked"); _hook.Install(); } } private static void SetActiveNew(GameObject go, bool value) { SetActiveProxy(go, value); Debug.LogFormat("[Hooked] [{0}] SetActive {1}", go.name, value); } private static void SetActiveProxy(GameObject go, bool value) { // dummy } } |
Hook的逻辑也很简单,之前也有介绍过了,就是使用两个函数替换原有函数地址。只不过前面的动态编译代码中限定了类名和执行的函数名,这里把对应的函数替换了一下就可以用了~