WisdomSoft - for your serial experiences.

11.4 セッション管理

セッションには、他のゲーマーの参加を待機しているロビー状態と、ゲームを開始ししている状態などがあり、初期設定ではゲームプレイ中の場合はセッション検索に引っかからない仕組みになっています。しかし、ゲームが対応すればプレイ中でもセッションに参加できる仕組みも実現できます。

11.4.1 セッションの状態

セッションには、他のゲーマーの参加を待機しているロビー状態や、ゲームを開始して対戦中の状態、そしてセッションを閉じて終了しているといった状態に分けられます。

セッションを作成後は、他の参加者を募るロビー状態になってゲーマーの参加を待機し、十分な人数がそろった時点でゲームを開始します。勝敗が決した時点でゲームを終了し、再びロビー状態に戻ります。ホストがセッションを終了させるまで、この流れを繰り返すことになります。

セッションの状態は SessionState プロパティから取得できます。

NetworkSession クラス SessionState プロパティ
public NetworkSessionState SessionState { get; }

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

Microsoft.Xna.Framework.Net.NetworkSessionState 列挙型
public enum NetworkSessionState

この列挙型には、セッションが他のゲーマーの参加を待っている状態を表す Lobby メンバ、ゲームプレイ中であることを表す Playing メンバ、セッションが終了したことを表す Ended メンバが定義されています。

Create() メソッドによって作成されたセッションのデフォルトの状態は Lobby です。ホストがセッションを Dispose() メソッドで解放するか、またはサインアウトするなどしてセッションが終了されると、自動的に Ended 状態になります。

通常、他のゲーマーが参加するのは Lobby 状態のセッションです。Playing や Ended 状態のセッションは、Find() メソッドの検索で見つかりません。途中参加可能な対戦ゲームの開発も可能なので、その方法については後述します。

セッションの状態を Playing に変更するには、セッションのホストが StartGame() メソッドを呼び出します。ホスト権限のない NetworkSession オブジェクトの StartGame() メソッドを呼び出すと、例外が発生してしまいます。

NetworkSession クラス StartGame() メソッド
public void StartGame ()

対して、対戦が終了してセッションの状態を元に戻すには EndGame() メソッドを呼び出します。 

NetworkSession クラス EndGame() メソッド
public void EndGame ()

これで、セッションの状態が Lobby に戻ります。

StartGame() メソッドや EndGame() メソッドは、セッションの状態を遷移させるだけです。ゲーム画面の移行や通信対戦の機能は、これまでと同じように Update() メソッドや Draw() メソッドの中に記述してください。

セッションに参加しているゲーマーは、ゲーム開始の準備が整ったかどうかを IsReady プロパティに設定できます。このプロパティの値を変更するには、状態が Lobby であり、ローカルゲーマーのオブジェクトでなければなりません。そうでなければ例外が発生します。

NetworkGamer クラス IsReady プロパティ
public bool IsReady { get; set; }

ゲーム開始の準備が整っている場合は true、そうでなければ false を返します。ゲームを開始させるタイミングはホストが任意で決定できますが、1 つの参考値としてセッション参加者の IsReady プロパティの値を利用できます。各ゲーマーの IsReady プロパティは、セッションの状態が Playing から Lobby に戻った時点で自動的に false に設定されます。

セッションの参加者全員の準備が整ったかどうかは NetworkSession クラスの IsEveryoneReady プロパティから取得できます。

NetworkSession クラス IsEveryoneReady プロパティ
public bool IsEveryoneReady { get; }

セッションに参加しているすべてのゲーマーの IsReady プロパティが true であれば、このプロパティの結果は true となります。

コード1
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 AvailableNetworkSessionCollection sessions;
    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) UpdateSession(state);
            else if (sessions != null) UpdateFindSession(state);
            else UpdateMenu(state);
        }
        base.Update(gameTime);
    }

    private void UpdateMenu(GamePadState state)
    {
        text = "A BUTTON: Create a network session\n";
        text += "X BUTTON: Find a network session\n\n";

        if (state.Buttons.A == ButtonState.Pressed)
            session = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 16);
        else if (state.Buttons.X == ButtonState.Pressed)
            sessions = NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
    }

    private void UpdateSession(GamePadState state)
    {
        text = "Sesssion state=" + session.SessionState + "\n";

        //ホストならばゲームの状態を変更できる
        if (session.IsHost)
        {
            text += "A BUTTON: Game start\n";
            text += "X BUTTON: Game end\n";

            if (state.Buttons.A == ButtonState.Pressed && session.SessionState == NetworkSessionState.Lobby)
                session.StartGame();
            else if (state.Buttons.X == ButtonState.Pressed && session.SessionState == NetworkSessionState.Playing)
                session.EndGame();
        }
        text += "Y BUTTON: Ready\n";
        text += "B BUTTON: Dispose a network session\n\n";

        session.Update();

        //全員の準備が整っているかどうか
        if (session.IsEveryoneReady) text += "*Everyone Ready!!\n";

        //各ゲーマーの準備の処理
        foreach (NetworkGamer gamer in session.AllGamers)
        {
            //Y ボタンが押され、セッションがロビー状態であれば、ローカルゲーマーの IsReady を true に変更
            if (state.Buttons.Y == ButtonState.Pressed && gamer.IsLocal && session.SessionState == NetworkSessionState.Lobby)
                gamer.IsReady = true;

            text += gamer.Gamertag + (gamer.IsReady ? " *Ready!" : "") + "\n";
        }

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

    private void UpdateFindSession(GamePadState state)
    {
        text = "Network session search results\n";
        text += "B BUTTON: Return to menu\n\n";

        if (state.Buttons.B == ButtonState.Pressed) sessions = null;
        else if (sessions.Count == 0) text += "Session not found";
        else
        {
            session = NetworkSession.Join(sessions[0]);
            sessions = 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);
    }
}
実行結果
コード1 実行結果

コード1では、セッションに参加している状態で Y ボタンを押すとローカルゲーマーの IsReady プロパティを true に設定します。IsReady プロパティが true であれば、画面に列挙されているゲーマータグの直後に *Ready! という文字を表示します。全ての参加者の準備が完了しているかどうかは IsEveryoneReady プロパティで確認し、この値が true であれば *Everyone Ready!! という文字を表示します。

ホストであれば、A ボタンでゲームを開始し、X ボタンでゲームを終了できます。A ボタンが押され StartGame() メソッドが実行されると、セッションの状態が Playing に移行します。セッションが Playing の状態になると Find() メソッドで検索できなくなります。X ボタンが押され EndGame() メソッドが実行されると、セッションの状態が Lobby に戻り、参加者の IsReady プロパティが自動的に false に設定されます。

11.4.2 途中参加

デフォルトの設定のセッションは、StartGame() メソッドによってゲームが開始され、セッションの状態が Playing になると検索で見つからなくなります。人数固定の対戦ゲームで、引き継ぎが難しい、または引き継ぐことに意味を持たない種類のゲームでは、この設定で問題はないでしょう。

一方で、ゲーム中にプレイヤーの入退室があっても進行に大きな影響を与えないアクション系の大人数チーム戦などでは、プレイヤーを途中参加を認めることができるでしょう。途中参加を可能にした方が人が集まりやすくなり、多くのプレイヤーが参加することで価値の高まるオンライン対戦ゲームなどでは重要な機能となります。

途中参加を認めるには NetworkSession クラスの AllowJoinInProgress プロパティを true に設定します。ホスト以外がこのプロパティの設定を変更することはできません。

NetworkSession クラス AllowJoinInProgress プロパティ
public bool AllowJoinInProgress { get; set; }

このプロパティは、途中参加を許可するならば true を返し、そうでなければ false を返します。デフォルトでは false に設定されているため、ゲームプレイ中の途中参加はできませんが、ホストがこのプロパティを true に設定することで、ゲームプレイ中でもゲームが検索されるようになります。

コード2
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));
        text = "";
    }

    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)
                session = NetworkSession.Create(NetworkSessionType.SystemLink, 1, 16);
            UpdateMenu(state);
        }
        base.Update(gameTime);
    }

    private void UpdateMenu(GamePadState state)
    {
        text = "Session state=" + session.SessionState + "\n";
        text += "Allow join in progress=" + session.AllowJoinInProgress + "\n\n";

        text += "A BUTTON: Game start\n";
        text += "B BUTTON: Game end\n";
        text += "X BUTTON: Allow join in progress\n";
        text += "Y BUTTON: Don't allow join in progress\n";

        session.Update();

        if (state.Buttons.A == ButtonState.Pressed && session.SessionState == NetworkSessionState.Lobby)
            session.StartGame();
        else if (state.Buttons.B == ButtonState.Pressed && session.SessionState == NetworkSessionState.Playing)
            session.EndGame();
        else if (state.Buttons.X == ButtonState.Pressed)
            session.AllowJoinInProgress = true;
        else if (state.Buttons.Y == ButtonState.Pressed)
            session.AllowJoinInProgress = false;
    }

    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 実行結果

コード2は、A ボタンを押すとゲームを開始し、B ボタンを押すとゲームを終了します。X ボタンを押すと AllowJoinInProgress プロパティに true を設定し、Y ボタンを押すと false を設定します。セッションの状態と AllowJoinInProgress  プロパティの値が画面に表示されているので、AllowJoinInProgress プロパティが true であれば SessionState プロパティが Playing でも検索可能であることを確認してください。

コード2はセッションを作成するだけで、検索する機能は実装していません。「11.2 セッションの検索 コード1」 などからセッションを検索してください。