自動XNBファイルシリアライズ

コンテントタイプライター/リーダーを書くのは面倒

XNA Game Studioのコンテント・パイプラインでのデータの流れは下図のようになっています。オフラインプロセスは開発しているVisual Studio上でビルドしたときにWindows上で処理されるプロセスで、オフラインプロセスはWindows、Xbox 360、Zune上でゲームを実行したときに処理されるプロセスです。

cp-001

元からサポートされているデータをそのまま扱う場合には良いのですが、カスタムデータを扱う場合に書かないといけないのが、ContentTypeWrite/ContentTypeReaderでした。特に面倒だったのが、読み込みと書き込みという対称的な処理をするのにContentTypeWriterはオフラインプロセス側、ContentTypeReaderはオンラインプロセス側と別々の場所で書かないといけないことでした。

この面倒臭さを解決する為に、XNA GS 3.1では自動XNBシリアライズ機能が追加されました。この機能を使うことで、多くの場面でContentTypeWriter/ContentTypeReaderを書く必要がなくなりました。

自動XNBシリアライズ機能の使い方

自動XNBシリアライズは.Netのリフレクション機能を使って実装されています。ContentTypeWriter/ContentTypeReaderが指定されていない場合、自動XNAシリアライズが発生し、以下のルールに沿って動作します。

  • 公開(public)メンバー、プロパティがシリアライズの対象になる
  • シリアライズしたくないメンバーがある場合は、 [ContentSerializerIgnore] 属性を指定することで、シリアライズしないように指定できる
  • private, protected, internalなどの非公開メンバーをシリアライズしたい場合は [ContentSerializer] 属性を指定する
  • シリアライズしたいデータ構造が再帰的な参照を含む場合は [ContentSerializer(SharedResource = true)] を指定する
  • コンテント・パイプライン内とランタイム時の型が違う場合、 [ContentSerializerRuntimeType(“タイプ名”)] を指定する

シンプルな例

例えば以下のようなデータをゲームで扱うとします。ゲームとコンテント・パイプラインで共有するデータなので、ゲームプロジェクトの他に共有するデータを入れるためのプロジェクトを作る必要があります。例えばMyDataTypesという名前のプロジェクトです。

 // ねこクラス
public class Neko
{
    public string Name;     // 名前
    public float Weight;    // 体重
    public int Tails;       // 尻尾の数
}

このデータをコンテント・パイプライン内に読み込むために以下のようなXMLファイル、cats.xmlを書きます。

 <?xml version="1.0" encoding="utf-8" ?>
<XnaContent>
  <Asset Type="MyDataTypes.Neko[]">
    <Item>
      <Name>たま</Name>
      <Weight>8</Weight>
      <Tails>1</Tails>
    </Item>
    <Item>
      <Name>ねこまた</Name>
      <Weight>12</Weight>
      <Tails>2</Tails>
    </Item>
  </Asset>
</XnaContent>

作ったXMLファイルをコンテントとして追加した後に、そのデータを読み込むコードをゲーム側のコード(LoadContentメソッド内など)に以下のコードを追加します。

 Neko[] cats = Content.Load<Neko[]>("cats");

後は、F5を押すだけで変数catsには必要なデータが読み込まれます。3.0以前のようにContentTypeReader/ContentTypeReaderを書かなくても良いので、データ構造の宣言、データファイルの生成、読み込み部分のコードをゲーム内に追加という単純な作業でカスタムデータをゲーム内で読み込むことができます。

オフラインとランタイムで型が違う場合

XNA GS 3.1のコンテント・パイプライン内で使われる型の図を見ると、コンテント・パイプライン内ではTexture2DContentだけど、ゲーム内ではTexture2Dのように、オフライン時とランタイム時では型が異なるケースがあります。例えば、ゲーム内では以下のデータ構造にしたい場合があるとします。

 // ねこクラス
public class Neko
{
    public string Name;         // 名前
    public Texture2D Texture;   // テクスチャ
}

この場合、Textureはコンテント・パイプライン内で使う型がTexture2DContentとなるので、以下のようにContentSerializerRuntimeType属性を使ってランタイム時の型を指定します。

 // ねこコンテント
[ContentSerializerRuntimeType("MyGame.Neko, MyGame")]
public class NekoContent
{
    public string Name;         // 名前
    public Texture2DContent Texture;   // テクスチャ
}

パフォーマンス

以上のように自動XNBシリアライズ機能は非常に便利な機能ですが、前述のようにデータの書き込みと読み込み時にリフレクション機能を使用するのでContentTypeWriter/ContentTypeReaderに比べると不必要なメモリ確保や、ボクシングなどが発生し、処理時間も多く掛かるので理論的にはロード時間が以前より長くなります。

ですが、現実的にはカスタムデータの多くがゲーム内のパラメーターなど、他のコンテントに比べると数は少ないものなので、ロード時間が長くなるといっても無視できる範囲のことが多いと思われます。

ただし、カスタムデータを数百、数千の配列で持つようなケースでは極端にロード時間が長くなる場合もあります。その場合は、今までどおりContentTypeWriter/ContentTypeReaderを使うことによってリフレクションによる速度低下を防ぐことができます。便利さを優先するには自動XNBシリアライズ、速度を優先するならContentTypeWriter/ContentTypeReaderといった感じに上手に使い分けましょう。

サンプル・コード

自動XNBシリアライズ機能を使ってコンテント・パイプラインの紹介の時に作った日本語表示のサンプルを3.1用に書き換えてみました。元のサンプルと比べると、共有するデータ型を記述するプロジェクト自体を書く必要がなくなり、ContentTypeWriter/ContentTypeReaderを書く必要が無くなったのでシンプルになっています。

http://higeneko.net/hinikeni/sample/TextMessageSample31.zip

元ネタ:http://blogs.msdn.com/shawnhar/archive/2009/03/25/automatic-xnb-serialization-in-xna-game-studio-3-1.aspx