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);
}
原文地址:http://blog.youkuaiyun.com/qq18052887/article/details/52584058