ContentPipeline 算是比較進階的議題,但是也不會太困難,想要用自己定義的檔案時就需要用到了。先來看看 Pipeline 的組件
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiInBnauATNwETN1MUSvw1Ztlmb5R2Lc12bj5Cdm92cvJ3Yp1mLuR2ct5Savw1LcpDc0RHaiojIsJye.jpg)
圖片來源 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,而不必把初始資料都寫在程式碼裡了。