Unity的资源打包,如果要做完美,其实是很复杂的.那么为什么资源要打包呢,因为我们要做资源更新.没办法啊.
在我看来,完美的资源打包至少有以下几个方面要处理好:
1) 资源分类设计合理.控制包粒度,单个包不能太大.
2) 灵活的文件打包结构.支持文件和文件夹打包
3) 共用资源的完美处理.防止重复打包.
4) 快速增量打包.加快打包速度.
具体来聊一下我的一些思考.
1)资源粒度设计合理
其实就是如何划分资源.先说下我的划分.
UI .prefab
Model .prefab 需要动态加载的模型,其实就是角色
Effect .prefab 需要动态加载的特效,其实就是技能特效
Scene .unity
Sound .wav .mp3 .ogg
Animation .anim
Table .txt
Lua .lua
以上的划分也比较常规,只是动作文件的划分比较特殊点.因为动作文件共用性很强,比如所有主角的几十套装备模型都是共用的一套动作,所以把动作从模型独立出来,动态加载动作,这样打包模型,就不会打包动作进去.动态的加载,也有一些讲究,原本是每个动作单独打包,用到什么动作才加载,这样粒度最小,最完美.但是由于WWW加载是异步的,不支持同步加载动作,会有很多问题,所以最后的解决方案是,把这个模型的所有动作打成了一个Bundle包,加载一个模型前,先加载这个模型的所有动作.这样来实现同步效果.
2)灵活的文件打包结构
上面提到了动作打包,我们把一个文件夹下面的所有动作打成了一个包,我们也可以单个动作打成一个包,所谓灵活,就是这个包可以支持1个或者多个主资源.我设计了3种分组类型.这样一个包对应哪些资源,就可以划分出来.
// 分组类型 public enum GroupType { File_Recursion, // 按文件分组(递归遍历) Directory_Recursion, // 按文件夹分组(递归遍历) File_Directory, // 按文件和文件夹分组(第一级目录) }
// 获得主资源路径 public static Dictionary<string, List<string>> GetMainAssetPaths(string path, string[] fileExtArray, GroupType groupType) { Dictionary<string, List<string>> paths = new Dictionary<string, List<string>>(); path = path.Replace('\\', '/'); if (groupType == GroupType.File_Directory) { string[] files = Directory.GetFiles(path); string[] dictionaries = Directory.GetDirectories(path); // 按文件分组 foreach (string file in files) { foreach (string fileExt in fileExtArray) { if (file.EndsWith(fileExt)) { List<string> list = new List<string>(1); // 数量1 string filename = file.Replace(Application.dataPath, "Assets").Replace('\\', '/'); list.Add(filename); string key = file.Replace('\\', '/'); key = key.Substring(key.LastIndexOf('/') + 1); key = key.Substring(0, key.Length - fileExt.Length - 1); paths.Add(key, list); break; } } } // 按文件夹分组 foreach (string directory in dictionaries) { files = Directory.GetFiles(directory); string key = directory.Replace('\\', '/'); key = key.Substring(key.LastIndexOf('/') + 1); List<string> list = new List<string>(); foreach (string file in files) { foreach (string fileExt in fileExtArray) { if (file.EndsWith(fileExt)) { string filename = file.Replace(Application.dataPath, "Assets").Replace('\\', '/'); list.Add(filename); } } } paths.Add(key, list); } } else { GetMainAssetPathsRecursion(paths, path, path, fileExtArray, groupType); } return paths; } static void GetMainAssetPathsRecursion(Dictionary<string, List<string>> paths, string startPath, string curPath, string[] fileExtArray, GroupType groupType) { string[] files = Directory.GetFiles(curPath); string[] dictionaries = Directory.GetDirectories(curPath); foreach (string file in files) { foreach (string fileEnd in fileExtArray) { if (file.EndsWith(fileEnd)) { string key = ""; string filename = file.Replace(Application.dataPath, "Assets").Replace('\\', '/'); if (groupType == GroupType.Directory_Recursion) { key = curPath.Replace('\\', '/'); key = key.Substring(startPath.Length + 1); // 按文件夹分组的话,一个Key对应多个文件. if (!paths.ContainsKey(key)) { List<string> list = new List<string>(); list.Add(filename); paths.Add(key, list); } else { List<string> list = paths[key]; list.Add(filename); } } else { key = file.Replace('\\', '/'); key = key.Substring(startPath.Length + 1); key = key.Substring(0, key.Length - fileEnd.Length - 1); List<string> list = new List<string>(1); // 数量1 list.Add(filename); if (!paths.ContainsKey(key)) paths.Add(key, list); else LogSystem.WarningLog("GetMainAssetPathsRecursion 已经包含这个key了 key:{0}", key); } break; } } } // 递归 foreach (string directory in dictionaries) { GetMainAssetPathsRecursion(paths, startPath, directory.Replace('\\', '/'), fileExtArray, groupType); } }
3)共用资源的完美处理
游戏发布的时候.整个包小一些,推广费会少很多,要减少包大小,完美处理共用资源,防止资源重复打包就很重要.
怎么完美处理呢,其实找出共用资源,用依赖打包解决就行了.怎么处理依赖,这里就不赘述了.
一些常见的共用资源:
UI:
字体和一些常见的共用图集
Model & Effect:
共用的shader
Scene:
场景共用太多了,得用工具找出来才行,我写了一个工具来查找场景共用资源.一个场景用到了,UsedCounter就加1
1.可以筛选资源的后缀和路径.
2.列表点击标题可以排序,
3.点击资源名字,可以跳转到项目资源并选中,
4.点击使用次数按钮,可以打印哪些场景用到了这个资源.
共用资源找出来了,就可以把共用资源打成一个或者几个包,
处理场景的共用资源,我看有些人从prefab层级去处理,动态加载这些共用的prefab,这种做法其实很麻烦而没必要,你得从场景里面把这些prefab删除了,才能不打包这些共用资源到场景里面.改成从资源层级去处理就简单很多,因为资源才大,而prefab其实只是一些配置文件,不占空间.
4) 快速增量打包
在开发的时候特别重要,有了增量打包,就再也不用选中某个资源来打包了,比如UI改了,重打整个UI,自动找出哪些UI改变或者增减了,一下就打好了.
Unity5有用manifest文件来做增量打包,如果是Unity4,其实自己可以做一套manifest.比较MD5就行了.
分享一下我的代码:
using System; using System.IO; using System.Collections.Generic; using UnityEngine; using UnityEditor; using System.Security.Cryptography; namespace Luoyinan { // 依赖资源文件信息 public class DependencyAssetInfo { public string m_AssetMD5 { get; set; } public string m_AssetMetaMD5 { get; set; } public string m_AssetFileSize { get; set; } // 用来快速检查过大的依赖资源 } // 包信息 public class AssetBundleInfo { public bool m_IsNeedBuild = false; public string m_BundleName; public Dictionary<string, DependencyAssetInfo> m_Dependencies = new Dictionary<string, DependencyAssetInfo>(); public string WriteToString(int id) { string content = ""; foreach (KeyValuePair<string, DependencyAssetInfo> pair in m_Dependencies) { DependencyAssetInfo info = pair.Value; content += string.Format("{0}\t{1}\t{2}\t{3}\t{4}\t{5}\n" , id , m_BundleName , pair.Key , info.m_AssetMD5 , info.m_AssetMetaMD5 , info.m_AssetFileSize ); } return content; } } public class AssetBundleManifestMgr : Singleton<AssetBundleManifestMgr> { public class MD5Info { public string md5; public string filesize; public MD5Info(string md5, string filesize) { this.md5 = md5; this.filesize = filesize; } } public MD5CryptoServiceProvider m_Md5Generator = new MD5CryptoServiceProvider(); public Dictionary<string, AssetBundleInfo> m_LastABInfos = new Dictionary<string, AssetBundleInfo>(); public Dictionary<string, AssetBundleInfo> m_CurABInfos = new Dictionary<string, AssetBundleInfo>(); public Dictionary<string, MD5Info> m_MD5s = new Dictionary<string, MD5Info>(); public List<string> m_ToDeletedBundles = new List<string>(); #region public // 加载上次清单文件 public bool LoadManifestFile(string fileName) { // 清除数据 Clear(); fileName = fileName.Replace('\\', '/'); string path = GetManifestFilePath() + fileName; if (!File.Exists(path)) return true; DBC table = new DBC(); try { //table.Load(path); 这个函数只支持从表格文件目录加载文本.换成LoadFromStream. StreamReader sr = File.OpenText(path); table.LoadFromStream(sr); } catch (Exception ex) { LogSystem.ErrorLog(ex.ToString()); return false; } foreach (List<DBC_Row> list in table.HashData.Values) { AssetBundleInfo ab = new AssetBundleInfo(); ab.m_BundleName = list[0].SelectFieldByName("AssetBundleName"); for (int i = 0; i < list.Count; ++i) { DependencyAssetInfo info = new DependencyAssetInfo(); string name = list[i].SelectFieldByName("DependencyAssetName"); info.m_AssetMD5 = list[i].SelectFieldByName("DependencyAssetMD5"); info.m_AssetMetaMD5 = list[i].SelectFieldByName("DependencyAssetMetaMD5"); //da.m_DependencyAssetFileSize = list[i].SelectFieldByName("DependencyAssetFileSize"); ab.m_Dependencies.Add(name, info); } m_LastABInfos.Add(ab.m_BundleName, ab); } return true; } // 产生这次的打包信息. public void GenerateCurAssetBundleInfo(string bundleName, string[] mainAssetNames) { AssetBundleInfo ab = new AssetBundleInfo(); ab.m_BundleName = bundleName; // 依赖 string[] dependencies = AssetDatabase.GetDependencies(mainAssetNames); if (dependencies != null && dependencies.Length > 0) { foreach (string dFile in dependencies) { DependencyAssetInfo info = new DependencyAssetInfo(); MD5Info md5_info = GenerateFileMD5(dFile); info.m_AssetMD5 = md5_info.md5; info.m_AssetMetaMD5 = GenerateFileMD5(dFile + ".meta").md5; info.m_AssetFileSize = md5_info.filesize; ab.m_Dependencies.Add(dFile, info); } } m_CurABInfos.Add(ab.m_BundleName, ab); } // 分析对比两次打包数据 public void AnalyzeAssetBundleInfo() { // 1.删除多余的包 foreach (KeyValuePair<string, AssetBundleInfo> pair in m_LastABInfos) { string lastBundleName = pair.Key; if (!m_CurABInfos.ContainsKey(lastBundleName)) { if (!SelectSceneEditor.m_SelectSceneMode) // 如果是选择场景打包,不要删除未打包场景. { m_ToDeletedBundles.Add(lastBundleName); LogSystem.DebugLog("需要删除的包: " + lastBundleName); } } } // 2.哪些包需要打包 int i = 0; foreach (KeyValuePair<string, AssetBundleInfo> pair in m_CurABInfos) { string curBundleName = pair.Key; AssetBundleInfo curAB = pair.Value; if (!m_LastABInfos.ContainsKey(curBundleName)) { // 新增加的包,需要打包. curAB.m_IsNeedBuild = true; LogSystem.DebugLog("新增加的包: " + curBundleName); ++i; } else { curAB = m_CurABInfos[curBundleName]; AssetBundleInfo lastAB = m_LastABInfos[curBundleName]; // 原来的包被谁手贱删除了,需要打包. if (!File.Exists(lastAB.m_BundleName)) { curAB.m_IsNeedBuild = true; LogSystem.DebugLog("原来的包被谁手贱删除了,需要重新打包: " + curBundleName); ++i; continue; } // 改变了,需要打包. if (IsDependenciesChanged(curAB, lastAB)) { curAB.m_IsNeedBuild = true; LogSystem.DebugLog("依赖资源改变的包: " + curBundleName); ++i; } } } LogSystem.DebugLog("需要打包的的数量 : {0}", i); } // 是否需要重新打包. public bool IsNeedBuild(string bundleName) { if (!m_CurABInfos.ContainsKey(bundleName)) { LogSystem.ErrorLog("请先调用GenerateCurAssetBundleInfo!!! bundleName : {0}", bundleName); return false; } return m_CurABInfos[bundleName].m_IsNeedBuild; } // 打包结束后的处理。主要是删除多余文件和保存数据。 public void PostBundleBuild(string fileName) { DeleteUnusedAssetBundles(); SaveManifestFile(fileName); } #endregion #region private // 清空 private void Clear() { m_LastABInfos.Clear(); m_CurABInfos.Clear(); m_MD5s.Clear(); m_ToDeletedBundles.Clear(); } // 保存清单文件 private void SaveManifestFile(string fileName) { string header = "ID\tAssetBundleName\tDependencyAssetName\tDependencyAssetMD5\tDependencyAssetMetaMD5\tDependencyAssetFileSize"; string type = "INT\tSTRING\tSTRING\tSTRING\tSTRING\tSTRING"; string content = header + "\n" + type + "\n"; // 保存这次的打包信息. int id = 0; foreach (KeyValuePair<string, AssetBundleInfo> pair in m_CurABInfos) { content += pair.Value.WriteToString(id); ++id; } // 如果是选择场景打包,不要删除未打包场景的清单文件信息 if (SelectSceneEditor.m_SelectSceneMode) { foreach (KeyValuePair<string, AssetBundleInfo> pair in m_LastABInfos) { if (!m_CurABInfos.ContainsKey(pair.Key)) content += pair.Value.WriteToString(id); ++id; } } string path = GetManifestFilePath(); if (!Directory.Exists(path)) Directory.CreateDirectory(path); File.WriteAllText(path + fileName, content); } // 清单文件路径 private string GetManifestFilePath() { string assetPath = Application.dataPath; string projPath = assetPath.Remove(assetPath.IndexOf("/Assets")) + "/"; string folder = "Build/Luoyinan_Unknown"; #if UNITY_STANDALONE_WIN folder = "Build/Luoyinan_StandaloneWindows"; #elif UNITY_ANDROID folder = "Build/Luoyinan_Android"; #elif UNITY_WP8 #else folder = "Build/Luoyinan_iPhone"; #endif if (!Directory.Exists(projPath + folder)) { Debug.Log(projPath + folder); Directory.CreateDirectory(projPath + folder); } folder += "/AssetBundleManifest/"; return projPath + folder; } // 产生MD5 private MD5Info GenerateFileMD5(string fileName) { if (m_MD5s.ContainsKey(fileName)) { return m_MD5s[fileName]; } else { string md5 = ""; int size = 0; using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { byte[] hash = m_Md5Generator.ComputeHash(fs); md5 = BitConverter.ToString(hash); size = (int)((float)fs.Length / 1024.0f); } MD5Info info = new MD5Info(md5, size.ToString()); m_MD5s.Add(fileName, info); return info; } } // 删除多余包 private void DeleteUnusedAssetBundles() { if (m_ToDeletedBundles.Count > 0) { for (int i = 0; i < m_ToDeletedBundles.Count; ++i) { FileInfo file = new FileInfo(m_ToDeletedBundles[i]); if (file.Exists) { File.Delete(m_ToDeletedBundles[i]); } } AssetDatabase.SaveAssets(); EditorApplication.SaveAssets(); AssetDatabase.Refresh(); } } // 依赖资源是否改变 public bool IsDependenciesChanged(AssetBundleInfo curAB, AssetBundleInfo lastAB) { foreach (KeyValuePair<string, DependencyAssetInfo> pair in curAB.m_Dependencies) { if (!lastAB.m_Dependencies.ContainsKey(pair.Key)) { // 新的依赖项 LogSystem.DebugLog("新的依赖项目 : {0}", pair.Key); return true; } else { // 依赖项改变 DependencyAssetInfo lastInfo = lastAB.m_Dependencies[pair.Key]; DependencyAssetInfo curInfo = pair.Value; if (curInfo.m_AssetMD5 != lastInfo.m_AssetMD5 || curInfo.m_AssetMetaMD5 != lastInfo.m_AssetMetaMD5) { if (pair.Key.EndsWith(".cs")) { LogSystem.DebugLog("依赖项改变.但是是脚本.暂时不重新打包 : {0}", pair.Key); } else { LogSystem.DebugLog("依赖项改变 : {0}", pair.Key); return true; } } } } return false; } #endregion } }
依赖打包的一个例子部分代码:
static void BuildBundleDependencies(string selectPath, string outputPath, BuildTarget buildTarget, string[] fileEndArray, GroupType groupType, string manifestFile, string dependBundleName, List<FilterInfo> dependFilters) { // 主资源路径 Dictionary<string, List<string>> paths = GetMainAssetPaths(Application.dataPath + selectPath, fileEndArray, groupType); // 搜集依赖 List<string> dependPaths = GetDependAssetPaths(paths, dependFilters, false); Object[] dependObjects = GetMainAssetObjects(dependPaths); // 快速增量打包 if (m_UseFastBuild) { EditorUtility.DisplayProgressBar("增量打包 " + selectPath, "开始分析两次打包数据...", 0); LogSystem.DebugLog("-----------------开始新的增量打包.路径: {0}", selectPath); // 1.加载上次打包数据 if (!AssetBundleManifestMgr.Instance.LoadManifestFile(manifestFile)) return; // 2.产生这次打包数据 foreach (KeyValuePair<string, List<string>> pair in paths) { List<string> list = pair.Value; string[] mainAssetNames = new string[list.Count]; for (int i = 0; i < list.Count; ++i) { mainAssetNames[i] = list[i]; } string bundleName = outputPath + "/" + pair.Key + ".data"; AssetBundleManifestMgr.Instance.GenerateCurAssetBundleInfo(bundleName, mainAssetNames); } // 3.分析两次打包数据 AssetBundleManifestMgr.Instance.AnalyzeAssetBundleInfo(); } // 打依赖包 BuildAssetBundleOptions optionsDepend = BuildAssetBundleOptions.DeterministicAssetBundle | BuildAssetBundleOptions.CompleteAssets; BuildPipeline.PushAssetDependencies(); if (dependObjects != null) { string bundleName = outputPath + "/" + dependBundleName; bool needbuild = true; // 依赖包每次重打 if (needbuild) { Util.CheckTargetPath(bundleName); EditorUtility.DisplayProgressBar("依赖打包", "开始打包:" + dependBundleName, 0); BuildPipeline.BuildAssetBundle(null, dependObjects, bundleName, optionsDepend, buildTarget); } } // 打普通包 BuildAssetBundleOptions optionsNormal = BuildAssetBundleOptions.DeterministicAssetBundle | BuildAssetBundleOptions.CollectDependencies; foreach (KeyValuePair<string, List<string>> pair in paths) { bool needbuild = false; string bundleName = outputPath + "/" + pair.Key + ".data"; if (!m_UseFastBuild || AssetBundleManifestMgr.Instance.IsNeedBuild(bundleName)) needbuild = true; if (needbuild) { BuildPipeline.PushAssetDependencies(); EditorUtility.DisplayProgressBar("依赖打包", "开始打包:" + pair.Key, 0); Util.CheckTargetPath(bundleName); Object[] mainAssetObjects = GetMainAssetObjects(pair.Value); if (mainAssetObjects.Length == 1) BuildPipeline.BuildAssetBundle(mainAssetObjects[0], null, bundleName, optionsNormal, buildTarget); else BuildPipeline.BuildAssetBundle(null, mainAssetObjects, bundleName, optionsNormal, buildTarget); BuildPipeline.PopAssetDependencies(); } } BuildPipeline.PopAssetDependencies(); // 快速增量打包 if (m_UseFastBuild) AssetBundleManifestMgr.Instance.PostBundleBuild(manifestFile); }