天天看点

ContentPipeline

ContentPipeline 算是比較進階的議題,但是也不會太困難,想要用自己定義的檔案時就需要用到了。先來看看 Pipeline 的組件

ContentPipeline

圖片來源 http://msdn.microsoft.com/en-us/library/bb447745.aspx

如果都客製化就如下圖

ContentPipeline

圖片來源 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

ContentPipeline

再將 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

ContentPipeline

產生專案後,加入 DataType 參考,再將預設的 ContentProcessor1.cs 刪除,自己新增一個類別,取名為 AssetContent.cs,這是編譯時期的資源物件,我們不打算在編譯時期對資源設定檔的內容作任何處理,所以內容很簡單 

public class AssetContent {
    public string Content { get; set; }
}
      

接著再新增一個類別,在新增類別對話框可以發現有三種檔案可以選,Importer、Processor 和 Type Writer。

ContentPipeline

先新增一個 Content Importer,取名叫做 AssetImporter.cs。預設的內容會有一個附加屬性 ContentImporter,是用來在設定相關編譯訊息,第一個參數是可以讀取的檔案副檔名,DisplayName 是設定時的名稱,DefaultProcessor 是預設要使用的 Processor。

ContentPipeline

我們將 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。

ContentPipeline

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

ContentPipeline
ContentPipeline

這樣 Asset.xm l 檔案就會使用我們自訂的 Importer。

經過一番苦戰,以後再載入資源的時候只要如下面程式碼: 

asset = Content.Load<AssetData>("Asset");
AnimationPlayer = new AnimationPlayer(asset.Animations["Walk"]);
      

先載入 Asset 後,在跟 Asset 要需要的資源即可,要改變的話只需要編輯 XML,而不必把初始資料都寫在程式碼裡了。

继续阅读