AB的创建流程大致分为三步:
- 生成资源路径
- 获取资源之间的引用关系
- 整理、合并共同的引用关系
- 整理最终的AB包关系,把需要打包在一起的资源放在一起
- 生成AB包
1.生成资源路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 扫描指定文件夹 foreach (var scanFolder in BuildSetting.Instance.ScanFolders) { var paths = Directory.GetFiles(scanFolder, "*.*", SearchOption.AllDirectories); scannedAssets.Capacity += paths.Length; foreach (var path in paths) { var p = path.Replace('\\', '/'); // 排除两个特殊目录下的资源 if (IsLuaScript(p) || IsFmodPath(p)) continue; // 过滤掉一些不需要打包的文件,如.meta/.dll/.cs等 if (IsAssetbundleAsset(p)) scannedAssets.Add(p); } } // 收集上面需要打包的资源依赖的资源引用 allAssetPaths = CollectDependencies(scannedAssets); |
2.获取资源间的引用关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
private HashSet<string> CollectDependencies(List<string> assetPaths) { HashSet<string> allAssets = new HashSet<string>(assetPaths); Stack<string> untrackedAssets = new Stack<string>(); // 先把上面扫描出来的资源文件依赖的资源存下来 for (int i = 0; i < assetPaths.Count; ++i) { string[] dependencies = AssetDatabase.GetDependencies(assetPaths[i]); for (int j = 0; j < dependencies.Length; ++j) { var p = dependencies[j].Replace('\\', '/'); if (allAssets.Contains(p) || !IsAssetbundleAsset(p)) continue; untrackedAssets.Push(p); } } // 再把上面依赖的资源查找一下有没有其他依赖的文件,如果有也加入依赖列表 while (untrackedAssets.Count > 0) { var path = untrackedAssets.Pop(); allAssets.Add(path); string[] dependencies = AssetDatabase.GetDependencies(path); for (int j = 0; j < dependencies.Length; ++j) { var p = path.Replace('\\', '/'); if (allAssets.Contains(p) || !IsAssetbundleAsset(p)) continue; untrackedAssets.Push(p); } } return allAssets; } |
3.整理AB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
private void generateAssetReferences() { // 为每个资源建立一个容器存储 // key是资源路径,value是引用该资源的所有资源的路径 allAssetReferences = new Dictionary<string, List<string>>(allAssetPaths.Count); List<string> spriteAtlas = new List<string>(); foreach (var path in allAssetPaths) { if (IsLuaScript(path)) { allAssetReferences[path] = null; } else { allAssetReferences[path] = new List<string>(); } } // 开始整理资源拉 foreach (var path in allAssetPaths) { // lua不会引用其他资源,跳了 if (IsLuaScript(path)) continue; // 图片都被打成图集了,纪录一下所有图集都有啥 if (IsSpriteAtlas(path)) { spriteAtlas.Add(path); continue; } // 场景啥的先不打了,一般放在SceneManager里自动带到包里了 if (IsScene(path) || IsSceneBakeData(path)) continue; // 遍历这个资源的依赖列表,纪录每个资源都被哪些资源引用了。这里存的都是资源本身被哪些其他资源引用了。 string[] dependencies = AssetDatabase.GetDependencies(path); foreach (var dependency in dependencies) { if (dependency == path) continue; if (allAssetReferences.ContainsKey(dependency)) { bool isSpecialPrefab = IsPrefab(path) && (IsAnimation(dependency) || IsSpineAtlas(dependency) || IsSpineSkeleton(dependency) || IsSpineAtlasAsset(dependency)); if (isSpecialPrefab) continue; bool isSpineSklData = IsSpineSkeletonData(path) && IsSpineAtlas(dependency); if (isSpineSklData) continue; var references = allAssetReferences[dependency]; if (!references.Contains(path)) references.Add(path); } } } // 开始处理图集sprite atlas // 让sprite单独被sprite atlas引用,这样两者就可以打到一个ab包里了 foreach (var spriteAtla in spriteAtlas) { SpriteAtlas atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(spriteAtla); // 拿到sprite altas里的所有图片 var sprites = atlas.GetPackables(); foreach (var sprite in sprites) { var spritePath = AssetDatabase.GetAssetPath(sprite).Replace('\\', '/'); // 让图片的引用只有图集 if (allAssetReferences.ContainsKey(spritePath)) { var references = allAssetReferences[spritePath]; references.Clear(); references.Add(spriteAtla); } else allAssetReferences[spritePath] = new List<string>() { spriteAtla }; } } // 生成一下图集映射信息 // 肯定有很多预制体引用了图集内的图片,但是这些引用我们刚才都清掉了。 //我们图片按路径进行加载,图集都会被打在图片所在文件夹的上一级,所以在加载图片的时候我们可以从路径中切出图集名。 //接下来只要构造 图集名-图集路径 的映射,在加载图片的时候就可以加载对应图集AB了。 GenerateSpriteAtlasMap(spriteAtlas); } |
生成映射信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private void GenerateSpriteAtlasMap(List<string> atlasPaths) { // 其实就是个带序列化和反序列化的dictionary // 实际序列化后为一个二进制文件,序列化后也可以使用这个对象来取数据 Resource.SpriteAtlasMap atlasMap = new Resource.SpriteAtlasMap(); // 建立 图集名-图集路径 的映射表 for (int i = 0; i < atlasPaths.Count; ++i) var tag = Path.GetFileNameWithoutExtension(atlasPaths[i]); atlasMap.Add(tag, atlasPaths[i]); using (var fs = new FileStream(Path.Combine(Application.dataPath, BuildSetting.Instance.SpriteAtlasMapName), FileMode.Create, FileAccess.Write)) { using (var bw = new BinaryWriter(fs)) atlasMap.Serialize(bw); } } |
4. 整理最终的AB包关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
allAssetBundles = new Dictionary<string, List<string>>(allAssetReferences.Count); foreach (string assetPath in allAssetReferences.Keys) { // 根据资源路径获取对应的AB名 // 一般资源以资源名进行命名,每个资源带着自己的依赖资源进行打包 // Lua打在一个AB包内 string assetbundleName = GetAssetbundleName(assetPath); if (!allAssetBundles.ContainsKey(assetbundleName)) { allAssetBundles[assetbundleName] = new List<string>(); } allAssetBundles[assetbundleName].Add(assetPath); } |
还记得吗,刚才我们整理资源引用关系的时候使用了一个dictionary进行存储,其中key是资源的路径,value是引用到这个资源的所有路径。
我们在整理AB关联关系获取AB名的时候,需要注意几个关键信息:
- 一些资源可以按类型打在一个AB包内,比如Lua、Shader、Prefatch(在Patch前使用,这部分资源也可以被Patch,但要重启才能生效)。
- 如果一个AB被很多很多其他AB索引(value值的list.count>1),那么这个AB就要单独打一个AB,防止资源被同时打到很多个AB中。
- 如果一个AB只被一个AB引用,那么这个AB也许可以跟引用他的AB打在同一个AB包内。但是这时我们要考虑这个AB是否有可能是被循环引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
//为每个AB准备包名 private string GetAssetbundleName(string assetPath) { var references = allAssetReferences[assetPath]; string assetbundleName = assetPath; //当资源只有一个引用时 //判断一下是可以打入别的AB,还是被循环引用需要单独打AB if (references != null && references.Count == 1) { abLoopCheck.Clear(); assetbundleName = FindRootAssetPath(assetPath); } //特殊资源打到一起 if (IsLuaScript(assetbundleName)) return BuildSetting.Instance.LuaBundleName; else if ..... //被多次引用的资源,自己打一个AB return assetbundleName; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//检测单一引用究竟是打入一个AB还是循环引用单独打AB private string FindRootAssetPath(string assetPath) { var references = allAssetReferences[assetPath]; while (references.Count == 1) { //找到引用这个AB的AB名了,直接塞进去 if (allAssetBundles.ContainsKey(assetPath)) return assetPath; assetPath = references[0]; //循环引用检测 //如果真的循环了,就把最开始进来的资源单独打AB if (!abLoopCheck.Contains(assetPath)) abLoopCheck.Add(assetPath); else return abLoopCheck[0]; references = allAssetReferences[assetPath]; } return assetPath; } |
此外,Fmod等音频信息也不应该依赖到对应的AB包,我们的AB打包时都以全路径作为AB名进行打包,这样做是为了方便加载,但Fmod的语音也不应该带全路径打包,Fmod在加载时多使用一层目录/音频名称格式进行加载。所以我们还需要额外对Fmod的AB名单独处理一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
//只扫描特定的Fmod目录就行了 var paths = Directory.GetFiles(fmodFolder, "*.bytes", SearchOption.TopDirectoryOnly); for (int i = 0; i < paths.Length; ++i) { var fileName = Path.GetFileNameWithoutExtension(paths[i]); var abName = string.Format("{0}/{1}{2}", BuildSetting.Instance.FMODFolderName, fileName, BuildSetting.Instance.abExtName); List<string> assets = null; //把整理后的AB名和列表加入到待打包列表中 if (allAssetBundles.ContainsKey(abName)) assets = allAssetBundles[abName]; else { assets = new List<string>(); allAssetBundles[abName] = assets; } var assetName = paths[i].Replace("\\", "/"); assets.Add(assetName); } |
5.开始打包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
private void BuildAssetbundles(Dictionary<string, List<string>> allAssetBundles, BuildTarget buildTarget) { AssetBundleBuild[] abBuilds = new AssetBundleBuild[allAssetBundles.Count + 1]; var assetMap = new Resource.AssetMap(); int index = 0; foreach (var item in allAssetBundles) { //处理一下资源路径,把asset/路径去掉,并加上AB后缀 var assetbundleName = RemoveAssetbundlePrefix(item.Key.ToLower()); abBuilds[index].assetBundleName = assetbundleName; abBuilds[index].assetNames = item.Value.ToArray(); if (assetbundleName != BuildSetting.Instance.LuaBundleName + BuildSetting.Instance.abExtName) { foreach (var assetName in abBuilds[index].assetNames) { //建立映射关系,通过 资源路径 - AB名 进行映射 //在资源加载时传入资源路径后可以直接查找到对应的AB名,然后加载AB assetMap.Add(assetName, assetbundleName); Debug.Log("Add To AssetBundle: " + assetName + " ->> " + assetbundleName); } } ++index; } //把生成的映射关系序列化成对应文件 //包括 资源路径-AB名映射 与 图集名-图集路径映射 //然后把这两个文件也打成AB //create asset map asset and add asset map bundle, and sprite atlas map GenerateAssetMap(assetMap); var assetMapAssetPath = string.Format("Assets/{0}", BuildSetting.Instance.AssetMapName); AssetDatabase.ImportAsset(assetMapAssetPath); //var luaMapAssetPath = string.Format("Assets/{0}", BuildSetting.Instance.LuaFileMapName); //AssetDatabase.ImportAsset(luaMapAssetPath); var spriteAtlasMapAssetPath = string.Format("Assets/{0}", BuildSetting.Instance.SpriteAtlasMapName); AssetDatabase.ImportAsset(spriteAtlasMapAssetPath); abBuilds[index].assetBundleName = BuildSetting.Instance.AssetMapBundleName + BuildSetting.Instance.abExtName; abBuilds[index].assetNames = new string[] { assetMapAssetPath, spriteAtlasMapAssetPath }; //创建文件路径,开始打包~ string assetBundleDirectory = Path.Combine(BuildSetting.Instance.BuildFolder, BuildSetting.Instance.AssetBundleFolder); if (!Directory.Exists(assetBundleDirectory)) { Directory.CreateDirectory(assetBundleDirectory); } Debug.Log("BuildPipeline.BuildAssetBundles Begin: " + assetBundleDirectory); BuildPipeline.BuildAssetBundles(assetBundleDirectory, abBuilds, BuildAssetBundleOptions.ChunkBasedCompression, buildTarget); Debug.Log("BuildPipeline.BuildAssetBundles Success"); } |
6.AB包加固
这其实是一个可选项,为了游戏内的反作弊,我们可以接入一些SDK来对抗作弊行为。AB加固也是为了对应的SDK可以在游戏内正常运作。
AB包在加固前后我们使用时其实没有明显的感知,加固只要调用一下SDK对应的指令就可以了~这里就不具体举例了。