问题

创建并加入一个网络会话是一回事,但如果不能发送或接收任何数据那么网络会话有什么用呢?

解决方案

当玩家连接到会话时,你可以在一个PacketWriter流中存储所有想要发送的数据。完成这个操作后,你可以使用LocalNetworkPlayer.SendData方法将这个PacketWriter发送给会话中的所有玩家。

在玩家接收数据前,你应该检查他们的LocalNetworkGamer. IsDataAvailable是否被设为ture,这表示数据已经被接收并做好了处理的准备。

一旦IsDataAvailable为true,你就可以调用LocalNetworkGamer.ReceiveData方法,返回一个包含另一个玩家发送给本地玩家的所有数据的PacketReader。

工作原理

这个教程建立在前一个教程结果的基础上,前一个教程允许同一网络上的多个机器通过一个会话互联。程序结束于InSession状态,这个状态只是简单地调用会话的Update方法。

现在,你将在InSession状态中做点实际的操作,让你的玩家可以将一些数据发送到会话中的其他玩家那里。本例中,你将发送程序运行的分钟数和秒数。

然后,你监听可用的数据。如果有可用的数据,你会接收两个数字,将它们放在一个字符串中,显示在屏幕上。

要发送和接收数据,你需要一个PacketWriter对象和一个PacketReader对象,所以在代码中添加这两个变量:

PacketWriter writer = new PacketWriter(); 
PacketReader reader = new PacketReader(); 

在项目中使用超过一个的PacketWriter对象和PacketReader对象是毫无理由的。

将数据发送到会话中的另一个玩家

你需要在PacketWriter中存储所有要发送给其他玩家的数据,这可以通过将数据作为Write方法的参数做到:

writer.Write(gameTime.TotalGameTime.Minutes); 
writer.Write(gameTime.TotalGameTime.Seconds); 

在将所有数据存储到PacketWriter之后,你可以使用本地玩家的SendData方法将它发送到所有其他用户:

LocalNetworkGamer localGamer = networkSession.LocalGamers[0]; 
localGamer.SendData(writer, SendDataOptions.None);

SendDataOptions参数会在教程的最后解释。更重要的是,SendData有一个重载方法可以只将数据发送到指定玩家而不是会话中的所有玩家。

以上就是将数据发送到会话中的其他玩家需要的所用操作。

从会话中的另一个玩家处接收数据

从其他玩家接收数据大致就是反过程:调用本地玩家的ReceiveData方法,这会返回一个包含其他玩家发送数据的PacketReader。调用PacketReader. Read方法中的一个从PacketReader获取数据:

NetworkGamer sender;
localGamer.ReceiveData(reader, out sender);

string playerName = sender.Gamertag; 
int minutes = reader.ReadInt32();
int seconds = reader.ReadInt32();

ReceiveData方法存储PacketReader流中的数据,发送给你数据的玩家会存储在第二个参数中,这样你就可以知道数据来自于谁。

当从PacketReader读取数据时,你需要确保以与发送相同的顺序进行读取。而且,因为PacketReader只包含字节流,你需要告知你想从字节构建哪个对象。例如,一个整数需要的字节比矩阵少,所以需要在某些时候告知你想恢复为哪种类型的对象。

本例中分钟数和秒数为整数,所以你想从字节流中重新构建两个整数。看一下PacketReader的不同Read方法,注意支持哪个对象。如果你想重构矩阵,则应该使用ReadMatrix方法,使用ReadSingle方法重构float,ReadDouble方法重构double,ReadInt16 重构short。

LocalGamer.IsDataAvailable

如果多个玩家向你发送数据,可能会有多个字节流需要被读取,这种情况也会发生在其他玩家调用SendData的频率大于你调用ReceiveData的频率时。

在这种情况下,你可以查询localGamer.IsDataAvailable属性,因为只要有一个字节流正在等待本地游戏,这个属性就会为true。

只要数据对你的玩家可用,下面的代码就会接收一个新PacketReader并读取发送数据的玩家的GamerTag属性。然后,玩家程序运行的分钟数和秒数就会从PacketReader中读取。

while (localGamer.IsDataAvailable)
...{
    NetworkGamer sender;
    localGamer.ReceiveData(reader, out sender);

    string gamerTime = "";
    gamerTime += sender.Gamertag + ": "; 
    gamerTime += reader.ReadInt32() + "m "; 
    gamerTime += reader.ReadInt32() + "s"; 
    gamerTimes[sender.Gamertag] = gamerTime;
}

要让这个例子实际干点事情,数据被转换到一个叫做gamerTime的字符串中,它存储在一个Dictionary。Dictionary是默认的generic .NET查询表,可以使用以下代码创建:

Dictionary<string, string> gamerTimes = new Dictionary<string, string>();

前面的代码会在Dictionary中创建一个数据项,对应发送给你数据的玩家。当从一个玩家接收新数据时,Dictionary中的对应数据项会被更新,你可以在Draw方法中将Dictionary中的字符串显示在屏幕上。

当玩家离开会话时,你需要将它们对应的数据项从Dictionary移除,这可以在GamerLeft 事件中加以处理:

void GamerLeftEventHandler(object sender, GamerLeftEventArgs e) 
...{
    log.Add(e.Gamer.Gamertag + " left the current session"); 
    gamerTimes.Remove(e.Gamer.Gamertag);
}
SendDataOptions

当你将数据发送到会话中的其他玩家时,你期望到达接受者的信息的顺序与发送的顺序是相同的,但是基于Internet的原理,你的信息可能会以不同于发送的顺序到达,甚至更糟,有些数据可能根本就传不到!

幸运的是,你可以为发送的数据包指定两个重要的系数,在使用前你需要知道它们是什么,好处是什么,更重要的是,它们的缺点是什么:

  • 到达的顺序(Order of arrival):数据包接收的顺序是否与发送的顺序相同?
  • 安全性(Reliability):你发送的这个数据是否是至关紧要的,如果数据包丢失游戏还能进行吗?

以上两个问题不是是就是否,可以提供四种可能性。LocalNetworkGamer.SendData可以将SendDataOptions作为第二个参数,这个参数可以让你指定四种情况中的一个:

  • SendDataOptions.None:发送的数据不是关键的,你接收数据的顺序也无关紧要。
  • SendDataOptions.InOrder:接收数据的顺序必须和发送它们的顺序相同,但一些数据包丢失无大碍。
  • SendDataOptions.Reliable:与SendDatOptions.InOrder相反,你的数据是关键的,你发送的所有数据必须到达接收者。但是,接收数据的顺序是否和发送的顺序相同无关紧要。
  • SendDataOptions.ReliableInOrder:所有的数据必须以和发送顺序相同的顺序到达接收者。

不是很难,我选择最后一个!有些选项有点缺点,解释如下:

  • SendDataOptions.None:没有速度损失,只能指望数据能够成功发送。
  • SendDataOptions.InOrder:在数据发送前,所有的数据包被分配了一个序号。接收者检查这个序号,如果数据包A在一个更加新的数据包B之后被接收,数据包A会被抛弃。这是个简单的检查方法,几乎不花时间,但即使有些数据成功到达了目的地可能也会被抛弃。
  • SendDataOptions.Reliable:接收者会检查丢失了哪个数据包。当数据包C从数据包流ABDE中丢失时,接收者会要求发送者重新发送数据包C,同时,数据包D和E在XNA代码中可以被访问。
  • SendDataOptions.ReliableInOrder:只有在你的数据需要时才使用这个选项。当接收者法线数据包C从流ABDE中丢失时,它会让发送者重新发送数据包C。这次,其后的数据包D和E不会被接收者传递到XNA中,直到数据包C也被成功的传递。这会引起延迟,因为所有后继的数据包会保存在内存中直至数据包C被重新发送并收到才会被传递到XNA程序中。

普遍的原则是,对大多数数据来说SendDataOptions.InOrder是安全的,尽可能不要使用 SendDataOption.ReliableInOrder。

SendDataOption.Chat

在你开始发送数据前,你需要记住一件事情:在Internet上发送的聊天信息(chat message)不可以被加密,法律上是禁止的。

因为默认情况下使用localGamer.SendData方法发送数据都会进行加密,你必须使用SendDataOptions.Chat表示XNA不要加密聊天信息。你也可以使用SendDataOption的组合,如下所示:

localGamer.SendData(write,SengDataOptions.Chat|SendDataOptions.Reliable); 

注意你可以发送加密和不加密混合的信息。如果你这样干,排序过的聊天信息只根据聊天数据排序而不是根据加密过的数据。例如,让我们看一下发送第一条信息时的情况,依次是数据信息、聊天信息、数据信息,如图8-1左图所示。

image

图8-1 顺序发送4个数据包(左图)和4种接收数据的方式

图8-1的右图显示了数据如何到达接收端的4中可能方式。在a情况中,数据到达的顺序与发送的顺序一样。在情况b和c中,数据包的顺序发生了改变,但是,这两种情况中第一个聊天数据包A在在第二个聊天数据包C之前被接收,第一个数据包B在第二个数据包D之前。

因为两个数据包和两个聊天数据包可以在一帧中被发送,你需要确保在接收端将它们混合起来,一个方法是在发送数据包前给它们添加一个小说明,表示它们是数据包还是聊天数据包,看一下下面的代码,其中D表示一个数据包,C表示一个聊天数据包:

writer.Write("D");
writer.Write(gameTime.TotalGameTime.Minutes);
writer.Write(gameTime.TotalGameTime.Seconds);

LocalNetworkGamer localGamer = networkSession.LocalGamers[0];
localGamer.SendData(writer, SendDataOptions.ReliableInOrder);

writer.Write("C");
writer.Write("This is a chat message from " + localGamer.Gamertag);
localGamer.SendData(writer, SendDataOptions.Chat|SendDataOptions.ReliableInOrder);

在接收端,只是简单地检查数据包是D还是C,并处理对应的数据包:

while (localGamer.IsDataAvailable)
...{
    NetworkGamer sender;
    localGamer.ReceiveData(reader, out sender);

    string messageType = reader.ReadString();
    if (messageType == "D")
...    {
        string gamerTime = "";
        gamerTime += sender.Gamertag + ": ";
        gamerTime += reader.ReadInt32() + "m ";
        gamerTime += reader.ReadInt32() + "s";
        gamerTimes[sender.Gamertag] = gamerTime;
    }
    else if (messageType == "C")
...    {
        lastChatMessage[sender.Gamertag] = reader.ReadString();
    }
}
多个本地玩家

如果多个玩家连接在同一个机器上,你需要通过迭代器发送和接受数据。

将数据发送到所有玩家很简单:

//send data from all local players to all other players in session
foreach (LocalNetworkGamer localGamer in networkSession.LocalGamers)
...{
    writer.Write(gameTime.TotalGameTime.Minutes); 
    writer.Write(gameTime.TotalGameTime.Seconds);
    localGamer.SendData(writer, SendDataOptions.ReliableInOrder);
}

记住你可以使用SendData的一个重载方法将数据只发送到一个指定玩家。

接收数据也不难,只需循环代码直到所有本地玩家的IsDataAvailable为false:

foreach (LocalNetworkGamer localGamer in networkSession.LocalGamers) 
...{
    while (localGamer.IsDataAvailable)
    ...{
        NetworkGamer sender;
        localGamer.ReceiveData(reader, out sender);

        string gamerTime = localGamer.Gamertag + " received from "; 
        gamerTime += sender.Gamertag + ": ";
        gamerTime += reader.ReadInt32() + "m ";
        gamerTime += reader.ReadInt32() + "s";
        gamerTimes[sender.Gamertag] = gamerTime;
    }
}
代码

下面是Update方法的代码,包括扩展过的InSession状态,你的机器上的所有玩家将数据发送到会话中的所有玩家。然后,他们会接收发送给他们的数据。

如果你在多个机器上运行这个代码,他们会自动连接到第一个机器创建的会话上。然后,开始发送时间信息并在Draw方法中将接受到的数据显示在屏幕上。

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

    if (this.IsActive)
    ...{
        switch (currentGameState) 
        ...{
            case GameState.SignIn:
...            {
                if (Gamer.SignedInGamers.Count < 1)
...                {
                    Guide.ShowSignIn(1, false);
                    log.Add("Opened User SignIn Interface");
                }
                else
                ...{
                    currentGameState = GameState.SearchSession;
                    log.Add(Gamer.SignedInGamers[0].Gamertag + " logged in - proceed to SearchSession"); 
                }
            }
            break;
            case GameState.SearchSession:
...            {
                AvailableNetworkSessionCollection activeSessions =  NetworkSession.Find(NetworkSessionType.SystemLink, 4, null);
                if (activeSessions.Count == 0)
...                {
                    currentGameState = GameState.CreateSession;
                    log.Add("No active sessions found - proceed to CreateSession");
                }
                else
...                {
                    AvailableNetworkSession networkToJoin = activeSessions[0];
                    networkSession = NetworkSession.Join(networkToJoin);

                    string myString = "Joined session hosted by " + networkToJoin.HostGamertag; 
                    myString += " with " + networkToJoin.CurrentGamerCount.ToString() + " players"; 
                    myString += " and " + networkToJoin.OpenPublicGamerSlots.ToString() + " open player slots.";
                    log.Add(myString);

                    HookSessionEvents();
                    currentGameState = GameState.InSession;
                }
            }
            break;
            case GameState.CreateSession:
...            {
                networkSession = NetworkSession.Create(NetworkSessionType.SystemLink, 4, 16);
                networkSession.AllowHostMigration = true; 
                networkSession.AllowJoinInProgress = false; 
                log.Add("New session created");

                HookSessionEvents();
                currentGameState = GameState.InSession;
            }
            break;
            case GameState.InSession:
...            {
                //send data from all local players to all other players in session
                foreach (LocalNetworkGamer localGamer in networkSession.LocalGamers)
                ...{
                    writer.Write(gameTime.TotalGameTime.Minutes); 
                    writer.Write(gameTime.TotalGameTime.Seconds); 
                    localGamer.SendData(writer, SendDataOptions.ReliableInOrder);
                }
                //receive data from all other players in session
                foreach (LocalNetworkGamer localGamer in networkSession.LocalGamers) 
                ...{
                    while (localGamer.IsDataAvailable)
...                    {
                        NetworkGamer sender;
                        localGamer.ReceiveData(reader, out sender);

                        string gamerTime = localGamer.Gamertag + " received from "; 
                        gamerTime += sender.Gamertag + ": ";
                        gamerTime += reader.ReadInt32() + "m ";
                        gamerTime += reader.ReadInt32() + "s";
                        gamerTimes[sender.Gamertag] = gamerTime;
                   }
                }

                networkSession.Update();
            }
            break;
        }
        base.Update(gameTime);
    }
}