11.2 セッションの検索
11.2.1 接続先を探す
他のゲームと通信するには、セッションに参加しなければなりません。ホストは、セッションを参加して他のプレイヤーがセッションに参加してくれるのを待機しています。ホスト以外のプレイヤーがセッションに参加するには、最初に参加可能なセッションを検索する必要があります。セッションの検索には Find() メソッドを使います。
public static AvailableNetworkSessionCollection Find ( NetworkSessionType sessionType, int maxLocalGamers, NetworkSessionProperties searchProperties )
sessionType パラメータには、検索するセッションの種類を指定します。maxLocalGamers パラメータには、セッションに参加可能なローカルゲーマーの数を指定します。searchProperties は、ゲーム独自のオプションでセッションの種類を分けてフィルタ処理を行うために用いられます。例えば、協力プレイや対戦プレイなどの異なる通信モードを持つゲームで利用できます。null を指定するとフィルタ処理は行われません。検索の絞り込みについては後述するので、この場では null を指定するものとします。
このメソッドは、パラメータに指定された条件に一致するセッションをネットワークから検索し、結果を Microsoft.Xna.Framework.Net.AvailableNetworkSessionCollection クラスのオブジェクトとして返します。このクラスは、ReadOnlyCollection クラスを継承する参加可能なセッションのコレクションです。
public sealed class AvailableNetworkSessionCollection : ReadOnlyCollection<AvailableNetworkSession>, IDisposable
検索はネットワークを介して行われるため、状態によっては検索結果を得るまでに時間がかかることがあります。Find() メソッドは結果が得られるまで処理を返しませんが、非同期で検索を行う BeginFind() メソッドと EndFind() メソッドも用意されています。
AvailableNetworkSessionCollection クラスは、参加可能なセッションを表す Microsoft.Xna.Framework.Net.AvailableNetworkSession クラスのオブジェクトを管理しています。
public sealed class AvailableNetworkSession
AvailableNetworkSession クラスは、検索した結果として見つかった参加可能な 1 つのセッションを表します。ここから、現在のセッションの状態を得られるので、どのセッションに参加するかをプレイヤーに決定させる情報に利用できます。
ホストのゲーマータグは HostGamertag プロパティから取得できます。
public string HostGamertag { get; }
このプロパティは、セッションのホストになっているゲーマーのゲーマータグを表す文字列を返します。
セッションに参加しているゲーマー数は CurrentGamerCount プロパティから取得できます。
public int CurrentGamerCount { get; }
このプロパティは、現在セッションに参加しているゲーマー数を整数で返します。
セッションに参加可能なゲーマー数は、誰でも参加できる人数を OpenPublicGamerSlots プロパティから、招待者だけが参加できる人数を OpenPrivateGamerSlots プロパティから取得できます。
public int OpenPublicGamerSlots { get; }
public int OpenPrivateGamerSlots { get; }
これらのプロパティは、それぞれ公開されているスロットと非公開のスロットに参加可能な人数を整数で返します。
11.2.2 検索対象となるゲーム
Find() メソッドのパラメータに、検索対象となるゲームの情報が含まれていないことを不思議に思うかもしれません。ところが、Find() メソッドは自分を呼び出したゲームのセッションだけを正しく検索できます。他人が作った無関係のゲームのセッションをネットワークから拾ってくることはありません。これは、Find() メソッドがアプリケーションの GUID (Globally Unique Identifier) を用いてセッションを識別しているためです。
GUID はグローバル一意識別子とも呼ばれる 128 ビットの整数で、.NET Framework のアセンブリに GuidAttribute 属性として設定されています。GUID は、世界中で一意であることを保証するものではありませんが、表現できる値の範囲が限りなく大きいので、乱数で生成された GUID が衝突する可能性が限りなく低いことを利用しています。
XNA Game Studio で作成したプロジェクトであれば Properties フォルダ内にある AssemblyInfo.cs ファイルに、アセンブリに対する属性の設定が記述されています。ここに、アプリケーションのタイトルや著作権、バージョンなどの情報を記すことができます。アセンブリの属性については MSDN の開発者ガイド「アセンブリを使用したプログラミング」などが参考になります。
http://msdn.microsoft.com/ja-jp/library/8wxf689z(VS.80).aspx
AssemblyInfo.cs ファイルの中に、GuidAttribute 属性を設定している部分があるので、ここを編集することで GUID を明示的に指定することも可能です。デフォルトでは、乱数によって作られた GUID が設定されています。
// The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("ef9ae864-4987-4a37-ae2c-99c83bcbaf85")]
GUID をテキストで表現するときは、16 進数を順に 8 桁、4 桁、4 桁、4 桁、12 桁ごとにハイフンで区切ります。XNA Framework ゲームのアセンブリも GUID で区別されます。Xbox 360 の場合、同じ GUID の XNA Framework ゲームを複数配置することはできません。同じ GUID のゲームを転送すると、元のゲームを上書きします。一方、同じタイトル名のゲームでは GUID が異なれば複数配置できます。
Find() メソッドは、GUID が同じゲームによって作られたセッションだけを検索します。異なるプロジェクトのゲームから作られたセッションを検索したければ、ゲームの GUID を一致させてください。例えば、異なるプロジェクトとして開発された Windows 用のゲームと Xbox 360 用のゲームをネットワークで通信させるには、GUID を一致させる必要があります。
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Net; public class Test : Game { public static void Main(string[] args) { using (Game game = new Test()) game.Run(); } private GraphicsDeviceManager graphicsDeviceManager; private AvailableNetworkSessionCollection result; private SpriteBatch sprite; private SpriteFont font; private string text; public Test() { graphicsDeviceManager = new GraphicsDeviceManager(this); Components.Add(new GamerServicesComponent(this)); } protected override void LoadContent() { sprite = new SpriteBatch(GraphicsDevice); font = Content.Load<SpriteFont>("Content/TestFont"); base.LoadContent(); } protected override void Update(GameTime gameTime) { GamePadState state = GamePad.GetState(PlayerIndex.One); if (state.Buttons.Back == ButtonState.Pressed) Exit(); SignedInGamer gamer = Gamer.SignedInGamers[PlayerIndex.One]; if (gamer == null) { text = "Please sign in as player one."; if (!Guide.IsVisible) Guide.ShowSignIn(1, true); } else { text = "X BUTTON: Find a network session\n\n"; if (state.Buttons.X == ButtonState.Pressed) result = NetworkSession.Find(NetworkSessionType.SystemLink, 1, null); if (result != null) { if (result.Count == 0) text += "Network session not found"; else foreach (AvailableNetworkSession item in result) { text += item.HostGamertag + "\n" + " GamerCount=" + item.CurrentGamerCount + "\n" + " PublicSlots=" + item.OpenPublicGamerSlots + "\n" + " PrivateSlots=" + item.OpenPrivateGamerSlots + "\n"; } } } 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); } }
コード1は Find() メソッドでセッションを検索し、見つかった有効なセッションの情報を画面に列挙するプログラムです。この処理の中でセッションを作成することはありません。「11.1 セッションの作成 コード1」と同じ GUID に設定して、別のコンピュータ、または Xbox 360 で「11.1 セッションの作成 コード1」を実行してセッションを作成している状態で、コード1を実行してセッションを検索してください。GUID さえ一致していれば、有効なセッションを発見できます。
11.2.3 カスタムプロパティ
同一のゲーム内でも、異なる種類のセッションを作成したいことがあります。同じ通信対戦でも、参加者全員が敵となる個人戦や、2 チームに分かれて競い合うチーム戦などでは、通信の内容が異なります。そのため、これらは異なるセッションとして区別する仕組みが必要です。そこで、セッション作成時にセッションの種類を表す整数をユーザー定義のプロパティとして設定し、検索時にセッションを識別できます。これを、セッションのカスタムプロパティと呼びます。
カスタムプロパティを設定するには、5 つのパラメータを受け取る次の Create() メソッドを使います。
public static NetworkSession Create ( NetworkSessionType sessionType, int maxLocalGamers, int maxGamers, int privateGamerSlots, NetworkSessionProperties sessionProperties )
第 3 パラメータまでは、前述した Create() メソッドと同じです。privateGamerSlots パラメータには、招待用の参加枠を指定します。sessionProperties パラメータには、生成するセッションの固有情報を表す Microsoft.Xna.Framework.NetNetworkSessionProperties クラスのオブジェクトを指定します。
public class NetworkSessionProperties
このクラスのコンストラクタは、パラメータを受け取りません。
public NetworkSessionProperties ()
セッションを種別するためのプロパティは、インデクサから設定します。このインデクサに指定するインデックスが 1 つのカスタムなプロパティに相当し、インデックスに関連付けられた要素がプロパティの値となります。
public Nullable<int> this [ int index ] { get; set; }
index パラメータにインデックスを指定します。このインデクサから、プロパティの値を設定または取得したりできます。プロパティの値は Nullable<int> 型なので、null を設定できます。すべてのインデックスに設定されているデフォルトの値は null です。どのインデックスに、どのような値を設定するかは、開発者が自由に定められます。
例えば、インデックス 0 番はゲームの種類を表すプロパティであると定めて、プロパティの値が 0 なら個人戦、1 ならチーム戦というようにアプリケーション側で値の意味を定めます。インデックス 1 番は難易度を表すプロパティで、値が 0 ならば Easy、1 ならば Normal、2 ならば Hard を表す、というようにカスタムプロパティによってセッションの種類を分けます。当然、プロパティの値を調べて適切に処理を行う責任はアプリケーションにあります。
このインデクサに指定できるインデックスの範囲は Count プロパティから取得できます。
public int Count { get; }
このプロパティは、プロパティの数を表す値を返します。このプロパティから、インデクサに指定できるインデックスの範囲を調べられます。本書執筆時点の Windows 実装では常に 8 を返します。よって、有効なインデックスの範囲は 0 ~ 7 となります。
生成した NetworkSession オブジェクトからカスタムプロパティを得るには SessionProperties プロパティを使います。
public NetworkSessionProperties SessionProperties { get; }
このプロパティは、セッションに設定されているカスタムプロパティを表す NetworkSessionProperties オブジェクトを返します。
同様に、検索結果として得られた AvailableNetworkSession オブジェクトも SessionProperties プロパティからカスタムプロパティを提供しています。
public NetworkSessionProperties SessionProperties { get; }
Find() メソッドは、searchProperties パラメータに null ではない有効な NetworkSessionProperties オブジェクトを渡すと、指定されたカスタムプロパティに一致する値を持ったセッションだけが検索されます。
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Net; public class Test : Game { public static void Main(string[] args) { using (Game game = new Test()) game.Run(); } private GraphicsDeviceManager graphicsDeviceManager; private NetworkSession session; private SpriteBatch sprite; private SpriteFont font; private string text; public Test() { graphicsDeviceManager = new GraphicsDeviceManager(this); Components.Add(new GamerServicesComponent(this)); } protected override void LoadContent() { sprite = new SpriteBatch(GraphicsDevice); font = Content.Load<SpriteFont>("Content/TestFont"); base.LoadContent(); } protected override void Update(GameTime gameTime) { GamePadState state = GamePad.GetState(PlayerIndex.One); if (state.Buttons.Back == ButtonState.Pressed) Exit(); SignedInGamer gamer = Gamer.SignedInGamers[PlayerIndex.One]; if (gamer == null) { text = "Please sign in as player one."; if (!Guide.IsVisible) Guide.ShowSignIn(1, true); } else { if (session == null) { text = "A BUTTON: Create a network session1\n"; text += "B BUTTON: Create a network session2\n"; text += "X BUTTON: Create a network session3\n"; text += "Y BUTTON: Create a network session4\n\n"; NetworkSessionProperties sessionProperties = new NetworkSessionProperties(); if (state.Buttons.A == ButtonState.Pressed) { sessionProperties[0] = 1; session = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 16, 0, sessionProperties); } else if (state.Buttons.B == ButtonState.Pressed) { sessionProperties[0] = 2; session = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 16, 0, sessionProperties); } else if (state.Buttons.X == ButtonState.Pressed) { sessionProperties[0] = 3; session = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 16, 0, sessionProperties); } else if (state.Buttons.Y == ButtonState.Pressed) { sessionProperties[0] = 4; session = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 16, 0, sessionProperties); } } else { text = "Network session" + session.SessionProperties[0] + " is available\n\n"; text += "BACK BUTTON: Dispose a network session\n"; if (state.Buttons.Back == ButtonState.Pressed) { session.Dispose(); session = null; } else session.Update(); } } 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); } }
コード2は、A、B、X、Y ボタンを押すと、それぞれ異なる値をカスタムプロパティに設定してセッションを作成します。カスタムプロパティには、インデックス 0 番に対して、セッションの番号を表す値を設定しています。A ボタンが押されるとセッション 1 番を作成し、B ボタンが押されるとセッション 2 番を作成するという形です。
Find() メソッドは searchProperties パラメータに指定したカスタムプロパティの値と一致するセッションだけを検索します。
using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.GamerServices; using Microsoft.Xna.Framework.Net; public class Test : Game { public static void Main(string[] args) { using (Game game = new Test()) game.Run(); } private GraphicsDeviceManager graphicsDeviceManager; private AvailableNetworkSessionCollection result; private SpriteBatch sprite; private SpriteFont font; private string text; public Test() { graphicsDeviceManager = new GraphicsDeviceManager(this); Components.Add(new GamerServicesComponent(this)); } protected override void LoadContent() { sprite = new SpriteBatch(GraphicsDevice); font = Content.Load<SpriteFont>("Content/TestFont"); base.LoadContent(); } protected override void Update(GameTime gameTime) { GamePadState state = GamePad.GetState(PlayerIndex.One); if (state.Buttons.Back == ButtonState.Pressed) Exit(); SignedInGamer gamer = Gamer.SignedInGamers[PlayerIndex.One]; if (gamer == null) { text = "Please sign in as player one."; if (!Guide.IsVisible) Guide.ShowSignIn(1, true); } else { text = "A BUTTON: Find a network session1\n"; text += "B BUTTON: Find a network session2\n"; text += "X BUTTON: Find a network session3\n"; text += "Y BUTTON: Find a network session4\n\n"; NetworkSessionProperties sessionProperties = new NetworkSessionProperties(); if (state.Buttons.A == ButtonState.Pressed) { sessionProperties[0] = 1; result = NetworkSession.Find(NetworkSessionType.SystemLink, 1, sessionProperties); } else if (state.Buttons.B == ButtonState.Pressed) { sessionProperties[0] = 2; result = NetworkSession.Find(NetworkSessionType.SystemLink, 1, sessionProperties); } else if (state.Buttons.X == ButtonState.Pressed) { sessionProperties[0] = 3; result = NetworkSession.Find(NetworkSessionType.SystemLink, 1, sessionProperties); } else if (state.Buttons.Y == ButtonState.Pressed) { sessionProperties[0] = 4; result = NetworkSession.Find(NetworkSessionType.SystemLink, 1, sessionProperties); } if (result != null) { if (result.Count == 0) text += "Network session not found"; else { foreach (AvailableNetworkSession item in result) text += "Session" + item.SessionProperties[0] + ", " + item.HostGamertag + "\n"; } } } 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); } }
コード3は、A、B、X、Y のいずれかのボタンが押されると、ボタンに対応するセッション番号を持つセッションを検索します。コード2で作成したセッションを検索し、カスタムプロパティの値が一致するセッションだけが見つかることを確認してください。
上記のサンプルではセッションを識別するプロパティの値に整数リテラルを代入していましたが、整数リテラルは人間にとって直観的な表現ではありません。本格的な開発においては、列挙型などを使って値に識別子を関連付けて管理するべきです。推奨されるモデルは、プロパティのインデックスを提供する列挙型と、各インデックスで用いられる値を提供する列挙型を作ります。
enum SessionProperty { Difficulty, GameMode } enum Difficulty { Easy, Normal, Hard } enum GameMode { FreeForAll, TeamDeathmatch, SearchAndDestroy }
上記の列挙型のうち、SessionProperty 列挙型がプロパティのインデックスを表すものとすると、各インデックスのプロパティの値を提供する列挙型の識別子は、SessionProperty 列挙型のメンバの識別子に対応します。このような規則を固めておくことで、インデックスとプロパティの関係が明確になります。
NetworkSessionProperties properties = new NetworkSessionProperties(); properties[(int)SessionProperty.Difficulty] = (int)Difficulty.Normal; properties[(int)SessionProperty.GameMode] = (int)GameMode.TeamDeathmatch;
このように、整数リテラルをそのまま設定するのではなく識別子を用いるべきでしょう。1 という値がプロパティとして設定されていたとしても、1 という値から意味を想像することはできません。上記のように列挙型を用いることで、プロパティのインデックスや値に関連付けれた意味が分かりやすくなります。