目录
前言:
本人刚刚接触编程的菜鸟,很少写过文章,在这里记录学习笔记并跟大家分享学习经验,
如有不对的地方,愿不吝赐教。
unity C#代码,在运行的时候,先把所有代码(框架+逻辑)都加载到内存,然后执行代码,所以代码加载完成,就无法再改了, 所以普通的C#代码,无法做代码热更新。
代码热更新的基本原理是内置一个代码解释器,游戏app运行的时候,先把底层C#代码与代码解释器运行起来,进入入口函数后,再加载游戏逻辑代码,加载完成,再使用解释器来解释执行 “逻辑代码”。这样就有机会在入口函数的时候,先到云端的服务器上把最新的逻辑代码下载下来,然后再加载到内存,完成加载后,跑最新的代码逻辑。
搞懂的热更原理以后,问题就变成了,内置哪个脚本语言的解释器?使用哪个脚本语言开发。游戏行业和其他行业不一样,大部分的核心功能都在游戏引擎代码中,而游戏引擎的代码,基本上每个引擎各成一体,没有统一的标准, 所以做为游戏行业的脚本语言,就需要2大特点:高效,轻量级。高效指的是脚本语言执行高效,轻量级指的是脚本语言很容易嵌入游戏引擎,同时脚本的体系不大,没有过多的与语言本身无关的系统和库内置在里面。满足这两大特点,的脚本语言其实不多。有一个编程语言,在它创立之初就是保持这2大理念,在2001年左右的时候被魔兽世界选为脚本语言,而风靡游戏行业,这个语言就是Lua。当Unity 需要做热更新的时候(2013年开始),而普通的C#又做不到的时候,而对于游戏行业来说Lua脚本热更新已经是很成熟的方案,自然Lua 热更新就成为了Unity热更新的首选。
正题:
要实现热更新我们需要做以下几点:
1.一个服务器来存放资源
2.一套打包工具(包含打AB包,生成对比文件)
3.一个从服务器下载资源的脚本 AssetBundleDownloader
4.腾讯的Xlua插件(连接虚拟机,在lua脚本上写游戏逻辑,以便实现代码热更)
实现思路:
在准备工作齐全后,我们先要打包游戏中所需要的资源(打包的同时生成这一批资源的version版本文件),至于游戏里资源的加载和使用,可以封装一个ABManager用来加载P目录的资源,打包好之后把打好的AB包上传到资源服务器,游戏中监选资源服务器的变动(这里我简单实现一下,只在游戏开始时对比版本文件判断是否需要更新)。如果需要更新,把资源下载到P目录就实现了资源热更新。
打包工具 Builder:
using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System;
/// <summary>
/// 把Resource下的资源打包成.unity3d 到StreamingAssets目录下
/// </summary>
public class Builder : Editor
{
public static string ResPath = Application.dataPath + "/Resources";
const string AssetBundlesOutputPath = "Assets/StreamingAssets";
[MenuItem("Tools/Builder/一键打包")]
public static void BuildAssetBundle()
{
//ClearAssetBundlesName();
List<string> all=new List<string>();
string resPath=Path.Combine(Application.dataPath, "Resources");
//获取Resources目录下所有文件
string[] allfiles=Directory.GetFiles(resPath, "*",SearchOption.AllDirectories);
foreach (var item in allfiles)
{
//Path.GetExtension返回指定的路径字符串的扩展名
if (!Path.GetExtension(item).Equals(".meta"))
{
string temp=Path.Combine(Application.dataPath,"Resources\\");
string p = item.Replace(temp, "");
all.Add(p);
}
}
AssetBundleBuild[] buildMap = new AssetBundleBuild[all.Count];
for(int i = 0; i < all.Count; i++)
{
buildMap[i].assetBundleName = all[i].Substring(0, all[i].LastIndexOf('.'));
buildMap[i].assetBundleVariant = "u3d";
buildMap[i].assetNames = new string[] { "Assets/Resources/" + all[i] };
}
string outputPath = Path.Combine(AssetBundlesOutputPath, Platform.GetPlatformFolder(EditorUserBuildSettings.activeBuildTarget));
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}
//根据BuildSetting里面所激活的平台进行打包
BuildPipeline.BuildAssetBundles(outputPath, buildMap,0, EditorUserBuildSettings.activeBuildTarget);
CreateVerisonTxt();
AssetDatabase.Refresh();
Debug.Log("打包完成");
}
/// <summary>
/// 清除之前设置过的AssetBundleName,避免产生不必要的资源也打包
/// 之前说过,只要设置了AssetBundleName的,都会进行打包,不论在什么目录下
/// </summary>
[MenuItem("Tools/Builder/清空包名")]
public static void ClearAssetBundlesName()
{
int length = AssetDatabase.GetAllAssetBundleNames().Length;
Debug.Log("请空前:"+length);
string[] oldAssetBundleNames = new string[length];
for (int i = 0; i < length; i++)
{
oldAssetBundleNames[i] = AssetDatabase.GetAllAssetBundleNames()[i];
}
for (int j = 0; j < oldAssetBundleNames.Length; j++)
{
AssetDatabase.RemoveAssetBundleName(oldAssetBundleNames[j], true);
}
length = AssetDatabase.GetAllAssetBundleNames().Length;
Debug.Log("清空后:"+length);
}
static string Replace(string s)
{
return s.Replace("\\", "/");
}
[MenuItem("Tools/Builder/按包名打包")]
public static void BuildPackByName()
{
string outPutPath = Path.Combine(AssetBundlesOutputPath, Platform.GetPlatformFolder(EditorUserBuildSettings.activeBuildTarget));
if (!Directory.Exists(outPutPath))
{
Directory.CreateDirectory(outPutPath);
}
BuildPipeline.BuildAssetBundles(outPutPath, 0, EditorUserBuildSettings.activeBuildTarget);
//CreateVerisonTxt();
AssetDatabase.Refresh();
}
/// <summary>
/// 一键设置AB包名
/// </summary>
[MenuItem("Tools/Builder/设置所选资源的AB包名")]
public static void SetABName()
{
//返回当前选中的所有对象(Project目录下的)
UnityEngine.Object[] selects = Selection.objects;
foreach (var item in selects)
{
string path = AssetDatabase.GetAssetPath(item);
//把一个目录的对象检索为AssetImporter
AssetImporter asset = AssetImporter.GetAtPath(path);
asset.assetBundleName = item.name;
asset.assetBundleVariant = "u3d";
asset.SaveAndReimport();
}
AssetDatabase.Refresh();
}
[MenuItem("Tools/Builder/打开P目录")]
public static void OpenPml()
{
System.Diagnostics.Process.Start(Application.persistentDataPath);
//EditorUtility.OpenFilePanel("P目录", Application.persistentDataPath, "");
}
[MenuItem("Tools/Builder/生成索引文件")]
public static void CreateVerisonTxt()
{
string[] allFiles= Directory.GetFiles(Application.streamingAssetsPath, "*.*", SearchOption.AllDirectories);
StringBuilder sb= new StringBuilder();
sb.Append(Application.version + "\r\n");
foreach (string file in allFiles)
{
if (!Path.GetExtension(file).Contains(".meta")&&!Path.GetExtension(file).Contains(".manifest"))
{
string tmp = file.Split(new string[] { "StreamingAssets\\" }, System.StringSplitOptions.RemoveEmptyEntries)[1];
FileInfo fio = new FileInfo(file);
sb.Append(tmp + "|" + fio.Length +"|"+ GetMD5(file).Replace("-","") + "\r\n");
}
}
File.WriteAllText(Application.streamingAssetsPath + "/Windows/version.txt", sb.ToString());//
AssetDatabase.Refresh();
}
public static string GetMD5(string path)
{
byte[] bytes= File.ReadAllBytes(path);
System.Security.Cryptography.MD5 md5= new System.Security.Cryptography.MD5CryptoServiceProvider();
byte[] data= md5.ComputeHash(bytes);
return BitConverter.ToString(data);
}
}
public class Platform
{
public static string GetPlatformFolder(BuildTarget target)
{
switch (target)
{
case BuildTarget.Android:
return "Android";
case BuildTarget.iOS:
return "IOS";
case BuildTarget.WebGL:
return "WebGL";
case BuildTarget.StandaloneWindows:
case BuildTarget.StandaloneWindows64:
return "Windows";
case BuildTarget.StandaloneOSX:
return "OSX";
default:
return null;
}
}
}
资源下载 AssetBundleDownloader:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.Networking;
using System.IO;
using System.Text;
public class AssetBundleDownloader : MonoBehaviour
{
private Version local_version;
private Version sever_version;
private string localURL = string.Empty;
private string severURL = string.Empty;
private Dictionary<string, ABitem> dic_local;
private Dictionary<string, ABitem> dic_sever;
private Queue<ABitem> Que_ABitem;
private string SavePath=string.Empty;
private void Awake()
{
localURL = Application.persistentDataPath + "/Windows/";
severURL = "http://127.0.0.1/";
dic_local = new Dictionary<string, ABitem>();
dic_sever = new Dictionary<string, ABitem>();
Que_ABitem = new Queue<ABitem>();
}
// Start is called before the first frame update
void Start()
{
string localPath = localURL + "version.txt";
if(File.Exists(localPath))
{
string[] arr = File.ReadAllLines(localPath);
local_version = Version.Get(arr[0]);
for (int i = 1; i < arr.Length; i++)
{
ABitem ab = new ABitem();
ab.name = arr[i].Split('|')[0];
ab.len = int.Parse(arr[i].Split('|')[1]);
ab.md5 = arr[i].Split('|')[2];
dic_local.Add(ab.name, ab);
}
}
string sever = severURL + "Windows/version.txt";
DownLoadAB(sever, (data) =>
{
SavePath = Encoding.UTF8.GetString(data);
string[] arr = SavePath.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
sever_version = Version.Get(arr[0]);
for (int i = 1; i < arr.Length; i++)
{
ABitem ab = new ABitem();
ab.name = arr[i].Split('|')[0];
ab.len = int.Parse(arr[i].Split('|')[1]);
ab.md5 = arr[i].Split('|')[2];
dic_sever.Add(ab.name, ab);
}
if(local_version!=null)
{
if(local_version.Big!=sever_version.Big)
{
print("强制更新");
}
else if(local_version.Small<sever_version.Small)
{
print("开始更新");
GetNeedDownLoadABitem();
DownLoadABitem();
}
else
{
EnterGame();
}
}
else
{
GetNeedDownLoadABitem();
DownLoadABitem();
}
});
}
private void EnterGame()
{
print("进入游戏");
}
private void DownLoadABitem()
{
if(Que_ABitem.Count>0)
{
ABitem ab = Que_ABitem.Dequeue();
string path = severURL + "/" + ab.name;
DownLoadAB(path, (data) =>
{
string localsave = Application.persistentDataPath + "/" + ab.name;
string localDir = Path.GetDirectoryName(localsave);
if(!Directory.Exists(localDir))
{
Directory.CreateDirectory(localDir);
}
File.WriteAllBytes(localsave, data);
if(Que_ABitem.Count>0)
{
DownLoadABitem();
}
else
{
File.WriteAllText(localURL + "version.txt", SavePath);
EnterGame();
}
});
}
}
private void GetNeedDownLoadABitem()
{
foreach (var item in dic_sever.Values)
{
dic_local.TryGetValue(item.md5, out ABitem ab);
if (ab != null)
{
if(ab.md5!=item.md5)
{
Que_ABitem.Enqueue(ab);
}
}
else
{
Que_ABitem.Enqueue(item);
}
}
}
// Update is called once per frame
void Update()
{
}
public void DownLoadAB(string url,Action<byte[]> completeCallBack)
{
StartCoroutine(Run(url, completeCallBack));
}
IEnumerator Run(string url, Action<byte[]> completeCallBack)
{
UnityWebRequest www = UnityWebRequest.Get(url);
yield return www.SendWebRequest();
if(!(www.isNetworkError || www.isHttpError))
{
completeCallBack?.Invoke(www.downloadHandler.data);
}
else
{
print(www.error);
}
}
}
public class Version
{
public int Big;
public int Small;
public static Version Get(string str)
{
Version version = new Version();
version.Big = int.Parse(str.Split('.')[0]);
version.Small = int.Parse(str.Split('.')[1]);
return version;
}
}
public class ABitem
{
public string name;
public int len;
public string md5;
}
至于代码热更,和资源热更同样思路。我们先打包资源,把lua脚本的文件夹放在AB包文件夹下面
。这样我们在修改脚本中的游戏逻辑后,然后生成版本文件,就会对应生成lua脚本的MD5码。
我们只需要把原本lua连接虚拟机部分的连接路径改为P目录下的lua接口在,对比完版本文件后把新的资源(包含lua脚本)下载下来,就实现了脚本热更
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;
/// <summary>
/// 连接虚拟机
/// </summary>
public class LuaManager : MonoBehaviour
{
LuaEnv m_le;
Action m_Start, m_Update;
void Start()
{
m_le=new LuaEnv();
m_le.AddLoader(GetBytes);
m_le.DoString("require'Main'");
m_Start = m_le.Global.GetInPath<Action>("Start");
m_Update = m_le.Global.GetInPath<Action>("Update");
m_Start.Invoke();
}
private byte[] GetBytes(ref string filepath)
{
//连接p目录下的lua脚本
string path = Application.persistentDataPath + "/Windows/LuaScripts/" + filepath + ".lua";
if(File.Exists(path))
{
return File.ReadAllBytes(path);
}
return null;
}
// Update is called once per frame
void Update()
{
m_Update?.Invoke();
}
}
xlua热更新基本实现,再次感谢