WisdomSoft - for your serial experiences.

サービスによる疎結合

複数のゲーム部品が同一のゲームデータや、何らかのオブジェクトを共有しなければならない場合は IServiceProvider インタフェースを用いた開発モデルを応用します。サービスは、任意の型をキーにオブジェクトを提供する辞書の一種です。

オブジェクトの共有

ゲームを部品化していくと、ゲーム全体でどのようにデータを共有し、どこで統合管理するべきか問題になります。ゲーム部品が他のクラスに依存してしまう場合、部品としての再利用性が失われる可能性があります。ゲーム部品は、他のゲーム部品や Game クラスと直接の関係を持つべきではありません。異なるゲームでも再利用できるように、具体的なクラスへの依存は避けるべきです。

例えば、Game クラスを継承する ShooterGame ゲームがスコアなどの情報をプロパティで公開し、スコアを描画する ScoreComponent クラスをゲーム部品として作ることを考えてください。一般的な手法では、ゲーム部品からゲームのスコア情報にアクセスして描画するという方法が考えられますが、この方法を採用してしまうと、ScoreComponent が ShooterGame クラスに依存してしまい、他のゲームで ScoreComponent を再利用できなくなってしまいます。スコアを描画するという機能は、特定のゲームに依存しない抽象的な機能として分離独立できるので、もう少し良い設計を考えるべきです。

ゲーム部品に限らず、ゲームに関連する情報を複数のモジュールから共有する必要がある場合は、型に依存しない方法で、複数のオブジェクトを疎結合で連結させる必要があります。Game クラスは、ゲームが持つ機能や情報を、抽象的なサービスという概念で提供できます。ゲーム部品は、ゲームに登録されているサービスを検索し、目的の機能にアクセスすることができます。

サービスを利用するには Game クラスの Services プロパティを使います。

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

このプロパティは、サービスの登録や検索などを管理する Microsoft.Xna.Framework.GameServiceContainer クラスのオブジェクトを返します。このクラスは、任意の数のサービスを保有するコンテナ(入れ物)として機能します。

Microsoft.Xna.Framework.GameServiceContainer クラス
public class GameServiceContainer : IServiceProvider

GameServiceContainer クラスは、IServiceProvider インターフェイスを実装しています。このインターフェイスは XNA Framework ではなく .NET Framework で提供されているもので、登録されているサービスを提供するサービスプロバイダを表します。よって、GameServiceContainer クラスは .NET Framework のサービスプロバイダと互換性があります。

ゲームに新しくサービスを登録するには Services プロパティが返した GameServiceContainer オブジェクトの AddService() メソッドに、サービスとなるオブジェクトを渡します。 

GameServiceContainer クラス AddService() メソッド
public void AddService (Type type, Object provider)

type パラメータには、追加するサービスの型を表す Type オブジェクト、provider には type パラメータに指定した型のサービスを指定します。

サービスは、型に依存しない疎結合を実現するための仕組みなので、サービスとして登録する型にはインターフェイスが用いられます。そうすることで、必要に応じてサービスを組み替えることができ、サービスの実体が変更されても、サービスの利用者はインターフェイスを通して機能にアクセスしているため、変更の影響を受けません。

登録したサービスを削除するには RemoveService() メソッドを使います。 

GameServiceContainer クラス RemoveService() メソッド
public void RemoveService (Type type)

type パラメータには、削除するサービスの型を指定します。

登録されているサービスを取得するには GetService() メソッドを使います。 

GameServiceContainer クラス GetService() メソッド
Object GetService(Type serviceType)

serviceType パラメータには、取得するサービスの型を指定します。結果は常に Object 型となるため、serviceType パラメータに指定した型にキャスト変換して利用します。

これらの機能を使って、ゲーム全体で共有したい機能や情報は、サービスとしてゲームに登録し、他のモジュールからは Services プロパティから取得したサービスプロバイダを通して、目的の機能や情報を利用するという形になります。  

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

public interface IScoreService
{
    int Score { get; set; }
}

public class ScoreService : IScoreService
{
    private int score;

    public int Score
    {
        set { this.score = value; }
        get { return score; }
    }
}

public class TestComponent : GameComponent
{
    private IScoreService score;
    public TestComponent(Game game) : base(game) { }

    public override void Initialize()
    {
        score = (IScoreService)Game.Services.GetService(typeof(IScoreService));
        base.Initialize();
    }

    public override void Update(GameTime gameTime)
    {
        score.Score += 1;
        base.Update(gameTime);
    }
}

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

    private GraphicsDeviceManager graphicsDeviceManager;
    private ScoreService score;
    private SpriteBatch sprite;
    private SpriteFont font;

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

        score = new ScoreService();
        Components.Add(new TestComponent(this));
        Services.AddService(typeof(IScoreService), score);
    }

    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, "Score=" + score.Score, Vector2.Zero, Color.Black);
        sprite.End();

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

コード1では、全体で共有するべきゲームスコアを表すデータを IScoreService インターフェイスで表すものとします。Test クラスで、IScoreService インターフェイスを実装する ScoreService クラスのインスタンスを生成し、これを IScoreService 型のサービスとして登録しています。ゲームスコアは Test クラスの Draw() メソッドでゲーム画面に表示されます。実行結果は、ゲームスコアをフレーム毎にインクリメントするという単純なものですが、TestComponent クラスからサービスプロバイダを通じて IScoreService 型のオブジェクトとして操作している点に注目してください。

Test クラスのコンストラクタで、Services プロパティが返す GameServiceContainer オブジェクトの AddService() メソッドを使って ScoreService オブジェクトを IScoreService 型のサービスとして追加しています。 登録されたサービスは GetService() メソッドから取得できるため、他のゲーム部品などからは、抽象化されたインターフェイス型として登録したサービスを取得し、機能にアクセスします。ゲーム部品である TestComponent クラスからゲームスコアをインクリメントするために、ゲームのサービスプロバイダから IScoreService を取得しています。

ゲーム全体で共有されるべき重要な状態や機能や、このようにサービス化することで柔軟な対応が可能となります。この開発モデルを採用することによって、ゲーム部品が互いに依存することなく、外部の機能にアクセスして何らかの要求を行ったり、データの取得や設定を行ったりすることが可能となります。