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