assetbundle打包
打包使用assetbundle browser 插件即可,具体打包流程可参考Unity3d的AssetBundle打包,这个工具的好处在于方便打包,能更好的解决资源依赖复用的问题。
打包lua文件
unity是默认不支持.lua文件类型的,所以也不能对后缀为.lua的文件打包,但是unity官方是有相关接口的。具体实现代码如下ST_LuaImporter.cs
[ScriptedImporter(1, ".lua")]
public class ST_LuaImporter : ScriptedImporter
{
public override void OnImportAsset(AssetImportContext ctx)
{
var luaTxt = File.ReadAllText(ctx.assetPath);
var assetText = new TextAsset(luaTxt);
ctx.AddObjectToAsset("main obj", assetText);
//将对象assetText作为导入操作的主要对象。
ctx.SetMainObject(assetText);
}
}
将此代码放到Editor目录下即可正常打包lua文件。
开始打包
创建一个Cube预制体并给他绑定一个Lua脚本,设置他的bundle标签model
至于Cube对象是如何绑定TestMono脚本的,可以参见文章Unity + XLua 简单框架搭建的详细过程,这里不做过多描述
TestMono.lua文件内容如下
local TestMono = class()
-- 自定义数据
-- 构造函数
function TestMono:Ctor(params)
end
-- 开始函数
function TestMono:Start()
end
function TestMono:Update()
--self.transform:Rotate(CS.UnityEngine.Vector3(0,1,0));
self.transform:Rotate(CS.UnityEngine.Vector3(1,0,0));
end
return TestMono
同样给TestMono脚本打上lua标签
开始打包
打包输出路径是自定的,可上传至服务器方便热更新,这个后面介绍。压缩方式随意,勾选拷贝到StreamAssets方便本地测试(如果用服务器数据,那么不勾选)。打包成功如下
可以看到除了lua包和model包还有一个StandloneWindows包,这个包里面包含了bundle各个模块的信息以及依赖。Md5FileList.txt为了方便后面做热更对比文件对比而生成的,打包时生成这个文件需要修改assetbundle browser源码,具体修改如下
在assetbundle browser找到AssetBundleBuildTab文件,添加以下代码
/// <summary>
/// Create FileList
/// </summary>
void CreateFileList()
{
string filePath = m_UserData.m_OutputPath + "\\Md5FileList.txt";
if (File.Exists(filePath))
{
File.Delete(filePath);
}
StreamWriter streamWriter = new StreamWriter(filePath);
string[] files = Directory.GetFiles(m_UserData.m_OutputPath);
for (int i = 0; i < files.Length; i++)
{
string tmpfilePath = files[i];
if (tmpfilePath.Equals(filePath) || tmpfilePath.EndsWith(".manifest"))
continue;
Debug.Log(tmpfilePath);
tmpfilePath.Replace("\\", "/");
streamWriter.WriteLine(tmpfilePath.Substring(m_UserData.m_OutputPath.Length + 1) + "|" + GetFileMD5(tmpfilePath));
}
streamWriter.Close();
streamWriter.Dispose();
}
/// <summary>
/// 获取文件的MD5
/// </summary>
string GetFileMD5(string filePath)
{
System.Security.Cryptography.MD5 MD5 = new System.Security.Cryptography.MD5CryptoServiceProvider();
System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();
FileStream fileStream = new FileStream(filePath, FileMode.Open);
byte[] bytes = MD5.ComputeHash(fileStream);
fileStream.Close();
for (int i = 0; i < bytes.Length; i++)
{
stringBuilder.Append(bytes[i].ToString("x2"));
}
return stringBuilder.ToString();
}
在Build 事件里加上函数即可。这样就可以在打包的同时生成包体MD5,Md5FileList.txt内容如下,格式是包名加生成的16进制哈希码。
加载本地Bundle资源测试
/// <summary>
/// 加载资源
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
/// <param name="assetBundleName">资源名</param>
/// <param name="assetBundleGroupName">组名</param>
/// <returns></returns>
public T LoadResource<T>(string assetBundleName, string assetBundleGroupName) where T : UnityEngine.Object
{
if (string.IsNullOrEmpty(assetBundleGroupName))
{
return default(T);
}
if (!DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
{
assetbundle = AssetBundle.LoadFromFile(GetStreamingAssetsPath() + assetBundleGroupName);
DicAssetBundle.Add(assetBundleGroupName, assetbundle);
}
object obj = assetbundle.LoadAsset(assetBundleName, typeof(T));
var one = obj as T;
return one;
}
/// <summary>
/// 卸载资源组
/// </summary>
/// <param name="assetBundleGroupName">组名</param>
public void UnLoadResource(string assetBundleGroupName)
{
if (DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
{
Debug.Log(assetBundleGroupName);
assetbundle.Unload(false);
if (assetbundle != null)
{
assetbundle = null;
}
DicAssetBundle.Remove(assetBundleGroupName);
Resources.UnloadUnusedAssets();
}
}
public string GetStreamingAssetsPath()
{
string StreamingAssetsPath =
#if UNITY_EDITOR || UNITY_STANDALONE_WIN
Application.streamingAssetsPath + "/";
#elif UNITY_ANDROID
"jar:file://" + Application.dataPath + "!/assets/";
#elif UNITY_IPHONE
Application.dataPath + "/Raw/";
#else
string.Empty;
#endif
return StreamingAssetsPath;
}
修改Lua自定义Loader,require时使用Bundle资源
ST_LuaManger.cs
public static byte[] LuaLoader(ref string filename)
{
TextAsset ta = ST_BundleManager.Instance.LoadResource<TextAsset>(filename,"lua");
return System.Text.Encoding.UTF8.GetBytes(ta.text);
}
//移除requier(lua环境只会Require 文件一次,移除后会从新require)
//require 会调用自定义Loader
public void RemoveRequire(string name)
{
MyLuaEnv.DoString($@"
for key, _ in pairs(package.preload) do
if string.find(tostring(key),'{name}') == 1 then
package.preload[key] = nil
end
end
for key, _ in pairs(package.loaded) do
if string.find(tostring(key), '{name}') == 1 then
package.loaded[key] = nil
end
end");
}
测试脚本
if (GUILayout.Button("Cube"))
{
GameObject cube = ST_BundleManager.Instance.LoadResource<GameObject>("Cube", "model");
Instantiate(cube,new Vector3(0,y++,0),Quaternion.identity);
}
测试结果
可以看到资源被正确加载了。
远程资源下载与热更
第一次使用服务器数据的时候要清空本地StreamAsset文件夹。热更新的思路是对比本地的Md5FileList.txt,和服务器上的,如果有md5不同的包,或者新的包那么就请求下载,并保存到本地(也可以用先对比版本号的方法。可以将打包的资源放到了[GitCode]方便下载(https://about.gitcode.net/)
右键点击这个按钮即可复制原始下载链接,亲测可用
ST_AssetBundleManager.cs 完整代码
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using UnityEngine.Networking;
public static class BundleConfig{
public static readonly string AssetBundlePath = "https://。。。。";
public static readonly string Md5FileName = "Md5FileList.txt";
public static readonly string OnFireUpdate = "OnFireUpdate";
public static readonly string OnStarDownLoad = "OnStarDownLoad";
public static readonly string OnStarSave = "OnStarSave";
public static readonly string OnUpdateEnd = "OnUpdateEnd";
}
public class ST_BundleManager:MonoBehaviour
{
static bool _isDesdroy = false;
private static readonly object padlock = new object();
private static ST_BundleManager instance;
static AssetBundle assetbundle = null;
static Dictionary<string, AssetBundle> DicAssetBundle;
public static ST_BundleManager Instance
{
get
{
if (_isDesdroy)
{
return null;
}
if (instance == null)
{
lock (padlock)
{
if (instance == null)
{
GameObject go = new GameObject("AssetBundleManager");
DontDestroyOnLoad(go);
instance = go.AddComponent<ST_BundleManager>();
DicAssetBundle = new Dictionary<string, AssetBundle>();
}
}
}
return instance;
}
}
/// <summary>
/// bundel资源加载
/// </summary>
/// <typeparam name="T">资源类型</typeparam>
/// <param name="assetBundleName">资源名</param>
/// <param name="assetBundleGroupName">资源组</param>
/// <returns></returns>
public T LoadResource<T>(string assetBundleName, string assetBundleGroupName) where T : UnityEngine.Object
{
if (string.IsNullOrEmpty(assetBundleGroupName))
{
return default(T);
}
if (!DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
{
assetbundle = AssetBundle.LoadFromFile(GetStreamingAssetsPath() + assetBundleGroupName);
DicAssetBundle.Add(assetBundleGroupName, assetbundle);
}
object obj = assetbundle.LoadAsset(assetBundleName, typeof(T));
var one = obj as T;
return one;
}
/// <summary>
/// 资源卸载
/// </summary>
/// <param name="assetBundleGroupName">资源名</param>
public void UnLoadResource(string assetBundleGroupName)
{
if (DicAssetBundle.TryGetValue(assetBundleGroupName, out assetbundle))
{
assetbundle.Unload(false);
if (assetbundle != null)
{
assetbundle = null;
}
DicAssetBundle.Remove(assetBundleGroupName);
Resources.UnloadUnusedAssets();
}
}
/// <summary>
/// 获取本地资源路径
/// </summary>
/// <returns></returns>
public string GetStreamingAssetsPath()
{
string StreamingAssetsPath =
#if UNITY_EDITOR || UNITY_STANDALONE_WIN
Application.streamingAssetsPath + "/";
#elif UNITY_ANDROID
"jar:file://" + Application.dataPath + "!/assets/";
#elif UNITY_IPHONE
Application.dataPath + "/Raw/";
#else
string.Empty;
#endif
return StreamingAssetsPath;
}
//下载跟新资源
public IEnumerator DownloadAssetBundles(UnityWebRequest www)
{
List<string> downLoads = GetDownloadMoudle(www.downloadHandler.text);
SaveAssetBundle(BundleConfig.Md5FileName,www.downloadHandler.data);
int i = 0;
if(downLoads.Count > 0)
{
//触发热更事件
ST_EventManager.PostEvent(BundleConfig.OnFireUpdate, downLoads.Count);
}
while (i < downLoads.Count)
{
www = UnityWebRequest.Get(BundleConfig.AssetBundlePath + downLoads[i]);
//触发下载事件
ST_EventManager.PostEvent(BundleConfig.OnStarDownLoad, downLoads[i]);
yield return www.SendWebRequest();
if(www.result != UnityWebRequest.Result.ConnectionError)
{
//触发保存事件
ST_EventManager.PostEvent(BundleConfig.OnStarSave, downLoads[i]);
SaveAssetBundle(downLoads[i],www.downloadHandler.data);
}
else
{
throw new Exception(www.error);
}
i++;
}
//触发跟新完事件
ST_EventManager.PostEvent(BundleConfig.OnUpdateEnd);
}
/// <summary>
/// 检查跟新
/// </summary>
/// <param name="callback">回调函数</param>
/// <returns></returns>
public IEnumerator CheckUpdate(Action<bool,UnityWebRequest> callback)
{
UnityWebRequest www = UnityWebRequest.Get(BundleConfig.AssetBundlePath + BundleConfig.Md5FileName);
yield return www.SendWebRequest();
if (www.result != UnityWebRequest.Result.ConnectionError)
{
List<string> downLoads = GetDownloadMoudle(www.downloadHandler.text);
callback(downLoads.Count > 0, www);
}
else
{
throw new Exception(www.error);
}
}
/// <summary>
/// 根据Md5FileList获取需要跟新的模块
/// </summary>
/// <param name="webFileList"></param>
/// <returns></returns>
List<string> GetDownloadMoudle(string webFileList)
{
List<string> ret = new List<string>();
string[] sArray = webFileList.Split(new char[2] { '\r', '\n' });
//AssetBundleManifest assetBundleManifest;
if(!File.Exists(GetStreamingAssetsPath() + BundleConfig.Md5FileName))
{
foreach(string str in sArray)
{
if (str != "")
{
ret.Add(str.Split('|')[0]);
}
}
}
else
{
string pastMd5 = File.ReadAllText(GetStreamingAssetsPath() + BundleConfig.Md5FileName);
Dictionary<string, string> dict = new Dictionary<string, string>();
string[] psArray = pastMd5.Split(new char[2] { '\r', '\n' });
foreach (string str in psArray)
{
if (str != "")
{
var nv = str.Split('|');
dict.Add(nv[0], nv[1]);
}
}
foreach (string str in sArray)
{
if (str != "")
{
var nv = str.Split('|');
if (!dict.ContainsKey(nv[0]) || dict[nv[0]] != nv[1])
{
ret.Add(nv[0]);
}
}
}
}
return ret;
}
/// <summary>
/// 保存资源到本地
/// </summary>
/// <param name="fileName">资源名</param>
/// <param name="bytes">数据</param>
/// <param name="saveLocalComplate">完成回调</param>
void SaveAssetBundle(string fileName, byte[] bytes, Action saveLocalComplate = null)
{
string path = GetStreamingAssetsPath() + fileName;
FileInfo fileInfo = new FileInfo(path);
FileStream fs = fileInfo.Create();
fs.Write(bytes, 0, bytes.Length);
fs.Flush();
fs.Close();
fs.Dispose();
if (saveLocalComplate != null)
{
saveLocalComplate();
}
}
private void OnDestroy()
{
_isDesdroy = true;
}
}
测试热更
int y = 0;
void Start()
{
ST_EventManager.RegisterEvent<int>(BundleConfig.OnFireUpdate, (e) =>
{
Debug.Log("开始热跟新,资源数:" + e);
});
ST_EventManager.RegisterEvent<string>(BundleConfig.OnStarDownLoad, (e) =>
{
Debug.Log("开始下载资源:" + e);
});
ST_EventManager.RegisterEvent<string>(BundleConfig.OnStarSave, (e) =>
{
Debug.Log("开始保存资源:" + e);
});
ST_EventManager.RegisterEvent(BundleConfig.OnUpdateEnd, () =>
{
Debug.Log("更新完成");
//取消TestMono的Requrie,卸载lua,保证使用新的代码
ST_LuaMannager.Instance.RemoveRequire("TestMono");
ST_BundleManager.Instance.UnLoadResource("lua");
});
}
private void OnGUI()
{
if (GUILayout.Button("开始下载"))
{
StartCoroutine(ST_BundleManager.Instance.CheckUpdate((needUp, www) =>
{
if (needUp)
{
Debug.Log("检测到资源更新");
StartCoroutine(ST_BundleManager.Instance.DownloadAssetBundles(www));
}
else
{
Debug.Log("无更新");
}
}));
}
if (GUILayout.Button("Cube"))
{
GameObject cube = ST_BundleManager.Instance.LoadResource<GameObject>("Cube", "model");
Instantiate(cube,new Vector3(0,y++,0),Quaternion.identity);
ST_LuaMannager.Instance.RemoveRequire("TestMono");
}
}
点击开始下载,输出如下:
点击cube正常加载:
修改TestMono的Update函数,原来Cube是沿着X轴旋转,现在让他沿着Y周旋转,从新打包并上传到服务器
function TestMono:Update()
self.transform:Rotate(CS.UnityEngine.Vector3(0,1,0));
--self.transform:Rotate(CS.UnityEngine.Vector3(1,0,0));
end
可以看到只跟新了lua和standlone,并且跟新后创建的Cube是按Y轴旋转的,热更大功告成,,写完博客11点半了,睡觉睡觉。。。