WisdomSoft - for your serial experiences.

11.7 データ通信

セッションに接続しているゲーマー間でデータを送受信する方法を解説します。

11.7.1 データ送信

セッションに参加している状態であれば、セッションに参加している他のゲーマーに対してデータを送信したり、他のゲーマーから送られてきたデータを受信したりできます。XNA Framework によるデータ通信には、すべて UDP (User Datagram Protocol) が使われます。

一般的なアプリケーション開発者であれば、UDP よりも信頼性のある TCP (Transmission Control Protocol) を使った通信に慣れていることでしょう。TCP は、データの送受信を行うクライアントとサーバーの間で接続を確立し、通信経路を固定してから通信を行います。送受信するデータは確実に正しい順序で相手に届けられ、信頼性のある通信と呼ばれます。

これに対して UDP は接続先や経路を固定せず、データをパケット(またはデータグラムとも呼ぶ)という単位でまとめ、相手先のアドレスを含めて送受信します。通信経路は固定されないため、状況に応じてパケットの経路が動的に変更されてしまいます。パケットが到着する順番が入れ替わる可能性があり、またはパケットが途中で紛失する可能性もあるため、信頼性のない通信とも呼ばれます。

TPC による通信は電話、UDP による通信は手紙のようなものだと考えればよいかもしれません。確実にデータが届けられることが求められる通信の場合は TCP が用いられますが、エラー訂正などのために UDP にくらべて帯域を使用します。

XNA Framework による通信は UDP を使っていますが、信頼性が求められる場合はパケットの紛失などの検出を行い、確実にデータが届けられるまでパケットを再送してくれます。ゲーム開発者は、パケット単位の手続きを行う必要はなく、専用のメソッドを通して直接データを受け渡しできます。

データの送受信を行うには、セッションに参加しているゲーマーのうち、ローカルのコンピュータにサインインしているローカルゲーマーを LocalGamers プロパティから取得する必要があります。

NetworkSession クラス LocalGamers プロパティ
public GamerCollection<LocalNetworkGamer> LocalGamers { get; }

このプロパティは、セッションに参加しているローカルゲーマーを表す 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
)
public void SendData (
         byte[] data,
         SendDataOptions options,
         NetworkGamer recipient
)

data パラメータには送信するバイト配列を指定し、options パラメータに転送するパケットの信頼性を選択する Microsoft.Xna.Framework.Net.SendDataOptions 列挙型のいずれかのメンバを指定します。recipient パラメータには、データを送信するゲーマーを指定します。このパラメータを省略すると、送信したゲーマーを含むセッション参加者全員にデータが送られます。

Microsoft.Xna.Framework.Net.SendDataOptions 列挙型
[FlagsAttribute]
public enum SendDataOptions

この列挙型には、パケットの送信順序や紛失に対する保証を行わないことを表す None メンバ、パケットは確実に送信されるが順序は保証しないことを表す Reliable メンバ、パケットの順序は維持されるが紛失は保証しないことを表す InOrder メンバ、そしてパケットが紛失することなく順序を維持することを表す ReliableInOrder メンバが定義されています。

パケットは、ネットワークの品質によって途中で紛失することがあり、経路によっては後に送信したパケットが先に到着してしまうことがあります。SendData() メソッドの送信オプションに ReliableInOrder メンバを選択すれば、パケットが正しい順番で到着するまで再送します。信頼性のある通信が求められる場合に利用できますが、他のオプションに比べ伝送効率が悪く帯域幅のコストが最も大きくなってしまいます。

一方で、途中でデータが紛失しても問題のない通信においては None メンバや InOrder メンバを使用できます。プレイヤーの位置情報など、常に新しいデータで更新される情報であれば、一部が抜け落ちてもゲームの進行に支障をきたすことはありません。また、データ順序を問わない簡単なコマンドを送受信するような通信であれば Reliable メンバを使います。

11.7.2 データ受信

自分宛てにパケットが届けられると、受け取ったデータは取り出されるまでキューに保管されます。受け取るデータが存在するかどうかは IsDataAvailable プロパティから取得できます。

LocalNetworkGamer クラス IsDataAvailable プロパティ
public bool IsDataAvailable { get; }

受信したデータが存在する場合は true を、そうでなければ false を返します。

IsDataAvailable プロパティが true を返すのであれば、送られてきたデータを ReceiveData() メソッドから受け取ることができます。

LocalNetworkGamer クラス ReceiveData() メソッド
public int ReceiveData (
         byte[] data,
         out NetworkGamer sender
)

data パラメータには、データを受け取るために十分なサイズが割り当てられた配列を指定します。この配列に SendData() メソッドから送信したデータが保存されます。sender パラメータには、送信者を表す NetworkGamer オブジェクトを受け取るための変数を指定します。メソッドは、パケットから読み込んだバイト数を結果として返します。

受け取ったデータが不要であることが事前に把握できていたとしても、受け取ったパケットはキューに蓄積されるため、必ず ReceiveData() メソッドで読む込むようにしてください。そうでなければ、キューに不要なデータが残ったままになってしまいます。

コード1 (Win, Xbox 360)
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;
    private Color color;

    public Test()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Components.Add(new GamerServicesComponent(this));
        color = Color.Black;
    }

    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)
    {
        if (session.SessionState == NetworkSessionState.Ended)
        {
            session.Dispose();
            session = null;
            return;
        }

        //送受信する 3 バイトの配列
        byte[] data = new byte[3];

        //受信するデータがあるかどうか
        if (session.LocalGamers[0].IsDataAvailable)
        {
            //データを受信
            NetworkGamer sender;
            session.LocalGamers[0].ReceiveData(data, out sender);

            //受信したデータを Color 構造体に復元
            color = new Color(data[0], data[1], data[2]);
        }

        //ホストであれば現在の色を送信
        if (session.IsHost)
        {
            text = "Please push A or B or X button\n\n";

            //ボタンに対応する色要素を設定
            if (state.Buttons.A == ButtonState.Pressed) data[1] = 0xFF;
            if (state.Buttons.B == ButtonState.Pressed) data[0] = 0xFF;
            if (state.Buttons.X == ButtonState.Pressed) data[2] = 0xFF;

            //現在の色と異なる色が作られれば送信
            if (color != new Color(data[0], data[1], data[2]))
                session.LocalGamers[0].SendData(data, SendDataOptions.None);
        }
        else text = "Waiting for data reception\n\n";


        foreach (NetworkGamer gamer in session.AllGamers)
            text += gamer.Gamertag + (gamer.IsHost ? ", HOST" : "") + "\n";

        session.Update();
    }

    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);

        sprite.Begin();
        sprite.DrawString(font, text, Vector2.Zero, Color.Gray);
        sprite.End();

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

コード1は、ホストが背景色を表す色を 3 バイトの配列としてセッションに送信し、セッションの参加者は受信した 3 バイトの値を Color 構造体に変換して背景色として設定するというプログラムです。UpdateSession() メソッドでは、3 つの要素を持つbyte 型の配列 data 変数を作成し、戦闘の要素から順に赤、緑、青を表します。ボタンが押されると、押されたボタンに対応する色要素の値を変更し、作られた色が現在の背景色と異なれば SendData() メソッドで送信します。

セッション参加者は、ローカルゲーマーの IsDataAvailable プロパティが true のとき、受け取るべきデータがあると判断して ReceiveData() メソッドでデータを受け取り、受け取ったバイト配列を元に Color オブジェクトを生成して新しい背景色として設定します。このプログラムでは、データをセッション参加者全員に送信しているため、データを送信したホストもデータを ReceiveData() メソッドで受信しています。実行結果を見れば、ホストとの背景色に、他のセッション参加者の背景色が同期することが確認できます。

11.7.3 送受信とデータ変換

どのようなデータ型でも、構成する要素の値を辿っていけば最終的には単純な数値で表されているものであり、それらはバイト配列に変換できます。データをバイナリ化することで、あらゆるデータ型をバイト配列で送受信できます。しかし、多くのゲームで共通して扱うだろう文字列や数値、座標、色といった基本的な情報を、送受信するたびに変換するのは面倒です。そこで XNA Framework では、基本的なデータ型をバイト配列に変換してネットワークに送信し、受信したバイト配列を元のデータ型に復元して結果を返してくれるクラスを提供しています。

効率的にデータを送信するには Microsoft.Xna.Framework.Net.PacketWriter クラスを使います。このクラスは System.IO 名前空間の BinaryWriter クラスを継承し、基本的なデータ型を出力する Write() メソッドに加えて、XNA Framework の基本的なデータ型である Vector2 や Vector3、Matrix なども出力できるようにオーバーロードしています。

Microsoft.Xna.Framework.Net.PacketWriter クラス
public class PacketWriter : BinaryWriter

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

PacketWriter クラスのコンストラクタ
public PacketWriter ()

PacketWriter クラスのインスタンスを作成し、送信するデータを Write() メソッドに書き込みます。Write() メソッドにデータを渡した時点では、まだ送信は行われません。データを送信するには PacketWriter オブジェクトを受け取る、次のような SendData() メソッドを呼び出します。

LocalNetworkGamer クラス SendData() メソッド
public void SendData (
         PacketWriter data,
         SendDataOptions options
)
public void SendData (
         PacketWriter data,
         SendDataOptions options,
         NetworkGamer recipient
)

data パラメータには、送信するデータを書き込んだ PacketWriter オブジェクトを指定します。options には、送信するデータの信頼性に関するオプションを指定します。recipient パラメータには、データの送信先となるゲーマーを指定します。recipient を省略した場合は、セッションの参加者全員に送信されます。

SendData() メソッドでデータを送信した後の PacketWriter オブジェクトは、そのまま再利用できます。ゲーム内でオブジェクトを何度もインスタンス化するとガベージコレクションが発生してしまうため、通常は 1 つのインスタンスをゲーム内で使いまわします。

受信したパケットの読み込みは Microsoft.Xna.Framework.Net.PacketReader クラスを使います。このクラスは PacketWriter と対になるクラスで System.IO 名前空間の BinaryReader クラスを継承しています。基本的な構造は PacketWriter と同じで、BinaryReader クラスで提供される Read~() メソッドに加えて、ReadVector2() メソッドや ReadMatrix() メソッドなどが追加されています。

Microsoft.Xna.Framework.Net.PacketReader クラス
public class PacketReader : BinaryReader

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

PacketReader クラスのコンストラクタ
public PacketReader ()

PacketReader クラスのインスタンスを生成した時点では、オブジェクトはデータを保有していません。PacketReader からデータを読み込むには、次の ReceiveData() メソッドを用いて受信したパケットを受け取る必要があります。 

LocalNetworkGamer クラス ReceiveData() メソッド
public int ReceiveData (
         PacketReader data,
         out NetworkGamer sender
)

data パラメータに、受信したデータを受け取る PacketReader オブジェクトを指定します。sender には、このパケットを送信したゲーマーを表す NetworkGamer オブジェクトを受け取る変数を指定します。メソッドは、読み込んだバイト数を返します。

PackWriter と同様、PacketReader オブジェクトも再利用できます。

コード2 (Win, Xbox 360)
using System;
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 PacketWriter writer;
    private PacketReader reader;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string text, messageLog;

    public Test()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Components.Add(new GamerServicesComponent(this));
        writer = new PacketWriter();
        reader = new PacketReader();
        messageLog = "";
    }

    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 CallbackShowKeyboardInput(IAsyncResult ar)
    {
        string message = Guide.EndShowKeyboardInput(ar);
        if (session.LocalGamers.Count > 0)
        {
            writer.Write(message);
            session.LocalGamers[0].SendData(writer, SendDataOptions.None);
        }
    }

    private void UpdateSession(GamePadState state)
    {
        text = "B BUTTON: Dispose a network session\n";
        text += "X BUTTON: Show keyboard input and send your message\n";

        if (state.Buttons.B == ButtonState.Pressed || session.SessionState == NetworkSessionState.Ended)
        {
            session.Dispose();
            session = null;
            messageLog = "";
            return;
        }

        //X ボタンが押されたらキーボード入力画面を表示
        if (!Guide.IsVisible && state.Buttons.X == ButtonState.Pressed)
        {
            Guide.BeginShowKeyboardInput(
                PlayerIndex.One, "メッセージ入力", "送信するメッセージを入力してください",
                "I LOVE XNA", CallbackShowKeyboardInput, null
            );
        }

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

            //受信したデータを文字列として取得、ログに追加
            string message = sender.Gamertag + ">" + reader.ReadString() + "\n";
            messageLog = message + messageLog;
        }

        //セッション参加者のリストを表示
        text += "Members:";
        foreach (NetworkGamer gamer in session.AllGamers)
            text += gamer.Gamertag + ", ";

        //メッセージのログを表示
        text += "\n\n" + messageLog;

        session.Update();
    }

    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);
    }
}
実行結果
コード2 実行結果

コード2は、セッションに参加しているメンバーがキーボード入力画面で入力した文字列を互いに送信し合うことで、チャットのような対話を可能としています。データを受信すると、文字列型の messageLog フィールドに送信者のゲーマータグと受け取った文字列を加算しています。

送受信するデータはテキストなので、本来ならばこれをバイト配列に変換して送受信しなければなりませんが、PacketWriter クラスと PacketReader クラスを用いることで、文字列や整数などの基本的なデータ型であれば、そのまま受け渡しできます。 

11.7.4 各ゲーマーの状態

ネットワークを介して複数の参加者が同じゲームの進行を共有するには、各々の参加プレイヤーの状態を保持する必要があります。例えば、アクションゲームの通信対戦を考えたとき、各プレイヤーが操作するキャラクターには、操作キャラクターの種類や体力、座標といった状態が存在します。ゲームは、セッションに参加しているゲーマーごとに状態を管理しながら、参加者全体で同期する必要があります。

セッションに参加しているゲーマー固有の状態を管理するために、ゲーマーとデータを結びつけるための辞書オブジェクトを用意する必要はありません。各ゲーマーの状態は Gamer オブジェクトの Tag プロパティを用いて関連付けることができます。このプロパティは、すべてのゲーマーの基底クラスである Gamer クラスの機能なので、NetworkGamer クラスでも利用できます。

Gamer クラス Tag プロパティ
public Object Tag { get; set; }

Tag プロパティには、対象のゲーマーに関連付ける任意のオブジェクトを設定できます。このプロパティをどのように利用するかは開発者の自由です。一般には、ゲーマーの状態を表すオブジェクトを設定し、セッションに参加している Gamer オブジェクトと、そのゲーマーが保有しているゲームデータを関連付けます。

コード3 (Win, Xbox 360)
using System;
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 PacketWriter writer;
    private PacketReader reader;
    private SpriteBatch sprite;
    private SpriteFont font;
    private string text;

    public Test()
    {
        graphicsDeviceManager = new GraphicsDeviceManager(this);
        Components.Add(new GamerServicesComponent(this));
        writer = new PacketWriter();
        reader = new PacketReader();
    }

    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);
            session.GamerJoined += session_GamerJoined;
        }
        else if (state.Buttons.X == ButtonState.Pressed)
            sessions = NetworkSession.Find(NetworkSessionType.SystemLink, 1, null);
    }

    private void UpdateSession(GamePadState state)
    {
        text = "B BUTTON: Dispose a network session\n";
        text += "Left Stick: Move your position\n";

        if (state.Buttons.B == ButtonState.Pressed || session.SessionState == NetworkSessionState.Ended)
        {
            session.Dispose();
            session = null;
            return;
        }

        LocalNetworkGamer localGamer = session.LocalGamers[0];

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

            //受信した座標を送信したゲーマーの Tag プロパティに設定
            sender.Tag = reader.ReadVector2();
        }

        //ローカルゲーマーの座標処理
        Vector2 oldPosition = (localGamer.Tag == null ? Vector2.Zero : (Vector2)localGamer.Tag);
        Vector2 newPosition = Vector2.Zero;

        newPosition.X = oldPosition.X + state.ThumbSticks.Left.X * 4;
        newPosition.Y = oldPosition.Y + (-state.ThumbSticks.Left.Y) * 4;

        //座標が移動したならば、座標データを送信
        if (oldPosition != newPosition)
        {
            writer.Write(newPosition);
            localGamer.SendData(writer, SendDataOptions.None);
        }
        session.Update();
    }

    //誰かがセッションに参加すると実行される
    void session_GamerJoined(object sender, GamerJoinedEventArgs e)
    {
        LocalNetworkGamer localGamer = session.LocalGamers[0];

        //全員の現在の座標を新しい参加者に送信
        foreach (NetworkGamer gamer in session.AllGamers)
        {
            //新しい参加者自身の情報は送信する必要がない
            if (gamer == e.Gamer) continue;

            Vector2 position = (gamer.Tag == null ? Vector2.Zero : (Vector2)gamer.Tag);
            writer.Write((Vector2)position);

            //新しい参加者にのみの座標を送信
            localGamer.SendData(writer, SendDataOptions.None, e.Gamer);
        }
    }

    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);
        if (session != null) {
            foreach (NetworkGamer gamer in session.AllGamers)
            {
                Vector2 position = (gamer.Tag == null ? Vector2.Zero : (Vector2)gamer.Tag);
                sprite.DrawString(font, "@<" + gamer.Gamertag, position, Color.Red);
            }
        }
        sprite.End();

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

コード3は、セッションに参加しているゲーマーに自分自身のいる座標を持たせ、ゲーマーが持つ座標にゲーマータグを表示します。各々のゲーマーが待つ座標は、Gamer オブジェクトの Tag プロパティに設定して処理させています。

UpdateSession() メソッドの処理では、コントローラの左スティックの値からローカルゲーマーの座標を移動させることができ、座標が移動するとネットワークに新しい座標を送信します。ネットワークから受信した座標は、座標の送信者となっている NetworkGamer オブジェクトの Tag プロパティに設定します。

Draw() メソッドでは、セッションが有効であれば foreach 文を使ってセッション参加者のゲーマータグを順に描画します。このとき、対象のゲーマーが保有している現在の座標を Tag プロパティから取得しています。