Tutorial: Pong - HLAPI Version

How to create an AR ping-pong game using HLAPI.

About this tutorial

This tutorial covers how to create a multiplayer AR ping-pong game using the HLAPI. The scene, code, and assets referenced can be found in the Assets/ARDKExamples/PongHLAPI folder of the ARDK Examples project.

It is recommended to read the HLAPI Overview and HLAPI FAQ pages before following this tutorial.

PongHLAPI uses the same base as the basic Pong demo (low level version), but makes some changes to take advantage of ARDK’s High Level Networking API (HLAPI). The first difference is the lack of a class to manage messages — the MessagingManager. All of the data that was previously passed through the MessagingManager in the low level version of Pong is now being passed through HLAPI objects such as the UnreliableTransformBroadcastPacker or the MessageStreamReplicator. Fields shared between players can be automatically set and synchronized by using NetworkedField instances. Finally, objects that were spawned locally for each player and synchronized through messages can now be created with the NetworkSpawner.

Let’s Get Started!

Networked Groups and Data Handlers

Each piece of shared data resides in its own unique < NetworkGroup, NetworkedDataHandler > combination, which each client creates, opens, and registers before communication. Think of a NetworkGroup as an entity to be replicated, with a NetworkedDataHandler for each property. For example, a “Player” Group could have data handlers registered for health and energy; an “Enemy” Group could have a handler for damage.

As long as the <NetworkGroup, NetworkedDataHandler> definitions match on all peers in the networking session, the HLAPI can handle sending and receiving data for you.

Example:

// Code snippet from GameController.cs

private IHlapiSession _manager;
private IAuthorityReplicator _auth;
private bool _isHost;
private IPeer _self;

private void OnDidConnect(ConnectedArgs connectedArgs)
{
  _isHost = connectedArgs.IsHost;
  _self = connectedArgs.Self;

  // 19244 is an arbitrary magic number for this HlapiSession's message tag
  // (so don't use 19244 again in the same project).
  _manager = new HlapiSession(19244);

  // Similarly, 4321 is an arbitrary magic number for this NetworkGroup
  // (so don't use it again for a different group).
  var group = _manager.CreateAndRegisterGroup(new NetworkId(4321));

  // An AuthorityReplicator is a type of NetworkedDataHandler,
  // and thus created using a reference to a NetworkGroup.
  _auth = new GreedyAuthorityReplicator("pongHLAPIAuth", group);

  // Only one peer should try to claim authority. In this example, it's easy
  // for that peer to be the host, since there is only a single host.
  // More explanation on Authority can be found in the HLAPI FAQ page.
  _auth.TryClaimRole(_isHost ? Role.Authority : Role.Observer, () => {}, () => {});

  // More code...
 }

As seen in the snippet above, groups and data handlers can be manually constructed from an HlapiSession instance. Helpfully, network spawned objects with NetworkedBehaviour components automatically create and manage an HlapiSession with groups and data handlers.

Sending Updates:

To send data updates, call the HlapiSession.SendQueuedData method. This can be called in the Update() loop to ensure that the latest data is sent once per frame. Each HlapiSession is only responsible for the groups and data handlers that it created.

private void Update()
{
  if (_manager != null)
    _manager.SendQueuedData();

  // More code...
}

NetworkedField

Some of the data that was explicitly serialized and sent as a message in the low level version of Pong are replicated as NetworkedField objects. That includes the score (a string), the game start flag (a bool), and the position of the field (a Vector3).

Networking Fields

// Code snippet from GameController.cs

private INetworkedField<NetString> _scoreText;
private Text score;

private void OnDidConnect(ConnectedArgs connectedArgs)
{
  // ...
  // Continuing from code snippet above...

  // A NetworkedDataDescriptor defines the sending/receiving rules for data,
  // as well as network protocol. This descriptor defines that only the peer
  // with Authority can make changes to the field, and all peers that are
  // Observers will see the change.
  var authToObserverDescriptor =
    _auth.AuthorityToObserverDescriptor(TransportType.ReliableUnordered);

  // ...

  // Create a NetworkedField using the NetworkedDataDescriptor and the NetworkGroup
  _scoreText = new NetworkedField<string>("scoreText", authToObserverDescriptor, group);

  // Events are also fired upon the field changing
  _scoreText.ValueChanged += OnScoreDidChange;

  // More code...
}

private void OnScoreDidChange(NetworkedFieldValueChangedArgs<string> args)
{
  score.text = args.Value.GetOrDefault();
}

Whenever the peer with the role of Authority wants to set the field’s value, it simply calls:

_scoreText.Value = string.Format("Score: {0} - {1}", RedScore, BlueScore);

In the rest of the OnDidConnect method in GameController.cs, similar fields are set up for the field position and game start flag.

Note

In order to replicate boolean fields, the bool must be serialized as a byte, and then converted back to a bool upon receipt. ARDK includes default serializers for commonly used types (see the ItemSerializers namespace), but any type can be networked as long as an BaseItemSerializer<T> is created and registered using GlobalSerializer.RegisterItemSerializer()

UnreliableBroadcastTransformPacker

The UnreliableBroadcastTransformPacker is another type of NetworkedDataHandler that can be used to automatically synchronize a UnityEngine.Transform across multiple devices. Once the packer is set up, it will update the given transform every time HlapiSession.SendQueuedData() is called, with no additional setup needed from observers.

// Code from BallBehaviour.cs
// The BallBehaviour class extends NetworkedBehaviour, so it uses its Owner to access the its Auth
// and Group (which both belong to an internal HlapiSession). The UnreliableBroadcastTransformPacker
// works with any authority and data handlers, regardless of how they were constructed.
protected override void SetupSession(out Action initializer, out int order)
{
  initializer = () =>
  {
    var auth = Owner.Auth;

    // If a peer with a role other than Authority changes the transform managed by the packer,
    // the changes will be seen locally but not broadcast to other peers.
    var descriptor = auth.AuthorityToObserverDescriptor(TransportType.UnreliableUnordered);

    // Simply creating the UnreliableBroadcastTransformPacker will set up broadcasting/receiving
    // the Position property of the given Transform (any combination of Position, Rotation,
    // and Scale can be replicated).
    new UnreliableBroadcastTransformPacker
    (
      "netTransform",
      transform,
      descriptor,
      TransformPiece.Position,
      Owner.Group
    );
  };

  // More code...
}

MessageStreamReplicator

The MessageStreamReplicator (also a NetworkedDataHandler) allows a peer to send a message (of any serializable type) to peer(s) in the session. Unlike the MultipeerNetworking.SendDataToPeer method, the MessageStreamReplicator has a NetworkGroup, so there is no need for a message tag.

// Code snippet from GameController.cs

private void OnDidConnect(ConnectedArgs connectedArgs)
{
  // ...
  // Continuing method from second code snippet...

  _hitStreamReplicator =
    new MessageStreamReplicator<Vector3>
    (
      "hitMessageStream",
      _arNetworking.Networking.AnyToAnyDescriptor(TransportType.ReliableOrdered),
      group
    );

  _hitStreamReplicator.MessageReceived +=
    (args) =>
    {
      Debug.Log("Ball was hit");

      if (_auth.LocalRole != Role.Authority)
        return;

      _ballBehaviour.Hit(args.Message);
    };
}

Sending a message:

// Code from Update method in GameController.cs

// After some code to calculate the bounce direction vector...

// Send the hit message to the Authority (the host in this case)
_hitStreamReplicator.SendMessage(bounceDirection, _auth.PeerOfRole(Role.Authority));

Network spawning classes

All three of these classes are involved in spawning an object simultaneously for multiple peers and setting up the behavior of that object after spawning:

  • The NetworkedUnityObject component can be attached to an object to mark it valid for Network Spawning.

  • The NetworkedBehaviour component determines the behavior of a NetworkedUnityObject after spawning, akin to a Monobehaviour (in fact, it inherits from MonoBehaviour). Multiple can be attached to a single GameObject.

  • Finally, the NetworkSpawner is the static class that handles actually spawning the GameObject.

Extending NetworkedBehaviours

Like mentioned previously, when a NetworkedBehaviour is created it will open its own NetworkGroup with an internally managed HlapiSession. So all classes that inherit from NetworkedBehaviour only need to implement the abstract SetupSession method to:

  • Specify a callback that should be invoked when the object is spawned

  • Specify the relative order to invoke that callback (useful if one behaviour must be initialized before another). See the Hlapi Network Spawning page for details regarding setting up and using network spawning.

In PongHLAPI, both PlayingFieldBehaviour and PlayerAvatarBehaviour are simple NetworkedBehaviours that set up replicating their position and rotation when those values are changed by the peer with authority. (The NetTransform component is a helper component that implements that as well, if that is the only behavior needed.)

[RequireComponent(typeof(AuthBehaviour))]
public class PlayingFieldBehaviour: NetworkedBehaviour
{
  protected override void SetupSession
    (
    out Action initializer,
    out int order
  )
  {
    initializer = () =>
    {
      var auth = GetComponent<AuthBehaviour>();

      new UnreliableBroadcastTransformPacker
        (
        "netTransform",
        transform,
        auth.AuthorityToObserverDescriptor(TransportType.UnreliableUnordered),
        TransformPiece.Position,
        Owner.Group
      );
    };

    order = 0;
  }
}

If there is any behavior that should only occur for the peer with authority and not for the observing peers, methods in NetworkedBehaviour classes should first check the Owner.Authority.LocalRole value.

// Code snippet from GameController.cs

private void InstantiateObjects(Vector3 position)
{
  // ...

  // The playerPrefab is spawned for all players
  _player =
    playerPrefab.NetworkSpawn
    (
      _arNetworking.Networking,
      position + startingOffset,
      Quaternion.identity,
      Role.Authority
    ).gameObject;

  // Only the host should spawn the remaining objects
  if (!_isHost)
    return;

  // Instantiate the playing field at floor level
  _playingField =
    playingFieldPrefab.NetworkSpawn
    (
      _arNetworking.Networking,
      position,
      Quaternion.identity
    )
    .gameObject;

  // More code...
}