Peer-to-peer messaging on the Zune sample

Many thanks to Shane DeSeranno of the Zune team for helping me wrap my head around this concept.

One of my previous posts included a simple game called Robot Tag. The concept I used for this game is that each peer is always reporting its position and reading the position of the other. There is no mechanism for “events,” which can make it difficult to send game state messages.

I was originally hoping to be able to have peers subscribe directly to the events of some shared object like the NetworkSession events. While you can’t do that directly, it is possible to do it by sending and reading packets under the covers. The question I kept asking myself was, how do I know what kind of data to read in and when, and how can I be assured that data won’t get lost, etc.

Here, I’ll attempt to explain what I’ve learned about sending messages as packets.

First, let’s assume a GameScreen of some variety (part of the Zune Network State Management example). You can plug this screen into the RobotTag project if you want. Here is the code (not commented properly yet, sorry):

 using System;
using System.Collections.Generic;
using System.Text;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Input;

using ZuneScreenManager;

namespace ZuneSimpleEvents
{
    public enum MessageType : byte
    {
        UpPressed = 1,
        DownPressed = 2,
        LeftPressed = 3,
        RightPressed = 4
    }

    public class SimpleEventScreenTest : GameScreen
    {
        private string status = "";        

        public SimpleEventScreenTest()
        {
            status = "";
        }

        public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen)
        {
            UpdateNetworkSession();
        }

        public override void Draw(GameTime gameTime)
        {
            ScreenManager.GraphicsDevice.Clear(Color.Black);
            ScreenManager.SpriteBatch.Begin();
            ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, status, Vector2.Zero, Color.White);
            ScreenManager.SpriteBatch.End();
        }

        public override void HandleInput(InputState input)
        {                            
            if (input.IsNewButtonPress(Buttons.DPadUp))            
                SendMessage(MessageType.UpPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);

            if (input.IsNewButtonPress(Buttons.DPadDown))
                SendMessage(MessageType.DownPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);

            if (input.IsNewButtonPress(Buttons.DPadLeft))
                SendMessage(MessageType.LeftPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);

            if (input.IsNewButtonPress(Buttons.DPadRight))
                SendMessage(MessageType.RightPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);
        }

        private void UpdateNetworkSession()
        {
            // Read in any messages from the other gamers
            NetworkSessionManager.Update();            

            foreach (LocalNetworkGamer localGamer in NetworkSessionManager.NetworkSession.LocalGamers)
            {
                while (localGamer.IsDataAvailable)
                {                    
                    NetworkGamer sender;
                    localGamer.ReceiveData(NetworkSessionManager.PacketReader, out sender);

                    // Get the sender's event
                    MessageType type = (MessageType)NetworkSessionManager.PacketReader.ReadByte();

                    switch (type)
                    {
                        case MessageType.UpPressed:
                            status += NetworkSessionManager.PacketReader.ReadString() + " pressed Up." + "\r\n";
                            break;

                        case MessageType.DownPressed:
                            status += NetworkSessionManager.PacketReader.ReadString() + " pressed Down." + "\r\n";
                            break;

                        case MessageType.LeftPressed:
                            status += NetworkSessionManager.PacketReader.ReadString() + " pressed Left." + "\r\n";
                            break;

                        case MessageType.RightPressed:
                            status += NetworkSessionManager.PacketReader.ReadString() + " pressed Right." + "\r\n";
                            break;

                        default: break;
                    }
                }
            }
        }

        private void SendMessage(MessageType type, string gamer)
        {
            NetworkSessionManager.PacketWriter.Write((byte)type);
            NetworkSessionManager.PacketWriter.Write(gamer);

            foreach (LocalNetworkGamer localGamer in NetworkSessionManager.NetworkSession.LocalGamers)
            {
                localGamer.SendData(NetworkSessionManager.PacketWriter, SendDataOptions.InOrder);
            }
        }
    }
}

 

Sending Messages

The goal is to have a message sent on some event (in this case, a key press). First, let’s examine the MessageType enumeration:

 public enum MessageType : byte
{
    UpPressed = 1,
    DownPressed = 2,
    LeftPressed = 3,
    RightPressed = 4
}

For each message we have assigned a unique byte. This is what is actually sent over the airwaves to indicate what’s coming next.

When a button on the Zune is pressed, we send one of these messages, and then we send a string containing the gamertag as some sample data. Let’s look at the HandleInput method:

 public override void HandleInput(InputState input)
{                            
    if (input.IsNewButtonPress(Buttons.DPadUp))            
        SendMessage(MessageType.UpPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);

    if (input.IsNewButtonPress(Buttons.DPadDown))
        SendMessage(MessageType.DownPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);

    if (input.IsNewButtonPress(Buttons.DPadLeft))
        SendMessage(MessageType.LeftPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);

    if (input.IsNewButtonPress(Buttons.DPadRight))
        SendMessage(MessageType.RightPressed, NetworkSessionManager.NetworkSession.LocalGamers[0].Gamertag);
}

It uses the InputState class from the aforementioned Zune Network Game State Management sample to make input gathering simple. Then, when a button is pressed, we send out a packet using the SendMessage method (NetworkSessionManager is also part of the sample above).

 private void SendMessage(MessageType type, string gamer)
{
    NetworkSessionManager.PacketWriter.Write((byte)type);
    NetworkSessionManager.PacketWriter.Write(gamer);

    foreach (LocalNetworkGamer localGamer in NetworkSessionManager.NetworkSession.LocalGamers)
    {
        localGamer.SendData(NetworkSessionManager.PacketWriter, SendDataOptions.InOrder);
    }
}

First we write the byte, then the data, and send it to all local gamers. Note that a local gamer is someone playing on the same machine, so there shouldn’t be more than one, but I put the foreach in there for compatibility with Xbox and PC.

 

Receiving and Processing Messages

Finally, to tie it all together, we have the UpdateNetworkSession method, which is called in the screen’s Update. This is responsible for reading packets and doing things with them.

    1: private void UpdateNetworkSession()
    2: {
    3:     // Read in any messages from the other gamers
    4:     NetworkSessionManager.Update();            
    5:  
    6:     foreach (LocalNetworkGamer localGamer in NetworkSessionManager.NetworkSession.LocalGamers)
    7:     {
    8:         while (localGamer.IsDataAvailable)
    9:         {                    
   10:             NetworkGamer sender;
   11:             localGamer.ReceiveData(NetworkSessionManager.PacketReader, out sender);
   12:  
   13:             // Get the sender's event
   14:             MessageType type = (MessageType)NetworkSessionManager.PacketReader.ReadByte();
   15:  
   16:             switch (type)
   17:             {
   18:                 case MessageType.UpPressed:
   19:                     status += NetworkSessionManager.PacketReader.ReadString() + " pressed Up." + "\r\n";
   20:                     break;
   21:  
   22:                 case MessageType.DownPressed:
   23:                     status += NetworkSessionManager.PacketReader.ReadString() + " pressed Down." + "\r\n";
   24:                     break;
   25:  
   26:                 case MessageType.LeftPressed:
   27:                     status += NetworkSessionManager.PacketReader.ReadString() + " pressed Left." + "\r\n";
   28:                     break;
   29:  
   30:                 case MessageType.RightPressed:
   31:                     status += NetworkSessionManager.PacketReader.ReadString() + " pressed Right." + "\r\n";
   32:                     break;
   33:  
   34:                 default: break;
   35:             }
   36:         }
   37:     }
   38: }

Again, we have the foreach LocalNetworkGamer in LocalGamers line for compatibility with other platforms. In the context of this method, localGamer refers to “my Zune.”

The while loop is crucial to the correct operation of this method. As Shane mentioned, if you exclude the while loop, any incoming packets that are not read the first time around will be delayed 1/60th of a second (the frame rate).

What this method does is receive data and then pop it off the packet queue according to what the message type (which should always be first) is. This allows you to send different data structures back and forth without using the Tag property of the gamer. For example, you could send a message called ThreeInts and then write three ints, then in this method, you could have a case for the ThreeInts message type and make sure to tell the packet reader to read three consecutive integers.

How does it get all the data if you’re breaking after reading the data? Well, if, by some magical stroke, the same peer gets sent a few messages at once, for example:

  1. [byte] UpPressed
  2. [string] Player 1
  3. [byte] DownPressed
  4. [string] Player 2
  5. [byte] RightPressed
  6. [string] Player 3

… then after popping off UpPressed and “Player 1”, localGamer.IsDataAvailable will still be true, and will continue until all of that information has been emptied out of the packet reader.

Conclusion

Now, you can understand how you might build out a more complicated messaging framework using this approach. You can even (and should) employ events to respond when certain messages are received, just to make your life a little easier.

Up next: A big surprise…