unity资源打包可以分为一下几个过程:
1、先把图片批量生成图集
2、把其他路径下的资源,比如逻辑lua脚本拷贝到资源文件夹下,方便后面资源打包
3、自动给资源文件夹下所有资源设置AssetBundle的Name和variant
4、利用unity提供的api进行资源打包
5、创建XML资源列表
一、图集打包
图集打包使用的是unity新版的SpriteAtlas,舍弃了旧版的SpritePackage,主要是旧版的图集无法从图集获取图片资源
public static string[] textureExtensions = new[] {".png"}; [MenuItem("BuildAssetBundle/ScanSpriteAtlas")] public static void ScanSpriteAtlas() { var path = Path.Combine(Application.dataPath, "ResourcesAssets"); var dirInfo = new DirectoryInfo(path); ScanSprites(dirInfo); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } public static void ScanSprites(DirectoryInfo dirInfo) { var dirArr = dirInfo.GetDirectories(); if (dirArr.Length > 0) { for (int i = 0; i < dirArr.Length; i++) { ScanSprites(dirArr[i]); } } var fileArr = dirInfo.GetFiles(); if (fileArr.Length > 0) { var dirPath = dirInfo.FullName.Replace("\\", "/"); var subDirPath = dirPath.Replace(Application.dataPath, "Assets"); for (int i = 0; i < fileArr.Length; i++) { var label = ""; var variant = ""; if (textureExtensions.Contains(fileArr[i].Extension)) { var texImporter = AssetImporter.GetAtPath(Path.Combine(subDirPath, fileArr[i].Name)) as TextureImporter; if (texImporter.textureType == TextureImporterType.Sprite) { GetSpriteAtlas(dirInfo); break; } } } } } public static SpriteAtlas GetSpriteAtlas(DirectoryInfo dirInfo) { //文件夹路径 var assetDataPath = dirInfo.FullName.Replace("\\", "/").Replace(Application.dataPath, "Assets"); //图集名称 var atlasName = assetDataPath.Replace("Assets/ResourcesAssets/", "").Replace("/", "-"); //图集路径 var assetPath = $"{assetDataPath}/{atlasName}.spriteatlas"; //加载图集 var sa = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(assetPath); if (sa == null) { Debug.Log($"Creat SpriteAtlas at path : {assetDataPath}"); sa = new SpriteAtlas(); AssetDatabase.CreateAsset(sa, assetPath); Object texture = AssetDatabase.LoadMainAssetAtPath(assetDataPath); SpriteAtlasPackingSettings packset = new SpriteAtlasPackingSettings() { blockOffset = 1, enableRotation = false, enableTightPacking = false, padding = 4 }; sa.SetPackingSettings(packset); SpriteAtlasTextureSettings texSet = new SpriteAtlasTextureSettings() { readable = true, filterMode = FilterMode.Bilinear, sRGB = true, generateMipMaps = true }; sa.SetTextureSettings(texSet); sa.SetIncludeInBuild(false); sa.SetIsVariant(false); sa.Add(new Object[] { texture }); } return sa; }
二、资源拷贝
资源拷贝也是为了加密做准备,在把我们的lua脚本拷贝到资源文件夹下的时候可以进一步加密,把我们的lua脚本变为加密之后的脚本,可以防止软件被反编译之后拿到我们的lua脚本
public static void CopyLuaToSources(bool encode) { string outPath = Application.dataPath + "/ResourcesAssets/Lua"; try { if (!Directory.Exists(outPath)) { Directory.CreateDirectory(outPath); } } catch (System.UnauthorizedAccessException ex) { if (Debug.unityLogger.logEnabled) { Debug.Log(ex.Message); } } var hasErr = false; var strErr = new StringBuilder(); CopyLuaBytesFiles(ref hasErr, Application.dataPath+"/Lua/", outPath, encode, strErr); if (hasErr) { Exception ex = new Exception(strErr.ToString()); throw (ex); } } public static void CopyLuaBytesFiles(ref bool hasErr, string targetPath, string outPath, bool isencode = false, StringBuilder strErr = null) { if (!Directory.Exists(targetPath)) { return; } string[] files = Directory.GetFiles(targetPath, "*.lua", SearchOption.AllDirectories); int len = targetPath.Length; if (targetPath[len - 1] == '/' || targetPath[len - 1] == '\\') { --len; } LuaHashComparer hashComparer = new LuaHashComparer(); for (int i = 0; i < files.Length; i++) { //获取Assets/Lua之后的lua文件路径 string str = files[i].Remove(0, len); //lua文件copy到的目标文件 string desc = $"{outPath}{str}.bytes"; string dir = Path.GetDirectoryName(desc); Directory.CreateDirectory(dir); string src = files[i]; if (isencode) { //这是带加密的步骤,先放着 } else { //正常的copy,不带加密的流程 if (hashComparer.IsChanged(src, src.Replace(targetPath, "").ToLower())) { var _outPath = desc.ToLower(); if (File.Exists(_outPath)) { File.Delete(_outPath); } File.Copy(src, _outPath, true); File.SetAttributes(_outPath, FileAttributes.Normal); } } } EditorUtility.ClearProgressBar(); AssetDatabase.Refresh(); }
三、自动设置AssetBundle的Name和Variant
这一步其实做的就是资源分类,每个资源都有其对应的资源路径,包名其实就是它的路径
public static void AutoSetBundleName(bool clear) { string path = Path.Combine(Application.dataPath, "ResourcesAssets"); DirectoryInfo info = new DirectoryInfo(path); ScanDir(info, clear); } public static string[] IgnoreExtensions = new[] {".meta", ".DS_Store"}; public static string[] VideoExtensions = new[] { ".mp4"}; public static List<FileInfo> videoFileList = new List<FileInfo>(); public static void ScanDir(DirectoryInfo info, bool clear, bool smartSet = true) { DirectoryInfo[] dirArr = info.GetDirectories(); //递归遍历所有文件夹 for (int i = 0; i < dirArr.Length; i++) { ScanDir(dirArr[i], clear); //显示进度条 EditorUtility.DisplayProgressBar("扫描资源文件夹", $"资源文件夹名:{dirArr[i].Name}", i*1.0f/dirArr.Length); } FileInfo[] fileArr = info.GetFiles(); if (fileArr.Length > 0) { string dirPath = info.FullName.Replace("\\", "/"); string subDirPath = dirPath.Replace(Application.dataPath, "Assets"); string assetBundleLabel = subDirPath.Replace("Assets/ResourcesAssets/", "").ToLower(); //根据文件夹添加标签,自动打包 for (int i = 0; i < fileArr.Length; i++) { if (IgnoreExtensions.Contains(fileArr[i].Extension)) { continue; } string label = "None"; string variant = "None"; if (!clear) { if (textureExtensions.Contains(fileArr[i].Extension)) { //根据路径获取指定资源 var texImporter = AssetImporter.GetAtPath(Path.Combine(subDirPath, fileArr[i].Name)) as TextureImporter; if (texImporter.textureType == TextureImporterType.Sprite) { continue; } } label = assetBundleLabel; variant = "variant"; } string assetPath = $"{subDirPath}/{fileArr[i].Name}"; AssetImporter asset = AssetImporter.GetAtPath(assetPath); Debug.Log($"FileName:{fileArr[i].Name},Extension:{fileArr[i].Extension}"); if (asset.assetBundleName != label || !smartSet) { try { asset.SetAssetBundleNameAndVariant(label, variant); } catch (Exception ex) { Debug.Log(ex.Message); EditorUtility.ClearProgressBar(); } } EditorUtility.DisplayProgressBar("设置AB包名", $"包名:{label}", i*1.0f/fileArr.Length); } } EditorUtility.ClearProgressBar(); }
四、资源打包
使用unity提供的api进行资源打包,资源包构建选项为BuildAssetBundleOptions.ChunkBasedCompression
BuildAssetBundleOptions.ChunkBasedCompression采用LZ4的压缩格式,相比于LZMA而言文件体积更大,但是不要求在使用之前整个bundle都被解压。
LZ4使用chunk based 算法,这就运行文件以chunk或者piece的方式加载,只解压一个chunk文件,而无需解压bundle中其余不相关的chunk
public static void BuildAllAssetBundleToPersistent(BuildAssetBundleOptions bundleOptions) { string packPath = OriginPath; FileHelper.CreatDirectory(packPath); if (packPath.Length <= 0) return; Debug.Log($"OutPath:{packPath}"); BuildPipeline.BuildAssetBundles(packPath, bundleOptions, GetCurrBuildTarget()); AssetDatabase.Refresh(); }
五、创建资源列表XML
创建资源列表,用xml文件来存储,存储资源的名字、key值(即资源路径)和资源对呀ab包的名字(即MD5),用来进行资源加载的时候进行索引
public static void CreatVersionResXML() { string packagePath = hotfixPath; if (packagePath.Length <= 0 || !Directory.Exists(packagePath)) { return; } List<string> fileList = new List<string>(); fileList = FileHelper.GetFiles(OriginPath, fileList, ".variant"); XmlDocument xml = new XmlDocument(); xml.AppendChild(xml.CreateXmlDeclaration("1.0", "UTF-8", null)); //生成根节点 xml.AppendChild(xml.CreateElement("Root")); //生成大版本号 XmlNode root = xml.SelectSingleNode("Root"); //版本号赋值 XmlElement verNode = xml.CreateElement("version"); verNode.InnerText = $"{Application.version}.{GitCommitDataId}"; root.AppendChild(verNode); //是否加密 XmlElement encNode = xml.CreateElement("encryption"); encNode.InnerText = "true"; root.AppendChild(encNode); //添加资源加点 XmlElement resNode = xml.CreateElement("res"); root.AppendChild(resNode); string copyToPath = hotfixPath; FileHelper.RebuildDic(copyToPath); for (int i = 0; i < fileList.Count; i++) { FileInfo info = new FileInfo(fileList[i]); string md5 = FileHelper.BuildFileMD5(fileList[i]); string fileName = fileList[i].Substring(OriginPath.Length + 1).Replace("\\", "/").Replace(".variant", ""); File.Copy(fileList[i], Path.Combine(copyToPath, md5)); string asset = FileHelper.LoadFile($"{fileList[i]}.manifest"); int idx = asset.IndexOf("Dependencies:"); string subStr = Regex.Unescape(asset.Substring(idx + "Dependencies:".Length)); List<string> depsArr = new List<string>(); //判断是否拥有依赖资源文件 if (!subStr.Contains("[]")) { //去掉字符串首尾空格 subStr = subStr.Trim(); string[] deps = subStr.Split('-'); for (int j = 0; i < deps.Length; j++) { string key = deps[j].Trim().Replace(OriginPath + "/", "").Replace(".variant", ""); depsArr.Add(key); } } AddNodeToXML(xml, fileName, md5, info.Length, depsArr); EditorUtility.DisplayProgressBar("生成资源文件", $"添加资源信息:{fileName},md5为:{md5}", i*1.0f/fileList.Count); } string dirPath = resPath.Replace("/version", ""); FileHelper.CreatDirectory(dirPath); xml.Save(resPath + @".xml"); xml.Save(packagePath + @".xml"); } public static void AddNodeToXML(XmlDocument xml, string name, string md5str, long size, List<string> desp) { //创建根节点 XmlNode resNode = xml.SelectSingleNode("Root/res"); //添加元素 XmlElement element = xml.CreateElement("res"); element.SetAttribute("size", size.ToString()); element.SetAttribute("name", name); element.InnerText = md5str; if (desp.Count > 0) { XmlElement depEle = xml.CreateElement("dep"); for (int i = 0; i < desp.Count; i++) { XmlElement dep = xml.CreateElement("dep"); dep.InnerText = desp[i]; depEle.AppendChild(dep); } element.AppendChild(depEle); } resNode.AppendChild(element); }
然后一个资源打包流程就完成了