从其他应用切换回Unity使用VS的devenv.com自动编译Assets外部的C#工程(需含有.sln)

        最近调到新项目工作,为了热更将代码移到Assets外部,打成dll给Unity使用,导致Unity无法检测到是否修改,每次修改代码都要使用VS进行手动编译,特别麻烦,有时候都忘了是否进行手动,导致的各种bug,所以简单写了一个小工具进行检测,基本原理是:

  1. 切换到Unity有执行的函数
  2. 读取本地缓存的每个脚本对应的md5码
  3. 依次读取脚本的md5和缓存的md5进行比较
  4. 有修改差异,新增都会检测到,但对于删除的可能检测不到
  5. 调用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年春节假期结束的工作第一天,祝大家新年快乐,工作顺利

  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值