Unity3D手游开发日记(10) - 资源打包的一些思考

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);
        }


猜你喜欢

转载自blog.csdn.net/qq18052887/article/details/52584058