最近调到新项目工作,为了热更将代码移到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;
/// <summary>
/// 从其他应用切换回unity界面时调用,自动编译Hotfix的代码(代码有改动时调用)
/// 本脚本需挂到游戏物体上,最好是常用的场景,比如登陆场景,在其他场景无法执行到
/// </summary>
[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
}
/// <summary>
/// 开启线程延迟编译,否则会很卡
/// <summary>
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]);
}
/// <summary>
/// 检测代码是否被修改
/// 为了避免频繁的被执行到,有一个时间间隔的控制,距离上次编译5秒以内禁止编译
/// <summary>
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<string, string> md5 = GetMD5ByCache();
List<string> files = GetAllFilesByPath(path);
return CompilreMd5(files, md5);
}
/// <summary>
/// 遍历获取指定路径所有文件
/// <summary>
private List<string> GetAllFilesByPath(string path)
{
List<string> files = new List<string>();
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;
}
/// <summary>
/// 读取本地缓存的md5码
/// <summary>
private Dictionary<string, string> GetMD5ByCache()
{
Dictionary<string, string> md5s = new Dictionary<string, string>();
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;
}
/// <summary>
/// 最新的md5和缓存的md5进行对比,若有修改就更新缓存
/// <summary>
private bool CompilreMd5(List<string> files, Dictionary<string, string> 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;
}
/// <summary>
/// 获取指定文件的md5
/// <summary>
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();
}
/// <summary>
/// 获取编译器的安装路径,是读取本地的配置文件,主要配置vs的安装路径
/// 默认安装路径:C:/Program Files (x86)/Microsoft Visual Studio/2019/Enterprise/Common7/IDE
/// 路径需要定位到:xxx/xx/Common7/IDE
/// <summary>
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;
}
/// <summary>
/// 从本地配置的路径是否是正确,不正确需配置正确的路径
/// <summary>
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年春节假期结束的工作第一天,祝大家新年快乐,工作顺利