Tutorial: Pong - Sending and Receiving Messages

This is an example Unity project in which ARDK’s features are used to create an AR Multiplayer version of Pong. This tutorial will cover all of the Unity steps as well as C# script implementations in order to make the project work. Please note that this implementation uses low level messages to send data between players. Another version of this project will demonstrate the setup and use of High Level API objects (HLAPI) to streamline the message sending and receiving process for synchronizing players (here).

Sending and Receiving Messages with MessagingManager

The last script in this project is the MessagingManager. It is possible to have each script listen for and handle their own messages, but having a single message handling manager is useful for organization between many classes. The MessagingManager is not a Monobehaviour. Instead, it handles all of its methods through networking events and method calls.

Setup and References

The references in the MessagingManager are mostly networking related, and the MessagingManager holds references to the local GameController and BallBehaviour to signal events when a message is received.

/// A manager that handles outgoing and incoming messages
public class MessagingManager
{
  // Reference to the networking object
  private IMultipeerNetworking _networking;

  // References to the local game controller and ball
  private GameController _controller;
  private BallBehaviour _ball;

  private readonly MemoryStream _builderMemoryStream = new MemoryStream(24);

Initializing MessagingManager

The MessagingManager is initialized by the GameController, which will pass in some relevant information. The only networking event that we subscribe to is IARNetworking.Networking.PeerDataReceived. An enum is used to keep message tags consistent.

// Enums for the various message types
private enum _MessageType:
  uint
{
  BallHitMessage = 1,
  GoalScoredMessage,
  BallPositionMessage,
  SpawnGameObjectsMessage
}

// Initialize manager with relevant data and references
internal void InitializeMessagingManager
(
  IMultipeerNetworking networking,
  GameController controller
)
{
  _networking = networking;
  _controller = controller;

  _networking.PeerDataReceived += OnDidReceiveDataFromPeer;
}

// After the game is created, give a reference to the ball
internal void SetBallReference(BallBehaviour ball)
{
  _ball = ball;
}

Outgoing Messages

Whenever the local player needs to send some message to other player, it will call one of these methods. Each message contains a tag so that the receiving player knows how to parse the data. BroadcastData will send data to all other peers in the networking session, while SendDataToPeer will send data to a single peer. A helper method SerializeVector3 is used to convert a Vector3 into a byte[].

// Signal to host that a non-host has hit the ball, host should handle logic
internal void BallHitByPlayer(IPeer host, Vector3 direction)
{
  _networking.SendDataToPeer
  (
    (uint)_MessageType.BallHitMessage,
    SerializeVector3(direction),
    host,
    TransportType.UnreliableUnordered
  );
}

// Signal to non-hosts that a goal has been scored, reset the ball and update score
internal void GoalScored(String color)
{
  var message = new byte[1];

  if (color == "red")
    message[0] = 0;
  else
    message[0] = 1;

  _networking.BroadcastData
  (
    (uint)_MessageType.GoalScoredMessage,
    message,
    TransportType.ReliableUnordered
  );
}

// Signal to non-hosts the ball's position every frame
internal void BroadcastBallPosition(Vector3 position)
{
  _networking.BroadcastData
  (
    (uint)_MessageType.BallPositionMessage,
    SerializeVector3(position),
    TransportType.UnreliableUnordered
  );
}

// Spawn game objects with a position and rotation
internal void SpawnGameObjects(Vector3 position)
{
  _networking.BroadcastData
  (
    (uint)_MessageType.SpawnGameObjectsMessage,
    SerializeVector3(position),
    TransportType.ReliableUnordered
  );
}

Receiving Messages

On the other end, the callback OnDidReceiveDataFromPeer will be called every time a message is received from the network. This method will handle the message depending on the tag, and use references to the GameController and BallBehaviour to properly handle game logic. Similar to SerializeVector3, there is a helper method DeserializeVector3 that takes a byte[] and converts it back into a Vector3.

private void OnDidReceiveDataFromPeer(PeerDataReceivedArgs args)
{
  var data = args.CopyData();
  switch ((_MessageType)args.Tag)
  {
    case _MessageType.BallHitMessage:
      _ball.Hit(DeserializeVector3(data));
      break;

    case _MessageType.GoalScoredMessage:
      if (data[0] == 0)
      {
        Debug.Log("Point scored for team blue");
        _controller.BlueScore += 1;
      }
      else
      {
        Debug.Log("Point scored for team red");
        _controller.RedScore += 1;
      }

      _controller.score.text =
        string.Format
        (
          "Score: {0} - {1}",
          _controller.RedScore,
          _controller.BlueScore
        );

      break;

    case _MessageType.BallPositionMessage:
      _controller.SetBallLocation(DeserializeVector3(data));
      break;

    case _MessageType.SpawnGameObjectsMessage:
      Debug.Log("Creating game objects");
      _controller.InstantiateObjects(DeserializeVector3(data));
      break;

    default:
      throw new ArgumentException("Received unknown tag from message");
  }
}

Destroy

Make sure that we unsubscribe from networking events when destroying this object.

// Remove callback from networking object on destruction
internal void Destroy()
{
  _networking.PeerDataReceived -= OnDidReceiveDataFromPeer;
}

Serialization

Sending data over the network requires a byte[] of data. ARDK provides some helpers for serializing and deserializing various object types to facilitate message sending. This project mostly used Vector3 s for position data, so there are two helpers for serializing and deserializing Vector3 s.

// Helper to serialize a Vector3 into a byte[] to be passed over the network
private byte[] SerializeVector3(Vector3 vector)
{
  _builderMemoryStream.Position = 0;
  _builderMemoryStream.SetLength(0);

  using (var binarySerializer = new BinarySerializer(_builderMemoryStream))
    Vector3Serializer.Instance.Serialize(binarySerializer, vector);

  return _builderMemoryStream.ToArray();
}

// Helper to deserialize a byte[] received from the network into a Vector3
private Vector3 DeserializeVector3(byte[] data)
{
  using(var readingStream = new MemoryStream(data))
    using (var binaryDeserializer = new BinaryDeserializer(readingStream))
      return Vector3Serializer.Instance.Deserialize(binaryDeserializer);
}

Previous page: Game Logic and AR Events