ContentPipelineのWriteRawObject

久しぶりにブログを更新したと思ったら、去年から更新してなかったのね。

というわけで今年最初の更新。

最近のお勧めは『ライデンファイターズエイシズ』です。

PSPのアクションゲームはかなりやばい移植ということで盛り上がっていますが、こっちはかなりすばらしい移植ですよ。

ここまで愛に溢れた移植は珍しい。『ケツイ』とかもこれくらい頑張って移植してほしいところ。

最近XNAをいじり始めて、結構面白いと思うようになってきました。

今後、シェーダ関係のサンプルプログラムはこいつで作ろうかと思ったり思わなかったり。

でまあ、自作のデータを読み込んでみたりする方法について調べて、これが結構面白いと思ったわけで。

XNAではContentPipelineというものがデータの変換から読み込みまでをサポートしています。

個人でゲーム製作をやっているとかなり適当になってしまうのがゲームコンテンツの管理です。

コンテンツは基本的に実行ファイル以外のデータの事を指します。グラフィックデータやSE、BGMにキャラクタのパラメータデータなんかもコンテンツです。

会社によってこれらの管理方法は色々違うでしょうけど、たいていはコンバータがあって、必要なデータをコンバータに通してバイナリ変換するパイプラインを誰かがメイクファイルとか使って書くのが多いのではないかと思います。

もちろん、メイクファイルを直接書かずに自動的にメイクファイルを作成するプログラムを作っていたりもするでしょう。

こいつをプロジェクトとして作ってしまっているのがContentPipelineで、コンバートから読み込みまでC#のプログラムで書いてしまうわけです。

詳しくは”ひにけにXNA”とか見るといいかと。詳しく丁寧に書いてあります。

で、あちらではカスタムパイプラインの作成時に WriteObject() メソッドを使用しているのですが、これは規定のクラスや構造体にしか使えません。

たとえば、カスタムパイプラインのためのカスタムクラスを作成したとします。で、このカスタムクラス内に別のカスタムクラスが使用されているとします。

これを WriteObject() で出力しようとするとエラーになります。そんなの知らねぇよ、って言われて。

C#でバイナリ出力する場合によく(?)使われる BinaryFormatter は[Serialize]属性さえ付いていれば内包しているクラスや構造体でも出力してくれるので楽なんですが、ContentPipelineではそうもいかないようです。

そこで使われるのが WriteRawObject() メソッドです。これはあるオブジェクトを対応したContentTypeWriterで出力するという代物です。

例えばこんな感じで構造体を内包するクラスがあるとします。

public struct MyStruct{

    public int data;

};

public class MyClass{

    public float Data;

    public MyStruct subData;

}

MyClass.Data は ContentWriter.Write() メソッドで出力できますが、MyClass.subData は簡単には出力できません。

まず、MyStruct を出力する ContentTypeWriter を作成します。

[ContentTypeWriter]

public class MyStructContentWriter : ContentTypeWriter<MyStruct>

{

    protected override void Write(ContentWriter output, MyStruct value)

    {

        output.Write( value.data );

    }

    public override string GetRuntimeType(Microsoft.Xna.Framework.TargetPlatform targetPlatform)

    {

        return typeof( MyStruct ).AssemblyQualifiedName;

    }

    public override string GetRuntimeReader(Microsoft.Xna.Framework.TargetPlatform targetPlatform)

    {

        return typeof( MyStructReader ).AssemblyQualifiedName;

    }

}

次にこいつを使って出力する処理を MyClass の ContentTypeWriter で行います。

[ContentTypeWriter]

public class MyClassContentWriter : ContentTypeWriter<MyClass>

{

    protected override void Write(ContentWriter output, MyClass value)

    {

        output.Write( value.Data );

        output.WriteRawObject<MyStruct>( value.subData, new MyStructContentWriter() );

    }

}

とこんな感じです。それほど難しくはありませんが、fwrite() とかと比べると面倒ですね。

とはいえ、Xbox360とPCではエンディアンの違いがあるので、ここで吸収されると思えば安いものかもしれませんが。

ContentTypeReader の方も同様に ReadRawObject を使用します。使い方はほぼ同じです。

一応これでカスタムクラス/構造体内部のカスタムクラス/構造体の出力方法はわかりましたが、内部のクラス/構造体が配列だったりしたらどうなるでしょう?

もっとスマートなやり方もあるのかもしれませんが、私はこうやりました。

int  num = value.subData.GetLength( 0 );

output.Write( num );

for( int i=0; i<num; i++ ){

    output.WriteRawObject<MyStruct>( value.subData[i], new MyStructContentWriter() );

}

まあ、綺麗とは言いがたいですね。でも、一応これで問題なかったことだけは確かです。

派生クラスが多い場合は派生クラスが対応する ContentTypeWriter を作成するようにするのがスマートかな。

派生クラスならどうなる?とかも試してみないとなんとも言えませんね。

今度試してみます。

追記

いきなりですが、派生クラスを試してみました。

クラスは以下のように基底クラスと派生クラスを作成。

public abstract class MyBaseClass

{

    public abstract ContentTypeWriter GetWriter();

    public abstract ContentTypeReader GetReader();

}

public class MyDerivativeClass1 : MyBaseClass

{

    protected internal int  m_ObjNo;

    protected internal string m_ObjName;

    public override ContentTypeWriter GetWriter()

    {

        return new MyDerivativeClass1ContentWriter();

    }

    public override ContentTypeReader GetReader()

    {

        return new MyDerivativeClass1ContentReader();

    }

}

派生クラスのコンストラクタとContentTypeWriter、ContentTypeReaderの実装は省いていますが、問題ないでしょう。

で、まずはWriteの方。

MyBaseClass obj = new MyDerivativeClass1( 25, "Derivative!" );

output.WriteRawObject<MyBaseClass>( obj, obj.GetWriter() );

コンパイルも通りますし、動作もきちんと確認しました。

で、Readの方。

MyBaseClass obj = new MyDerivativeClass1();

data.MyDC = input.ReadRawObject< MyDerivativeClass1 >( obj.GetReader() );

これでOKです。きちんと動作を確認。

さて、ここで問題発生。当たり前ですが、Readの方はきちんと読み込むクラスの型がわかっていなければいけません。

Writeの方は元々実体があるためクラスの型は明確ですが、Readの方はバイナリになっているクラスがどんな型なのかわかりません。そのため、読み込み側で明確にしてやらなければいけないわけです。

考えられる方法は、まずクラスを書き込む前にクラスの型を特定できる何かを書き込み、読み込みのときに型に合わせた処理をする方法。ファクトリを使えば何とかなるような気もします。

ただ、ファクトリを使うならそもそもWrite側もそうやってしまえば済むことなので、本末転倒な気もします。

何かいい方法はないですかねぇ?

MemoryStreamにSerializeして書き込み、読み込んだやつをMemoryStreamに戻してDeserializeとか?

それがうまくいくなら今までの苦労もだいぶ意味がなくなりそうですが(笑