Silverlight をインストールするには、ここをクリックします*
Japan変更|すべてのMicrosoft のサイト|サインイン
Visual Studio 2005
|MSDN ライブラリ|デベロッパー センター|ダウンロード情報|開発ツール製品|コミュニティ|ご意見・ご要望|サイトマップ
MSDN Home > Visual Studio > Visual Studio 2008 Express Edition > 学習情報 > XNA > 第 02 回
ホーム Web インストール 製品の特徴 製品の機能 サポート 前のバージョン
XNA で作る、あなただけのマインスイーパ

XNA Framework 第 02 回

赤坂玲音

文字列の描画

プログラミングの世界では、最初に学習するプログラムのサンプルで有名な「Hello world」がありますが、本稿の第一回では、Hello world のような簡単なテキストを描画するプログラムを書きませんでした。3 次元グラフィックスが基本となる XNA Framework では、テキストも画像ファイルと同じスプライトの一種として捉えられています。

具体的には、ゲームで用いられる個々の文字をビットマップとして用意し、これを読み込んでスプライトとして描画するという方法が用いられます。PC だけではなく、Xbox 360 のような多様なプラットフォームで動作することが前提である XNA Framework では、システムのフォントを利用するという考えを持ちません。

しかし、文字列を直接描画する機能が存在しないというのは大きな問題です。文字単位で画像を用意して、画像として文字を描画すれば良いという発想は、使用する字数が少ないラテン文字文化を中心としたもので、日本語や中国語のような数千字を必要とする漢字文化では受け入れがたいものがあります。

幸い、XNA Framework 1.0 が正式に Vista に対応したアップデートである XNA Game Studio Express 1.0 Refresh と、そのランタイムである XNA Framework 1.0 Refresh でビットマップフォントがサポートされるようになりました。文字列から、直接テキストを描画できるようになったのです。

フォントを利用するには、プロジェクトに使用するフォントの情報を設定しなければなりません。XNA Framework にとって、フォントもテクスチャと同じようにリソースの一種として扱われます。フォントを使用するにはメニューの「プロジェクト」から「新しい項目の追加」項目を選択してください。

表示された「新しい項目の追加」ダイアログボックスから「Sprite Font」を選択し、任意の名前を入力して「追加」ボタンを押してください。spritefont という拡張子のファイルがプロジェクトに追加されます。このファイルは、使用するフォントの情報を記した XML 形式のテキストです。作成されたファイルを開くと、次のような XML 文書であることが確認できます。

■ Sample01 Test.spritefont

<?xml version="1.0" encoding="utf-8"?>

<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
 <Asset Type="Graphics:FontDescription">
  <FontName>MS Pゴシック</FontName>
  <Size>22</Size>
  <Spacing>2</Spacing>
  <Style>Regular</Style>
  <CharacterRegions>
   <CharacterRegion>
    <Start>&#32;</Start>
    <End>&#126;</End>
   </CharacterRegion>
  </CharacterRegions>
 </Asset>
</XnaContent>

重要なのは、使用するフォント名を表す FontName 要素、フォントサイズを表す Size 要素、文字の間隔を表す Spacing 要素、そしてスタイルを表す Style 要素です。Style 要素は、通常の状態を表す Regular の他に、太字を表す Bold、斜体を表す Italic、太字であり斜体であることを表す "Bold, Italic" を指定することができます。

ビルド時に、上記の XML ファイルに記載されているフォントの情報に従ってフォントのテクスチャを提供するデータが生成されます。フォントのテクスチャを提供するのは Microsoft.Xna.Framework.Graphics.SpriteFont クラスのオブジェクトです。

■ Microsoft.Xna.Framework.Graphics.SpriteFont クラス

public class SpriteFont

ビルド時に事前に使用するフォントのビットマップをデータとして作成し、実行時に SpriteFont オブジェクトとして作成したデータを読み込むことで、フォントを利用することができます。

SpriteFont オブジェクトは、画像ファイルから生成したテクスチャの場合と同じように ContentManager の Load() メソッドから読み込むことができます。

テキストを画面に描画するには SpriteBatch クラスの DrawString() メソッドを用います。

■ SpriteBatch クラス DrawString() メソッド

public void DrawString (
         SpriteFont spriteFont,
         string text,
         Vector2 position,
         Color color
)

spriteFont パラメータにはフォントを提供する SpriteFont オブジェクトを指定します。text パラメータには描画する文字列、position にはテキストを描画する座標、color にはテキストの色を指定します。

このとき、SpriteFont オブジェクトが text パラメータに指定した文字列の各文字のコードに対応するフォントを持たない場合、実行時例外になります。プロジェクトに追加した SpriteFont の XML ファイルのデフォルトでは、ASCII コードのアルファベットと数字、記号のみが含まれています。日本語フォントを指定した場合でも、日本語が含まれているわけではないので注意してください。

■ Sample01 test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch spriteBatch;
    private SpriteFont font;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            font = content.Load<SpriteFont>("TestFont");
        }
        spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        spriteBatch.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);

        spriteBatch.Begin();
        spriteBatch.DrawString(
            font, "Your potential. Our passion.",
            new Vector2(10, 10), Color.Black
        );
        spriteBatch.End();

        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

このプログラムは、ラテン文字だけを使った "Your potential. Our passion." というテキストを描画します。オーバーライドした LoadGraphicsContent() メソッド内で、ContentManager から SpriteFont オブジェクトを取得しています。Load() メソッドのパラメータに指定している文字列はフォント名ではなく、プロジェクトに追加した spritefont ファイルの Asset Name  プロパティに設定されている名前であることに注意してください。

日本語を描画する

プロジェクトに追加した spritefont ファイルは、デフォルトでは 10 進数で 32 〜 126 までの文字コードしか使えません。このフォントは、アルファベット、数字、記号、そしてスペースのみを描画することができます。DrawString() メソッドで指定した文字列で、これ以外の文字コードが存在する場合は例外が発生します。

日本語を描画するには、spritefont ファイルに使用する文字コードを追加しなければなりません。使用する文字コードの範囲は CharacterRegions 要素内に記述します。

<CharacterRegions>
 <CharacterRegion>
  <Start>&#32;</Start>
  <End>&#126;</End>
 </CharacterRegion>
</CharacterRegions>

CharacterRegions 要素は、使用する文字コードの範囲を表す CharacterRegion 要素を任意の数だけ指定することができます。Start 要素には開始文字、End 要素には終端文字を指定します。上記の場合、文字コード 32 番から 126 番までの文字をフォントに含めることを表しています。

ラテン文字以外を利用する場合は、新しい CharacterRegion 要素を追加し、Start 要素と End 要素にそれぞれ適切な Unicode 文字を指定します。

http://www.unicode.org/charts/

すべての文字を対象とするのは無駄が多くなるため、ゲームで使用する文字だけを含めることが理想です。たとえば、日本語の平仮名を利用する場合は 16 進数で 3040 から 309F の範囲となります。片仮名であれば 30A0 〜 30FF となります。

■ Sample02 TestFont.spritefont

<?xml version="1.0" encoding="utf-8"?>

<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
 <Asset Type="Graphics:FontDescription">
  <FontName>MS Pゴシック</FontName>
  <Size>22</Size>
  <Spacing>2</Spacing>
  <Style>Regular</Style>
  <CharacterRegions>
   <CharacterRegion>
    <Start>&#32;</Start>
    <End>&#126;</End>
   </CharacterRegion>
   <CharacterRegion>
    <Start>&#12448;</Start>
    <End>&#12543;</End>
   </CharacterRegion>
  </CharacterRegions>
 </Asset>
</XnaContent>

■ Sample02 test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch spriteBatch;
    private SpriteFont font;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            font = content.Load<SpriteFont>("TestFont");
        }
        spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        spriteBatch.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);

        spriteBatch.Begin();
        spriteBatch.DrawString(
            font, "マイクロソフト",
            new Vector2(10, 10), Color.Black
        );
        spriteBatch.End();

        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample02 は、片仮名の文字を描画するプログラムです。C# 言語のソースコードは、DrawString() メソッドのパラメータに渡している文字列を除いて Sample01 と変わりありません。重要なのは、spritefont ファイルに日本語の片仮名を範囲とする CharacterRegion 要素を追加したことです。

コントローラ入力

これまでのサンプルはゲームを起動して何らかのプログラムの結果を表示するものが中心でした。この場では、コントローラなどを使ったプレイヤーからの入力を認識する方法をご説明します。Xbox 360 コントローラを使ってプログラムを動かすことができれば、かなりゲームらしくなってきます。XNA Framework は、PC で代表的な入力デバイスであるキーボードとマウスに加えて Xbox 360 用のコントローラを使うこともできます。PC 用のゲームでも、Xbox 360 コントローラを使った振動などの演出が使えるのは大きな魅力です。

■ 図 00 Xbox 360 用コントローラ

Xbox 360 コントローラ

コントローラからの入力を受け取るには、まず Microsoft.Xna.Framework.Input.GamePad クラスを利用します。

■ Microsoft.Xna.Framework.Input.GamePad クラス

public static class GamePad

このクラスは static なクラスなのでインスタンスを持ちません。コントローラの状態は static な GetState() メソッドから取得することができます。

■ GamePad クラス GetState() メソッド

public static GamePadState GetState (
         PlayerIndex playerIndex
)

playerIndex パラメータには Microsoft.Xna.Framework.PlayerIndex 列挙体のメンバのいずれかを指定します。

■ Microsoft.Xna.Framework.PlayerIndex 列挙体

public enum PlayerIndex

PlayerIndex 列挙体は、コントローラの番号を表すメンバを提供しています。Xbox 360 の制約で、Xbox 360 用のコントーらの最大同時接続数は 4 台までと定められています。PlayerIndex はこれに従い、第 1 プレイヤーから順に One、Two、Three、Four という名前のメンバを持ちます。

例えば、GetState() メソッドに PlayerIndex.One を渡した場合は第 1 プレイヤーのコントローラの状態を返します。

GetState() メソッドの戻り値は、コントローラの状態を表す Microsoft.Xna.Framework.Input.GamePadState 構造体の値です。

■ Microsoft.Xna.Framework.Input.GamePadState 構造体

public struct GamePadState

この構造体は、コントローラのボタンやスティックの状態を提供します。コントローラのボタンが押されているかどうかを調べるには、Buttons プロパティを用います。

■ GamePadState 構造体 Buttons プロパティ

public GamePadButtons Buttons { get; }

Buttons プロパティは、コントローラの各種ボタンの状態を提供する Microsoft.Xna.Framework.Input.GamePadButtons 構造体の値を返します。

■ Microsoft.Xna.Framework.Input.GamePadButtons 構造体

public struct GamePadButtons

GamePadButtons 構造体には、Xbox 360 コントローラの各種ボタンを名前をそのまま表すプロパティが公開されています。たとえば、A ボタンの状態を調べるには A プロパティを使います。

■ GamePadButtons 構造体 A プロパティ

public ButtonState A { get; }

A プロパティは、ボタンの状態を表す Microsoft.Xna.Framework.Input.ButtonState 列挙体のいずれかのメンバを返します。

■ Microsoft.Xna.Framework.Input.ButtonState 列挙型

public enum ButtonState

この列挙体には、ボタンが押されている状態を表す Pressed メンバと、離されている状態を表す Released が定義されています。

GamePadButtons 構造体には、A プロパティと同じ形で各ボタンごとのプロパティを用意しています。プロパティ名は、基本的にボタンの名前がそのまま使われています。B ボタンの状態は B プロパティ、X ボタンの状態は X プロパティ、Y ボタンの状態は Y プロパティから取得できます。

■ Sample03 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private Color color;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.A == ButtonState.Pressed)
            color = Color.Green;
        else if (state.Buttons.B == ButtonState.Pressed)
            color = Color.Red;
        else if (state.Buttons.X == ButtonState.Pressed)
            color = Color.Blue;
        else if (state.Buttons.Y == ButtonState.Pressed)
            color = Color.Orange;
        else color = Color.White;

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(color);
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample03 は、コントローラの A、B、X、Y ボタンに反応するプログラムです。Xbox 360 専用のコントローラを PC の USB 端子から接続した状態でボタンを押すと、ボタンに対応して画面の色が変化します。

トリガー

コントローラの上部にある LT と RT ボタンは、通常のボタンではなくトリガーです。レースのアクセルや戦争ゲームの銃の発射に RT  がよく使われているのをご存じでしょうか。トリガーは、ボタンとは異なり押されているかどうかではなく、どのくらい引かれているかを数値で取得できます。精密な入力を必要とする場面で活用できます。

トリガーの状態を取得するには GamePadState 構造体の Triggers プロパティを利用します。

■ GamePadState 構造体 Triggers プロパティ

public GamePadTriggers Triggers { get; }

このプロパティは、対象のコントローラのトリガーの状態を表す Microsoft.Xna.Framework.Input.GamePadTriggers 構造体の値を返します。

■ Microsoft.Xna.Framework.Input.GamePadTriggers 構造体

public struct GamePadTriggers

GamePadTriggers 構造体は、左トリガーの値を取得する Left プロパティと、右トリガーの値を取得する Right プロパティを提供しています。

■ GamePadTriggers 構造体 Left プロパティ

public float Left { get; }

■ GamePadTriggers 構造体 Right プロパティ

public float Right { get; }

これらのプロパティは、トリガーが引かれていない状態を 0、完全にトリガーが引かれている状態を 1 とした float 型の値を返します。

■ Sample04 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private Color color;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        color = new Color((byte)(255 * state.Triggers.Left), 0, (byte)(255 * state.Triggers.Right));

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(color);
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample04 は、左右のトリガーの値と色の要素を乗算させることで、トリガーに合わせて色要素が強くなるというプログラムです。左トリガーを押すと画面はより赤色に、右トリガーを押すとより青色に近づきます。

スティック

コントローラの左右にあるスティックの状態は、GamePadState 構造体の ThumbSticks プロパティから取得することができます。

■ GamePadState 構造体 ThumbSticks プロパティ

public GamePadThumbSticks ThumbSticks { get; }

このプロパティは、スティックの状態を表す Microsoft.Xna.Framework.Input.GamePadThumbSticks 構造体の値を返します。

■ Microsoft.Xna.Framework.Input.GamePadThumbSticks 構造体

public struct GamePadThumbSticks

左スティックの状態は Left プロパティから、右スティックの状態は Right プロパティから取得できます。

■ GamePadThumbSticks 構造体 Left プロパティ

public Vector2 Left { get; }

■ GamePadThumbSticks 構造体 Right プロパティ

public Vector2 Right { get; }

これらのプロパティが返す値は、スティックが倒されている方向を表す Vector2 構造体の値です。スティックが中心にあるデフォルトの状態を 0 とし、スティックがいずれかの方向に完全に倒された状態の絶対値が 1 となります。

左右いずれかの水平方向に倒された場合は X 座標に影響し、左側に倒した場合は -1、右側に倒した場合は 1 となります。同様に、上下いずれかの垂直方向に倒された場合は Y 座標に影響し、上に倒された場合は -1、下に倒された場合は -1 となります。

■ Sample05 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private Texture2D texture;
    private Vector2 position;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            texture = content.Load<Texture2D>("TestTexture");
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        position = new Vector2(
            100 + (100 * state.ThumbSticks.Left.X), 100 + (100 * state.ThumbSticks.Left.Y));

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);

        sprite.Begin();
        sprite.Draw(texture, position, Color.White);
        sprite.End();

        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample05 は、描画される画像の座標をコントローラの左スティックで操作することができます。スティックをいずれかの方向に倒すと、描画されている画像を最大で 100 ピクセル移動させることができます。

方向キー

方向キーの状態を取得するには GamePadState 構造体の DPad プロパティを使います。

■ GamePadState 構造体 DPad プロパティ

public GamePadDPad DPad { get; }

DPad プロパティは、方向キーの状態を表す Microsoft.Xna.Framework.Input.GamePadDPad 構造体の値を返します。

■ Microsoft.Xna.Framework.Input.GamePadDPad 構造体

public struct GamePadDPad

この構造体には、方向キーの各ボタンを表すプロパティを提供しています。たとえば、下キーの状態を取得するには Down プロパティを利用します。

■ GamePadDPad 構造体 Down プロパティ

public ButtonState Down { get; }

このプロパティの結果は、ボタンが押されているかどうかを表す ButtonState 列挙体の値です。同じように、上キーを表す Up、左キーを表す Left、右キーを表す Right プロパティが用意されています。

■ Sample06 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private Texture2D texture;
    private Vector2 position;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
        position = new Vector2(0, 0);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            texture = content.Load<Texture2D>("TestTexture");
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);

        if (state.DPad.Up == ButtonState.Pressed)
            position.Y -= 1;
        else if (state.DPad.Down == ButtonState.Pressed)
            position.Y += 1;

        if (state.DPad.Left == ButtonState.Pressed)
            position.X -= 1;
        else if (state.DPad.Right == ButtonState.Pressed)
            position.X += 1;

        Window.Title = "X=" + position.X + ", Y=" + position.Y;

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);

        sprite.Begin();
        sprite.Draw(texture, position, Color.White);
        sprite.End();

        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample06 は、方向キーが押されている間、画像ファイルをキーが押されている方向に向かって移動させます。Update() メソッドでは、単純に方向キーが押されているかどうかを調べ、押されている間はキーの方向に向かって position フィールドが表している座標を調整しているだけです。

最後に、これまでのコントローラのボタンやスティックなどの状態を一度に表示するプログラムを作成してみましょう。コントローラの入力に対して GamePadState の値がどのように変化するのか、視覚的に確認することができます。

■ Sample07 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string text;
    private Vector2 position;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
        position = new Vector2(0, 0);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            font = content.Load<SpriteFont>("TestFont");
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        text = "A=" + state.Buttons.A + ", B=" + state.Buttons.B +
            ", X=" + state.Buttons.X + ", Y=" + state.Buttons.Y + "\n" +

            "LB=" + state.Buttons.LeftShoulder + ", RB=" + state.Buttons.RightShoulder + "\n" +

            "Back=" + state.Buttons.Back + ", Start=" + state.Buttons.Start + "\n" +

            "LeftStickButton=" + state.Buttons.LeftStick +
            ", RightStickButton=" + state.Buttons.RightStick + "\n" +

            "Up=" + state.DPad.Up + ", Down=" + state.DPad.Down +
            ", Left=" + state.DPad.Left + ", Right=" + state.DPad.Right + "\n" +

            "LT=" + state.Triggers.Left + ", RT=" + state.Triggers.Right + "\n" +

            "RightStick=" + state.ThumbSticks.Right + "\n" +
            "LeftStick=" + state.ThumbSticks.Left;
            
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);

        sprite.Begin();
        sprite.DrawString(font, text, position, Color.Black);
        sprite.End();

        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample07 では、Update() メソッド内で GamePadState の各種プロパティの値を文字列化して text フィールドに格納しています。この結果は Draw() メソッドで描画され、コントローラの最新の状態を表示します。

キーボード入力

キーボードは、PC と Xbox 360 で共通して使える入力デバイスです。Xbox 360 プラットフォームでは、コントローラが標準の入力デバイスでありキーボードは必須の入力デバイスではありませんが、USB キーボードを接続することで利用することができます。

キーボードからの入力を認識する方法は、コントローラとほぼ同じです。キーボードを管理する Microsoft.Xna.Framework.Input.Keyboard クラスからキーボードの状態を取得します。

■ Microsoft.Xna.Framework.Input.Keyboard クラス

public static class Keyboard

コントローラと異なりキーボードはシステムに対して常に 1 つです。複数のプレイヤーごとにキーボードが割り当てられるということはありません。

現在のキーボードの状態を得るには static な GetState() メソッドを用います。

■ Keyboard  クラス GetState() メソッド

public static KeyboardState GetState ()

このメソッドは、キーボードの状態を表す Microsoft.Xna.Framework.Input.KeyboardState 構造体の値を返します。

■ Microsoft.Xna.Framework.Input.KeyboardState 構造体

public struct KeyboardState

コントローラとは異なり、キーボードには多くのキーが存在します。それぞれのキーの状態は KeyboardState 構造体のインデクサから配列のように取得することができます。

■ KeyboardState 構造体のインデクサ

public KeyState this [
         Keys key
] { get; }

key パラメータには、キーボードの各種キーを表す Microsoft.Xna.Framework.Input.Keys 列挙体のいずれかのメンバを指定します。

■ Microsoft.Xna.Framework.Input.Keys 列挙型

public enum Keys

Keys 列挙体のメンバは、キーボードのキーの名前がそのまま使われています。文字キーであればアルファベットに対応しているので A キーの状態を取得したい場合は Keys.A を指定します。文字キー以外にも Enter やSpace、Escape など、一般的なキーの名前に対応したメンバが用意されています。

KeyboardState 構造体のインデクサが返す値は、パラメータに指定されたキーが押されているかどうかを表す Microsoft.Xna.Framework.Input.KeyState 列挙体の値です。

■ Microsoft.Xna.Framework.Input.KeyState 列挙型

public enum KeyState

この列挙体には、キーが押されている場合を表す Down メンバと、キーが話されている状態を表す Up メンバが定義されています。

■ Sample08 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private Color background;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
    }

    protected override void Update(GameTime gameTime)
    {
        KeyboardState state = Keyboard.GetState();
        if (state[Keys.R] == KeyState.Down)
            background = Color.Red;
        else if (state[Keys.B] == KeyState.Down)
            background = Color.Blue;
        else if (state[Keys.G] == KeyState.Down)
            background = Color.Green;
        else background = Color.White;

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(background);
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample08 は、R キーを押すと画面が赤色に、G キーを押すと緑色に、B キーを押すと青色に塗りつぶすというプログラムです。キーボードのキー入力にプログラムが正しく反応することを確認してください。

しかし、個々のキーの状態には興味がなく、現在押されているキーにのみ興味があるという場合、インデクサからキーの状態を個別に取得する方法は効率的ではありません。そこで、KeyboardState は、現在押されているキーの配列を返す GetPressedKeys() メソッドを用意しています。

■ KeyboardState 構造体 GetPressedKeys() メソッド

public Keys[] GetPressedKeys ()

このメソッドの戻り値は、キーが押されている状態のキーの配列です。この配列を調べることで、押されている状態のキーを知ることができます。

■ Sample09 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private SpriteFont font;
    private Vector2 position;
    private string text;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
        position = new Vector2(10, 10);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            font = content.Load<SpriteFont>("TestFont");
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Update(GameTime gameTime)
    {
        KeyboardState state = Keyboard.GetState();
        text = "Pressed=";
        foreach (Keys key in state.GetPressedKeys())
        {
            text += key.ToString() + ", ";
        }
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);

        sprite.Begin();
        sprite.DrawString(font, text, position, Color.Black);
        sprite.End();

        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample09 は、現在押されているキーのリストを表示するプログラムです。Update() メソッド内で GetPressedKeys() メソッドが返した Keys 列挙型の配列から要素を個別に取り出し、文字列表現を text フィールドに追加しています。

マウス入力

Xbox 360 ではマウスを使うことができませんが、PC にとってマウスによる操作は非常に直観的で、重要な入力デバイスです。PC 用のゲーム開発の場合、マウスからの入力に対応する必要があるでしょう。XNA Framework では、マウスからの入力を認識することも可能です。ただし、Xbox 360 用のプロジェクトではマウスに関連する機能は使えないので注意してください。

マウスからの入力を得る方法もまた、コントローラやキーボードと同じです。まずは、マウスそのものを表する Microsoft.Xna.Framework.Input.Mouse クラスから始まります。

■ Microsoft.Xna.Framework.Input.Mouse クラス

public static class Mouse

マウスもキーボードと同じようにシステムに対して常に 1 つの実体しかありません。複数のマウスがプレイヤーごとに割り当てられることはありません。

マウスの現在の状態を取得するには Mouse クラスの static な GetState() メソッドを使います。

■ Mouse クラス GetState() メソッド

public static MouseState GetState ()

GetState() メソッドは、現在のマウスの状態を表す Microsoft.Xna.Framework.Input.MouseState 構造体の値を返します。

■ Microsoft.Xna.Framework.Input.MouseState 構造体

[SerializableAttribute]
public struct MouseState

MouseState 構造体は、カーソルの座標やマウスボタンの状態などを保有しています。

カーソルの X 座標は X プロパティから、Y 座標は Y プロパティから取得することができます。

■ MouseState 構造体 X プロパティ

public int X { get; }

■ MouseState 構造体 Y プロパティ

public int Y { get; }

これらのプロパティが返す値は、ゲームウィンドウの左上隅を 0 とするクライアント座標です。

ボタンが押されているかどうかは、マウスのボタンごとに用意されたプロパティから取得することができます。たとえば、マウスの左ボタンが押されているかどうかを取得するには LeftButton プロパティを使います。

■ MouseState 構造体 LeftButton プロパティ
public ButtonState LeftButton { get; }

LeftButton プロパティは、ボタンが押されているかどうかを表す ButtonState 列挙体の値を返します。

マウスのホイールの状態は ScrollWheelValue プロパティから得ることができます。

■ MouseState 構造体 ScrollWheelValue プロパティ

public int ScrollWheelValue { get; }

ScrollWheelValue が返す整数は、アプリケーション起動時の状態を 0 として、回転させた方向に加減算された値が返されます。一般的なマウスであれば、ホイールを手前から奥に向かって回すと加算され、奥から手前に向かって回すと減算されます。

■ Sample10 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private SpriteFont font;
    private Vector2 position;
    private string text;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
        position = new Vector2(10, 10);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            font = content.Load<SpriteFont>("TestFont");
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Update(GameTime gameTime)
    {
        MouseState state = Mouse.GetState();
        text = "X=" + state.X + ", " + "Y=" + state.Y + "\n" +
            "LeftButton=" + state.LeftButton + "\n" +
            "MiddleButton=" + state.MiddleButton + "\n" +
            "RightButtom=" + state.RightButton + "\n" +
            "XButton1" + state.XButton1 + "\n" +
            "XButton2" + state.XButton2 + "\n" +
            "Wheel=" + state.ScrollWheelValue;
        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);
        
        sprite.Begin();
        sprite.DrawString(font, text, position, Color.Black);
        sprite.End();

        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample10 は、マウスカーソルの座標と、各種ボタン、ホイールの状態を文字列かして text フィールドに保存し、この文字列を Draw() メソッドで描画しています。プログラムが正しくマウスの状態を取得していることを確認してください。

ゲームの部品化

本稿のサンプル程度の規模のゲームであれば、すべてのコードを Game クラスを継承したクラスに収めることができるかもしれませんが、ストーリー構成をもつ本格的なゲームを開発する場合、無数にあるゲーム場面の処理を Update() メソッドや Draw() メソッドの中に書くことは困難です。ゲームの処理コードを部品化して組み替えられる仕組みが必要になります。

Game クラスでは、ゲームの構成要素を部品化してデータや描画の管理を行う統一的な方法を提供しています。再利用や、組み換え可能な、柔軟性の高いゲーム部品を開発するには GameComponent クラスを用います。

■ Microsoft.Xna.Framework.GameComponent クラス

public class GameComponent : IGameComponent, IUpdateable, IDisposable

GameComponent クラスのコンストラクタには、このゲーム部品を利用するゲームを表す Game オブジェクトを指定します。

■ GameComponent クラスのコンストラクタ

public GameComponent (
         Game game
)

コンストラクタの game パラメータに渡した Game オブジェクトは、protected な Game プロパティから取得することができます。

■ GameComponent クラス Game プロパティ

protected Game Game { get; }

GameComponent クラスは外部からインスタンス化して直接利用することはありません。通常は GameComponent クラスを継承し、独自のゲーム部品を開発します。

ゲーム部品の初期化は Initialize() メソッドで行います。このメソッドは GameComponent が使われる直前に、初期化が必要なタイミングで呼び出されます。このメソッドをオーバーライドし、必要な情報の初期化やデータの読み込みなどを行います。

■ GameComponent クラス Initialize() メソッド

public virtual void Initialize ()

ちなみに GameComponent クラスは IDisposable インタフェースを継承しているため、オブジェクトが破棄されるときに Dispose() メソッドが呼び出されます。手動で解除しなければならないリソースなどを持つ場合は、このメソッドをオーバーライドしてください。

ゲーム部品の振る舞いも、基本的に Game クラスの流れと同じです。Game クラスの Update() メソッドが呼び出されると、Game クラスは登録されている GameComponent クラスの Update() メソッドを呼び出します。

■ GameComponent クラス Update() メソッド

public virtual void Update (
         GameTime gameTime
)

ゲーム部品を更新するコードは、この Update() メソッド内に記述します。gameTime には、現在のゲーム時間を表す GameTime オブジェクトが渡されます。

作成した GameComponent は Game クラスの Components プロパティから追加したり取得することができます。GameComponent の Initialize() メソッドや Update() メソッドは Game クラスから呼び出されるものです。よって GameComponent は Game クラスに登録しなければ機能しません。

■ Game クラス Components プロパティ

public GameComponentCollection Components { get; }

このプロパティは、任意の数の GameComponent を管理する Microsoft.Xna.Framework.GameComponentCollection クラスのオブジェクトを返します。

■ Microsoft.Xna.Framework.GameComponentCollection クラス

public sealed class GameComponentCollection : Collection<IGameComponent>

GameComponentCollection クラスは、Collection クラスを継承するコレクションの一種です。Add() メソッドや Remove() メソッドを使って任意の数だけオブジェクトを追加したり、削除することができます。このクラスに追加することができるのは IGameComponent インタフェースを実装しているオブジェクトです。GameComponent クラスは IGameComponent を実装しています。

■ Sample11 Test.cs

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestComponent : GameComponent
{
    private TimeSpan lastGameTime;
    private Color color;
    private int addValue;

    public Color Color
    {
        get { return color; }
    }

    public TestComponent(Game game) : base(game) {}
    public override void Initialize()
    {
        color = Color.White;
        addValue = -1;
        lastGameTime = TimeSpan.Zero;
        base.Initialize();
    }

    public override void Update(GameTime gameTime)
    {
        if (gameTime.TotalGameTime.TotalMilliseconds - lastGameTime.TotalMilliseconds > 30)
        {
            if (color == Color.Black)
                addValue = 1;
            else if (color == Color.White)
                addValue = -1;

            color = new Color(
                (byte)(color.R + addValue),
                (byte)(color.G + addValue),
                (byte)(color.B + addValue)
            );
            lastGameTime = gameTime.TotalGameTime;
        }
        base.Update(gameTime);
    }
}

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private TestComponent component;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        component = new TestComponent(this);
        Components.Add(component);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(component.Color);
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample11 の TestComponent  クラスは GameComponent クラスを継承しています。このクラスは、ゲーム時間の進行とともに少しずつ変化する Color オブジェクトを提供します。作成した TestComponent オブジェクトを Game クラスの Components プロパティが返すコレクションに追加することで、TestComponent クラスの Update() メソッドが呼び出されるようになります。

このように、GameComponent クラスを継承する再利用可能なゲーム部品を開発することで、必要な時に必要な数だけインスタンスを生成し、ゲームに追加することができます。不要になればコレクションから削除することで処理を停止することができます。

GameComponent クラスの Initialize() メソッドや Update() メソッドを呼び出しているのは、GameComponent を持つ Game クラスの Initialize() メソッドや Update() メソッドです。よって、Game クラスの派生クラスでこれらのメソッドをオーバーライドしたとき、基底クラスである Game クラスのメソッドを呼び出さなければなりません。そうしなければ、GameComponent が機能しなくなってしまいます。

GameComponent の更新を一時的に停止させたい場合、GameComponent クラスの Enable プロパティを使います。

■ GameComponent クラス Enabled プロパティ

public bool Enabled { get; set; }

Game クラスは、GameComponent の Enable プロパティが true のときに Update() メソッドを呼び出します。よって、このプロパティを false に変更するとで、GameComponent の Update() メソッドの呼び出しを停止できます。ゲーム全体の更新処理をそのままに、特定のゲーム部品だけを停止させることができます。

■ Sample12 Test.cs

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class TestComponent : GameComponent
{
    private TimeSpan lastGameTime;
    private Color color;
    private int addValue;

    public Color Color
    {
        get { return color; }
    }

    public TestComponent(Game game) : base(game) {}
    public override void Initialize()
    {
        color = Color.White;
        addValue = -1;
        lastGameTime = TimeSpan.Zero;
        base.Initialize();
    }

    public override void Update(GameTime gameTime)
    {
        if (gameTime.TotalGameTime.TotalMilliseconds - lastGameTime.TotalMilliseconds > 30)
        {
            if (color == Color.Black)
                addValue = 1;
            else if (color == Color.White)
                addValue = -1;

            color = new Color(
                (byte)(color.R + addValue),
                (byte)(color.G + addValue),
                (byte)(color.B + addValue)
            );
            lastGameTime = gameTime.TotalGameTime;
        }
        base.Update(gameTime);
    }
}

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private TestComponent component;
    private KeyboardState lastKeyState;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        component = new TestComponent(this);
        Components.Add(component);
    }

    protected override void Initialize()
    {
        lastKeyState = Keyboard.GetState();
        base.Initialize();
    }

    protected override void Update(GameTime gameTime)
    {
        KeyboardState keyState = Keyboard.GetState();
        if (lastKeyState[Keys.Enter] == KeyState.Down && keyState[Keys.Enter] == KeyState.Up)
        {
            component.Enabled = !component.Enabled;
            Window.Title = "Component Enabled = " + component.Enabled;
        }
        lastKeyState = keyState;

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(component.Color);
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample12 は、Sample11 と同じ TestComponent オブジェクトをゲームに追加して、ゲーム時間の進行とともに画面の色を変化させるプログラムですが、キーボードの Enter キーを押すと TestComponent オブジェクトの Enable プロパティを false に設定して Update() メソッドの呼び出しを一時停止することができます。もう一度 Enter キーを押すと Enable プロパティを true に設定して Update() メソッドの呼び出しを再開します。

描画機能を持つ部品

GameComponent クラスを継承するオブジェクトを Game クラスに登録することで Update() メソッドが呼び出され、ゲーム部品の更新処理を行うことができましたが、このゲーム部品は描画には関与しません。そのため、GameComponent クラスから派生するクラスは、データの提供が主な役割となります。

これに加えて、Game クラスとは別に独立した描画機能を持つ部品を開発したい場合、データの更新と描画の両方の機能を持つ Microsoft.Xna.Framework.DrawableGameComponent クラスを使います。

■ Microsoft.Xna.Framework.DrawableGameComponent クラス

public class DrawableGameComponent : GameComponent, IDrawable

このクラスは GameComponent クラスから派生しているため、基本的な機能や使い方は GameComponent と同じです。加えて、描画機能を宣言する IDrawable インタフェースを実装しています。

このクラスのコンストラクタには、GameComponent クラスと同じように Game オブジェクトを指定します。

■ DrawableGameComponent クラスのコンストラクタ

public DrawableGameComponent (
         Game game
)

game には、このクラスのオブジェクトをゲーム部品として利用する Game オブジェクトを指定します。

このゲーム部品が描画を行うべきタイミングで Draw() メソッドが呼び出されます。基本的な仕組みは GameComponent クラスの Update() メソッドと同じで、このオブジェクトを登録している Game クラスから呼び出されます。

■ DrawableGameComponent クラス Draw() メソッド

public virtual void Draw (
         GameTime gameTime
)

gameTime には、現在のゲーム時間が格納されています。このゲーム部品の描画処理は DrawableGameComponent クラスの派生クラスで Draw() メソッドをオーバーライドして記述します。

また、描画に必要なテクスチャなどのグラフィックス リソースを読み込むための LoadGraphicsContent() メソッドと、解放処理を行うための UnloadGraphicsContent() メソッドも用意されています。これらのメソッドの働きは、Game クラスと同じです。

■ Sample13 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

public class TestComponent : DrawableGameComponent
{
    private string assetName;
    private IGraphicsDeviceService graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private Texture2D texture;
    private Vector2 position;

    public Texture2D Texture
    {
        get { return texture; }
    }

    public TestComponent(Game game, string assetName) : base(game)
    {
        content = new ContentManager(game.Services);
        graphics =(IGraphicsDeviceService)
            game.Services.GetService(typeof(IGraphicsDeviceService));

        this.assetName = assetName;
        this.position = new Vector2(0, 0);
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            texture = content.Load<Texture2D>(assetName);
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }
    
    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    public override void Draw(GameTime gameTime)
    {
        sprite.Begin();
        sprite.Draw(Texture, position, Color.White);
        sprite.End();

        base.Draw(gameTime);
    }
}

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);

        TestComponent component = new TestComponent(this, "TestTexture");
        Components.Add(component);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample13 は、DrawableGameComponent クラスを継承し、指定されたアセット名のテクスチャを読み込んで描画する TestComponent クラスを作成します。DrawableGameComponent クラスは、Game クラスと同じように Draw() メソッドや LoadGraphicsContent() メソッドを持つため、子ゲームのような機能を持たせることができます。

ゲーム部品を描画するかどうかは Visible プロパティで設定することができます。一時的に DrawableGameComponent の描画を停止したい場合に利用してください。

■ DrawableGameComponent クラス Visible プロパティ

public bool Visible { get; set; }

このプロパティの値が true のときに Draw() メソッドが呼び出されます。よって、このプロパティの値を false に設定することで、ゲーム部品の描画を停止させることができます。GameComponent クラスの Enabled プロパティと Update() プロパティの関係と同じです。

■ Sample14 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class TestComponent : DrawableGameComponent
{
    private string assetName;
    private IGraphicsDeviceService graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private Texture2D texture;
    private Vector2 position;

    public Texture2D Texture
    {
        get { return texture; }
    }
    public Vector2 Position
    {
        set { this.position = value; }
        get { return position; }
    }

    public TestComponent(Game game, string assetName) : base(game)
    {
        content = new ContentManager(game.Services);
        graphics =(IGraphicsDeviceService)
            game.Services.GetService(typeof(IGraphicsDeviceService));

        this.assetName = assetName;
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            texture = content.Load<Texture2D>(assetName);
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }
    
    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    public override void Update(GameTime gameTime)
    {
        KeyboardState state = Keyboard.GetState();
        if (state[Keys.Up] == KeyState.Down) position.Y -= 1;
        if (state[Keys.Down] == KeyState.Down) position.Y += 1;
        if (state[Keys.Left] == KeyState.Down) position.X -= 1;
        if (state[Keys.Right] == KeyState.Down) position.X += 1;

        base.Update(gameTime);
    }

    public override void Draw(GameTime gameTime)
    {
        sprite.Begin();
        if (Enabled) sprite.Draw(Texture, position, Color.White);
        else sprite.Draw(Texture, position, Color.Gray);
        sprite.End();

        base.Draw(gameTime);
    }
}

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private int index;
    private KeyboardState lastKeyState;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        index = 0;

        TestComponent component1 = new TestComponent(this, "TestTexture");
        TestComponent component2 = new TestComponent(this, "TestTexture");
        component2.Enabled = false;
        component2.Position = new Vector2(50, 50);
        TestComponent component3 = new TestComponent(this, "TestTexture");
        component3.Enabled = false;
        component3.Position = new Vector2(100, 100);
        TestComponent component4 = new TestComponent(this, "TestTexture");
        component4.Enabled = false;
        component4.Position = new Vector2(150, 150);

        Components.Add(component1);
        Components.Add(component2);
        Components.Add(component3);
        Components.Add(component4);
    }

    protected override void Initialize()
    {
        lastKeyState = Keyboard.GetState();
        base.Initialize();
    }

    protected override void Update(GameTime gameTime)
    {
        TestComponent component = (TestComponent)Components[index];
        KeyboardState keyState = Keyboard.GetState();

        if (lastKeyState[Keys.Tab] == KeyState.Down && keyState[Keys.Tab] == KeyState.Up)
        {
            if (index + 1 == Components.Count) index = 0;
            else index++;
            component.Enabled = false;

            TestComponent nextComponent = (TestComponent)Components[index];
            nextComponent.Enabled = true;
        }
        if (lastKeyState[Keys.Enter] == KeyState.Down && keyState[Keys.Enter] == KeyState.Up)
        {
            component.Visible = !component.Visible;
        }
        
        Window.Title = "Enabled Index=" + index + ", Visible=" + component.Visible;
        lastKeyState = keyState;

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample14 の TestComponent クラスは、コンストラクタで指定されたアセット名のテクスチャを Position プロパティが表す座標に描画します。Enable が false の場合は Color.Gray と合成して画像の色を暗く描画します。このクラスの Update() メソッド内では、キーボードの状態を調べ、方向キーが押されている場合は、その方向に向かって座標を移動させます。

Game クラスを継承する Test クラスでは、TestComponent クラスのインスタンスを 4 つ生成しています。最初に生成したオブジェクトを除き、Enable プロパティの値を false に設定することで、実際に操作できるオブジェクトを 1 つだけに限定しています。操作対象となるオブジェクトは index フィールドに保存しているインデックスで識別しています。

Test クラスの Update() メソッドでは、キーボードの状態を調べて Tab キーが押された場合に操作対象となる有効なオブジェクトを切り替えます。また、Enter キーが押された場合は操作対象のオブジェクトの Visible プロパティの値を反転させます。

ゲームにとってのコンテンツ

素晴らしいゲームを生み出すための要素とは何かという問いに明確な答えを出すことは困難です。美しいグラフィックス、臨場感のあるサウンド、感動的なストーリー、好感が持てるキャラクターの存在など、現代のゲームは総合芸術のような側面があります。

これに対し私たち開発者が作るゲームとは、プレイヤーの目には見えないロジック部分に限られてしまいます。70年代には生粋のプログラマーだけで作られたテキストゲームが人気になることもありましたが、ロジックよりもグラフィックスや音楽のようなコンテンツが重要視されている現代のゲーム開発をプログラマだけで行うのは困難です。

グラフィックスや音楽、効果音、キャラクターの声や動きなど、ゲームを構成するこれらのコンテンツを作成するのは、それぞれの専門家です。私たち開発者は、こうしたコンテンツのクリエイターと適切にコミュニケーションを行って、彼らが作成したコンテンツを受け取り、正しく管理を行いながらプログラムに取り込む必要があります。

プログラムから見ると、画像ファイルや音声ファイルはプログラムの外にあるデータです。前回のテクスチャでも簡単に触れましたが、XNA Game Studio ではこうしたプログラムの外部に配置されているデータを統一して扱うことができます。この場では、画像ファイルなどのリソースを XNA Framework がどのように扱っているのか、もう少し詳しく見てみましょう。

グラフィックスであれサウンドであれ、またはゲーム独自の形式のデータファイルであれ、外部のファイルを実行時に読み込むには、ファイルに書き出されたデータをメモリイメージ(オブジェクト)に変換する作業が必要になります。たとえば、画像ファイルからテクスチャを取得する場合でも、BMP 形式のファイルと PNG 形式のファイルではフォーマットが異なります。それぞれのファイルのフォーマットに従って読み込みや変換を行う必要があります。

このようなフォーマットの違いは PC では大きな問題になりません。しかし、複雑なグラフィックス処理が中心のゲームでは、実行時の処理に負担をかけることは極力避けなければなりません。標準的なファイルフォーマットは、ゲームで直接使えるように最適化されておらず、不要な情報が含まれていることもあります。

そこで XNA Framework では、プロジェクトのビルド時に関連するリソースをゲームに最適化されたバイナリ形式に変換し、実行時には変換されたデータを読み込むという方法を採用しています。XNA Game Studio では、ファイルをプロジェクトに追加することで、ビルド時に合わせて変換することができ、リソースとソースコードを統合管理することができます。

コンテンツ・パイプラインの仕組み

要約すると、コンテンツ・パイプラインとはビルド時に用意された様々なリソースをゲームに最適化されたデータに変換し、実行時にこれを読み込む一連の機能と流れを表します。ユーザー定義のデータを変換するプログラムを作成して拡張することも可能です。

これまで、テクスチャとして表示するための画像ファイルや、フォントを作成するための spritefont ファイルなどを使いました。これらのファイルはプロジェクトに追加した時点で自動的にコンテンツ・パイプラインに組み込まれ、ビルド時に変換されています。

プロジェクトの実行可能ファイルが生成されているフォルダを見ると xnb という拡張子のファイルが生成されていることが確認できます。この xnb ファイルに、画像ファイルや XML ファイルなどから変換されたゲーム用のデータが格納されている。このデータのことをアセットと呼びます。アセットは、実行時に ContentManager オブジェクトから取得できます。

XNA Framework は、画像ファイルや音声、3D モデル、フォントなどの、ゲームに必要な基本的データに対応しています。デフォルトで対応しているフォーマットであれば、プロジェクトにファイルを追加した時点でコンテンツとして認識されます。

インポータの開発

独自形式のファイルをコンテンツとしてゲームで利用したい場合は、事前にファイルをアセットに変換しなければなりません。ファイルからアセットを生成する過程には決められた作業工程が存在し、必要な部分を拡張することで対応できます。XNB ファイルの詳細なフォーマットを理解する必要はなく、アセット生成するコンパイラを自前で開発する必要もありません。

コンテンツ・パイプラインを拡張する場合、ゲームとは別にプロジェクトを立ち上げます。アセットの生成は、ゲームの実行時に行われるものではありません。通常は、XNA Framework の DLL を生成するプロジェクトとして作成します。

■ 図 01 Windows Game Library プロジェクトの作成

ライブラリ用のプロジェクト

コンテンツ・パイプラインの最初の工程は、コンテンツとなるファイルを読み込むことから始まります。この作業はインポータと呼ばれるクラスによって行われます。独自のファイル形式に対応するには、ファイルからデータを読み込む専用のインポータを開発しなければなりません。

コンテンツ・パイプラインの拡張には、主に Microsoft.Xna.Framework.Content.Pipeline 名前空間の型を利用します。この名前空間を利用するには、プロジェクトの参照設定に Microsoft.Xna.Framework.Content.Pipeline を追加しなければなりません。参照を追加するには、プロジェクトを選択している状態でメニューの「プロジェクト」から「参照の追加」を選択します。

■ 図 02 参照の追加

参照の追加

次に「参照の追加」ダイアログが表示されるので「.NET」タブを選択してください。下部のリスト内から Microsoft.Xna.Framework.Content.Pipeline という名前のコンポーネント名を探して選択してください。最後に「OK」ボタンを押すと、選択した参照がプロジェクトに追加されます。

■ 図 03 Microsoft.Xna.Framework.Content.Pipelineの追加

参照の追加

通常、インポータを開発するには Microsoft.Xna.Framework.Content.Pipeline.ContentImporter<T> クラスを継承する新しいクラスを作成します。

■ Microsoft.Xna.Framework.Content.Pipeline.ContentImporter クラス

public abstract class ContentImporter<T> : IContentImporter

インポータの役割は、指定されたファイルからデータを読み取り、適切なオブジェクトに変換して結果を返すことです。T 型パラメータには、インポータが返す型を指定します。ファイルの読み込みは Import() メソッドによって行われます。このメソッドをオーバーライドしてください。

■ ContentImporter クラス Import() メソッド

public virtual abstract T Import (
         string filename,
         ContentImporterContext context
)

filename にはインポートするファイル名が格納されているので、インポータはこのファイルを読み込みます。context には、Microsoft.Xna.Framework.Content.Pipeline.ContentImporterContext クラスのオブジェクトが格納されています。このオブジェクトを通じて、進捗状況などを報告することができます。

■ Microsoft.Xna.Framework.Content.Pipeline.ContentImporterContext クラス

public sealed class ContentImporterContext

このクラスは、主にインポータが処理状況の報告を行うために用いられますが、通常の Visual Studio のビルド用の出力には表示されないため、この場では割愛します。

Import() メソッド内で行うファイルの読み込みは、任意の API を利用することができます。コンテンツ・パイプラインの処理はゲームの実行時ではなく、ビルド時に行われるため、Windows に依存した API を利用することに問題ありません。通常は System.IO を使うことになるでしょう。

最後に、ContentImporter を継承したクラスは、自分自身がインポータとして利用可能であることを開発環境に通知するために Microsoft.Xna.Framework.Content.Pipeline.ContentImporterAttribute 属性クラスを指定します。

■ Microsoft.Xna.Framework.Content.Pipeline.ContentImporterAttribute クラス

public class ContentImporterAttribute : Attribute

このクラスのコンストラクタには、次のようなものがあります。

■ ContentImporterAttribute クラスのコンストラクタ

public ContentImporterAttribute (
         string fileExtension
)

fileExtension に、インポータが対応するファイルの拡張子を指定します。たとえば、テキストファイルを読み込むインポータであれば ".txt" を指定します。開発環境は、この情報からファイルに対応するインポータを関連付けることができます。

また、ContentImporterAttribute クラスはインポータの名前を提供する DisplayName プロパティを持ちます。名前付き属性パラメータとして、インポータの表示名を設定することができます。

■ ContentImporterAttribute  クラス DisplayName プロパティ

public virtual string DisplayName { get; set; }

開発環境は、インポータの設定などに DisplayName プロパティの文字列を使うことが期待できます。実際に XNA Game Studio では DisplayName に設定した文字列を設定時に表示してくれます。

この場では、テキストファイルから文字列を読み込み、string 型のオブジェクトに変換して結果を返す単純なインポータを開発してみましょう。

■ Sample15 TextImporter.cs

using System.IO;
using Microsoft.Xna.Framework.Content.Pipeline;

[ContentImporter(".txt", DisplayName = "Text Importer")]
public class TextImporter : ContentImporter<string>
{
    public override string Import(string filename, ContentImporterContext context)
    {
        FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read);
        StreamReader reader = new StreamReader(stream);
        string result = "";
        while (!reader.EndOfStream)
            result += reader.ReadLine();

        return result;
    }
}

TextImporter は、ContentImporter を継承するインポータです。Import() メソッドでは filename パラメータに指定されたファイルを読み込み、string 型で結果を返します。

これで、インポータの開発プロジェクトは終了です。次に、このインポータをゲーム用のプロジェクトに設定し、ゲーム用のプロジェクトをビルドしたときに、テキストファイルからアセットを生成できることを確認してみましょう。

独自に開発したインポータを利用するには XNA Game Studio のプロジェクトにインポータを認識させる必要があります。インポータを追加するには、プロジェクトのプロパティを開いて「Content Pipeline」タブを選択してください。次に「Add」ボタンを押して、インポータを含むアセンブリを追加してください。

■ 図 04 Cotent Pipeline の設定

アセンブリの追加

これで作業は終了です。正しくインポータが作られていれば、XNA Game Studio はインポータを認識します。

インポータが認識されているかどうかは、プロジェクトにファイルを追加してファイルを選択し、ファイルの「プロパティ」ウィンドウ内にある「Content Importer」を変更してください。表示されたリストの中に、インポータの属性 ContentImporterAttribute の DisplayName  に指定した名前が表示されていれば成功です。適当なテキストファイルを追加して Text Importer を選択してください。

■ Sample16 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private SpriteFont font;
    private Vector2 position;
    private string text;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
        position = new Vector2(10, 10);
    }

    protected override void Initialize()
    {
        text = content.Load<string>("TestText");
        base.Initialize();
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            font = content.Load<SpriteFont>("TestFont");
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);
        sprite.Begin();
        sprite.DrawString(font, text, position, Color.Black);
        sprite.End();
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample16 は、TestText という名前のアセットを文字列型として読み込んで描画します。事前に、このプロジェクトに txt 拡張子のテキストファイルを追加し、インポータに Sample15 で開発した Text Importer を選択してください。インポータを設定するには、追加したテキストファイルを選択している状態で「プロパティ」ウィンドウの「Content Importer」という名前のプロパティを変更します。

■ 図 05 インポータの設定

インポータの設定

プロジェクトに正しくインポータが認識されている場合、リスト内に「Text Importer」が表示されます。ここに表示されるテキストは ContentImporterAttribute 属性の DisplayName パラメータに指定した文字列です。このとき「Content Processor」には「No Processing Required」を指定してください。テキストファイルの内容は、フォントに含まれている文字であれば任意のテキストでかまいません。

プロセッサの開発

インポータの役割はファイルからデータを読み込むことだけです。データをゲーム環境に最適化する変換処理は、インポータではなくプロセッサと呼ばれる部品の役割となります。プロセッサは、入力されたデータをゲーム用に最適化されたデータに変換して結果を返します。

独自のプロセッサを開発するには Microsoft.Xna.Framework.Content.Pipeline.ContentProcessor<TInput, TOutput> クラスを継承します。基本的な開発の流れはインポータと同じです。

■ Microsoft.Xna.Framework.Content.Pipeline.ContentProcessor クラス

public abstract class ContentProcessor<TInput,TOutput> : IContentProcessor

TInput 型パラメータには入力されるデータ型、TOutput には出力するデータ型を指定します。

ContentProcessor クラスには、インポータから入力されたデータを受け取り、変換した結果を返す Process() メソッドが公開されています。独自のプロセッサでは、このメソッドをオーバーライドして実装してください。

■ ContentProcessor クラス Process () メソッド

public virtual abstract TOutput Process (
         TInput input,
         ContentProcessorContext context
)

このメソッドは、input パラメータから受け取ったデータを変換し、結果を戻り値で返します。context には、インポータと同じようにログなどの報告を出力するための ContentProcessorContext オブジェクトが渡されます。

また、ContentProcessor クラスを継承しプロセッサとなるクラスには、Microsoft.Xna.Framework.Content.Pipeline.ContentProcessorAttribute 属性クラスを指定しなければなりません。

■  Microsoft.Xna.Framework.Content.Pipeline.ContentProcessorAttribute クラス

public class ContentProcessorAttribute : Attribute

このクラスのコンストラクタは、パラメータを受け取りません。

■ ContentProcessorAttribute クラスのコンストラクタ

public ContentProcessorAttribute ()

この属性も、インポータと同じようにプロセッサの名前を表す DisplayName プロパティが提供されています。

■ ContentProcessorAttribute クラス DisplayName プロパティ

public virtual string DisplayName { get; set; }

ゲーム用のプロジェクトにプロセッサを認識させる方法は、インポータと同じです。プロセッサを含むアセンブリをプロジェクトに追加すれば、自動的にプロセッサが認識されるようになります。

この場では、入力された文字列の並びを反対に変換した文字列を返すプロセッサを作成してみます。Sample15 で作成したプロジェクトに次のコードを追加してください。

■ Sample15 TextProcessor.cs

using Microsoft.Xna.Framework.Content.Pipeline;

[ContentProcessor(DisplayName="Text Processor")]
public class TextProcessor : ContentProcessor<string, string>
{
    public override string Process(string input, ContentProcessorContext context)
    {
        string result = "";
        for (int i = input.Length - 1 ; i >= 0 ; i--)
        {
            result += input[i];
        }
        return result;
    }
}

■ 実行結果

実行結果

Sample15 の TextProcessor は、入力された文字列を反転させた結果を返します。Sample16 のプロジェクトに追加したテキストファイルのプロパティから、プロセッサを Text Processor に設定してください。プロセッサを設定するには、対象のファイルを選択している状態で「プロパティ」ウィンドウの「Content Processor」という名前のプロパティを変更します。

■ 図 06 プロセッサの設定

プロセッサの設定

この設定でビルドすれば、TextImporter が読み込んだデータを TextProcessor が受け取り、変換した結果をアセットとして保存します。プロセッサによって、テキストファイルの文字列が反転されたことが結果から確認できます。

コンパイラとタイプライタ

これまで作成したインポータやプロセッサは、ファイルから読み込んで作成したオブジェクト表現のデータを返すメソッドを提供していました。しかし、ビルド時に XNA Game Studio は、インポータやプロセッサが返したデータを XNB 形式のファイルに出力しています。ここで、オブジェクト表現のデータをどのようにファイルに変換しているのか、疑問が生まれます。インポータやプロセッサのサンプルでは、オブジェクトをファイルに変換する方法については記述していません。

コンテンツ・パイプラインは、ビルド時の最終工程でインポータやプロセッサが返したデータをコンパイラによってファイルに保存可能な形式に変換します。これをシリアライズと呼び、XNB ファイルに保存されるアセットは、インポータやプロセッサが返したデータをシリアライズしたものです。

データのシリアライズは、コンパイラによって行われます。ここでいうコンパイラとは、開発ツールではなく XNA Framework の Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentCompiler クラスを表します。

■ Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentCompiler クラス

public sealed class ContentCompiler

コンパイラの役割は、インポータ、またはプロセッサが出力したデータを XNB ファイルに変換することです。コンパイラは、型ごとに専用のタイプライタと呼ばれるオブジェクトを使ってファイルにデータを書き出します。

これまでのインポータやプロセッサを使ったサンプルは、.NET の基本データ型である string 型だったため、専用のタイプライタを書くことなく XNB ファイルに変換することができたのです。XNA Framework は、int や string といった .NET の基本型と、Vector2 や Color のような XNA Framework の基本的なデータ型をサポートしています。また、List<T> や Dictionary<T> といったジェネリックもサポートしています。これらの基本的なデータ型であれば、特に XNB ファイルへの変換を意識することなく読み書きを行うことができます。

問題は、独自に開発した型をコンテンツとして利用したい場合です。インポータやプロセッサが生成したオブジェクトの型に対応するタイプライタが発見できない場合、アセットに変換する手段がないためゲーム用のプロジェクトはビルド時にエラーを発生させます。

この場では、実験用に表示させるテキストを保有するだけの Message クラスを作成します。

■ Sample17 Message.cs

public class Message
{
    private string text;
    public string Text
    {
        set { this.text = value; }
        get { return text; }
    }
}

次に、テキストファイルから読み込んだ文字列を Text プロパティに設定した Message オブジェクトを返すインポータを作ります。

■ Sample17 TextImporter.cs

using System.IO;
using Microsoft.Xna.Framework.Content.Pipeline;

[ContentImporter(".txt", DisplayName = "Text Importer")]
public class TextImporter : ContentImporter<Message>
{
    public override Message Import(string filename, ContentImporterContext context)
    {
        FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read);
        StreamReader reader = new StreamReader(stream);
        Message result = new Message();
        while (!reader.EndOfStream)
            result.Text += reader.ReadLine();

        return result;
    }
}

これで、このインポータは.NET の基本型である string ではなく、独自に開発した Message 型のオブジェクトを返すようになりました。ゲーム用のプロジェクトにテキストファイルを追加して、このインポータを設定した状態でビルドすると、次のようなエラーが発生します。

エラー 1 Unsupported type. Cannot find a ContentTypeWriter implementation for Message.

この問題を解決するには、Message 型専用のタイプライタを提供しなければなりません。新しいタイプライタを開発するには Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentTypeWriter<T> クラスを継承させます。

■ Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentTypeWriter クラス

public abstract class ContentTypeWriter<T> : ContentTypeWriter

ContentTypeWriter クラスの型パラメータ T に、このタイプライタが扱う型を指定します。

このクラスは、型パラメータ T の値をアセットに書き込む Write() 抽象メソッドを宣言しています。サブクラスで Write() メソッドを実装して、適切に出力してください。

■ ContentTypeWriter クラス Write() メソッド

protected internal abstract void Write (
         ContentWriter output,
         T value
)

output には、System.IO 名前空間の BinaryWriter を継承する Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentWriter クラスのオブジェクトが格納されています。value パラメータに変換するべきオブジェクトが格納されているので、value パラメータのオブジェクトを ContentWriter で出力します。

■ Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentWriter クラス

public sealed class ContentWriter : BinaryWriter

このクラスの基本的な仕組みは BinaryWriter と同じです。基本型は Write() メソッドから出力することができるため、オブジェクトのデータを正しい手順で書き込んでください。

Write() メソッドの他にもう 1 つ、GetRuntimeReader() 抽象メソッドをオーバーライドして実装する必要があります。このメソッドは、このタイプライタで出力したアセットを読み込むための型を提供します。

■ ContentTypeWriter クラス GetRuntimeReader () メソッド

public abstract string GetRuntimeReader (
         TargetPlatform targetPlatform
)

targetPlatform には、データを読み込みを行うプラットフォームを表す Microsoft.Xna.Framework.TargetPlatform 列挙型の値が格納されています。

■ Microsoft.Xna.Framework.TargetPlatform 列挙型

public enum TargetPlatform

この列挙型は Windows プラットフォームを表す Windows メンバと、Xbox 360 プラットフォームを表す Xbox360 メンバ、そして対象のプラットフォームが不明であることを表す Unknown メンバを持ちます。

GetRuntimeReader() メソッドは、対象のプラットフォーム用にアセットの読み込み処理を行う型とアセンブリ名をカンマで区切った文字列で返します。

"型名,アセンブリ名"

型名は名前空間も含む完全限定名で指定しなければなりません。アセットの読み込みについては後述します。

最後に、ContentTypeWriter<T> クラスを継承したタイプライタに Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentTypeWriterAttribute 属性クラスを指定します。

■ Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler.ContentTypeWriterAttribute クラス

[AttributeUsageAttribute(4)]
public sealed class ContentTypeWriterAttribute : Attribute

この属性には、特にパラメータはありません。これで、独自の型のオブジェクトを書き込むタイプライタを作成することができます。Message 型のオブジェクトを書き込むタイプライタを作成してみましょう。

■ Sample17 MessageTypeWriter.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;

[ContentTypeWriter]
public class MessageTypeWriter : ContentTypeWriter<Message>
{
    protected override void Write(ContentWriter output, Message value)
    {
        output.Write(value.Text);
    }
    public override string GetRuntimeReader(TargetPlatform targetPlatform)
    {
        return "MessageTypeReader," + GetType().Assembly.FullName;
    }
}

Sample17 の MessageTypeWriter クラスは、Write() メソッドに渡された Message オブジェクトの Text プロパティを、ContentWriter オブジェクトから文字列として出力します。これで、読み込んだテキストファイルの文字列を Message オブジェクトに変換し、アセットに書き込むことができます。

GetRuntimeReader() メソッドは、このオブジェクトが出力した Message オブジェクトを読み込むクラスの情報を提供します。この場では、MessageTypeWriter クラスと同じアセンブリ内に MessageTypeReader というクラスを作成して、このクラスから書き込んだ Message オブジェクトを読み込むものとします。

タイプリーダ

自作のタイプライタで出力したオブジェクトを実行時に読み込むには、タイプライタに対応する読み込み処理を行う専用のクラスが必要になります。これをタイプリーダと呼びます。

アセットを読み込むのは ContentManager クラスの Load() メソッドですが、このメソッドは型に対応するタイプリーダを使ってアセットを読み込んでいます。そのため、タイプライタを使ってアセットを生成しても、これを読み込むタイプリーダがなければゲームの実行時に例外が発生します。

読み込みはゲームの実行時に行われる点に注意してください。リソースをアセットにコンパイルする処理は、ゲーム用プロジェクトのビルド時に行われるため Windows に依存したコードが使えますが、読み込みは Xbox 360 プラットフォームでも行われることを想定しなければなりません。

タイプリーダを作成するには、Microsoft.Xna.Framework.Content.ContentTypeReader <T>クラスを継承します。

■ Microsoft.Xna.Framework.Content.ContentTypeReader クラス

public abstract class ContentTypeReader<T> : ContentTypeReader

型パラメータ T には、このタイプリーダが読み込むデータ型を指定します。

サブクラスでは、アセットからデータを読み込み、型パラメータ T のオブジェクトとして結果を返す Read() 抽象メソッドを実装しなければなりません。

■ ContentTypeReader クラス Read() メソッド

protected internal abstract T Read (
         ContentReader input,
         T existingInstance
)

input には、アセットからの読み込みを行う Microsoft.Xna.Framework.Content.ContentReader クラスのオブジェクトが格納されています。このクラスは System.IO 名前空間の BinaryReader から派生しているため、読み込み方法は従来のファイル入力処理と変わりません。型ごとの Read〜() メソッドからデータを読み込むことができます。

■ Microsoft.Xna.Framework.Content.ContentReader クラス

public sealed class ContentReader : BinaryReader

既存のインスタンスに対してデータを読み込む場合は existingInstance に T 型のオブジェクトが与えられます。null の場合は新しいインスタンスを生成してください。どちらの場合でも、Read() メソッドはアセットから読み込んだオブジェクトを結果として返します。

インポータやプロセッサ、タイプライタとは異なり、タイプリーダには属性を指定する必要はありません。タイプリーダの情報はビルド時にタイプライタの GetRuntimeReader() メソッドから得ることができ、XNB ファイルに含まれています。

■ Sample17 MessageTypeReader.cs

using Microsoft.Xna.Framework.Content;

public class MessageTypeReader : ContentTypeReader<Message>
{
    protected override Message Read(ContentReader input, Message existingInstance)
    {
        string text = input.ReadString();
        Message message = new Message();
        message.Text = text;
        return message;
    }
}

■ Sample18 Test.cs

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

public class Test : Game
{
    public static void Main(string[] args)
    {
        using (Game game = new Test()) game.Run();
    }

    private GraphicsDeviceManager graphics;
    private ContentManager content;
    private SpriteBatch sprite;
    private SpriteFont font;
    private Vector2 position;
    private Message message;

    public Test()
    {
        graphics = new GraphicsDeviceManager(this);
        content = new ContentManager(Services);
        position = new Vector2(10, 10);
    }

    protected override void Initialize()
    {
        message = content.Load<Message>("TestText");
        base.Initialize();
    }

    protected override void LoadGraphicsContent(bool loadAllContent)
    {
        if (loadAllContent)
        {
            font = content.Load<SpriteFont>("TestFont");
        }
        sprite = new SpriteBatch(graphics.GraphicsDevice);
        base.LoadGraphicsContent(loadAllContent);
    }

    protected override void UnloadGraphicsContent(bool unloadAllContent)
    {
        if (unloadAllContent)
        {
            content.Unload();
        }
        sprite.Dispose();
        base.UnloadGraphicsContent(unloadAllContent);
    }

    protected override void Draw(GameTime gameTime)
    {
        graphics.GraphicsDevice.Clear(Color.White);
        sprite.Begin();
        sprite.DrawString(font, message.Text, position, Color.Black);
        sprite.End();
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample18 は、Sample17 で作成したインポータを使ってプロジェクトに追加されているテキストファイルを Message 型のオブジェクトに変換し、これを読み込んで描画するプログラムです。正しく、タイプライタやタイプリーダが作られていれば、ゲーム用のプロジェクトではデータ変換を意識することなく Message 型のオブジェクトとしてアセットを読み込むことができます。

2D マインスイーパの作成

前回作成したマインスイーパは、地雷原の情報を作成して、作られた地雷原の情報を基に対応するテクスチャを描画するというものでした。これをゲームと呼べるものに進化させるには、コントローラからの入力に反応して隠された地雷原を選択できるようにしなければなりません。今回は、コントローラの左スティックを操作して地雷原のマスを選択できるようにし、A ボタンでマスを開く動作を追加します。基本的な設計は前回作成したマインスイーパのプロジェクトを引き継ぎます。

まず最初に、地雷原のデータを提供する MineField クラスに、現在選択されている列番号と行番号を表すプロパティを追加します。コントローラからの入力でこの値を変更すれば、選択されているマスを移動させることができます。選択されているマスをどのように描画するかは Draw() メソッドの責任となります。

■ MineField クラス SelectedColumn プロパティ

public int SelectedColumn

■ MineField クラス SelectedRow プロパティ

public int SelectedRow

SelectedColumn プロパティは選択されている列番号、SelectedRow は行番号を提供します。

また、ゲーム開始時に MineField のすべてのマスは隠されている状態になります。プレイヤーは、コントローラを操作して地雷を踏まないようにマスを開いていきます。前回、地雷原のマスは地雷が存在するかどうかの 2 つの状態しか存在しなかったため、MineField のインデクサは bool 型で実装していました。結果が true ならば地雷があると判断できます。しかし、今回からはマスが隠されているかどうかといった新しい状態が追加されたため bool ではなく列挙型によるフラグに変更します。

■ MineField クラスのインデクサ

public FieldState this[int column, int row]

FieldState は、地雷原のマスの状態を表す列挙型です。この列挙型はフラグとして利用可能にし、いくつかの状態を組み合わせることができるものとします。

■ FieldState 列挙型

[System.Flags]
public enum FieldState
{
    None = 0,   //デフォルト
    Mine = 1,   //地雷
    Opened = 2  //開かれている
}

FieldState の None はデフォルトの状態を表します。このマスは、隠されている状態で地雷はありません。Mine は地雷のあるマスを表し、Opened は開かれているマスを表します。FieldState には FlagsAttribute 属性を指定しているため、ビットごとの論理和演算子で組み合わせることができます。Mine メンバと Opened メンバを組み合わせることで、地雷のあるマスが開かれている状態であることを表せます。フラグが設定されているかどうかを調べるには、ビットごとの論理積演算子を使います。

列挙型を用いることで、地雷原のマスの状態に柔軟性を持たせることができます。たとえば、本稿のサンプルに加えて、地雷があると思われる場所をマークする機能を加えたい場合、マスがマークされている状態を表すメンバを FieldState に追加することで実装できます。

ビット単位のデータを意識してプログラムすることを避けたいのであれば、FieldState を列挙型ではなく構造体で実装するという方法もあります。今回はデータが単純だったために列挙型で実装しましたが、マスの状態が複雑な場合は構造体の方が良いでしょう。構造体であれば、状態の設定や比較もプロパティ単位で行えるため簡単になります。

マスを開く処理は、MineField のインデクサに FieldState.Open を加えることで実現することもできますが、今回は MineField クラス内に、現在選択されているマスを開く Open() メソッドを用意しました。

■ MineField クラス Open() メソッド

public bool Open()

このメソッドは、MineField の現在選択されているマスの状態に FieldState.Open を加え、開いたマスが地雷かどうかを結果として返します。

次に、マスの状態が増えたため、前回に加えて専用の画像を用意しなければなりません。新しく必要なテクスチャは、隠されている状態のマスと、選択されているマスを表すカーソルです。ImageName 列挙型のメンバに、これらのテクスチャを表すメンバを追加してください。

■ ImageName 列挙型

public enum ImageName
{
    NoneField = 0,  //隣接するマスに地雷がない空マス
    OneField = 1,   //1
    TwoField = 2,   //2
    ThreeField = 3, //3
    FourField = 4,  //4
    FiveField = 5,  //5
    SixField = 6,   //6
    SevenField = 7, //7
    EightField = 8, //8
    MineField = 16, //地雷のマス
    HideField = 32, //隠されたマス
    Selector = 64   //選択中のマスを表すカーソル
}

今回で追加したのは、HideField メンバと Selector メンバです。追加したメンバに対応する画像を読み込むために ImageManager クラスの読み込み処理も修正してください。

後は、Game クラスを継承する Minesweeper クラスの Update() メソッドと Draw() メソッドを修正して、コントローラからの入力に従ってデータを更新し、追加した機能に対応した描画処理を行えば完成です。Update() メソッドでは GamePad クラスからコントローラの状態を取得し、ボタンやスティックの状態に応じて MineField オブジェクトの値を変更します。

左スティックがいずれかの方向に倒されている場合、倒されている方向に従って選択されているマスを移動させます。スティックは傾きによって 0 〜 1 までの範囲の座標を返します。この場では、水平方向、垂直方向ともに 0.9 または -0.9 以上倒されているときに反応するように条件を付けています。よって、スティックがいずれかの方向に、ほぼ完全に倒されると、選択されているマスが移動します。

Update() メソッドは連続的に呼び出されいるため、スティックの傾きだけを条件にした場合は何度も実行されてしまいます。そこで、一度スティックによる入力を感知すると、一定時間が経過するまで次の入力を無効にするなどの処置が必要です。今回は、スティックの傾きを感知して選択されているマスを移動させると、その時のゲーム時間を prevTime フィールドに保存し、この時間から INPUT_WAIT 定数のミリ秒が経過するまで入力を受け付けないように制御しています。こうすることで、一瞬で選択されているマスが端まで移動してしまう現象を防ぐことができます。

また、A ボタンを押すと MineField オブジェクトの Open() メソッドを呼び出して、現在選択されているマスを開きます。

■ Minesweeper クラス Update() メソッド

private TimeSpan prevTime;          //最後にスティックを操作したゲーム時間
private GamePadState prevState;     //直前のコントローラの状態
private const int INPUT_WAIT = 150; //次のスティック操作までのミリ秒単位の時間間隔
protected override void Update(GameTime gameTime)
{
    GamePadState state = GamePad.GetState(PlayerIndex.One);

    if (state.Buttons.Back == ButtonState.Pressed)
        this.Exit();

    if (state.ThumbSticks.Left.X > 0.9F && mineField.SelectedColumn < mineField.Column - 1)
    {
        if (gameTime.TotalGameTime.TotalMilliseconds > (prevTime.TotalMilliseconds + INPUT_WAIT))
        {
            mineField.SelectedColumn += 1;
            prevTime = gameTime.TotalGameTime;
        }
    }
    else if (state.ThumbSticks.Left.X < -0.9F && mineField.SelectedColumn > 0)
    {
        if (gameTime.TotalGameTime.TotalMilliseconds > (prevTime.TotalMilliseconds + INPUT_WAIT))
        {
            mineField.SelectedColumn -= 1;
            prevTime = gameTime.TotalGameTime;
        }
    }
    if (state.ThumbSticks.Left.Y > 0.9F && mineField.SelectedRow > 0)
    {
        if (gameTime.TotalGameTime.TotalMilliseconds > (prevTime.TotalMilliseconds + INPUT_WAIT))
        {
            mineField.SelectedRow -= 1;
            prevTime = gameTime.TotalGameTime;
        }
    }
    else if (state.ThumbSticks.Left.Y < -0.9F && mineField.SelectedRow < mineField.Row - 1)
    {
        if (gameTime.TotalGameTime.TotalMilliseconds > (prevTime.TotalMilliseconds + INPUT_WAIT))
        {
            mineField.SelectedRow += 1;
            prevTime = gameTime.TotalGameTime;
        }
    }

    if (prevState.Buttons.A == ButtonState.Released && state.Buttons.A == ButtonState.Pressed)
    {
        if (mineField.Open())
        {
            //ゲームオーバー時の処理
        }
    }

    prevState = state;
    base.Update(gameTime);
}

Draw() メソッドの構造は、前回と比べても大きな違いはありません。追加したマスの状態に対応して、マスが隠されている場合は ImageName.HideField に対応するテクスチャを描画し、そうでなければ地雷か、または周囲にある地雷の数を表すテクスチャを描画します。また、マスが選択されているかどうかを調べ、選択されている場合は ImageName.Selector に対応するテクスチャを重ねます。

■ Minesweeper クラス Draw() メソッド

protected override void Draw(GameTime gameTime)
{
    graphics.GraphicsDevice.Clear(Color.White);
    int x = 0, y = 0;

    spriteBatch.Begin();
    for (int r = 0; r < mineField.Row; r++)
    {
        for (int c = 0; c < mineField.Column; c++)
        {
            Rectangle rect = new Rectangle(x, y, images.Width, images.Height);

            if ((mineField[c, r] & FieldState.Opened) == FieldState.Opened)  //開かれている
            {
                if ((mineField[c, r] & FieldState.Mine) == FieldState.Mine)    //地雷
                {
                    spriteBatch.Draw(images[ImageName.MineField], rect, Color.White);
                }
                else
                {
                    int mines = mineField.GetAdjacentMines(c, r);
                    ImageName imageName = (ImageName)Enum.Parse(typeof(ImageName), mines.ToString(), true);
                    spriteBatch.Draw(images[imageName], rect, Color.White);
                }
            }
            else //隠されている
            {
                spriteBatch.Draw(images[ImageName.HideField], rect, Color.White);
            }

            if (mineField.SelectedColumn == c && mineField.SelectedRow == r) //選択されている
            {
                spriteBatch.Draw(images[ImageName.Selector], rect, Color.White);
            }
            x += images.Width;
        }
        x = 0;
        y += images.Height;
    }
    spriteBatch.End();

    base.Draw(gameTime);
}

■ 実行結果

実行結果

時間制限や得点、ゲームオーバーなどは実装していません。コントローラからの入力でゲームデータが更新された時にゲーム全体の状態を調べ、進行状況などを表示することでゲームらしくなるでしょう。いろいろなアイデアを試してみてください。



Top of Page Top of Page
Visual Studio 2008 Express

Microsoft