ContentPipeline 算是比較進階的議題,但是也不會太困難,想要用自己定義的檔案時就需要用到了。先來看看 Pipeline 的組件
圖片來源 http://msdn.microsoft.com/en-us/library/bb447745.aspx
如果都客製化就如下圖
圖片來源 http://msdn.microsoft.com/en-us/library/ff433775.aspx
藍色是編譯時期,紅色是執行時期。Soure Asset 就是自定義的檔案,經過 Importer 讀入成 Content DOM Type,然後再經過 Processor 轉換成 Output Type,然後經過 Writer 編譯成 XNB 檔案,最後在遊戲執行時讀入 XNB 檔,使用 Reader 轉換成可以使用的物件。白話一點的說就是一個檔案藉由 Importer 讀入成一個物件,在經過 Processor 轉換成另一個物件,最後經由 Writer 存成 XNB 檔,以上這些動作都是編譯時期的動作,然後遊戲要載入資源的時候會經由 Reader 將 XNB 檔轉成遊戲內的物件。
因此我們可以藉由設定 XML 來設定動畫,雖然預設有 XmlImport,但是它能讀的 XML 的內容就必須符合規定,使用起來礙手礙腳的,所以打算自己做一個 Importer,而單純的資源設定檔不必多做特別的處哩,所以不必有 Processor,再來 Writer 和 Reader 是必要的,大概決定之後就開始設計內容。
我們要用 XML 檔案的方式設定資源,先決定資料的結構:
Element
Texture | 素材的資料 |
Animation | 動畫的資料 |
Frame | 動畫單格的資料 |
Attribute
Texture | |
Name | 素材的名稱 |
Path | 素材檔案的路徑 |
Animation | |
Name | 動畫的名稱 |
TextureName | 使用素材的名稱 |
IsLoop | 是否重複撥放 |
Origin | 原點位移 |
Frames | 所有的畫格資料 |
Frame | |
Rect | 來源位置 |
Time | 停留時間 (毫秒) |
接著加入新的 Windows Phone Game Library 專案,取名為 DataType
再將 DataType 專案裡自動產生的 Class1.cs 砍掉,增加四個檔案,分別為 TextureData.cs,AnimationData.cs,FrameData.cs、AssetData.cs。 TextureData.cs 的內容如下:
public class TextureData { public Texture2D Image { get; set; } public string Name { get; set; } public string Path { get; set; } }
FrameData.cs 內容如下:
public class FrameData { public Rectangle Rect { get; set; } public float Time { get; set; } }
AnimationData.cs 內容如下:
public class AnimationData { public Texture2D Texture { get; set; } public string Name { get; set; } public string TextureName { get; set; } public bool IsLoop { get; set; } public Vector2 Origin { get; set; } public List<FrameData> Frames { get; set; } public AnimationData() { Frames = new List<FrameData>(); } }
AssetData.cs 內容如下,:
public class AssetData { public Dictionary<string, TextureData> Textures { get; set; } public Dictionary<string, AnimationData> Animations { get; set; } public AssetData() { Textures = new Dictionary<string, TextureData>(); Animations = new Dictionary<string, AnimationData>(); } }
前三個類別都只是設定動畫需要的基本資料而已,第四個類別是集合所有資源用的,我們讀取 XML 資料後要轉換成這四種類別。
然後是 Importer。新增一個 Content Pipeline Extension Library 專案,取名叫做 DataTypeContentPipeline
產生專案後,加入 DataType 參考,再將預設的 ContentProcessor1.cs 刪除,自己新增一個類別,取名為 AssetContent.cs,這是編譯時期的資源物件,我們不打算在編譯時期對資源設定檔的內容作任何處理,所以內容很簡單
public class AssetContent { public string Content { get; set; } }
接著再新增一個類別,在新增類別對話框可以發現有三種檔案可以選,Importer、Processor 和 Type Writer。
先新增一個 Content Importer,取名叫做 AssetImporter.cs。預設的內容會有一個附加屬性 ContentImporter,是用來在設定相關編譯訊息,第一個參數是可以讀取的檔案副檔名,DisplayName 是設定時的名稱,DefaultProcessor 是預設要使用的 Processor。
我們將 Importer 修改如下:
using TImport = DataTypeContentPipeline.AssetContent; [ContentImporter(".xml", DisplayName = "Asset Importer")] public class AssetImporter : ContentImporter<TImport> { public override TImport Import(string filename, ContentImporterContext context) { TImport import = new TImport(); using (StreamReader sr = new StreamReader( File.OpenRead(filename))){ import.Content = sr.ReadToEnd(); } return import; } }
他要做的事情很簡單,就是讀取檔案然後輸出 AssetContent 物件。
沒有 Processor,下一步就是 Writer,新增一個 Content Type Writer ,取名為 AssetWriter.cs。內容如下:
using TWrite = DataTypeContentPipeline.AssetContent; [ContentTypeWriter] public class AssetWriter : ContentTypeWriter<TWrite> { protected override void Write(ContentWriter output, TWrite value) { XDocument xdoc = XDocument.Parse(value.Content); WriteTextures(output, xdoc.Root); WriteAnimations(output, xdoc.Root); } private void WriteTextures(ContentWriter output, XElement xdoc) { var textures = xdoc.Elements("Texture"); output.Write(textures.Count()); foreach (var item in textures) { output.Write(item.Attribute("Name").Value); output.Write(item.Attribute("Path").Value); } } private void WriteAnimations(ContentWriter output, XElement xdoc) { var animations = xdoc.Elements("Animation"); output.Write(animations.Count()); foreach (var item in animations) { output.Write(item.Attribute("Name").Value); output.Write(item.Attribute("TextureName").Value); output.Write(bool.Parse(item.Attribute("IsLoop").Value)); string[] origin = item.Attribute("Origin").Value.Split(' '); output.Write(new Vector2(float.Parse(origin[0]), float.Parse(origin[1]))); //System.Diagnostics.Debug.Assert(false, string.Format("{0}", v)); WriteFrames(output, item); } } private void WriteFrames(ContentWriter output, XElement xdoc) { var frames = xdoc.Elements("Frame"); output.Write(frames.Count()); foreach (var item in frames) { string[] rect = item.Attribute("Rect").Value.Split(' '); output.WriteObject<Rectangle>(new Rectangle(int.Parse(rect[0]), int.Parse(rect[1]), int.Parse(rect[2]), int.Parse(rect[3]))); output.Write(int.Parse(item.Attribute("Time").Value)); } } public override string GetRuntimeReader(TargetPlatform targetPlatform) { return "DataType.AssetDataReader, DataType"; } }
內容看起來複雜,其實只是把 XML 檔案依序讀出來然後利用 ContentWriter 物件寫入 XNB 檔,寫入時最好轉換成正確的型態,這樣可以減少執行時期的轉換時間,基本上常用的資料型態 Write 函式都有多載,如果沒有的話就使用 WriteObject,如果要寫入自己定義的物件,在使用 WriteObject 時還要傳入自訂的 TypeWriter。
而 GetRuntimeReader 是用來決定執行遊戲時,用這個 Writer 寫入的 XNB 需要用哪個 Reader 來讀取,回傳的字串格是必須固定為 ”Namespace.Reader, Assembly”,所以 "DataType.AssetDataReader, DataType" 就表示 Reader 是在 DataType 組件裡的 DataType.AssetDataReader 物件。 編譯時期的工作都做完了,接著是執行時期的工作,只有 Reader 需要寫。在 DataType 專案內新增一個 Content Type Reader 類別,取名為 AssetDataReader.cs。
Reader 和 Writer 必須互相配合,Writer 怎麼寫 Reader 就要怎麼讀,順序和資料型態都不可以不同,用 Write 寫就要用 Read 讀、用WriteObject 寫就要用 ReadObject 讀。AssetDataReader 程式碼如下:
using TRead = DataType.AssetData; public class AssetDataReader : ContentTypeReader<TRead> { protected override TRead Read(ContentReader input, TRead existingInstance) { TRead data = existingInstance; if (data == null) data = new TRead(); ReadTextures(input, data); ReadAnimations(input, data); return data; } private void ReadTextures(ContentReader input, TRead data) { int count = input.ReadInt32(); for (int i = 0; i < count; i++) { string name = input.ReadString(); string path = input.ReadString(); Texture2D texture = input.ContentManager.Load<Texture2D>(path); data.Textures.Add(name, new TextureData { Image = texture, Name = name, Path = path }); } } private void ReadAnimations(ContentReader input, TRead data) { int count = input.ReadInt32(); for (int i = 0; i < count; i++) { string name = input.ReadString(); string textureName = input.ReadString(); bool isLoop = input.ReadBoolean(); Vector2 origin = input.ReadVector2(); AnimationData animation = new AnimationData { Name = name, TextureName = textureName, IsLoop = isLoop, Origin = origin, Texture = data.Textures[textureName].Image, }; ReadAnimationFrames(input, animation); data.Animations.Add(name, animation); } } private void ReadAnimationFrames(ContentReader input, AnimationData animation) { int count = input.ReadInt32(); for (int i = 0; i < count; i++) { Rectangle rect = input.ReadObject<Rectangle>(); int time = input.ReadInt32(); animation.Frames.Add(new FrameData { Rect = rect, Time = time }); } } }
看起來複雜,其實和 Writer 類似,將資料一筆一筆讀出來傳給 AssetData,最後組成完整的 AssetData。
Content Pipeline 相關程式完成之後,在 Content 專案加入 DataTypeContentPipeline 參考,然後增加一個 Asset.xml 檔案,將 Asset.xml 的 Content Importer 屬性設定成 Asset Import
這樣 Asset.xm l 檔案就會使用我們自訂的 Importer。
經過一番苦戰,以後再載入資源的時候只要如下面程式碼:
asset = Content.Load<AssetData>("Asset"); AnimationPlayer = new AnimationPlayer(asset.Animations["Walk"]);
先載入 Asset 後,在跟 Asset 要需要的資源即可,要改變的話只需要編輯 XML,而不必把初始資料都寫在程式碼裡了。