Understanding Networking and LLAPI

Using ARDK’s peer-to-peer networking LLAPI for a fluid multiplayer experience.

Overview

Fast networking is necessary for powering shared AR experiences, particularly when synchronizing virtual content with real world objects that players can see through their camera feeds. Imagine an AR air hockey game where messages had a 0.5 second lag — a player could see their opponent move in real life and “miss” the puck while the game registers a hit.

To enable fluid multiplayer experiences, ARDK provides a LLAPI peer-to-peer networking stack (covered in this page) as well as a HLAPI layer of game abstraction APIs.

Client - Server Architecture

ARDK is backed by a server (ARBE: AR BackEnd) that provides session management, peer discovery, message routing, and a session-based data store. As the server is mostly stateless, experiences are powered by devices in the session. Experiences use either a single “authority” doing computation and sending state messages to other peers, or a distributed logic system.

Sessions

Sessions are a fundamental networking concept in ARDK. When a client sends a join request with a specified session identifier, the server will either:

  • Add the client to the session, if the session exists.

or

  • Create the session and designate the client as the “host”.

All subsequent clients that join the same session will know which client is the host. The IMultipeerNetworking.Connected(ConnectedArgs args) event is raised when a client has successfully connected to the session and is able to start receiving and sending messages.

To create and join a session:

using Niantic.ARDK.AR.Networking;
using Niantic.ARDK.Networking.MultipeerNetworkingEventArgs;
using Niantic.ARDK.Networking;
using System.Text;

...

void CreateAndJoinNetworking()
{
  // Create a networking session with default ARBE server endpoint
  var networking = MultipeerNetworkingFactory.Create();

  // Subscribe to the connected event to be notified upon connection success
  networking.Connected += OnNetworkingConnected;

  // Get a session ID from a string. In a real application, all clients that join
  //   the same session will need to agree upon a session ID.
  string sampleSessionString = "sample_id";
  var sessionIdFromString = Encoding.UTF8.GetBytes(sampleSessionString);

  // Send a Join message to ARBE with the session ID
  networking.Join(sessionIdFromString);
  return;
}

void OnNetworkingConnected(ConnectedArgs args)
{
  // Log some information from the connected message
  Debug.LogFormat
  (
    "Connected to session with client ID: {0}, and host ID: {1}. Am I the host? {2}",
    args.Self.Identifier,
    args.Host.Identifier,
    args.IsHost
  );

  // Do something now that the session is connected
}

Session Identifier Collisions

A session will time out 30 seconds after the last client leaves the session. It is possible for a new client to accidentally join a session during that timeout period, entering an in-progress or completed gamestate and triggering unintended behaviour.

It is therefore recommended to use a unique session identifiers for each session. Furthermore, appending a prefix or suffix (for example, your application name) to all session identifiers by default will also help prevent collisions.

Using NetworkingManager

To simplify the process of creating and joining a networking session, we provide a Manager you can add to your scene. The API reference and in-code comments/tool tips for NetworkSessionManager explains how to use it.

Peers

Peers are the in-code representation of clients in a networking session. Within a few seconds of connecting, a client will be notified of peers that were already in the session through the IMultipeerNetworking.PeerAdded(PeerAddedArgs args) event. That same event is raised for peers that join after the local client. The complete list of peers (not counting the local client) currently in the session can be accessed through IMultipeerNetworking.OtherPeers.

Similarly, the event IMultipeerNetworking.PeerRemoved(PeerRemovedArgs args) will be raised whenever a peer leaves the session. Peers that join and leave a session before the local client joins will not be surfaced.

To add/remove peers:

void SubscribeToPeerAddedRemoved(IMultipeerNetworking networking)
{
  networking.PeerAdded += OnPeerAdded;
  networking.PeerRemoved += OnPeerRemoved;
}

void OnPeerAdded(PeerAddedArgs args)
{
  Debug.LogFormat("Peer joined: {0}", args.Peer.Identifier);
}

void OnPeerRemoved(PeerRemovedArgs args)
{
  Debug.LogFormat("Peer left: {0}", args.Peer.Identifier);
}

Messages

The bulk of networking logic centers around sending and receiving messages - informing peers of actions, changes in game state, synchronizing animations, and so on. At the low level API (covered on this page), network messages consist of a uint tag and byte[] message.

Tags are useful for creating a contract regarding the contents of a message. For example, in a Pong game, messages tagged 1 might contain a serialized Vector3 representing the local player’s position, while messages tagged 2 contain a pair of ints representing the current score.

Sending Messages

There are a few APIs for sending messages, as well as options for the transport protocol used for sending the message.

Here are three examples of sending messages to a variety of peers, with different protocols:

// A message sent to a single peer in the session
void SendToASinglePeer(IMultipeerNetworking networking, IPeer peer, byte[] data)
{
  networking.SendDataToPeer(tag: 0, data: data, peer: peer, transportType: TransportType.UnreliableUnordered);
}

// Send data all peers in the session except self and host
void SendToAllPeersButHost(IMultipeerNetworking networking, byte[] data)
{
  // Generate a list of all other peers in the session
  var peerListCopy = networking.OtherPeers.ToList();

  // Remove the host from the list
  peerListCopy.Remove(networking.Host);

  networking.SendDataToPeers(tag: 1, data: data, peers: peerListCopy, transportType: TransportType.ReliableOrdered);
}

// Send data to everyone in the session, including the local peer
void BroadCastToSession(IMultipeerNetworking networking, byte[] data)
{
  // SendToSelf: true indicates that this message will be sent to the local peer as well
  networking.BroadcastData(tag: 2, data: data, transportType: TransportType.UnreliableOrdered, sendToSelf: true);
}

Receiving Messages

On the other end of the pipe, all peer messages received will fire an event IMultipeerNetworking.PeerDataReceived(PeerDataReceivedArgs args), regardless of the method that the message was sent with.

Note that if an empty message (data is null or empty array) is sent, you will receive an empty message (args.DataLength == 0) on receiving side as well. This can be useful when used together with Tags for debugging purposes.

using System.IO;

void SubscribeToPeerDataReceived(IMultipeerNetworking networking)
{
  networking.PeerDataReceived += OnPeerDataReceived;
}

// Every time a message is received, this will be called
void OnPeerDataReceived(PeerDataReceivedArgs args)
{
  // Log the details of the peer message
  Debug.LogFormat
  (
    "Received a message from {0}, with tag {1} and length {2}",
    args.Peer,
    args.Tag,
    args.DataLength
  );

  MemoryStream data = args.CreateDataReader();

  // Properly handle the message depending on the tag and contents
}

Transport Types

ARDK offers four different transport types to use to send messages: UnreliableUnordered, UnreliableOrdered, ReliableUnordered, and ReliableOrdered.

Note

As of ARDK 2.2, UnreliableOrdered and ReliableOrdered are supported but marked as deprecated, and may be removed in a future release. The UnreliableUnordered and ReliableUnordered types should be used instead.

Depending on the transport type selected by the sender, the receiver will (or potentially will not) receive messages with different behaviors.

Unreliable type messages may be dropped or never received. This is good for ephemeral data that is acceptable to drop, such as current position of an avatar, or a single frame of a video. If a client misses 1 message out of 20, there may be a slight stutter, but attempting to resend or recover that data will take overhead, and rolling back/waiting on a message for “perfection” will lead to a laggy experience. On the other hand, Reliable type messages are “guaranteed” to arrive, though they require more overhead, and may be slower in general.

Unordered messages can arrive out of order, while Ordered messages will retain their order upon arrival. However, this means that a chain of ReliableOrdered messages can be blocked on a single message that has not arrived yet, so be wary of using ReliableOrdered as a default.

UnreliableUnordered is currently implemented as server-relayed UDP. Messages may be received out of order (first-come, first-served). Unreliable messages don’t have the bandwidth overhead of Reliable type messages, and are less prone to large delays. In general, unreliable messages are good for time-sensitive, “droppable” actions - such as playing an animation, latest position data (which will be overwritten by the next position update), or low-overhead messaging.

ReliableUnordered is currently implemented as server-relayed TCP. Messages will be received in order. However, TCP messages come with additional overhead, and multiple consecutive dropped messages will cause larger delays in handling subsequent messages. In general, reliable messages are good for game state information, such as score, spawning assets, or anything that would degrade the shared experience if not synchronized across all devices.

In some use cases, you might want an “unreliable but sequenced” behavior that drops out-of-order messages. For example, if a sender sends messages {1, 2, 3, 4} and the receiver receives them out of order as {1, 4, 2, 3}, you would only handle messages 1 and 4 (2 and 3 are dropped). You can achieve this using the UnreliableUnordered type and embedding an increasing sequence number in the payload data that the sender increments for each new message. On the receiver side OnPeerDataReceived(), keep track of the latest sequence number you got and drop any incoming messages with an older sequence number.

See Networking Limits and Best Practices for additional best practices with transport types.

See Also