圖集
什么是圖集?
在使用3D技術開發2D游戲或制作UI時(即使用GPU繪制),都會使用到圖集,而使用CPU渲染的2D游戲和UI則不存在圖集這個概念(比如Flash的原生顯示列表),那么什么是圖集呢?准確的說法圖集是一張包含了多個小圖的大圖和一份記錄了每個小圖id、位置、尺寸等數據的數據文件,一個圖集應該對應兩個文件,當然也有人把數據集成到圖片中,導致看起來只有一張圖片(參考自DragonBones的做法)。
為什么要用圖集?
在GPU已經成為PC、手機等設備的必備組件的現在,把所有顯示的繪制操作交給專門處理圖像的GPU顯然比交給CPU更合適,這樣空閑下來的CPU可以集中力量處理游戲的邏輯運算。
而GPU處理圖像的做法和CPU是不一樣的,在GPU中,我們要繪制一個圖像需要提交圖片(即紋理)到顯存,然后在進行繪制(這個過程稱為一次DrawCall),那么如果我們一幀要繪制100個就需要提交100次圖片,如果使用包含了這100圖片的圖集,只需要一次提交即可,即一次DrawCall就搞定,處理效率上會有很大的提升。
另外使用圖集也方便管理和歸類各個模塊的圖片,可以通過一次加載和一次卸載完成多個圖片的處理,同理,加載次數也下來了,可以提升運行效率。
Unity2D中的圖集
其實用過NGUI的同學都知道在拼界面之前都必須先制作好對應的圖集才行,然而在Unity2D或UGUI中,Unity卻自己集成了圖集的操作,目的是讓我們忘掉圖集的存在,更加關注設計這個層面。
如何使用圖集
在Unity中我們只要使用小圖片即可,可以通過設置圖片的Packing Tag來指定小圖會被打包到的圖集,比如2個小圖的Packing Tag都叫“MyAtlas”,則Unity會將這兩個小圖打包到名為“MyAtlas”的圖集中。
注意圖片不能放在Resources文件夾下面,Resources文件夾下的資源將不會被打入圖集。
是否打包圖集的控制選項:Editor->Project Settings 下面有sprite packer的模式。Disabled表示不打包圖集,Enabled For Builds 表示只有打包應用的時候才會打包圖集,Always Enabled 表示始終會打包圖集。
在Windows->Sprite Packer 里,點擊packer 在這里你就可以預覽到你的圖集信息,圖集文件被保存在和Assets文件夾同級的目錄,Libary/AtlasCache里面。圖集的大小還有圖集的格式等等很多參數我們都是可以控制的,也可以通過腳本來設置。
通過設置我們就可以發現多個同一Packing Tag的小圖放到場景中只會消耗一個DrawCall,表示我們的圖集已經開始起作用了。
動態設置的問題
可是Unity的這種設計在下面的這種情況下卻給我們帶來了很大的困擾:
當我們希望通過代碼的方式修改紋理時會無從下手,因為Unity3D並沒有給我們提供加載圖集和獲取圖集中指定小圖的API。
解決方案
目標是我想把圖集放在Resources中加載。
首先我們的原圖不能放在Resources文件夾中,這樣會導致資源大小翻倍。
具體的思路是通過腳本創建對應的名稱的預制件,同時將加載的圖片(Sprite)作為該預制件的SpriteRenderer組件的sprite屬性即可。
打包代碼
打包代碼來自我的開源項目中,有一些具體的規則,大家可以看注釋:
1 // ================================================================================================= 2 // 3 // Hammerc Library 4 // Copyright 2015 hammerc.org All Rights Reserved. 5 // 6 // See LICENSE for full license information. 7 // 8 // ================================================================================================= 9 10 using System.IO; 11 using UnityEditor; 12 using UnityEngine; 13 14 /// <summary> 15 /// 16 /// 2D 圖集生成對應預制件腳本類. 17 /// 18 /// 解決問題: 19 /// 解決不能動態設置 Sprite 對象的貼圖的問題, 因為分散的小圖最終需要打包為圖集而 Unity3D 又將圖集的處理隱藏, 所以需要將小圖的信息存放到預制件中. 20 /// 21 /// 使用方法及步驟: 22 /// 1.修改源目錄及目標目錄為自己使用的目錄; 23 /// 2.把小圖存放到源目錄中; 24 /// 3.在菜單欄點擊 Hammerc/2D/MakeSpritePrefabs 即可在目標目錄下生成對應的預制件; 25 /// 4.通過獲取生成的預制件的 sprite 對象即可在程序中使用小圖. 26 /// 27 /// </summary> 28 public class MakeSpritePrefabsScript 29 { 30 /// <summary> 31 /// Assets 目錄下的小圖片目錄, 包括子目錄的所有圖片文件都會進行處理. 32 /// </summary> 33 private const string ORIGIN_DIR = "\\RawData\\Sprites"; 34 35 /// <summary> 36 /// Assets 目錄下的小圖預制件生成的目標目錄, 注意該目錄下不要存放其他資源, 每次生成時都會清空該目錄下的所有文件. 37 /// </summary> 38 private const string TARGET_DIR = "\\Resources\\Sprites"; 39 40 /// <summary> 41 /// 將制定目錄下的原始圖片一對一打包成 Prefab 方便在游戲運行中讀取指定的圖片. 42 /// </summary> 43 [MenuItem("Hammerc/2D/MakeSpritePrefabs")] 44 private static void MakeSpritePrefabs() 45 { 46 EditorUtility.DisplayProgressBar("Make Sprite Prefabs", "Please wait...", 1); 47 48 string targetDir = Application.dataPath + TARGET_DIR; 49 //刪除目標目錄 50 if(Directory.Exists(targetDir)) 51 Directory.Delete(targetDir, true); 52 if(File.Exists(targetDir + ".meta")) 53 File.Delete(targetDir + ".meta"); 54 //創建空的目標目錄 55 if(!Directory.Exists(targetDir)) 56 Directory.CreateDirectory(targetDir); 57 58 //獲取源目錄的所有圖片資源並處理 59 string originDir = Application.dataPath + ORIGIN_DIR; 60 DirectoryInfo originDirInfo = new DirectoryInfo(originDir); 61 MakeSpritePrefabsProcess(originDirInfo.GetFiles("*.jpg", SearchOption.AllDirectories), targetDir); 62 MakeSpritePrefabsProcess(originDirInfo.GetFiles("*.png", SearchOption.AllDirectories), targetDir); 63 64 EditorUtility.ClearProgressBar(); 65 } 66 67 static private void MakeSpritePrefabsProcess(FileInfo[] files, string targetDir) 68 { 69 foreach(FileInfo file in files) 70 { 71 string allPath = file.FullName; 72 string assetPath = allPath.Substring(allPath.IndexOf("Assets")); 73 //加載貼圖 74 Sprite sprite = AssetDatabase.LoadAssetAtPath(assetPath, typeof(Sprite)) as Sprite; 75 //創建綁定了貼圖的 GameObject 對象 76 GameObject go = new GameObject(sprite.name); 77 go.AddComponent<SpriteRenderer>().sprite = sprite; 78 //獲取目標路徑 79 string targetPath = assetPath.Replace("Assets" + ORIGIN_DIR + "\\", ""); 80 //去掉后綴 81 targetPath = targetPath.Substring(0, targetPath.IndexOf(".")); 82 //得到最終路徑 83 targetPath = targetDir + "\\" + targetPath + ".prefab"; 84 //得到應用當前目錄的路徑 85 string prefabPath = targetPath.Substring(targetPath.IndexOf("Assets")); 86 //創建目錄 87 Directory.CreateDirectory(prefabPath.Substring(0, prefabPath.LastIndexOf("\\"))); 88 //生成預制件 89 PrefabUtility.CreatePrefab(prefabPath.Replace("\\", "/"), go); 90 //銷毀對象 91 GameObject.DestroyImmediate(go); 92 } 93 } 94 }
加載代碼
加載就很簡單了,加載對應的預制件,然后取出Sprite即可,核心代碼如下:
1 public Sprite LoadSprite(string spriteName) 2 { 3 return Resources.Load<GameObject>("Sprites/" + spriteName).GetComponent<SpriteRenderer>().sprite; 4 }
設置貼圖就是向GameObject添加SpriteRenderer腳本后設置其sprite屬性即可,如下:
this.gameObject.GetComponent<SpriteRenderer>().sprite = LoadSprite("img1");
打包的問題
雖然解決了動態設置的問題,但是如果我們要讓圖集支持熱更新,使用AssetBundle來打包又該怎么辦呢?
解決方案
目標是我想把圖集放在AssetBundle中加載,AssetBundle的處理比Resources的處理要簡單得多了。
和打包到Resources不同,在AssetBundle中,我們添加圖片就是以Sprite的類型存在的,所以打包以后,實際上是已經制作為圖集了,但是還是可以按獲取單個文件的方式獲取指定的圖片,也就是Sprite,獲取后可以直接使用。
打包代碼
打包代碼同樣來自我的開源項目中,有一些具體的規則,大家可以看注釋:
1 // ================================================================================================= 2 // 3 // Hammerc Library 4 // Copyright 2015 hammerc.org All Rights Reserved. 5 // 6 // See LICENSE for full license information. 7 // 8 // ================================================================================================= 9 10 using UnityEditor; 11 using UnityEngine; 12 13 /// <summary> 14 /// 15 /// 簡單的 AssetBundle 創建類. 16 /// 17 /// 解決問題: 18 /// 實現簡單的資源打包. 19 /// 20 /// 使用方法及步驟: 21 /// 1.修改要打包到的目標平台枚舉; 22 /// 2.選中要打包的文件在菜單欄點擊對應的功能菜單. 23 /// 24 /// </summary> 25 public class SimpleCreateAssetBundle 26 { 27 /// <summary> 28 /// 打包的目標平台. 29 /// </summary> 30 private const BuildTarget BUILD_TARGET = BuildTarget.StandaloneWindows64; 31 32 /// <summary> 33 /// 將選定的一個對象進行打包, 同時包含依賴項, 可通過 AssetBundle 的 main 屬性獲取. 34 /// </summary> 35 [MenuItem("Hammerc/AssetBundle/CreateSingleAssetBundle")] 36 private static void CreateSingleAssetBundle() 37 { 38 if(Selection.activeObject != null) 39 { 40 //顯示保存窗口 41 string path = EditorUtility.SaveFilePanel("Create Single AssetBundle:", "", "New AssetBundle", "assetbundle"); 42 43 if(path.Length > 0) 44 { 45 //打包 46 BuildPipeline.BuildAssetBundle(Selection.activeObject, null, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BUILD_TARGET); 47 } 48 } 49 } 50 51 /// <summary> 52 /// 將選定的多個對象進行打包, 同時包含依賴項, 不指定 AssetBundle 的 main 屬性獲取. 53 /// </summary> 54 [MenuItem("Hammerc/AssetBundle/CreateMultipleAssetBundle")] 55 private static void CreateMultipleAssetBundle() 56 { 57 if(Selection.objects.Length > 0) 58 { 59 //顯示保存窗口 60 string path = EditorUtility.SaveFilePanel("Create Multiple AssetBundle:", "", "New AssetBundle", "assetbundle"); 61 62 if(path.Length > 0) 63 { 64 //打包 65 BuildPipeline.BuildAssetBundle(null, Selection.objects, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BUILD_TARGET); 66 } 67 } 68 } 69 }
我們不需要關心文件類型,Unity3D會自動幫我們把圖片格式的文件轉換為Sprite類型。
加載代碼
加載代碼就比較簡單了,加載AssetBundle后從這個AssetBundle中直接加載圖片即可。
1 public Sprite LoadSprite(string spriteName) 2 { 3 if(assetbundle == null) 4 assetbundle = AssetBundle.CreateFromFile(Application.streamingAssetsPath +"/Img.assetbundle"); 5 return assetbundle.Load(spriteName) as Sprite; 6 }
設置貼圖就是向GameObject添加SpriteRenderer腳本后設置其sprite屬性即可,如下:
this.gameObject.GetComponent<SpriteRenderer>().sprite = LoadSprite("img1");