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

XNA Framework 第 06 回

赤坂玲音

通信機能

本稿では、XNA Framework 2.0 で追加された機能の中でも目玉とされる通信についてご説明します。Microsoft.Xna.Framework.Net 名前空間内にあるクラスを使うことで、ネットワーク上の異なる Xbox 360 や Windows PC で実行されているゲームが互いに通信し、データを交換することができるようになります。

本稿でご説明する通信機能を利用するには、ゲームへのサインインが必要になります。Windows PC 向けのゲームで、LAN 内でのみ通信を行うシステムリンクを対象とする場合であれば、LIVE や Creators Club のメンバシップは必要ありません。LIVE ネットワークを介して通信対戦を行う場合は、Xbox 360 または Windows PC のどちらでも LIVE ゴールドメンバシップと Creators Club メンバシップが必要になります。そのため、本稿では敷居が低くデバッグしやすいシステムリンクを使って通信を行います。

Windows PC だけを対象とした XNA Framework ゲームを開発する場合であれば、.NET Framework の通信機能を使って、制限されない自由な通信を行うことも可能ですが、Xbox 360 では使えなくなります。加えて、ゲームの性質上、通信内容を改ざんされるとチート行為が可能となってしまうため、セキュリティにも注意を払う必要があります。LIVE ネットワークを利用すると、Microsoft によって管理された信頼性の高いネットワーク環境を使うことがでます。また、Xbox 360 と Windows PC のどちらのゲームも互いに通信できるため、同じプラットフォーム間の通信はもちろんですが、Xbox 360 と Windows PC のゲームによるクロスプラットフォーム対戦も可能です。

ゲームを通信させるには 2 つ以上のゲームを起動させる必要があります。ゲーマーサービスを利用するゲームは複数起動することができません。また、パフォーマンスの問題からも、ゲームは多重起動できないように制限するのが一般的です。通信機能を使ったゲームをデバッグするには、2 つの異なる PC を使ってゲームを起動する方法と Xbox 360 を使う方法があるでしょう。本稿のサンプルの動作確認には Xbox 360 と PC を通信させる方法を使っていますが、この場合は Xbox 360 で起動させる LIVE と Creators Club のメンバシップを持つアカウントが必要になります。

セッションの作成

XNA Framework 2.0 でサポートされたネットワーク機能は、セッションという単位で管理し、通信を行います。セッションを作成したゲームをホストと呼び、それ以外のゲームはホストを検索して接続します。一般的な対戦ゲームでは、ホストの検索を行い、適切なホストが見つからなければ自分がホストとなって誰かが接続してくるのを待つという形になるでしょう。どちらにしても、ネットワークを介してゲームを接続させるにはセッションを作成する必要があります。

まずは、セッションの作成についてご説明します。セッションを作成したゲームは、自動的にホストとして扱われます。セッションは Microsoft.Xna.Framework.Net.NetworkSession クラスで表します。

■ Microsoft.Xna.Framework.Net.NetworkSession クラス

public sealed class NetworkSession : IDisposable

このクラスのインスタンスを取得するには、コンストラクタではなく静的な Create() メソッドを使います。このメソッドを呼び出すには、プレイヤーがサインインしていなければなりません。

■ NetworkSession クラス Create() メソッド

public static NetworkSession Create (
         NetworkSessionType sessionType,
         int maxLocalGamers,
         int maxGamers
)

sessionType パラメータには、セッションの種類を表す Microsoft.Xna.Framework.Net.NetworkSessionType 列挙型のいずれかのメンバを指定します。

■ Microsoft.Xna.Framework.Net.NetworkSessionType 列挙型

public enum NetworkSessionType

この列挙型には、ネットワークを使わずにローカルのプレイヤーだけでセッションする Local メンバ、ローカルネットワーク上でゲームを接続する SystemLink メンバ、LIVE サービスを介してインターネット上の他のゲームに接続する PlayerMatch メンバ、そして、自分の実力と近いプレイヤーを探して接続するランクマッチを表すRanked メンバがあります。このうち、ランクマッチを表す Ranked は商業ゲームを対象としたもので、通常の開発では利用できません。

maxLocalGamers パラメータには、このセッションに参加できる最大ローカルプレイヤーの数を 1 〜 4 までの範囲で、maxGamers にはこのセッションに参加できるプレイヤー全体の数を 2 〜 31 までの範囲で指定します。

自宅の PC や Xbox 360 でゲームを起動し、通信させたい場合は SystemLink メンバを使ってセッションを作成するのが一般的になります。LIVE を介したインターネット対戦となる PlayerMatch は、完成したゲームを配布するときに使うことになると思われますが、接続に Creators Club メンバシップが必要な現時点では現実的ではありません。ただし、すでに発表されている XNA Framework 3.0 でゲーム配信が始まれば状況は変わってくるでしょう。

作成したゲームセッションを稼働させるために、ゲームループの中で Update() を呼び出してください。通常は、Game クラスの Update() メソッド内で、NetworkSession の Update()を呼び出します。これが行われていないと、通信が途絶えてしまいます。

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

public void Update ()

このメソッドの内部で、パケットの送受信やセッションの管理などが行われています。

これで、他のゲームから作成したセッションに接続することができます。作成したセッションを破棄するには Dispose() メソッドを呼び出してください。

■ Sample01

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Net;
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 NetworkSession session;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string text;

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

    protected override void Initialize()
    {
        text = "";
        sprite = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        font = Content.Load<SpriteFont>("Content/TestFont");
        base.LoadContent();
    }

    protected override void UnloadContent()
    {
        Content.Unload();
        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.Back == ButtonState.Pressed) Exit();

        if (SignedInGamer.SignedInGamers.Count != 0)
        {
            if (session == null)
            {
                text = "A: Create session\nBack: Exit\n\nSession is null.";
                if (!Guide.IsVisible && state.Buttons.A == ButtonState.Pressed)
                {
                    session = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16);
                }
            }
            else
            {
                text = "B: Dispose session\nBack: Exit\n\nSession is running.";
                session.Update();
                if (!Guide.IsVisible && state.Buttons.B == ButtonState.Pressed)
                {
                    session.Dispose();
                    session = null;
                }
            }
        }
        else
        {
            if (session != null)
            {
                session.Dispose();
                session = null;
            }
            text = "Please sing in.";
            if (!Guide.IsVisible) Guide.ShowSignIn(1, false);
        }

        base.Update(gameTime);
    }

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

■ 実行結果

実行結果

Sample01 を実行すると、最初にサインインが要求されます。ネットワークへの接続にはプロファイルが必須となるため、サインインをしていない状態で実行すると例外が発生してしまいます。そのため、サインアウトしている状態の場合は、常にセッションを解除し、強制的にガイドを表示してサインインするように指示します。

このプログラムでは A ボタンを押すと新規にセッションを作成し session 変数にオブジェクトを格納します。session が null の場合は、セッションを作成できます。session が null ではない場合、NetworkSession クラスの Update() メソッドを、Game クラスの Update() メソッド内で繰り返し呼び出します。B ボタンを押すことでセッションを破棄し、session 変数に null を代入します。

この段階では、他のプレイヤーとの通信は行っていません。セッションを作成し、他のプレイヤーからの接続を待機しているところまでです。次に、開かれている有効なセッションを検索し、参加する方法をご紹介します。

セッションの検索

他のゲームが作成したセッションに接続するには、最初にセッションを検索しなければなりません。ゲームを接続させるには、誰かがセッションを作成してホストとなり、他のゲームがホストを検索して参加するという手順になります。セッションの検索には Find() メソッドを使います。

■ NetworkSession クラス Find() メソッド

public static AvailableNetworkSessionCollection Find (
         NetworkSessionType sessionType,
         int maxLocalGamers,
         NetworkSessionProperties searchProperties
)

sessionType パラメータには、検索するセッションの種類を表す NetworkSessionType 列挙型のメンバを指定します。ホストが SystemLink としてセッションを作成していれば、このパラメータに SystemLink メンバを指定することでセッションを見つけられます。maxLocalGamers パラメータには、このセッションに参加する最大ローカルゲーマー数を指定します。searchProperties パラメータには、検索するセッションの追加情報を指定します。

最後の searchProperties パラメータは、ゲーム固有のセッション情報を表すもので、これによって検索するゲームを特定することができます。たとえば、同一のゲームでもいくつかのセッションに種類を分けたい場合があるでしょう。個人戦やチームデスマッチなどのゲームモードでセッションを分けることができます。この方法については後述するので、この場では null を指定しましょう。

ここで気になるのは、パラメータに探すゲームの情報を指定しないことです。当然、このメソッドは、公開されているゲームのセッションすべてを網羅して検索するわけではありません。別の誰かが作った見ず知らずのゲームのセッションを検索することに意味はありませんし、接続することも想定していないでしょう。では、どうやって自分が作ったゲームのセッションだけを探せるのでしょうか。

答えは、コンパイルして作られたバイナリに含まれている GUID (Global Unique Identifier) にあります。GUID とは 128 ビットで構成される整数のことで、乱数によって識別用に生成される値です。GUID は、基本的には乱数によって作られるため、絶対に一意であることを保証するものではありませんが、非常に大きな値なので、他の値と衝突する可能性は極めて低いです。.NET のアセンブリには GuidAttribute 属性によって GUID を設定することができ、Find() メソッドは、ゲームのアセンブリに設定されている GUID に一致するセッションを検索します。

GUID を設定するには、プロジェクトの Properties フォルダ内にある AssemblyInfo.cs ファイルを開いて、次の行を編集します。

[assembly: Guid("c50ced0a-d11f-4128-867d-e2cd4d7d68f2")]

これは、アセンブリに対して GuidAttribute 属性を指定しています。この属性のパラメータに、アセンブリに設定する GUID を指定してください。「Create Copy of Project for ...」でプロジェクトを複製した場合は GUID も一致するため、この作業は必要ありません。

Xbox 360 の XNA Game Studio Connect による配置は、GUID によってゲームを識別します。よって、同じタイトルのゲームであっても GUID が異なっていれば配置することができます。逆に、異なるタイトル、異なるプロジェクトであっても GUID が同じであれば、配置時に元のゲームを上書きします。

通常の XNA Framework ゲームでは、ホストと接続するゲームを個別に開発するようなことはありません。セッションを検索する処理と、ホストを生成する処理を分離して、同じゲームの中で選択できるようにするべきです。しかし、Windows 用のゲームと Xbox 360 のゲームを異なるプロジェクトとして開発してセッションを共有したい場合は、それぞれのアセンブリに設定する GUID を一致させる必要があります。

Find() メソッドは、見つかったセッションのコレクションを表す Microsoft.Xna.Framework.Net.AvailableNetworkSessionCollection クラスのオブジェクトを返します。

■ Microsoft.Xna.Framework.Net.AvailableNetworkSessionCollection クラス

public sealed class AvailableNetworkSessionCollection : ReadOnlyCollection<AvailableNetworkSession>, IDisposable

このクラスは、ReadOnlyCollection を継承する任意の数の Microsoft.Xna.Framework.Net.AvailableNetworkSession クラスのオブジェクトを読み取り専用の要素として提供するコレクションです。このコレクションから取得する AvailableNetworkSession オブジェクトから、接続可能なセッションの情報を得られます。

■ Microsoft.Xna.Framework.Net.AvailableNetworkSession クラス

public sealed class AvailableNetworkSession

たとえば、セッションに接続する前に、そのセッションに何人が接続しているのか、最大で何人まで接続できるのかといった情報が必要になるでしょう。セッションに参加しているプレイヤーの数は CurrentGamerCount プロパティから取得できます。

■ AvailableNetworkSession クラス CurrentGamerCount プロパティ

public int CurrentGamerCount { get; }

このプロパティは、セッションに参加しているプレイヤー数を表す整数を返します。

同じように、LIVE 対戦などで外部からセッションに参加可能な人数は OpenPublicGamerSlots プロパティから取得できます。

■ AvailableNetworkSession クラス OpenPublicGamerSlots プロパティ

public int OpenPublicGamerSlots { get; }

この人数には、ホストのローカルなプレイヤーや招待用に確保されている参加人数は含まれていません。

セッションを作成したホストのゲーマータグは HostGamertag プロパティから取得できます。 セッション選択画面などで、ホストの情報を提供したい場合などに利用できます。

■ AvailableNetworkSession クラス HostGamertag プロパティ

public string HostGamertag { get; }

Find() メソッドから得られた AvailableNetworkSessionCollection オブジェクトが要素として保有する AvailableNetworkSession オブジェクトを foreach 文などで順に処理し、画面に参加可能なセッションの情報を描画して、どのセッションに参加するかをプレイヤーに選択させるといった方法が一般的な形になるでしょう。セッションへの参加方法は後述するので、まずは Sample01 で作成したセッションを検索し、正しく見つかるかどうかを調べてみましょう。 セッションの検索は GUID によって選別されるため Sample02 に設定する GUID を Sample01 と同じ値にしてください。

■ Sample02

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Net;
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 AvailableNetworkSessionCollection sessions;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string text;

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

    protected override void Initialize()
    {
        text = "";
        sprite = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        font = Content.Load<SpriteFont>("Content/TestFont");
        base.LoadContent();
    }

    protected override void UnloadContent()
    {
        Content.Unload();
        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.Back == ButtonState.Pressed) Exit();

        if (SignedInGamer.SignedInGamers.Count != 0)
        {
            if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
            {
                sessions = NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
            }

            text = "X: Find session\nBack: Exit\n\n";
            if (sessions != null)
            {
                text += "Search results=" + sessions.Count + "\n";
                foreach (AvailableNetworkSession session in sessions)
                {
                    text += session.HostGamertag + " ";
                    text += "Join=" + session.CurrentGamerCount + ", Slots=" + session.OpenPublicGamerSlots + "\n";
                }
            }
        }
        else
        {
            text = "Please sing in.";
            if (!Guide.IsVisible) Guide.ShowSignIn(1, false);
        }

        base.Update(gameTime);
    }

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

■ 実行結果

実行結果

Sample02 は、Find() メソッドでセッションを検索し、取得した AvailableNetworkSessionCollection オブジェクトから、見つかったセッションの情報を列挙します。セッションの情報は、ホストのゲーマータグ、現在の参加者数、参加可能な人数が表示されます。Sample01 を、別の Windows PC か、Xbox 360 で実行し、セッションを作成している状態で Sample02 を実行してください。セッションが見つからない場合は、ネットワークに問題がないか、ホストと GUID が一致しているか、セッションの Update() メソッドが実行されているかなどを調べてください。

セッションへの参加

他のゲームに接続してデータを交換するには、ゲームが互いにセッションに参加していなければなりません。ホスト側は、セッションを作成した後、他のプレイヤーがセッションに接続してくるのを待つだけとなります。ホスト以外のゲームは Find() メソッドで検索した結果から、目的のセッションに参加しなければなりません。セッションへの参加には Join() メソッドを使います。

■ NetworkSession クラス Join() メソッド

public static NetworkSession Join (
         AvailableNetworkSession availableSession
)

availableSession パラメータには、接続するセッションを表す AvailableNetworkSession  オブジェクトを指定します。このオブジェクトは、Find() メソッドが返した AvailableNetworkSessionCollection オブジェクトが持つ要素の中から選択することになるでしょう。

これだけで、セッションへの参加処理は終わりです。Join() メソッドは、接続したセッションの情報を表す NetworkSession オブジェクトを返します。このオブジェクトは、基本的にホストが Create() メソッドで生成した NetworkSession と同じですが、Join() メソッドから得られたオブジェクトにはホストの権限がありません。細かいセッションの制御などにはホストの権限が必要であり、参加者側の NetworkSession オブジェクトでは一部の機能が使えません。

NetworkSession オブジェクトは、現在のセッションの参加者全体の情報を返す AllGamers プロパティを公開しています。

■ NetworkSession クラス AllGamers プロパティ

public GamerCollection<NetworkGamer> AllGamers { get; }

このプロパティは、Microsoft.Xna.Framework.Net.NetworkGamer クラスのオブジェクトを管理する GamerCollection オブジェクトを返します。このコレクションから、セッションに接続しているプレイヤーのゲーマー情報を得られます。

■ Microsoft.Xna.Framework.Net.NetworkGamer クラス

public class NetworkGamer : Gamer

NetworkGamer クラスは、ネットワークを介して接続しているゲーマー情報を提供する Gamer の派生クラスです。プログラムは、このオブジェクトを用いてセッションに参加しているプレイヤーを識別できます。

対象のプレイヤーがホストとして機能しているかどうかは IsHost プロパティから取得します。ここから、誰がホストなのかを判断することができます。

■ NetworkGamer クラス IsHost プロパティ

public bool IsHost { get; }

このプロパティの結果が true であればホスト、false であれば参加者ということになります。

AllGamers プロパティが返したコレクションには、セッションに参加している自分自身も含まれています。対象のプレイヤーが、実行中のゲームにサインインしているローカルなプレイヤーなのか、またはネットワーク上のプレイヤーなのかは IsLocal プロパティから取得できます。

■ NetworkGamer クラス IsLocal プロパティ

public bool IsLocal { get; }

このプロパティの結果が true であればローカル、そうでなければネットワークを介して接続しているプレイヤーということになります。

ホストを検索することが目的であれば、セッションに参加しているプレイヤー全員を IsHost プロパティで調べる必要はありません。NetworkSession クラスは、セッションを開いたホストの NetworkGamer オブジェクトを返す Host プロパティを提供しています。

■ NetworkSession クラス Host プロパティ

public NetworkGamer Host { get; }

実行しているゲームが保有しているセッションがホストなのかどうかは、NetworkSession クラスの IsHost プロパティで調べられます。後述する一部の機能は、ホストのセッションでなければ実行できないものもあるため、セッションがホストであるかどうかを調べることは重要になります。

■ NetworkSession クラス IsHost プロパティ

public bool IsHost { get; }

セッションがホストであれば true、そうでなければ false が返されます。よって、この NetworkSession オブジェクトが Create() メソッドによって作られたのであれば true となり、Join() メソッドから得られたものであれば false となるでしょう。

複数のプレイヤーを管理するときに、各プレイヤーのゲームの状態を表すオブジェクトと NetworkGamer オブジェクトを関連付ける必要があります。Gamer クラスは、開発者が自由にオブジェクトを設定できる Tag プロパティを提供しているので、プレイヤーに関連付ける必要があるデータは Tag プロパティに設定します。

■ Gamer クラス Tag プロパティ

public Object Tag { get; set; }

このプロパティには、任意のオブジェクトを設定できます。

■ Sample03

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Net;
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 NetworkSession session;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string text;

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

    protected override void Initialize()
    {
        text = "";
        sprite = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        font = Content.Load<SpriteFont>("Content/TestFont");
        base.LoadContent();
    }

    protected override void UnloadContent()
    {
        Content.Unload();
        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.Back == ButtonState.Pressed) Exit();

        if (SignedInGamer.SignedInGamers.Count != 0)
        {
            if (session == null) UpdateMenu(state);
            else UpdateSession(state);
        }
        else
        {
            if (session != null)
            {
                session.Dispose();
                session = null;
            }
            text = "Please sing in.";
            if (!Guide.IsVisible) Guide.ShowSignIn(1, false);
        }

        base.Update(gameTime);
    }

    private void UpdateMenu(GamePadState state)
    {
        text = "A: Create session\nX: Find session\nBack: Exit\n\nSession is null.";
        if (!Guide.IsVisible && state.Buttons.A == ButtonState.Pressed)
        {
            session = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16);
        }
        else if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
        {
            AvailableNetworkSessionCollection sessions =
                NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
            if (sessions.Count == 0)
                Guide.BeginShowMessageBox(
                    PlayerIndex.One, "Search results", "Network session was not found.",
                    new string[] { "OK" }, 0, MessageBoxIcon.Alert, null, null
                );
            else session = NetworkSession.Join(sessions[0]);
        }
    }
    private void UpdateSession(GamePadState state)
    {
        text = "B: Dispose session\nBack: Exit\n\nSession is running.\n";
        session.Update();

        foreach (NetworkGamer gamer in session.AllGamers)
        {
            text += gamer.Gamertag + " " +
                (gamer.IsHost ? "Host" : "Join") + " " +
                (gamer.IsLocal ? "Local" : "Network") + "\n";
        }

        if (!Guide.IsVisible && state.Buttons.B == ButtonState.Pressed)
        {
            session.Dispose();
            session = null;
        }
    }

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

■ 実行結果

実行結果

Sample03 は、A ボタンを押すとホストとしてセッションを作成し、X ボタンを押すとセッションを検索して参加します。多くのゲームは、このように同一のゲームでセッションの作成と参加の両方をサポートします。Sample01 と Sample02 のように、ホスト用のゲームと参加用のゲームを分離することもできますが、そのようなケースは稀でしょう。

このプログラムでは、X ボタンを押してセッションを検索し、得られた AvailableNetworkSessionCollection クラスの最初の要素となっているセッションに参加します。本来であれば、参加可能なセッションの一覧を表示し、プレイヤーに選択させるべきですが、コードが長くなるのでこの場では割愛させていただきました。

セッションの状態

セッションには、3 つの状態が定められています。セッションがホストによって作られると、他のプレイヤーの参加を待機するロビーと呼ばれる状態に設定されています。他のプレイヤーがセッションに参加し、対戦などの準備が整えばゲームを開始します。ゲームを開始すると、セッションはプレイ状態となります。通常、ゲームがプレイ中の場合、セッションの検索や参加はできなくなります。そして、ゲームが終了するとロビーの状態に戻るか、もしくは終了状態となります。セッションを終了させると、すべての接続が解除されます。

このようなセッションの状態は SessionState プロパティから取得します。

■ NetworkSession クラス SessionState プロパティ

public NetworkSessionState SessionState { get; }

このプロパティは、セッションの状態を表す Microsoft.Xna.Framework.Net.NetworkSessionState 列挙型のいずれかのメンバを返します。

■ Microsoft.Xna.Framework.Net.NetworkSessionState 列挙型

public enum NetworkSessionState

この列挙型には、ゲームへの参加を待機する Lobby メンバ、プレイ中であることを表す Playing メンバ、セッションが終了していることを表す Ended メンバを定義しています。対戦ゲームを作る時には、Lobby 状態のときに他のプレイヤーの参加を待機するロビー画面を作成し、接続してきたプレイヤーとの対戦準備が整った時点で Playing に移行するという形になるでしょう。ホストがゲームを終了させたり、Dispose() メソッドを呼び出してセッションを削除すると、ゲームは Ended 状態になります。

セッションの状態を Lobby から Playing に移行するには StartGame() メソッドを呼び出します。このメソッドは、ホストのみが実行できるもので、ホストではない NetworkSession で StartGame() を呼び出すと例外が発生します。

■ NetworkSession クラス StartGame() メソッド

public void StartGame ()

Playing 状態となったセッションは、Find() メソッドで見つけることができなくなります。対戦が終了してセッションを再び Lobby 状態に戻すには EndGame() メソッドを呼び出します。

■ NetworkSession クラス EndGame() メソッド

public void EndGame ()

このメソッドもまた、ホスト以外が呼び出すことはできません。

デフォルトの設定では、Playing 状態のセッションは他のゲームから検索されなくなり、ゲームへの途中参加はできません。テーブルゲームなど、途中参加が難しいルールのゲームに適していますが、First Person Shooting 系のチームバトルのような形式であれば、途中参加を認めることも珍しくありません。プレイ中の途中参加を可能にする場合は、AllowJoinInProgress プロパティを設定します。

■ NetworkSession クラス AllowJoinInProgress プロパティ

public bool AllowJoinInProgress { get; set; }

このプロパティの値を変更できるのは、ホストのみです。このプロパティが true であれば途中参加が認められ、Playing の状態でも検索や参加ができるようになります。

セッションに参加しているプレイヤーは、自らの NetworkGamer オブジェクトの IsReady プロパティを設定することで、準備が整ったかどうかを知らせることができます。ホストは、参加者の IsReady プロパティを調べ、全員の準備が整っていれば StartGame() で対戦を開始するといった流れになります。

■ NetworkGamer クラス IsReady プロパティ

public bool IsReady { get; set; }

このプロパティの値が true であれば、参加者の準備が整ったことを表します。このプロパティは、読み書き可能ですが、ネットワーク上の他のプレイヤーのインスタンスの値を書き換えることはできません。ローカルの NetworkGamer オブジェクト以外に IsReady を設定しようとすると例外が発生します。プロパティに設定できるのは IsLocal プロパティが true のオブジェクトのみとなります。セッションが Playing 状態から Lobby 状態に移行すると、自動的に参加者全員の IsReady が false に設定されます。

すべての参加者の準備が整っているかどうかは NetworkSession クラスの IsEveryoneReady を使うと便利です。

■ NetworkSession クラス IsEveryoneReady プロパティ

public bool IsEveryoneReady { get; }

すべての参加者の IsReady が true であれば、このプロパティは true を返します。もちろん、参加者の状態を無視してホストが強制的にゲームを開始することも可能です。

■ Sample04

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Net;
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 NetworkSession session;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string text;

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

    protected override void Initialize()
    {
        text = "";
        sprite = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        font = Content.Load<SpriteFont>("Content/TestFont");
        base.LoadContent();
    }

    protected override void UnloadContent()
    {
        Content.Unload();
        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.Back == ButtonState.Pressed) Exit();

        if (SignedInGamer.SignedInGamers.Count != 0)
        {
            if (session == null) UpdateMenu(state);
            else UpdateSession(state);
        }
        else
        {
            if (session != null)
            {
                session.Dispose();
                session = null;
            }
            text = "Please sing in.";
            if (!Guide.IsVisible) Guide.ShowSignIn(1, false);
        }

        base.Update(gameTime);
    }

    private void UpdateMenu(GamePadState state)
    {
        text = "A: Create session\nX: Find session\nBack: Exit\n\nSession is null.";

        if (!Guide.IsVisible && state.Buttons.A == ButtonState.Pressed)
        {
            session = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16);
        }
        else if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
        {
            AvailableNetworkSessionCollection sessions =
                NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
            if (sessions.Count == 0)
                Guide.BeginShowMessageBox(
                    PlayerIndex.One, "Search results", "Network session was not found.",
                    new string[] { "OK" }, 0, MessageBoxIcon.Alert, null, null
                );
            else session = NetworkSession.Join(sessions[0]);
        }
    }
    private void UpdateSession(GamePadState state)
    {
        if (session.IsHost)
        {
            if (session.SessionState == NetworkSessionState.Lobby)
                text = "Start: Game start\n";
            else text = "X: Game end\n";
        }
        else text = "Please wait until a host changes state of this session.\n";

        text += "Y: Ready\nB: Dispose session\nBack: Exit\n\nSession state: " + session.SessionState + "\n";

        session.Update();

        if (session.IsEveryoneReady) text += "Everyone Ready!!\n";
        foreach (NetworkGamer gamer in session.AllGamers)
        {
            text += gamer.Gamertag + " " +
                (gamer.IsHost ? "Host" : "Join") + " " +
                (gamer.IsLocal ? "Local" : "Network") + " " + 
                (gamer.IsReady ? "Ready!!" : "...") + "\n";
        }

        if (!Guide.IsVisible && state.Buttons.B == ButtonState.Pressed)
        {
            session.Dispose();
            session = null;
        }
        else if (!Guide.IsVisible &&
            session.SessionState == NetworkSessionState.Lobby &&
            state.Buttons.Y == ButtonState.Pressed)
        {
            foreach (NetworkGamer gamer in session.AllGamers)
                if (gamer.IsLocal) gamer.IsReady = true;
        }
        else if (!Guide.IsVisible && session.IsHost)
        {
            if (session.SessionState == NetworkSessionState.Lobby &&
                state.Buttons.Start == ButtonState.Pressed) session.StartGame();
            else if (session.SessionState == NetworkSessionState.Playing &&
                state.Buttons.X == ButtonState.Pressed) session.EndGame();
        }
    }

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

■ 実行結果

実行結果

Sample04 は、セッションに参加すると、セッションと参加者の状態が列挙されます。セッションが Lobby 状態の場合は検索で見つけることができますが、Playing 状態の場合は見つけられないことを確認してください。ただし、AllowJoinInProgress プロパティを true に設定すると、Playing 状態でも検索で見つかるようになります。また、セッション参加者は Y ボタンでゲームの準備が整ったことを報告できます。

セッション イベント

セッションに誰かが参加したり、または誰かがセッションを抜け出したり、セッションの状態が変更されたタイミングで何らかの初期化処理や、終了処理を実行したいことがあるでしょう。他のプレイヤーの参加や退室、セッションの状態の変更などは、NetworkSession クラスが公開しているイベントから知ることができます。

セッションにプレイヤーが参加すると GamerJoined イベントに登録されているデリゲートが呼び出されます。

■ NetworkSession クラス GamerJoined イベント

public event EventHandler<GamerJoinedEventArgs> GamerJoined

このイベントのデリゲートは、パラメータに GamerJoinedEventArgs クラスのオブジェクトを受け取ります。このクラスは、セッションに参加したプレイヤーの情報を提供します。

■ Microsoft.Xna.Framework.Net.GamerJoinedEventArgs クラス

public class GamerJoinedEventArgs : EventArgs

このクラスは、セッションに参加してきたプレイヤーの NetworkGamer オブジェクトを返す Gamer プロパティを公開しています。

■ GamerJoinedEventArgs クラス Gamer プロパティ

public NetworkGamer Gamer { get; }

プレイヤーが参加してきた時点で、ホストや他のプレイヤーとのデータの初期化処理を一度行いたい場合などに、このイベントを利用します。

プレイヤーがセッションから抜け出すと、GamerLeft イベントが発生します。基本的な構造は GamerJoined イベントと同じです。

■ NetworkSession クラス GamerLeft イベント

public event EventHandler<GamerLeftEventArgs> GamerLeft

このイベントは、GamerLeftEventArgs クラスのオブジェクトを情報として受け取ります。型は異なりますが、構造は GamerJoinedEventArgs クラスと同じなので詳細は割愛します。Gamer プロパティから、セッションから抜けたプレイヤーの情報を得ることができます。

セッションの状態が Lobby から Playing に移行すると GameStarted イベントが発生し、逆に、Playing から Lobby に移行すると GameEnded イベントが発生します。

■ NetworkSession クラス GameStarted イベント

public event EventHandler<GameStartedEventArgs> GameStarted

■ NetworkSession クラス GameEnded イベント

public event EventHandler<GameEndedEventArgs> GameEnded

これらのイベントに登録するデリゲートが受け取る GameStartedEventArgs や GameEndedEventArgs 型のオブジェクトは、特にプロパティを持たないため詳細は割愛します。

これらのイベントを用いることで、ゲーム開始や終了時に何らかの処理を実行できます。ロビーからゲーム画面への移行処理、データの生成、通信など、ゲーム開始前に実行する処理は、デリゲートを GameStarted イベントに登録してください。同じように、ゲーム終了時のデータの解放やロビーへの移行処理などに GameEnded イベントを使えるでしょう。

ホストがセッションを終了させると SessionEnded イベントが発生します。ホストがゲーム中に突然セッションを終了させたり、ネットワークの障害でセッションが終了するといった可能性もあるため、セッション終了時に何らかの処理を実行してゲーム中からでも終了処理を実行して元の画面に復帰できるように仕掛ける必要があります。

■ NetworkSession クラス SessionEnded イベント

public event EventHandler<NetworkSessionEndedEventArgs> SessionEnded

このイベントに登録するデリゲートは Microsoft.Xna.Framework.Net.GameStartedEventArgs クラスのオブジェクトを受け取ります。

■ Microsoft.Xna.Framework.Net.GameStartedEventArgs クラス

public class GameStartedEventArgs : EventArgs

このクラスは、セッションが終了した理由を表す EndReason プロパティを提供しています。

■ GameStartedEventArgs クラス EndReason プロパティ

public NetworkSessionEndReason EndReason { get; }

セッションが終了した理由は、Microsoft.Xna.Framework.Net.NetworkSessionEndReason 列挙型のメンバによって表されます。

■ Microsoft.Xna.Framework.Net.NetworkSessionEndReason 列挙型

public enum NetworkSessionEndReason

この列挙型には、セッションに参加しているゲームがセッションからサインアウトしたことを表す ClientSignedOut メンバ、ホストがセッションを終了させたことを表す HostEndedSession メンバ、ホストによってセッションから解除されたことを表す RemovedByHost メンバ、ネットワークの障害によってセッションが終了したことを表す Disconnected メンバが定義されています。

■ Sample05

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Net;
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 NetworkSession session;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string commandText, eventText;

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

    protected override void Initialize()
    {
        commandText = "";
        eventText = "";
        sprite = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        font = Content.Load<SpriteFont>("Content/TestFont");
        base.LoadContent();
    }

    protected override void UnloadContent()
    {
        Content.Unload();
        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.Back == ButtonState.Pressed) Exit();

        if (Guide.IsVisible)
        {
            base.Update(gameTime);
            return;
        }

        if (SignedInGamer.SignedInGamers.Count != 0)
        {
            if (session == null) UpdateMenu(state);
            else UpdateSession(state);
        }
        else
        {
            if (session != null)
            {
                session.Dispose();
                session = null;
            }
            commandText = "Please sing in.";
            if (!Guide.IsVisible) Guide.ShowSignIn(1, false);
        }

        base.Update(gameTime);
    }

    private void InitSession(NetworkSession session)
    {
        session.GamerJoined += delegate(object sender, GamerJoinedEventArgs e)
        {
            eventText += "Joined gamer: " + e.Gamer.Gamertag + "\n";
        };
        session.GamerLeft += delegate(object sender, GamerLeftEventArgs e)
        {
            eventText += "Left gamer: " + e.Gamer.Gamertag + "\n";
        };
        session.GameStarted += delegate(object sender, GameStartedEventArgs e)
        {
            eventText += "Game start" + "\n";
        };
        session.GameEnded += delegate(object sender, GameEndedEventArgs e)
        {
            eventText += "Game end" + "\n";
        };
        session.SessionEnded += delegate(object sender, NetworkSessionEndedEventArgs e)
        {
            eventText += "Session ended: " + e.EndReason + "\n";
        };
    }

    private void UpdateMenu(GamePadState state)
    {
        commandText = "A: Create session\nX: Find session\nBack: Exit";

        if (!Guide.IsVisible && state.Buttons.A == ButtonState.Pressed)
        {
            session = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16);
            InitSession(session);
        }
        else if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
        {
            AvailableNetworkSessionCollection sessions =
                NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
            if (sessions.Count == 0)
                Guide.BeginShowMessageBox(
                    PlayerIndex.One, "Search results", "Network session was not found.",
                    new string[] { "OK" }, 0, MessageBoxIcon.Alert, null, null
                );
            else
            {
                session = NetworkSession.Join(sessions[0]);
                InitSession(session);
            }
        }
    }
    private void UpdateSession(GamePadState state)
    {
        if (session.IsHost)
        {
            if (session.SessionState == NetworkSessionState.Lobby)
                commandText = "Start: Game start\n";
            else commandText = "X: Game end\n";
        }
        else commandText = "Please wait until a host changes state of this session.\n";
        commandText += "B: Dispose session\nBack: Exit\n";
        commandText += "Session state: " + session.SessionState;

        session.Update();
        if (!Guide.IsVisible && state.Buttons.B == ButtonState.Pressed)
        {
            session.Dispose();
            session = null;
        }
        else if (!Guide.IsVisible && session.IsHost)
        {
            if (session.SessionState == NetworkSessionState.Lobby &&
                state.Buttons.Start == ButtonState.Pressed)
                session.StartGame();
            else if (session.SessionState == NetworkSessionState.Playing &&
                state.Buttons.X == ButtonState.Pressed) 
                session.EndGame();
        }
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);
        sprite.Begin();
        sprite.DrawString(font, commandText, Vector2.Zero, Color.Black);
        sprite.DrawString(font, eventText, new Vector2(0, 100), Color.Black);
        sprite.End();
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample05 の InitSession() メソッドに注目してください。このメソッドでは、パラメータから受け取ったセッションの各イベントにデリゲートを登録し、各イベント発生時に eventText 変数にイベントの発生情報を加えています。ホストを作成し、他のゲームからセッションに参加してイベントを発生させてください。他のプレイヤーが参加すると GamerJoined イベントが、退室すると GamerLeft イベントが発生することを確認できます。

データの送信

セッションを作成し、他のプレイヤーが参加している状態であれば、互いにデータを送受信することができます。データの送受信は、セッションの状態に関係なく行うことができます。特定の参加者にだけデータを送ることも、参加者全員にデータを送ることも可能です。通信は、信頼性のない UDP で行われ、データはパケット単位で送られます。しかし、XNA Framework がデータの信頼性を保証してくれるため、伝送順序やデータの到達が重要なデータでも、特別な処理を施す必要はありません。

データの送受信が行えるのは、ローカルでセッションに参加しているプレイヤーです。ローカルでセッションに参加しているプレイヤーは NetworkSession クラスの LocalGamers プロパティから取得できます。

■ NetworkSession クラス LocalGamers プロパティ

public GamerCollection<LocalNetworkGamer> LocalGamers { get; }

このプロパティが返す GamerCollection オブジェクトの要素は、セッションに参加しているローカルプレイヤーを表す Microsoft.Xna.Framework.Net.LocalNetworkGamer クラスのオブジェクトです。データの送受信を行うには、このクラスのオブジェクトが必要になります。 

■ Microsoft.Xna.Framework.Net.LocalNetworkGamer クラス

public sealed class LocalNetworkGamer : NetworkGamer

LocalNetworkGamer クラスは、NetworkGamer クラスを継承し、データの送受信を行う機能を追加しています。

データを送信するには、データを送信するプレイヤーを表す LocalNetworkGamer オブジェクトの SendData() メソッドを使います。このメソッドは、送信手段に応じていくつかにオーバーロードされています。もっとも単純な送信方法は、バイト配列を送信する次のメソッドを使うことです。

■ LocalNetworkGamer クラス SendData() メソッド

public void SendData (
         byte[] data,
         SendDataOptions options
)

このメソッドは、指定されたデータをローカルプレイヤーを含める、セッションの参加者全員に送信します。data には、送信するデータのバイト配列を指定します。options には、送信するデータの信頼性に関連したオプションを表す Microsoft.Xna.Framework.Net.SendDataOptions 列挙型のいずれかのメンバを指定します。

■ Microsoft.Xna.Framework.Net.SendDataOptions 列挙型

[FlagsAttribute]
public enum SendDataOptions

信頼性のない UDP プロトコルによる本来のパケットの送信は、伝送経路を固定しないため、パケットの到着順序が保証されません。しかし、下位のパケット単位のデータ送受信は XNA Framework が管理してくれます。SendDataOptions 列挙型は、データの送信に信頼性や順序を保証するべきかどうかを指定するメンバを定義しています。

もっとも伝送効率の良いオプションは、信頼性や順序を保証しないことを表す None メンバによる送信です。この場合、パケットが喪失する可能性があり、パケットの到着順序が送信順とは異なることもありますが、データの再送が行われないためネットワークの使用は最小限に留められます。

データを必ず届けなければならない場合には Reliable メンバを使います。このメンバを用いたデータの送信は、到着順序は保証されませんが、パケットが失われることはありません。

データの順序を保証するには InOrder メンバを使います。このメンバによるデータの送信では、必ずデータを送信した順番で相手に到着します。ただし、パケットが失われる可能性はあります。

信頼性と順序の双方を保証するには ReliableInOrder メンバを使用します。このメンバによる通信は、パケットが正しく到着するまで再送するため、最もネットワークに負荷をかけます。信頼性を維持し、送信順で必ず相手にデータを届けたい場合に利用します。

システム上、重要になるデータは信頼性を維持して確実にデータを届け、その後のデータの更新には負荷の低い None または Reliable を使って通信する方法が考えられます。

データの受信

データを受信するには ReceiveData() メソッドを使います。このメソッドも、受信する方法に応じてオーバーロードされていますが、もっとも簡単な方法はバイト配列としてデータを受信する次のメソッドを使うことです。セッション参加者全員にデータを送信した場合は、自分自身にもデータが届くので注意してください。

■ LocalNetworkGamer クラス ReceiveData() メソッド

public int ReceiveData (
         byte[] data,
         out NetworkGamer sender
)

data パラメータには、受信するデータを格納するバイト配列、sender 出力パラメータには、送信者を表す NetworkGamer を保存するための変数を指定します。バイト配列は、十分なサイズが確保されていなければなりません。メソッドは、読み込んだデータのバイト数を返します。

受信するデータが届いているかどうかは、IsDataAvailable プロパティから取得できます。

■ LocalNetworkGamer クラス IsDataAvailable プロパティ

public bool IsDataAvailable { get; }

受信するデータが存在する場合は true、そうでなければ false を返します。基本的な流れは Update() メソッド内で IsDataAvailable プロパティを調べ、受信しているデータが存在すれば ReceiveData() メソッドでデータを取得するという形になるでしょう。

書き込みと読み込みのサポート

バイト配列としてデータを送受信できるため、直列化できればあらゆるデータやオブジェクトをネットワークを介してやり取りすることができます。しかし、文字列や整数など、一般的なデータ型もすべてバイト配列に変換し、受信側では復元しなければなりません。幸い、この面倒な作業を補ってくれるクラスが用意されています。

データの送信には Microsoft.Xna.Framework.Net.PacketWriter クラスを使うことができます。このクラスは System.IO 名前空間の BinaryWriter クラスを継承したもので、.NET Framework の基礎的なデータ型の書き込みに加えて、XNA Framework の基礎的なデータ型の書き込みに対応しています。

■ Microsoft.Xna.Framework.Net.PacketWriter クラス

public class PacketWriter : BinaryWriter

PacketWriter クラスには、BinaryWriter クラスで実装されている Write() メソッドに加えて Matrix 構造体や Vector2 構造体、Vector3 構造体などを受け取る Write() メソッドがオーバーロードされています。

PacketWriter オブジェクトをパラメータに受け取る SendData() メソッドが用意されているので、データ送信時には必要なデータを書き込んだ PacketWriter オブジェクトを渡します。

■ LocalNetworkGamer クラス SendData() メソッド

public void SendData (
         PacketWriter data,
         SendDataOptions options
)

これで、書き込んだ順番でデータが相手に送信されます。データを送信した時点で、PacketWriter の内容は自動的にクリアされるため、オブジェクトを別の送信用に再利用することが許されています。他のプレイヤーにデータを送信するために用いても問題ありません。

PacketWriter で送信したデータを受信するには、Microsoft.Xna.Framework.Net.PacketReader クラスを使うと便利です。

■ Microsoft.Xna.Framework.Net.PacketReader クラス

public class PacketReader : BinaryReader

このクラスは、System.IO 名前空間の BinaryReader クラスを継承している PacketWriter に対となる読み取り用の機能を提供します。PackWriter クラスと同様に、BinaryReader クラスの機能に加えて、XNA Framework の基本的なデータ型を読み込む Read〜() メソッドを持ちます。

受信したデータを PacketReader クラスから受け取るには、PacketReader オブジェクトをパラメータとして受け取る ReceiveData() メソッドを使います。

■ LocalNetworkGamer クラス ReceiveData() メソッド

public int ReceiveData (
         PacketReader data,
         out NetworkGamer sender
)

このメソッドは、data パラメータに渡された PacketReader オブジェクトに受信したパケットのデータをコピーします。開発者は、このメソッドの後に PacketReader オブジェクトの Read〜() メソッドから任意のデータ型を読み込むことができます。

PacketReader オブジェクトも、PacketWriter と同様に再利用可能です。これらのオブジェクトは、起動時に 1 度だけインスタンス化し、1 つのインスタンスを様々な通信で共有するという利用が一般的でしょう。通信を行うたびにインスタンス化するよりも、1 つのインスタンスを再利用した方が効率的です。

■ Sample06

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Net;
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 NetworkSession session;
    private SpriteBatch sprite;
    private SpriteFont font;
    private PacketWriter writer;
    private PacketReader reader;
    private string commandText, messageText;

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

    protected override void Initialize()
    {
        writer = new PacketWriter();
        reader = new PacketReader();
        commandText = "";
        messageText = "";
        sprite = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        font = Content.Load<SpriteFont>("Content/TestFont");
        base.LoadContent();
    }

    protected override void UnloadContent()
    {
        Content.Unload();
        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.Back == ButtonState.Pressed) Exit();

        if (SignedInGamer.SignedInGamers.Count != 0)
        {
            if (session == null) UpdateMenu(state);
            else UpdateSession(state);
        }
        else
        {
            if (session != null)
            {
                session.Dispose();
                session = null;
            }
            commandText = "Please sing in.";
            if (!Guide.IsVisible) Guide.ShowSignIn(1, false);
        }

        base.Update(gameTime);
    }

    private void UpdateMenu(GamePadState state)
    {
        commandText = "A: Create session\nX: Find session\nBack: Exit";

        if (!Guide.IsVisible && state.Buttons.A == ButtonState.Pressed)
        {
            session = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16);
        }
        else if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
        {
            AvailableNetworkSessionCollection sessions =
                NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
            if (sessions.Count == 0)
                Guide.BeginShowMessageBox(
                    PlayerIndex.One, "Search results", "Network session was not found.",
                    new string[] { "OK" }, 0, MessageBoxIcon.Alert, null, null
                );
            else
            {
                session = NetworkSession.Join(sessions[0]);
            }
        }
    }
    private void KeyboardInputCallback(IAsyncResult ar)
    {
        string message = Guide.EndShowKeyboardInput(ar);
        if (message == null) return;

        writer.Write(message);
        session.LocalGamers[0].SendData(writer, SendDataOptions.ReliableInOrder);
    }
    private void UpdateSession(GamePadState state)
    {
        commandText = "X: Send message\nB: Dispose session\nBack: Exit\n";
        foreach(NetworkGamer gamer in session.AllGamers)
            commandText += gamer.Gamertag + ", ";

        session.Update();
        if (session.LocalGamers.Count > 0 && session.LocalGamers[0].IsDataAvailable)
        {
            NetworkGamer sender;

            session.LocalGamers[0].ReceiveData(reader, out sender);
            messageText += sender.Gamertag + ">" + reader.ReadString() + "\n";
        }

        if (!Guide.IsVisible && state.Buttons.B == ButtonState.Pressed)
        {
            session.Dispose();
            session = null;
        }
        else if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
        {
            Guide.BeginShowKeyboardInput(PlayerIndex.One, "Input message", "Input your message.", "", KeyboardInputCallback, null);
        }
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);
        sprite.Begin();
        sprite.DrawString(font, commandText, Vector2.Zero, Color.Black);
        sprite.DrawString(font, messageText, new Vector2(0, 100), Color.Black);
        sprite.End();
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample06 は、X ボタンを押すとキーボード入力画面が表示されます。任意のテキストを入力すると、KeyboardInputCallback() メソッドが実行されます。ここでは、PacketWriter クラスのオブジェクトを用意し、入力されたテキストを書き込み、SendData() メソッドからセッションの参加者全員に送信します。

UpdateSession() メソッドでは、IsDataAvailable プロパティの値を調べ、受信しているデータがある場合は PacketReader クラスを使って文字列としてデータを読み込みます。読み込まれた文字列は messageText プロパティに追加され、画面に描画されます。互いにテキストを送ることで、簡単なチャットのような会話ができます。

通常、SendData() メソッドは、自分自身も含めたセッション参加者全員にパケットを送信しますが、特定のプレイヤーにだけデータを送信することも可能です。特定のプレイヤーを指定してデータを送信するには、次の SendData() メソッドを使います。

■ LocalNetworkGamer クラス SendData() メソッド

public void SendData (
         byte[] data,
         SendDataOptions options,
         NetworkGamer recipient
)
public void SendData (
         PacketWriter data,
         SendDataOptions options,
         NetworkGamer recipient
)

recipient パラメータに、セッションに参加しているプレイヤーを表す NetworkGamer オブジェクトを指定します。この場合、指定したプレイヤーにのみデータが送信されます。

検索パラメータ

協力プレイトと対戦プレイなど、マルチプレイの中から、さらにいくつかのゲームモードを選択できるゲームがあります。このように、同一ゲームのネットワーク機能の中でも、いくつかのゲームモードに分離してセッションを作成したい場合があるでしょう。セッションをいくつかの種類に分離するには、セッションにカスタムプロパティを設定する機能を利用します。この情報は、検索用のパラメータとして用いられます。カスタムプロパティを設定することで、Find() メソッドで指定したパラメータに一致するセッションだけを検索できるようになります。

カスタムプロパティをセッションに設定するには、オーバーロードされている次の Create() メソッドを使います。

■ NetworkSession クラス Create() メソッド

public static NetworkSession Create (
         NetworkSessionType sessionType,
         int maxLocalGamers,
         int maxGamers,
         int privateGamerSlots,
         NetworkSessionProperties sessionProperties
)

第 3 パラメータまでは、前述した Create() メソッドと同じです。加えて、privateGamerSlots パラメータに非公開の参加者数を maxGamers よりも小さな値で指定します。privateGamerSlots で確保した領域には、招待状を送って参加するプレイヤーに予約されます。通常のセッションの検索から、参加することはできません。

重要なのは、最後の sessionProperties パラメータです。このパラメータには、セッションのカスタムプロパティを保存する Microsoft.Xna.Framework.Net.NetworkSessionProperties クラスのオブジェクトを指定します。

■ Microsoft.Xna.Framework.Net.NetworkSessionProperties クラス

public class NetworkSessionProperties

このクラスは、セッションの検索に用いられる、いくつかのカスタムプロパティを提供します。名前からコレクションを想像してしまいますが NetworkSessionProperty クラスは存在しません。カスタムプロパティは、単純な整数で表現されるので、このクラスは、一種の null 可能な整数配列を提供するものだと考えてください。

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

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

public NetworkSessionProperties ()

セッションの検索に用いるカスタムプロパティは、次のインデクサから設定、または取得できます。

■ NetworkSessionProperties クラスのインデクサ

public Nullable<int> this [
         int index
] { get; set; }

index には、設定、または取得するプロパティのインデックスを指定します。インデクサは、指定したインデックスの値を返します。値が設定されていない場合は null を返します。この整数の持つ意味は、アプリケーションごとに自由に設定できます。たとえば、インデックス 0 番に難易度を表す値、インデックス 1 番にゲームモードを表す値といった形で分けることができます。

インデクサに指定する index パラメータの値が範囲外である場合は、例外が発生するので注意が必要です。インデックスの範囲は Count プロパティから取得できます。

■ NetworkSessionProperties クラス Count プロパティ

public int Count { get; }

カスタムプロパティの数は 8 に固定されています。

NetworkSessionProperties に設定する値は、インデックスに関連付けられる単純な整数にすぎません。Find() メソッドは、searchProperties パラメータに指定した NetworkSessionProperties  オブジェクトと同じ値を持つセッションを検索します。単純な整数に、どのような意味を持たせるかは任意です。

■ Sample07

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Net;
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 NetworkSession session;
    private AvailableNetworkSessionCollection sessions;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string commandText, findText;

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

    protected override void Initialize()
    {
        commandText = "";
        findText = "";
        sprite = new SpriteBatch(GraphicsDevice);
        base.Initialize();
    }

    protected override void LoadContent()
    {
        font = Content.Load<SpriteFont>("Content/TestFont");
        base.LoadContent();
    }

    protected override void UnloadContent()
    {
        Content.Unload();
        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        GamePadState state = GamePad.GetState(PlayerIndex.One);
        if (state.Buttons.Back == ButtonState.Pressed) Exit();

        if (SignedInGamer.SignedInGamers.Count != 0)
        {
            if (session == null)
            {
                UpdateMenu(state);
                if (sessions != null) UpdateFind();
            }
            else UpdateSession(state);
        }
        else
        {
            if (session != null)
            {
                session.Dispose();
                session = null;
            }
            commandText = "Please sing in.";
            if (!Guide.IsVisible) Guide.ShowSignIn(1, false);
        }

        base.Update(gameTime);
    }

    private void UpdateFind()
    {
        findText = "Search results=" + sessions.Count + "\n";
        foreach (AvailableNetworkSession item in sessions)
            findText += "sessionA: " + item.HostGamertag + "\n";
    }

    private void UpdateMenu(GamePadState state)
    {
        commandText = "A: Create sessionA\nB: Create sessionB\n" + 
            "X: Find sessionA\nY: Find sessionB\nBack: Exit\n\n";
        NetworkSessionProperties properties = new NetworkSessionProperties();

        if (!Guide.IsVisible && state.Buttons.A == ButtonState.Pressed)
        {
            properties[0] = 0;
            session = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16, 0, properties);
        }
        else if (!Guide.IsVisible && state.Buttons.B == ButtonState.Pressed)
        {
            properties[0] = 1;
            session = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16, 0, properties);
        }
        else if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
        {
            properties[0] = 0;
            sessions = NetworkSession.Find(NetworkSessionType.SystemLink, 1, properties);
        }
        else if (!Guide.IsVisible && state.Buttons.Y == ButtonState.Pressed)
        {
            properties[0] = 1;
            sessions = NetworkSession.Find(NetworkSessionType.SystemLink, 1, properties);
        }
    }
    private void UpdateSession(GamePadState state)
    {
        commandText = "Back: Exit\n\n";

        if (session.SessionProperties[0] == 0) commandText += "SessionA is running.\n";
        else if (session.SessionProperties[0] == 1) commandText += "SessionB is running.\n";
        session.Update();
    }

    protected override void Draw(GameTime gameTime)
    {
        GraphicsDevice.Clear(Color.White);
        sprite.Begin();
        sprite.DrawString(font, commandText, Vector2.Zero, Color.Black);
        sprite.DrawString(font, findText, new Vector2(0, 150), Color.Black);
        sprite.End();
        base.Draw(gameTime);
    }
}

■ 実行結果

実行結果

Sample07 は、セッションの作成や検索に利用する NetworkSessionProperties オブジェクトの 0 番インデックスに、セッションの種類を分けるための整数を代入します。A ボタンを押して作成したセッションを sessionA とし、B ボタンを押して作成したセッションを sessionB とします。実際には sessionA を 0、sessionB を 1 で表しています。

セッションを個別に検索するために、sessionA は X ボタンで、sessionB は Y ボタンで検索するものとします。異なる PC または Xbox 360 で Sample07 を複数起動し、ホスト側は A ボタンを押して sessionA を作成し、もう一方で X ボタンを押して sessionA を検索すれば発見できますが、Y ボタンを押して sessionB を検索すると見つからないことを確認してください。ホスト側で sessionB を作成すれば、sessionB を検索できます。

Sample07 では、カスタムプロパティの影響を理解するために、NetworkSessionProperties オブジェクトのインデクサに、純粋な整数リテラルを設定していますが、通常はこのようなコードを書くべきではありません。この値がどのような意味を持っているのか、整数リテラルからは判断することは難しく、コードの可読性に問題があります。

よりよい書き方は、カスタムプロパティの各設定に対応するインデックスを表す列挙型と、各設定の値を表す列挙型をそれぞれ作成し、設定する方法です。たとえば、難易度とゲームモードをカスタムプロパティに設定する場合は、次のような列挙型を作成し、メンバの値をカスタムプロパティに設定することになります。

enum SessionProperty { GameLevel, GameMode }
enum GameLevel { Easy, Normal, Hard }
enum GameMode { Cooperation, Deathmatch, TeamDeathmatch }

上記の列挙型のうち SessionProperty 列挙型は、カスタムプロパティに設定するインデックスを表すもので、例えば難易度は GameLevel メンバ、ゲームモードは GameMode メンバが表すものとします。これらのメンバの値が、直接インデックスに対応します。

プロパティとして設定する値は、さらに個別に用意します。上記の例では、難易度を表す GameLevel 列挙型と、ゲームモードを表す GameMode 列挙型を記述しています。NetworkSessionProperties  のインデクサには、これらの列挙型のメンバの値を、インデックスに対応して設定します。難易度を表す GameLevel を設定する場合は、次のようになるでしょう。

properties[(int)SessionProperty.GameLevel] = (int)GameLevel.Easy;

こうすることで、NetworkSessionProperties  オブジェクトのインデクサに指定するインデックスの意味や、設定する値の意味を理解しやすくなります。

マインスイーパのネットワーク対戦

最後に、これまで作成したマインスイーパにネットワーク対戦機能を追加しましょう。ルールは、ホストが作成したデータを参加者全員で共有し、最初にクリアできた人が勝利とします。プレイ中は、参加者全員の状態を表示して、各プレイヤーがどのくらい進んでいるかを確認できるようにするものとします。誰かがクリアするか、全員が地雷を踏んでしまうまでゲームが続きます。

ゲームはタイトル画面から始まります。タイトル画面ではセッションを作成してホストになるか、セッションを検索するかを選択します。ホストになった場合は、他の参加者を待機するロビーに移行します。検索を行った場合は、見つかったセッションのリストを画面に表示し、どのセッションに参加するかを選択してロビーに入ります。セッションの作成や検索、ロビーの作り方などは、本稿でご説明したとおりです。

ホストがゲームを開始すると、ホストはすべての参加者にゲームデータを送信します。マインスイーパのデータは、縦横のマス目の数と、各マスに地雷が配置されているかどうかで構成されています。参加者は受信したデータからホストが生成したゲームデータを再現します。これによって、参加者全員が同じゲームをプレイすることになります。データを受信した時点で、ゲームを開始します。

プレイヤーがマスを開くと、開いているマスの数を他の参加者に送信します。また、地雷を踏んでしまったり、クリアした場合も、他の参加者にそれを通知します。Update() メソッドの処理の中で、セッションがデータを受信しているかどうかを常に調べ、データを受信している場合は、データを読み込んで他の参加者の状態を更新します。地雷を踏んだ場合は、ゲームを中断して他のプレイヤーが終了するのを待ちます。誰かがゲームをクリアするか、全員が地雷を踏んでしまった時点でゲームを終了し、ロビーに戻ります。

これらの流れをゲームの状態として定義し、それぞれの状態に分離して Update() メソッドと Draw() メソッドを実装します。各々の状態を、この場では次のように呼ぶことにします。

  • Title: タイトル画面
  • Lobby: ロビー
  • Selection: セッション検索
  • Loading: データ読み込み
  • Playing: ゲームプレイ
  • Dropout: 脱落

上記の状態に分割して開発する方法は、前回の「マインスイーパの場面分割」でご説明しています。まず、上記の状態を表す列挙型を作成し、この列挙型をゲームのプロパティとして保持します。

■GameState 列挙型

public enum GameState
{
    Unknown = 0, Title, Lobby, Selection, Loading, Playing, Dropout
}

ゲームはプロパティから状態を変更すると、自動的に状態を移行させるものとします。状態の移行には、必要なインスタンスの生成や初期化などが必要になるので、各状態ごとに初期化用のメソッドを用意します。この場では Init〜() という名前に統一します。今回は、GameState プロパティの中で自動的に初期化メソッドを呼び出すようにしましょう。

■ Minesweeper クラス GameState プロパティ

public GameState GameState
{
    get { return gameState; }
    set { 
        gameState = value;
        if (value == GameState.Title) InitTitle();
        else if (value == GameState.Lobby) InitLobby();
        else if (value == GameState.Selection) InitSelection();
        else if (value == GameState.Loading) InitLoading();
        else if (value == GameState.Playing) InitPlaying();
        else if (value == GameState.Dropout) InitDropout();
    }
}

これで、ゲームの状態が変更されると自動的に初期化が行われ、場面が置き換わります。特にネットワークに対応している場合、ホストが突然落ちる、ゲームが中断されるなどの可能性が考えられるため、柔軟な状態の移行処理が求められます。

同様の設計で、各状態ごとに Update〜() メソッド、Draw〜() メソッドが用意されています。Update() メソッドと Draw() メソッドでは、GameState プロパティの値を調べ、状態に応じて適切なメソッドを呼び出すように仕組みます。こうすることで、状態ごとの Init〜() メソッド、Update〜() メソッド、Draw〜() メソッドの記述に専念できます。

データの共有

セッションの管理や、データの通信方法は前述しました。本稿で作成するマインスイーパのマルチプレイは、ホスト作成したマインスイーパのデータとなる MineField オブジェクトを参加者に送信し、全員が同じデータでゲームをプレイします。そのためには、ゲームを開始すると同時にホストがデータをすべての参加者に送信する必要があります。

データの送信には PacketWriter クラスを用います。最初に、地雷原の列数と行数を整数で書き込み、各マスに地雷が配置されているかどうかを表す boolean 型の値を順に書き込みます。この処理は、InitLoading() メソッド内で、セッションがホストの場合に実行するものとしましょう。

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

private void InitLoading()
{
    stateText = "Now Loading...";
    textColor = Color.Black;

    //ホストならばデータを生成し、送信する
    if (session.IsHost)
    {
        MineField defaultField = new MineField(10, 8);
        defaultField.Random(10);
        MineField = defaultField;

        writer.Write(defaultField.Column);  //列数
        writer.Write(defaultField.Row);     //行数
        for (int c = 0; c < defaultField.Column; c++)
        {
            for (int r = 0; r < defaultField.Row; r++)
            {
                writer.Write((defaultField[c, r] & FieldState.Mine) == FieldState.Mine);
            }
        }

        LocalNetworkGamer gamer = session.LocalGamers[0];
        gamer.SendData(writer, SendDataOptions.Reliable);
    }
}

これで、ゲーム開始直前に、すべての参加者にデータが送信されます。参加者は、データが送られて来るのを待機しなければならないため、UpdateLoading() メソッド内で NetworkGamer の IsDataAvailable プロパティを調べ、データを受信できれば、そこから列数、行数、各マスの状態を読み込み、ホストの MineField を再現します。データを正しく読み込むことができれば、ゲームの状態を Playing に移行します。

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

private void UpdateLoading(GameTime gameTime, GamePadState padState)
{
    NetworkGamer sender;
    LocalNetworkGamer localGamer = session.LocalGamers[0];

    if (session.IsHost)
    {
        //ホストは待機する必要がないので、そのままゲームへ
        GameState = GameState.Playing;
    }
    else
    {
        //受信データがあるかどうか
        if (localGamer.IsDataAvailable)
        {
            localGamer.ReceiveData(reader, out sender);
            int column = reader.ReadInt32();    //列数
            int row = reader.ReadInt32();       //行数

            //ゲームデータの作成と初期化
            MineField = new MineField(column, row);
            for (int c = 0; c < column; c++)
            {
                for (int r = 0; r < row; r++)
                {
                    if (reader.ReadBoolean())
                        mineField[c, r] = FieldState.Mine;
                }
            }

            //ゲーム開始
            GameState = GameState.Playing;
        }
    }
}

このプログラムでは、コードを簡単にするため、すべての参加者がゲームを開始するのを待機しません。ホストは、他の参加者がゲームを開始するのを待たずに、即座にゲームを開始してしまいます。システムリンクで通信を行っている場合は、大きな差は感じられないでしょう。

データの更新

プレイヤーは、マスを開いたり、地雷を踏んだり、またはゲームをクリアすると、他のプレイヤーにデータを送信します。これによって、プレイヤー全員のゲームの進行度を表示できます。このプログラムでは、通知するメッセージの意味を表す NetworkCommand 列挙型を作成し、このメンバを送信することで、プレイヤーの状態がどのように変更したのかを通知します。

■NetworkCommand 列挙型

public enum NetworkCommand
{
    Unkown = 0,     //不明
    Open,       //マスを開いた
    Dropout,   //地雷を踏んだ
    Clear       //すべてのマスを開いた
}

通信時には、列挙型のメンバを int 型に変換して送信し、受信側では int 型の値を NetworkCommand に変換して比較できます。このプログラムでは、先頭に NetworkCommand のメンバを表す値を送信し、Open メンバであれば、その後に開いたマスの数を表す整数を送るものとします。送受信するデータが複雑になる場合は、データを表す構造体と通信をサポートするメソッドを用意するとよいでしょう。

■ Minesweeper クラス UpdatePlaiying() メソッド抜粋

if (prevPadState.Buttons.A == ButtonState.Released && padState.Buttons.A == ButtonState.Pressed)
{
    if (mineField.Open())
    {
        //ゲームオーバーに移行
        GameState = GameState.Dropout;
        writer.Write((int)NetworkCommand.Dropout);
        localGamer.SendData(writer, SendDataOptions.Reliable);
    }
    else
    {
        int opened = 0; //開かれているマスの数

        for (int c = 0; c < MineField.Column; c++)
        {
            for (int r = 0; r < MineField.Row; r++)
            {
                if ((MineField[c, r] & FieldState.Opened) == FieldState.Opened) opened++;
            }
        }

        //すべてのマスが開かれたかどうか
        if (MineField.Column * MineField.Row - opened == MineField.Mines)
        {
            writer.Write((int)NetworkCommand.Clear);
            localGamer.SendData(writer, SendDataOptions.Reliable);
        }
        else
        {
            writer.Write((int)NetworkCommand.Open);
            writer.Write(opened);
            localGamer.SendData(writer, SendDataOptions.None);
        }
    }
}

データの受信処理は、UpdateGamer() という別のメソッドに分離しています。これは、Playing 状態の他に、地雷を踏んでゲームの終了を待機している Dropout 状態でも、他のプレイヤーが送信したデータを受け取る必要があるためです。よって、このメソッドは UpdatePlaying() メソッドと UpdateDropout() メソッドから呼び出されます。

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

private void UpdateGamer()
{
    LocalNetworkGamer localGamer = session.LocalGamers[0];

    //受信するデータがあるかどうか
    if (localGamer.IsDataAvailable)
    {
        NetworkGamer sender;
        localGamer.ReceiveData(reader, out sender);

        //データの種類を表す
        NetworkCommand cmd = (NetworkCommand)reader.ReadInt32();

        //送信者の最新の状態を Tag プロパティに保存
        if (cmd == NetworkCommand.Open)
        {
            sender.Tag = reader.ReadInt32();
        }
        else if (cmd == NetworkCommand.Dropout)
        {
            sender.Tag = "Dropout";
        }
        else if (cmd == NetworkCommand.Clear)
        {
            sender.Tag = "Clear";
            //誰かがクリアしたら終了
            if (session.IsHost) session.EndGame();
        }
    }
}

このメソッドでは、受信するデータがあるかどうかを調べ、データが存在する場合は、先頭の 32 ビット整数を読み込んで NetworkCommand 列挙型の値に変換します。取得した NetworkCommand 列挙型の値に従って、データ送信者を表す NetworkGamer オブジェクトの Tag プロパティに、現在の状態を表す値を代入しています。Tag プロパティは、各プレイヤーの状態の描画に利用されます。各プレイヤーの状態を文字列化する、マクロ的な役割の GetScoreText() メソッドを用意しています。

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

private string GetScoreText()
{
    string result = "Score\n";
    foreach (NetworkGamer gamer in session.AllGamers)
    {
        if (gamer.Tag == null)
            result += gamer.Gamertag + ": 0\n";
        else result += gamer.Gamertag + ": " + gamer.Tag + "\n";
    }
    return result;
}

ロビーや、ゲームプレイ中には、このメソッドが返した文字列を描画します。

MineField クラスや、描画関連のコードに手を加える必要はありません。これまで作成したゲームの内容に、上記の通信機能を組み込むことで、多人数対戦を実現できます。

■ 実行結果

実行結果

■ 実行結果

実行結果



Top of Page Top of Page
Visual Studio 2008 Express

Microsoft