最近调到新项目工作,为了热更将代码移到Assets外部,打成dll给Unity使用,导致Unity无法检测到是否修改,每次修改代码都要使用VS进行手动编译,特别麻烦,有时候都忘了是否进行手动,导致的各种bug,所以简单写了一个小工具进行检测,基本原理是:
- 切换到Unity有执行的函数
- 读取本地缓存的每个脚本对应的md5码
- 依次读取脚本的md5和缓存的md5进行比较
- 有修改差异,新增都会检测到,但对于删除的可能检测不到
- 调用VS的devenv.com进行编译
至于VS的devenv.com编译以及编译指令可以自行百度学习,我使用的是VS2019,默认安装路径:C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/Common7/IDE
使用此工具需要安装vs,2015,2017,2019以及以上版本均可以,只不过没有测试,代码如下:
using Shark; using System; using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEditor; using System.IO; using System.Text; using System.Diagnostics; using Debug = UnityEngine.Debug; using System.Threading; ////// 从其他应用切换回unity界面时调用,自动编译Hotfix的代码(代码有改动时调用) /// 本脚本需挂到游戏物体上,最好是常用的场景,比如登陆场景,在其他场景无法执行到 /// [ExecuteInEditMode] public class AutoBuildScript : MonoBehaviour { //vs的默认安装路径 //[SerializeField] private string default_msbuild = "C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/Common7/IDE"; void Update() { //只在编辑模式下且无运行状态下进行编译 #if UNITY_EDITOR if (Application.isPlaying) return; CompilerHotfixDll(); #endif } private void CompilerHotfixDll() { #if UNITY_EDITOR if (!CheckChangeFiles()) return; if (Application.isPlaying) return; Debug.Log("检测到代码有变化,正在编译....."); string msbuild = GetMSbuildPath(); string path = Application.dataPath.Replace("\", "/").Replace("Assets", ""); string build = msbuild.Replace("\", "/") + "/devenv.com"; string slnPath = path + "hotfix/hotfix.sln"; string outpath = path.Replace("swGame", "") + "BuildLog"; if (!Directory.Exists(outpath)) Directory.CreateDirectory(outpath); outpath += "/hotfix_build_log.txt"; string parStr = slnPath + " /rebuild Release " + " /out " + outpath; Thread delay = new Thread(DelayBuild); delay.Start(new string[] { build , parStr, outpath }); #endif } ////// 开启线程延迟编译,否则会很卡 /// private void DelayBuild(object obj) { Thread.Sleep(50); string[] para = (string[])obj; Process process = new Process(); ProcessStartInfo info = new ProcessStartInfo(para[0], para[1]); info.UseShellExecute = false; info.RedirectStandardInput = false; info.RedirectStandardOutput = false; info.CreateNoWindow = true; process.StartInfo = info; process.Start(); while(!process.HasExited) process.WaitForExit(); int exitCode = process.ExitCode; process.Close(); if (exitCode == 0) UnityEngine.Debug.Log(DateTime.Now.ToString("[HH:mm:ss]") + "编译完成!!"); else//异常退出 UnityEngine.Debug.LogError(DateTime.Now.ToString("[HH:mm:ss]") +"ExitCode:"+ exitCode + " 编译失败!!!请检查热更代码或请查看日志:" + para[2]); } /// /// 检测代码是否被修改 /// 为了避免频繁的被执行到,有一个时间间隔的控制,距离上次编译5秒以内禁止编译 /// public bool CheckChangeFiles() { string oldTime = PlayerPrefs.GetString("AutoBuildCSFiles", "-1"); if (!oldTime.Equals("-1")) { long now = DateTime.Now.Ticks / 10000000; long diff = now - Int64.Parse(oldTime); if (diff <= 5 && diff >= -5) return false; else PlayerPrefs.SetString("AutoBuildCSFiles", now.ToString()); } else PlayerPrefs.SetString("AutoBuildCSFiles", (DateTime.Now.Ticks / 10000000).ToString()); //指定检测脚本路径 string path = Application.dataPath.Replace("\", "/").Replace("Assets", "hotfix/hotfix/iLScript"); if (!Directory.Exists(path)) return false; if (!CheckBuildTools()) return false; Dictionary md5 = GetMD5ByCache(); List files = GetAllFilesByPath(path); return CompilreMd5(files, md5); } /// /// 遍历获取指定路径所有文件 /// private List GetAllFilesByPath(string path) { List files = new List (); if (!Directory.Exists(path)) return files; string[] allfiles = Directory.GetFiles(path, "*.cs"); if (allfiles.Length > 0) files.AddRange(allfiles); string[] dirs = Directory.GetDirectories(path); if (dirs.Length == 0) return files; for (int i = 0; i < dirs.Length; i++) files.AddRange(GetAllFilesByPath(dirs[i])); return files; } /// /// 读取本地缓存的md5码 /// private Dictionary GetMD5ByCache() { Dictionary md5s = new Dictionary (); string path = Application.dataPath.Replace("\", "/").Replace("Assets", "hotfix/cache.md5"); if (!File.Exists(path)) { File.Create(path); return md5s; } using (StreamReader sr = new StreamReader(path, Encoding.UTF8)) { while (sr.Peek() != -1) { string[] line = sr.ReadLine().Trim().Split('t'); if (line.Length > 1) md5s.Add(line[0], line[1]); } sr.Dispose(); sr.Close(); } return md5s; } /// /// 最新的md5和缓存的md5进行对比,若有修改就更新缓存 /// private bool CompilreMd5(List files, Dictionary md5Dir) { bool changed = false; string path = Application.dataPath.Replace("\", "/").Replace("Assets", "hotfix/hotfix/iLScript/"); for (int i = 0; i < files.Count; i++) { string file = files[i].Replace("\", "/"); if (!File.Exists(file)) continue; string md = GetMD5FromStream(file); file = file.Replace(path, ""); if (md5Dir.ContainsKey(file)) { if (!md.Equals(md5Dir[file])) { md5Dir[file] = md; changed = true; } } else { md5Dir.Add(file, md); changed = true; } } //将最新的md5缓存到本地 if (changed && md5Dir.Count > 0) { string cache = Application.dataPath.Replace("\", "/").Replace("Assets", "hotfix/cache.md5"); using (StreamWriter sw = new StreamWriter(cache, false, Encoding.UTF8)) { var e = md5Dir.GetEnumerator(); while (e.MoveNext()) { sw.WriteLine(e.Current.Key + "t" + e.Current.Value); } sw.Flush(); sw.Dispose(); sw.Close(); } } return changed; } /// /// 获取指定文件的md5 /// public string GetMD5FromStream(string file) { FileStream fs = File.OpenRead(file); System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create(); byte[] targetData = md5.ComputeHash(fs); StringBuilder strBuilder = new StringBuilder(); for (int i = 0; i < targetData.Length; i++) { strBuilder.AppendFormat("{0:x2}", targetData[i]); } fs.Dispose(); fs.Close(); return strBuilder.ToString(); } /// /// 获取编译器的安装路径,是读取本地的配置文件,主要配置vs的安装路径 /// 默认安装路径:C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/Common7/IDE /// 路径需要定位到:xxx/xx/Common7/IDE /// private string GetMSbuildPath() { string path = Application.dataPath.Replace("\", "/").Replace("Assets", ""); string config = path + "hotfix/msbuild.config"; string msbuildpath = default_msbuild; if (File.Exists(config)) { using(StreamReader sr = new StreamReader(config, Encoding.UTF8)) { msbuildpath = sr.ReadToEnd().Replace("n", "").Trim(); sr.Dispose(); sr.Close(); } } else { using (StreamWriter sw = new StreamWriter(config,false, Encoding.UTF8)) { sw.WriteLine(msbuildpath); sw.Flush(); sw.Dispose(); sw.Close(); } } return msbuildpath; } /// /// 从本地配置的路径是否是正确,不正确需配置正确的路径 /// private bool CheckBuildTools() { string msbuild = GetMSbuildPath(); if (!Directory.Exists(msbuild)) { Debug.Log("自动编译检测失败n未找到VisualStudio安装路径, 可在xxxx/hotfix/msbuild.config 文件中配置正确的安装路径,默认路径:n" + default_msbuild); return false; } return true; } }
将上面的脚本挂载到场景中游戏对象(GameObject)上,才会生效 .除了VS的路径需要注意,涉及到其他的路径,均根据具体情况自行配置,比如:
- Assets外部C#项目的路径
- sln的路径,build输出的log路径
- md5缓存路径
- 编译器路径配置文件路径
结束!!2022年春节假期结束的工作第一天,祝大家新年快乐,工作顺利



