WisdomSoft - for your serial experiences.

ゲームの部品化

規模の大きなゲームを開発すると必然とコード量も増え、複雑化します。XNA Framework では再利用可能な処理をコンポーネント化し、ゲームに依存しないプラグ可能な部品として分離する仕組みを提供しています。

再利用可能な機能の分離

簡単なゲームであれば、Game クラス内にすべてのコードを書いて完成させることができるかもしれませんが、大規模なゲーム開発となると、すべての機能を Game クラスに詰め込むのは設計として大きな問題が生じます。組織的にゲームを開発するには、ゲームを倫理的な役割に分解して、部品単位に開発しなければなりません。加えて、個々の部品は互いに依存せず、再利用可能な形に設計する必要があります。こうした作業には、設計能力が十分にある経験豊富な開発者が必要になります。

幸い、XNA Framework では、アプリケーション開発モデルとして、基本的なゲームの部品化と、個々の部品の結合方法を統一する基盤を提供しています。ゲーム部品(コンポーネント)は、Game クラスとは別のクラスとして開発することができ、後に Game クラスに登録してゲームの一部として機能させることができます。

ゲーム部品は Microsoft.Xna.Framework.GameComponent クラスで表されます。このクラスのインスタンスが、ゲーム部品として扱われます。独自の機能を部品化する場合は、このクラスを継承して独自の機能をオーバーライドします。

Microsoft.Xna.Framework.GameComponent クラス
public class GameComponent : IGameComponent, IUpdateable, IDisposable

このクラスのコンストラクタには、ゲーム部品を利用する Game クラスを渡す必要があります。GameComponent クラスを継承する場合、基底クラスのコンストラクタに Game オブジェクトを渡してください。

GameComponent クラスのコンストラクタ
public GameComponent (Game game)

コンストラクタに渡した game オブジェクトは、Game プロパティから取得できます。GameComponent クラスを継承したサブクラス内で、自分自身に関連付けられているゲームにアクセスしたい場合は、このプロパティからオブジェクトを取得します。 

GameComponent クラス Game プロパティ
public Game Game { get; }

GameComponent クラスは、ゲーム中に含まれるサブゲーム的な役割を提供します。Game クラスと同じように、自分自身を初期化するための Initialize() メソッドUpdate() メソッドを公開しています。これらのメソッドは、基本的に Game クラスと同じです。

GameComponent クラス Initialize() メソッド
GameComponent クラス Initialize() メソッド
GameComponent クラス Update() メソッド
public virtual void Update (GameTime gameTime)

GameComponent クラスを継承し、これらのメソッドをオーバーライドして、ゲーム部品に必要な初期化処理と更新処理を記述します。Update() メソッドは、Game クラスの Update() メソッドと同じように、ゲーム部品の状態をフレーム単位で更新するために呼び出されます。gameTime パラメータには、現在のゲーム時間が渡されます。

ゲーム部品をゲームに登録する

GameComponent クラスの Initialize() メソッドや Update() メソッドは、登録している Game クラスから呼び出されます。よって、作成したゲーム部品を有効にするには、インスタンスを Game クラスに登録しなければなりません。ゲーム部品を登録するには Game クラスの Components プロパティが返すコレクションに追加します。

Game クラス Components プロパティ
public GameComponentCollection Components { get; }

このプロパティは、ゲーム部品を管理する Microsoft.Xna.Framework.GameComponentCollection クラスのオブジェクトです。

Microsoft.Xna.Framework.GameComponentCollection クラス
public sealed class GameComponentCollection : Collection<IGameComponent>

このクラスは、Collection クラスを継承するコレクションです。コレクションには IGameComponent インターフェイスを実装しているオブジェクトであれば、実体に関係なく追加できます。GameComponent クラスは、IGameComponent インターフェイスを実装しているので、GameComponent クラス、またはこれを継承しているクラスのオブジェクトであれば、Add() メソッドからコレクションに追加できます。

Game クラスの Components プロパティから得られるコレクションにゲーム部品を追加できれば、登録したゲーム部品の Initialize() メソッドや Update() メソッドが呼び出されるようになります。

コード1
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

public class TestComponent : GameComponent
{
    private string initText, updateText;

    public string Text
    {
        get { return initText + "\n" + updateText; }
    }

    public TestComponent(Game game) : base(game) { }

    public override void Initialize()
    {
        initText = "Initialized";
        base.Initialize();
    }

    public override void Update(GameTime gameTime)
    {
        updateText = "Update: " + gameTime.TotalGameTime.ToString();
        base.Update(gameTime);
    }
}

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

    private GraphicsDeviceManager graphicsDeviceManager;
    private TestComponent component;
    private SpriteBatch sprite;
    private SpriteFont font;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Content.RootDirectory = "Content";

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

    protected override void LoadContent()
    {
        sprite = new SpriteBatch(GraphicsDevice);
        font = Content.Load<SpriteFont>("TestFont");

        base.LoadContent();
    }

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

        sprite.Begin();
        sprite.DrawString(font, component.Text, Vector2.Zero, Color.Black);
        sprite.End();

        base.Draw(gameTime);
    }
}
実行結果
コード1 実行結果

コード1は、GameComponent クラスを継承する TestComponent クラスを作成し、Initialize() メソッドと Update() メソッドをオーバーライドしています。Game クラスを継承する Test クラスでは、TestComponent のインスタンスを生成し、Components プロパティが返すコレクションにゲーム部品として追加しています。

TestComponent クラスは、Initialize() メソッドや Update() メソッドが呼び出されたかどうかを Text プロパティを通じて返します。実行結果を見れば、これらのメソッドが正しく呼び出されていることが確認できます。ゲーム部品のメソッドは、Game クラスのメソッドから呼び出されるため、Game クラスの Initialize() メソッドや Update() メソッドをオーバーライドした場合は、必ず基底クラスのメソッドを呼び出してください。そうしなければ、ゲーム部品のメソッドが呼び出されなくなってしまいます。

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

    public TestGame()
    {
        Components.Add(new TestComponent(this));
    }

    protected override void Initialize()
    {
        //base.Initialize();
    }
    protected override void Update(GameTime gameTime)
    {
        //base.Update(gameTime);
    }
}

上記のように、Game クラスからオーバーライドした Initialize() メソッドや Update() メソッドで基底クラスの呼び出しを行わない場合、Game クラスが提供するゲーム部品に関連した基礎的な機能が失われ、ゲームに登録した GameComponent の Initialize() メソッドや Update() メソッドが呼び出されなくなってしまいます。

有効化と無効化

GameComponent クラスの Update() メソッドは、Game クラスの Update() メソッドから自動的に呼び出されますが、ゲームの状態によっては一時的にゲーム部品を無効化したいと考えることがあるでしょう。何らかの事情で、特定のゲーム部品だけを停止させたり、その後、何らかの条件によって再起動させることができれば便利です。

ゲーム部品の Update() メソッドを呼び出すかどうかは、GameComponent クラスの Enabled プロパティで制御されます。 

GameComponent クラス Enabled プロパティ
public bool Enabled { get; set; }

このプロパティの値が true であれば Game クラスの Update() メソッドが呼び出されたとき、GameComponent の Update() メソッドが呼び出されます。false であれば、呼び出されません。一時的に、ゲーム部品の更新を停止したい場合は、Enabled プロパティを false に設定してください。

コード2
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

public class TestComponent : GameComponent
{
    private Color color;
    private bool isInc;

    public Color Color
    {
        get { return color; }
    }

    public TestComponent(Game game)
        : base(game)
    {
        isInc = true;
    }

    public override void Update(GameTime gameTime)
    {
        if (isInc)
            color = new Color((byte)(color.R + 1), (byte)(color.G + 1), (byte)(color.B + 1));
        else color = new Color((byte)(color.R - 1), (byte)(color.G - 1), (byte)(color.B - 1));

        if (color == Color.White) isInc = false;
        else if (color == Color.Black) isInc = true;

        base.Update(gameTime);
    }
}

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

    private GraphicsDeviceManager graphicsDeviceManager;
    private TestComponent component;

    public TestGame()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        component = new TestComponent(this);
        Components.Add(component);
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.B == ButtonState.Pressed) component.Enabled = false;
        else component.Enabled = true;

        base.Update(gameTime);
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(component.Color);
        Window.Title = "Color=" + component.Color.ToString();

        base.Draw(gameTime);
    }
}
実行結果
コード1 実行結果

コード2は、フレーム毎に少しずつ色が変化するアニメーションを提供する TestComponent を作成しています。このクラスは GameComponent クラスを継承し、ゲーム部品として機能します。Update() めそっどが呼び出されるたびに color フィールドの値を更新し、黒から白へ、白から黒への遷移を繰り返します。色は Color プロパティで公開されているため、Test クラスの Draw() メソッドで、ゲーム画面の背景として利用しています。ゲームを実行すると、フレーム毎に画面の色が変化していることが確認できます。

Test クラスの Update() メソッドでコントローラの状態を調べ、B ボタンが押されていれば登録している TestComponent オブジェクトの Enable プロパティを false に設定します。B ボタンを押している間 TestComponent の更新が行われないことを確認してください。