Unity编辑器扩展: 程序化打图集工具
開始前的聲明:該案例中圖集所使用圖片資源均來源于網(wǎng)絡(luò),僅限于學(xué)習(xí)使用
一、前言
關(guān)于編輯器擴(kuò)展相關(guān)的知識(shí),在前面的兩篇內(nèi)容中做了詳細(xì)的描述,鏈接地址:
第一篇 :Unity編輯器擴(kuò)展 UI控件篇
第二篇 :Unity編輯擴(kuò)展:功能篇之Json數(shù)據(jù)編輯器
前兩篇著重于介紹編輯器界面擴(kuò)展相關(guān)控件接口的使用方式。作為系列文章的第三篇,會(huì)更偏重于引擎內(nèi)編輯器擴(kuò)展承擔(dān) 的提升開發(fā)效率的功能模塊設(shè)計(jì)
通過程序化打圖集減少工作量的同時(shí)可以穩(wěn)定全局的管理圖集,避免隨著項(xiàng)目膨脹手動(dòng)管理產(chǎn)生資源上的混亂。從圖集整個(gè)生命周期來說,對于圖集管理通常需要下面的模塊支持:
- 程序化圖集打包
- UI界面引用圖集檢測工具
- 圖集資源自動(dòng)/手動(dòng)加載、卸載框架支持
本篇文章介紹第一部分,即圖集程序化打包的邏輯執(zhí)行
1、關(guān)于圖集
官方文檔對精靈圖集的描述:
在2D 項(xiàng)目使用精靈和其他圖形來創(chuàng)建其場景的視覺效果。這意味著單個(gè)項(xiàng)目可能包含許多紋理文件。Unity 通常會(huì)為場景中的每個(gè)紋理發(fā)出一個(gè)繪制調(diào)用。但是,在具有許多紋理的項(xiàng)目中,多個(gè)繪制調(diào)用會(huì)占用大量資源,并會(huì)對項(xiàng)目的性能產(chǎn)生負(fù)面影響
精靈圖集 (Sprite Atlas) 是一種將多個(gè)紋理合并為一個(gè)組合紋理的資源。Unity 可以調(diào)用此單個(gè)紋理來發(fā)出單個(gè)繪制調(diào)用而不是發(fā)出多個(gè)繪制調(diào)用,能夠以較小的性能開銷一次性訪問壓縮的紋理。此外,精靈圖集 API 還可以控制如何在項(xiàng)目運(yùn)行時(shí)加載精靈圖集
圖集對性能開銷的正向影響:
從文檔描述中可以看出,圖集主要在兩方面影響影響性能開銷:
第一,減少繪制調(diào)用,即提升合批數(shù)量,減少Draw Call
從文檔的描述可以看出,圖集概念出現(xiàn)與渲染的繪制調(diào)用相關(guān),衡量繪制調(diào)用通常以Draw Call數(shù)量為標(biāo)準(zhǔn)。而在Unity中UI合批策略,不同的Image控件要執(zhí)行合批必須要有相同的材質(zhì)、Texture。為了滿足該條件,將一些小圖合并成為大圖片,就可以在渲染時(shí),盡可能的一次性的將渲染數(shù)據(jù)提交給GPU
第二,合理的圖集打包策略利于資源的壓縮
在前面的性能優(yōu)化文章中有提到過,某些壓縮策略通常只會(huì)對規(guī)則大小的圖片資源生效。如下圖提示,當(dāng)導(dǎo)入的圖片資源非2倍數(shù)時(shí),引擎會(huì)彈出對應(yīng)警告信息。Unity引擎對Texture資源的常用的DXT壓縮處理需要滿足其寬度與高度的必須為4的倍數(shù)(這是因?yàn)镈XT壓縮策略是以4X4的像素塊為基本單位做處理)
而對于圖集而言,其尺寸設(shè)定策略時(shí)基于是以2的次方為基本數(shù)值,即圖集是可以執(zhí)行壓縮的。而資源的壓縮無疑會(huì)提升資源的載入速度
圖集對性能開銷的負(fù)面影響:
如果圖集使用不當(dāng),也可能會(huì)額外占用大量的內(nèi)存,舉例來說,如果當(dāng)前界面只使用了某一圖集中很小的一張圖片,卻不得已將整張圖集加載到內(nèi)存中。亦或者說由于打入圖集的Sprite的尺寸不合理,使得圖集產(chǎn)生大量的空白,產(chǎn)生額外的性能消耗
2、Unity中圖集打包方法
在Unity中手動(dòng)打出圖集的方法,在之前的文章有描述,這里稍微做一些描述,如果想了解詳細(xì)的操作過程,可以查看該文章,鏈接地址:
Unity 將Sprite打包進(jìn)圖集
Sprite Packer:
在2020.1或更早的版本中,Unity提供了Sprite Packer圖集紋理的生成和使用方法。相比于其他的打圖集方式,Sprite Packer是封裝性較高的方式。通常只會(huì)對相應(yīng)的Sprite預(yù)設(shè)好指定圖集標(biāo)簽。后續(xù)圖集本身的資源管理基本由引擎自設(shè)定。這樣做的優(yōu)勢可以減少開發(fā)者的工作量,但同時(shí)也犧牲了開發(fā)者對資源管理的靈活性。而不同項(xiàng)目的資源利用策略的不同又很需要這樣的靈活性來定制
Sprite Atlas:
通過Sprite Atlas打出圖集會(huì)生成對應(yīng)的序列化配置文件,并且在資源面板是可見與可編輯的,可以靈活的控制圖集資源的載入與卸載,當(dāng)然也可以默認(rèn)使用Unity自設(shè)定的加載與刪除策略
動(dòng)態(tài)圖集:
動(dòng)態(tài)圖集是相對比較高階的打圖集解決方案,由于Unity沒有提供與之對應(yīng)的處理方法,意味著要自己實(shí)現(xiàn)一套集合動(dòng)態(tài)資源管理、高效的圖集生成算法等等
拋棄實(shí)現(xiàn)難度,動(dòng)態(tài)圖集目前是對于某些動(dòng)態(tài)UI元素(如王者榮耀英雄頭像)界面少有的解決方案
二、代碼結(jié)構(gòu)
在對圖集的理論知識(shí)做完解釋后,開啟核心的程序設(shè)計(jì)階段。后面的內(nèi)容主要集中于對圖集程序化過程的代碼解釋,主要是路徑圍繞編輯器內(nèi)資源的遍歷查詢、創(chuàng)建刪除與對圖集打出參數(shù)操作方面的功能模塊做設(shè)計(jì)
1、數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì):
該圖集生成工具的編輯器的界面操作邏輯不是很復(fù)雜,不過也需要維護(hù)一個(gè)簡單的數(shù)據(jù)類。來記錄編輯的緩存數(shù)據(jù)
除了一些常規(guī)的標(biāo)識(shí)ID字段與簡單的資源索引字段。稍微需要注意的是,在于對本地文件索引后SpriteAtlas的對象的直接保存在某些操作后造成索引丟失而出現(xiàn)空引用。所以這里的atlas字段會(huì)指向本地資源的實(shí)例話數(shù)據(jù)的緩存,來避免指向丟失,影響后續(xù)的數(shù)據(jù)操作
public class AtlasData {public string atlasName;public string assetPath;/// <summary>/// 緩存中的SpriteAtlas,不直接指向本地資源/// </summary>public SpriteAtlas atlas;public List<Sprite> sprites;//編輯器界面數(shù)據(jù)public bool isShowDital; }2、獲取某文件夾下的所有符合圖集生成規(guī)范的Sprite類型的資源:
通過遍歷該文件夾下所有文件(不包括子文件下的文件),并篩選出滿足條件的Sprite文件,得到該路徑文件夾下的Sprite列表。
這部分代碼的邏輯設(shè)計(jì)集中于資源路徑的讀取遍歷,基于DirectoryInfo得到所有的文件信息,得到文件路徑并通過AssetDataBase來加載特定的Sprite類型文件資源
在對特定類型資源文件執(zhí)行遍歷時(shí),可以優(yōu)先排除數(shù)量較大的Meta文件,減少查詢數(shù)量,提升文件遍歷的操作效率。Unity中文件類型通常有資源文件、代碼文件、文本文件、序列化文件、Meta文件等,通常來說,所有在Unity中Asset面板中可見的文件資源都會(huì)對應(yīng)一份同名的meta文件,用來記錄類似于GUID等標(biāo)識(shí)身份相關(guān)的信息或者與對應(yīng)資源相關(guān)的設(shè)定數(shù)據(jù)
List<Sprite> GetFileSprites(string relativePath){if (Directory.Exists(relativePath)){DirectoryInfo direction = new DirectoryInfo(relativePath);FileInfo[] files = direction.GetFiles("*");//只查找本文件夾下if (files == null) return null;List<Sprite> sprites = new List<Sprite>();foreach (var file in files){if(file.Name.EndsWith(".meta")) continue;var item = AssetDatabase.LoadAssetAtPath<Sprite>(relativePath + file.Name);if (item != null && ChackSpritePackerState(item)) {sprites.Add((Sprite)item); }}return sprites;}return null;}3、打入圖集策略
打圖集需要考慮合批效率與圖集空間利用率之間的平衡,通常與項(xiàng)目的具體設(shè)定有關(guān)
篩選策略主要集中于對Sprite尺寸的限制,避免在圖集中打入過大的紋理,之所以這么說,是因?yàn)樵酱蟪叽绲募y理,會(huì)有更高的概率產(chǎn)生更多的空白空間浪費(fèi)。即尺寸越大,對空間的利用率越低
- Sprite的寬度小于設(shè)定值
- Sprite的高度小于設(shè)定值
- Sprite的像素點(diǎn)小于設(shè)定值
對Sprite打入圖集時(shí),會(huì)對整個(gè)UI資源文件中的Sprite資源執(zhí)行遍歷,可以依托這個(gè)機(jī)會(huì)對圖片資源的美術(shù)規(guī)范執(zhí)行監(jiān)測。對不符合條件的資源及時(shí)檢測與處理。由于該操作不是本文的重點(diǎn),所以做了簡單的日志打印處理
private bool ChackSpritePackerState(Sprite sprite){if (sprite.rect.width > maxSpriteSize){if (sprite.rect.width % 2 != 0|| sprite.rect.height % 2 != 0) {Debug.LogError($"{sprite.name}尺寸不符合壓縮規(guī)范(寬高均為2的倍數(shù)),請注意");}return false;}if (sprite.rect.height > maxSpriteSize){if (sprite.rect.width % 2 != 0 || sprite.rect.height % 2 != 0){Debug.LogError($"{sprite.name}寬度不符合壓縮規(guī)范(寬高均為2的倍數(shù)),請注意");}return false;}if (sprite.rect.width * sprite.rect.height > maxSpritepixelNum *1024){return false;}return true;}上述的打圖集策略是對于單個(gè)UI面板內(nèi)的Sprite執(zhí)行管理。而對于整個(gè)項(xiàng)目而言,有一個(gè)比較重要的概念就是通用圖集。其存在的意義是為了解決項(xiàng)目中存在的在很多UI界面都有使用的Srpite資源,如返回Button控件的Texture,可能游戲中的每個(gè)界面使用的都是同一張。為了避免圖集的低效率引用,有下面兩種打圖集策略:
- 每個(gè)界面都單獨(dú)復(fù)制一張,優(yōu)點(diǎn)合批效率高,缺點(diǎn)會(huì)過多的增加包體的占用空間
- 以通用圖集方式管理多界面使用紋理,優(yōu)點(diǎn)減少資源載入(和資源管理策略有關(guān))
對通用圖集中也有一定的利弊權(quán)衡,首先最直接的是通用圖集與當(dāng)前界面圖集中的紋理無法合批,其次就是同一時(shí)間內(nèi)對通用圖集內(nèi)的資源使用率可能很低,造成一定的內(nèi)存浪費(fèi)
4、遞歸遍歷文件夾并根據(jù)獲取信息創(chuàng)建數(shù)據(jù)節(jié)點(diǎn):
簡單來說,該過程是通過遍歷文件夾下得到當(dāng)前文件夾下的Sprite資源與其子文件夾,同時(shí)對存在所有的子文件夾執(zhí)行遞歸遍歷,來得到該文件夾打成圖集需要的數(shù)據(jù)
對于項(xiàng)目而言,資源不可能都保存在一個(gè)層級(jí)下面,通常會(huì)根據(jù)UI功能模塊產(chǎn)生多級(jí)文件夾劃分。即某一文件夾下除了有當(dāng)前文件夾對應(yīng)界面UI元素的Texture資源,還有可能有其子功能界面的資源文件夾。為了可以完整的對資源文件執(zhí)行查詢與圖集打出,需要對整個(gè)文件夾的樹形結(jié)構(gòu)執(zhí)行遍歷,方式最簡單的就是遞歸調(diào)用
文件夾的操作遍歷依舊是基于DirectoryInfo來完成的,通過其接口GetDirectories可以得到該文件夾下所有子文件夾的信息,
void ChackAssetFile(string relativePath){List<Sprite> sprites = GetFileSprites(relativePath);if (sprites != null && sprites.Count > 1) {string atlasname = GetAtlasNameFromPath(relativePath);string atlasPath = relativePath + atlasname;CreateSpriteAtlas(atlasname, atlasPath, sprites); }DirectoryInfo direction = new DirectoryInfo(relativePath);if (direction == null) return;DirectoryInfo[] dirChild = direction.GetDirectories();foreach (var item in dirChild){ChackAssetFile(relativePath + item.Name + "/");}}在得到根據(jù)路徑索引的圖集設(shè)定數(shù)據(jù)后,創(chuàng)建實(shí)例數(shù)據(jù)類AtlasData并初始化相關(guān)字段保存到atlasDatas字典中,以便在后續(xù)的邏輯中使用
void CreateSpriteAtlas(string atlasname, string atlasPath, List<Sprite> sprites){if(atlasDatas.ContainsKey(atlasPath)){Debug.LogError("警告,有相同名字的Sprite資源文件夾!!!");return;} AtlasData data = new AtlasData(){atlasName = atlasname.Replace(".asset",""),assetPath = atlasPath,sprites = sprites};atlasDatas.Add(atlasPath,data);}5、全局設(shè)定圖集狀態(tài):
在上面的圖片中,標(biāo)識(shí)了對于圖集狀態(tài)設(shè)定的三個(gè)區(qū)域,分別用來設(shè)定打包資源策略的Packing、圖集資源設(shè)定Texture與圖集尺寸和壓縮格式的設(shè)定。 該其各項(xiàng)參數(shù)可以參考Unity官方文檔的解釋:
Include in build:
Unity官方描述對Include in build的描述文字”選中此復(fù)選框可在當(dāng)前構(gòu)建中包含精靈圖集資源“`比較簡潔,可能會(huì)讓開發(fā)者忽略其功能作用,但是實(shí)際上該參數(shù)選項(xiàng)代表著兩套較為復(fù)雜的圖集資源管理邏輯的切換系統(tǒng)
在Asset路徑下創(chuàng)建的Sprite Atlas是響應(yīng)圖集生成策略所對應(yīng)的序列化配置文件。在編輯器狀態(tài)下,真正的圖集紋理資源文件會(huì)暫時(shí)存儲(chǔ)于項(xiàng)目Asset文件夾同級(jí)的緩存Library文件夾下的AtlasCache目錄下,會(huì)游戲構(gòu)建中時(shí)將其打包進(jìn)游戲內(nèi)。編輯器狀態(tài)下項(xiàng)目文件目錄如下圖所示:
如果項(xiàng)目中圖集資源生命周期的管理依托引擎自設(shè)定策略,編輯器狀態(tài)下的圖集使用需確保勾選Include in build。如果未勾選,圖集紋理不會(huì)被自動(dòng)載入到內(nèi)存中,游戲中的UI會(huì)顯示白圖狀態(tài)。不過此狀態(tài)下運(yùn)行時(shí)會(huì)觸發(fā)SpriteAtlasManager.atlasRequested事件,開發(fā)可以注冊事件到該函數(shù)來手動(dòng)管理Sprite Atlas的加載、卸載
Use Crunch Compression:
文檔描述:Crunch 是一種基于 DXT 或 ETC 紋理壓縮的有損壓縮格式。Unity 在 CPU 上將紋理解壓縮為 DXT 或 ETC,然后在運(yùn)行時(shí)將其上傳到 GPU。Crunch 壓縮有助于紋理在磁盤上使用盡可能少的空間并方便下載。Crunch 紋理可能需要很長時(shí)間進(jìn)行壓縮,但在運(yùn)行時(shí)的解壓縮速度非常快
可以簡單的將其理解基于DXT的二次壓縮,該階段發(fā)生于CPU階段對Texure的載入,然后由CPU解碼為 DXT 或 ETC壓縮格式的資源并傳給GPU。即通過少量額外解壓縮計(jì)算提升資源的加載速度,但是需要注意該壓縮方式是有損壓縮,需要權(quán)衡性能與效果
sRGB:
可以默認(rèn)勾選,通常來說Color Space為Linear空間時(shí),勾選sRGB會(huì)對在伽馬空間內(nèi)繪制的圖片做一個(gè)伽馬矯正,來確保最終的顯示效果與美術(shù)繪制的效果相同
之所以要默認(rèn)勾選是因?yàn)槊佬g(shù)通過顯示器繪制圖片時(shí),是經(jīng)過被伽馬矯正后顯示的。而實(shí)際的圖片資源數(shù)據(jù)是處于伽馬空間下的。當(dāng)資源導(dǎo)入引擎內(nèi)后,如果直接按照Linear空間處理,色彩亮度就會(huì)出現(xiàn)偏差。就需要sRGB選項(xiàng)控制對其做伽馬矯正
代碼結(jié)構(gòu):
設(shè)定圖集的代碼如下。因?yàn)樯晕⑼祽辛艘幌?#xff0c;對于核心功能參數(shù)利用編輯器UI接口輸入控制,非核心參數(shù)就直接代碼寫死了
void SetUpAtlasInfo(ref SpriteAtlas atlas){atlas.SetIncludeInBuild(isIncludeInBuild);//A區(qū)域參數(shù)設(shè)定SpriteAtlasPackingSettings packSetting = new SpriteAtlasPackingSettings(){blockOffset = 1,enableRotation = false,enableTightPacking = false,padding = 2,};atlas.SetPackingSettings(packSetting);//B區(qū)域參數(shù)設(shè)定SpriteAtlasTextureSettings textureSetting = new SpriteAtlasTextureSettings(){readable = false,generateMipMaps = false,sRGB = true,filterMode = FilterMode.Bilinear,};atlas.SetTextureSettings(textureSetting);//C區(qū)域參數(shù)設(shè)定TextureImporterPlatformSettings platformSetting = new TextureImporterPlatformSettings(){maxTextureSize = (int)maxSpriteAtlasSize,format = TextureImporterFormat.Automatic,crunchedCompression = true,textureCompression = TextureImporterCompression.Compressed,compressionQuality = 50,};atlas.SetPlatformSettings(platformSetting);}6、將緩存數(shù)據(jù)本地化保存:
根據(jù)前面操作得到的atlasDatas緩存數(shù)據(jù),來對本地SpriteAtlas資源執(zhí)行修改或創(chuàng)建操作。
通過使用路徑信息對圖集資源執(zhí)行索引,如果返回結(jié)果不為空,則對返回SpriteAtlas執(zhí)行操作,設(shè)定圖集參數(shù)格式,并將符合條件未打入該圖集的Sprite添加到其中,如果不存在圖集資源,就設(shè)定好參數(shù)與數(shù)據(jù)后,利用AssetDataBase的接口來創(chuàng)建本地序列化文件SpriteAtlas
void SaveAtlasData(){foreach (var item in atlasDatas.Values){string path = item.assetPath;SpriteAtlas atlas = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(path);if (atlas != null) {SetUpAtlasInfo(ref atlas);List<Sprite> sprites = new List<Sprite>();foreach (var sprite in item.sprites){if(atlas.GetSprite(sprite.name)==null){sprites.Add(sprite);}}atlas.Add(sprites.ToArray());item.atlas = Instantiate(atlas);continue;}atlas = new SpriteAtlas();SetUpAtlasInfo(ref atlas);atlas.Add(item.sprites.ToArray());item.atlas = atlas;AssetDatabase.CreateAsset(atlas, path);}}7、編輯工具界面繪制:
參數(shù)控制區(qū)域:
對于要設(shè)定調(diào)整的參數(shù)通過編輯器擴(kuò)展的相關(guān)接口,這里挑選了幾個(gè)相對比較重要的參數(shù)通過編輯器控件APIU設(shè)計(jì)了UI輸入控制接口
void DrawSpriteAtlasSetting(){EditorGUILayout.LabelField("圖集相關(guān)設(shè)定:",titleStyle); GUILayout.Space(4);maxSpriteAtlasSize = EditorGUILayout.IntPopup("圖集最大尺寸為",maxSpriteAtlasSize, sizeStrs,sizes);isIncludeInBuild = EditorGUILayout.Toggle("是否將圖集打入包內(nèi)", isIncludeInBuild);GUILayout.Space(5);EditorGUILayout.LabelField("Sprite資源限制策略:",titleStyle); GUILayout.Space(4);maxSpritepixelNum = EditorGUILayout.Slider("Sprite最大像素量(單位K):", maxSpritepixelNum ,0,1024);maxSpriteSize = EditorGUILayout.IntPopup("Sprite最大尺寸限制", maxSpriteSize, sizeStrs, sizes);}打包數(shù)據(jù)列表顯示:
在atlasDatas里面記錄了所有圖集的狀態(tài)信息,為了更好的檢測管理,將其顯示在界面上:
在上圖的編輯器界面中,涉及到兩個(gè)特殊的編輯器效果
第一個(gè)是資源選中路徑索引與狀態(tài)展示,是通過對編輯器接口中的Selection.activeObject設(shè)定要選中的Object來實(shí)現(xiàn),注意要指向的對象是通過路徑加載的本地資源文件,而不是由之前編輯狀態(tài)所保存的實(shí)例緩存數(shù)據(jù)
第二個(gè)效果需要通過BeginFoldoutHeaderGroup與EndFoldoutHeaderGroup編輯器擴(kuò)展接口來完成界面UI的展開收起效果
代碼結(jié)構(gòu)為:
void DrawAtlasInfo(){GUILayout.Label("圖集資源列表: ",titleStyle);GUILayout.Space(10);foreach (var item in atlasDatas.Values){item.isShowDital = EditorGUILayout.BeginFoldoutHeaderGroup(item.isShowDital, "展開: " + item.atlasName);if (item.isShowDital){EditorGUILayout.TextField(" 圖集名字:", item.atlasName);EditorGUILayout.TextField(" 資源路徑:", item.assetPath);EditorGUILayout.IntField(" 圖集中Sprite數(shù)量", item.atlas.spriteCount);if(GUILayout.Button("打開并查看該圖集資源")){AssetDatabase.MakeEditable(item.assetPath);Selection.activeObject = AssetDatabase.LoadAssetAtPath<SpriteAtlas>(item.assetPath);}}EditorGUILayout.EndFoldoutHeaderGroup();GUILayout.Space(5);}}三、總結(jié)
程序化打出圖集,本質(zhì)是對本地資源的操作處理,邏輯上的東西不復(fù)雜。主要是通過DirectoryInfo與AssetDataBase兩個(gè)文件操作類執(zhí)行本地文件的查詢編輯與對SpriteAtlas的創(chuàng)建刪除等操作。核心還是對于圖集的功能理解與參數(shù)設(shè)定以及對打出圖集策略的考慮
總結(jié)
以上是生活随笔為你收集整理的Unity编辑器扩展: 程序化打图集工具的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C Primer Plus 第3章 数据
- 下一篇: UGUI图集制作