Use Relay with UTP

The Relay SDK works well with the Unity Transport Package (UTP), a modern networking library built for the Unity game engine to abstract networking. UTP lets developers focus on the game rather than low-level protocols and networking frameworks. UTP is netcode-agnostic, which means it can work with various high-level networking code abstractions, supports all Unity’s netcode solutions, and works with other netcode libraries.

Note: The recommended best practice is to use Relay with NGO. If you want to use something besides NGO (such as custom netcode) to synchronize GameObjects, you can use Relay and UTP for transport and Relay proxy purposes.

Configure the Relay SDK

You must configure your Unity project for Unity before using Relay. To do so, visit Get started with Relay. After you’ve enabled the Relay service, link your Unity Project ID through the Unity Editor.

Check out the Simple Relay sample (using UTP) to learn the basics of using Relay with UTP.

Simple Relay sample (using UTP)

This sample expands on the Simple Relay sample. It demonstrates the basics of building a multiplayer game using Relay with UTP. Import the sample from the Relay package in the Package Manager, and follow along with the included script.

This sample works with at least two separate clients. One host player and one or more joining players.

You might want to run multiple clients on the same machine when you test the sample project code. You can do so by building the project into an executable binary or using a solution, such as ParrelSync or Multiplayer Play Mode, that allows you to run multiple Unity Editor instances of the same project.

If you build the project into a binary, you can use the binary in conjunction with the open Unity Editor or with clones of the binary.

Upon playing the scene, you can start the client as a host player (who initiates the game session) or as a joining player.

You must have at least one host player, and the host player is necessary to run the game for the joining players.

The flow for the host player is as follows:

  • Sign in

  • Get regions (optional)

  • Select region (optional)

  • Allocate game session

  • Bind to the selected Relay server

  • Get join code

The host player shares the join code with joining players. After they join and connect to the host player, the host player can:

  • Send messages to all connected players

  • Disconnect all connected players

The flow for a player is as follows:

  • Sign in

  • Join the game session using the host player's join code

  • Bind to the selected Relay server

  • Request a connection to the host player

Once connected to the host player, a player can:

  • Send a message to the host player

  • Disconnect from the host player

Import the Simple Relay sample (using UTP) project

  1. Open a Relay project with the Unity Editor (version 2020.3). If you don’t have a Relay project, visit Set up a Relay project.

  2. Open the Package Manager and navigate to the Relay package.

  3. Expand the Samples section.

  4. Select Import to import the Simple Relay sample (using UTP) project.

  5. Now that you have imported the Simple Relay sample project, you can open it as a scene. It's located within the current project under Assets/Samples/Relay/1.0.4/Simple Relay sample (using UTP).

  6. Select File > Open Scene.

  7. Navigate to the Simple Relay sample (using UTP) scene.

Initialize Unity Services

Before using any Unity services, such as Relay, you must initialize the UnityServices singleton. Check out the Services Core API.

await UnityServices.InitializeAsync();

Authenticate the player

You must authenticate both the host player and the connecting players. The simplest way to authenticate players is with the Authentication service's SignInAnonymouslyAsync() method. Visit How to use Anonymous Sign-in and How to use Social Sign-in.

For more information, visit About Unity Authentication and Unity Authentication use cases.

public async void OnSignIn()
{
    	await  AuthenticationService.Instance.SignInAnonymouslyAsync();
    	playerId = AuthenticationService.Instance.PlayerId;

    	Debug.Log($"Signed in. Player ID: {playerId}");
}

Host player

When your game client functions as a host player, it must be able to create an allocation, request a join code, configure the connection type, and create an instance of the NetworkDriver to bind the Relay server and listen for connection requests from joining players.

The host player update loop

Before starting the first steps of the allocation flow, you need to set the host player’s update loop, which handles incoming connections and messages.

Note: For simplicity, the sample code calls this loop on every Update() frame. Handling the host player update loop in a separate coroutine makes more sense in practice.

void UpdateHost()
{
    	// Skip update logic if the Host isn't yet bound.
    	if (!hostDriver.IsCreated || !hostDriver.Bound)
    	{
        	return;
    	}

    	// This keeps the binding to the Relay server alive,
    	// preventing it from timing out due to inactivity.
    	hostDriver.ScheduleUpdate().Complete();

    	// Clean up stale connections.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	if (!serverConnections[i].IsCreated)
        	{
            	Debug.Log("Stale connection removed");
            	serverConnections.RemoveAt(i);
            	--i;
        	}
    	}

    	// Accept incoming client connections.
    	NetworkConnection incomingConnection;
    	while ((incomingConnection = hostDriver.Accept()) != default(NetworkConnection))
    	{
        	// Adds the requesting Player to the serverConnections list.
        	// This also sends a Connect event back the requesting Player,
        	// as a means of acknowledging acceptance.
        	Debug.Log("Accepted an incoming connection.");
        	serverConnections.Add(incomingConnection);
    	}

    	// Process events from all connections.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	Assert.IsTrue(serverConnections[i].IsCreated);

        	// Resolve event queue.
        	NetworkEvent.Type eventType;
        	while ((eventType = hostDriver.PopEventForConnection(serverConnections[i], out var stream)) != NetworkEvent.Type.Empty)
        	{
            	switch (eventType)
            	{
                	// Handle Relay events.
                	case NetworkEvent.Type.Data:
                    	FixedString32Bytes msg = stream.ReadFixedString32();
                    	Debug.Log($"Server received msg: {msg}");
                    	hostLatestMessageReceived = msg.ToString();
                    	break;

                	// Handle Disconnect events.
                	case NetworkEvent.Type.Disconnect:
                    	Debug.Log("Server received disconnect from client");
                    	serverConnections[i] = default(NetworkConnection);
                    	break;
            	}
        	}
    	}
}

Create an allocation

The following code snippet has a function, OnAllocate, that shows how to use the Relay SDK to create an allocation.

public async void OnAllocate()
{
    	Debug.Log("Host - Creating an allocation. Upon success, I have 10 seconds to BIND to the Relay server that I've allocated.");

    	// Determine region to use (user-selected or auto-select/QoS)
    	string region = GetRegionOrQosDefault();
    	Debug.Log($"The chosen region is: {region ?? autoSelectRegionName}");

    	// Set max connections. Can be up to 100, but note the more players connected, the higher the bandwidth/latency impact.
    	int maxConnections = 4;

    	// Important: After the allocation is created, you have ten seconds to BIND, else the allocation times out.
    	hostAllocation = await RelayService.Instance.CreateAllocationAsync(maxConnections, region);
    	Debug.Log($"Host Allocation ID: {hostAllocation.AllocationId}, region: {hostAllocation.Region}");

    	// Initialize NetworkConnection list for the server (Host).
    	// This list object manages the NetworkConnections which represent connected players.
    	serverConnections = new NativeList<NetworkConnection>(maxConnections, Allocator.Persistent);
}

Warning: You won't be able to connect to the Relay server if there's a mismatch between the information provided through SetRelayServerData / SetRelayClientData and the information obtained from the allocation. For example, you'll receive a "Failed to connect to server" error message if the isSecure parameter doesn't match.

One of the easiest ways to prevent this from happening is to build the RelayServerData directly from the allocation. However, you must use Netcode for GameObjects (NGO) version 1.1.0 or later.

Bind to the Relay server and listen for connections

The following code snippet has a function, OnBindHost, that shows how to use the Relay SDK to bind to a host player to the Relay server and listen for connection requests from joining players.

public void OnBindHost()
{
    	Debug.Log("Host - Binding to the Relay server using UTP.");

    	// Extract the Relay server data from the Allocation response.
    	var relayServerData = new RelayServerData(hostAllocation, "udp");

    	// Create NetworkSettings using the Relay server data.
    	var settings = new NetworkSettings();
    	settings.WithRelayParameters(ref relayServerData);

    	// Create the Host's NetworkDriver from the NetworkSettings.
    	hostDriver = NetworkDriver.Create(settings);

    	// Bind to the Relay server.
    	if (hostDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
    	{
        	Debug.LogError("Host client failed to bind");
    	}
    	else
    	{
        	if (hostDriver.Listen() != 0)
        	{
            	Debug.LogError("Host client failed to listen");
        	}
        	else
        	{
            	Debug.Log("Host client bound to Relay server");
        	}
    	}
}

Request a join code

The following code snippet has a function, OnJoinCode, that shows how to request a join code after successfully requesting an Allocation. The host player shares this join code with joining players so they can join the host player in their game session.

public async void OnJoinCode()
{
    	Debug.Log("Host - Getting a join code for my allocation. I would share that join code with the other players so they can join my session.");

    	try
    	{
        	joinCode = await RelayService.Instance.GetJoinCodeAsync(hostAllocation.AllocationId);
        	Debug.Log("Host - Got join code: " + joinCode);
    	}
    	catch (RelayServiceException ex)
    	{
        	Debug.LogError(ex.Message + "\n" + ex.StackTrace);
    	}
}

Joining player

When your game client functions as a joining player, it must be able to join an allocation, configure the connection type, and create an instance of its NetworkDriver to bind to the host player’s Relay server and send a connection request to the host player.

Note: The default behavior of the DefaultDriverBuilder.RegisterClientDriver method uses to create the NetworkDriver is to avoid using a socket connection if possible when a player joins the Relay server. However, the client uses a socket to connect to the server in the following scenarios:

  • The NetworkSimulator is enabled.
  • The ClientServerBootstrap.RequestPlayType is set to Client.
  • The ServerWorld isn't present (for any reason.)

The player update loop

As with the host player, before joining the host player’s game session, you need to set the joining player’s update loop, which handles incoming messages.

Note: For simplicity, the sample code calls this loop on every Update() frame. However, handling the player update loop in a separate coroutine makes more sense in practice.

void UpdatePlayer()
{
    	// Skip update logic if the Player isn't yet bound.
    	if (!playerDriver.IsCreated || !playerDriver.Bound)
    	{
        	return;
    	}

    	// This keeps the binding to the Relay server alive,
    	// preventing it from timing out due to inactivity.
    	playerDriver.ScheduleUpdate().Complete();

    	// Resolve event queue.
    	NetworkEvent.Type eventType;
    	while ((eventType = clientConnection.PopEvent(playerDriver, out var stream)) != NetworkEvent.Type.Empty)
    	{
        	switch (eventType)
        	{
            	// Handle Relay events.
            	case NetworkEvent.Type.Data:
                	FixedString32Bytes msg = stream.ReadFixedString32();
                	Debug.Log($"Player received msg: {msg}");
                	playerLatestMessageReceived = msg.ToString();
                	break;

            	// Handle Connect events.
            	case NetworkEvent.Type.Connect:
                	Debug.Log("Player connected to the Host");
                	break;

            	// Handle Disconnect events.
            	case NetworkEvent.Type.Disconnect:
                	Debug.Log("Player got disconnected from the Host");
                	clientConnection = default(NetworkConnection);
                	break;
        	}
    	}
}

Join an allocation

The following code snippet has a function, OnJoin, that shows how to use the Relay SDK to join an allocation with a join code and configure the connection type as UDP.

public async void OnJoin()
{
    	// Input join code in the respective input field first.
    	if (String.IsNullOrEmpty(JoinCodeInput.text))
    	{
        	Debug.LogError("Please input a join code.");
        	return;
    	}

    	Debug.Log("Player - Joining host allocation using join code. Upon success, I have 10 seconds to BIND to the Relay server that I've allocated.");

    	try
    	{
        	playerAllocation = await RelayService.Instance.JoinAllocationAsync(JoinCodeInput.text);
        	Debug.Log("Player Allocation ID: " + playerAllocation.AllocationId);
    	}
    	catch (RelayServiceException ex)
    	{
        	Debug.LogError(ex.Message + "\n" + ex.StackTrace);
    	}
}

Warning: You won't be able to connect to the Relay server if there's a mismatch between the information provided through SetRelayServerData / SetRelayClientData and the information obtained from the allocation. For example, you'll receive a "Failed to connect to server" error message if the isSecure parameter doesn't match.

One of the easiest ways to prevent this from happening is to build the RelayServerData directly from the allocation. However, you must use Netcode for GameObjects (NGO) version 1.1.0 or later.

Bind to the Relay server and connect to the host player

The following code snippet has a function, OnBindPlayer, that shows how to bind to the Relay server.

Note: This binds the player to the Relay server. The next step connects the player to the host player so they can begin exchanging messages.

public void OnBindPlayer()
{
    	Debug.Log("Player - Binding to the Relay server using UTP.");

    	// Extract the Relay server data from the Join Allocation response.
    	var relayServerData = new RelayServerData(playerAllocation, "udp");
    	// Create NetworkSettings using the Relay server data.
    	var settings = new NetworkSettings();
    	settings.WithRelayParameters(ref relayServerData);

    	// Create the Player's NetworkDriver from the NetworkSettings object.
    	playerDriver = NetworkDriver.Create(settings);

    	// Bind to the Relay server.
    	if (playerDriver.Bind(NetworkEndPoint.AnyIpv4) != 0)
    	{
        	Debug.LogError("Player client failed to bind");
    	}
    	else
    	{
        	Debug.Log("Player client bound to Relay server");
    	}
}

Bind to the Relay server and connect to the host player

The following code snippet has a function, OnConnectPlayer, that shows how to bind to send a connection request to the host player. This appears as an incoming connection in the host player’s update loop, as it calls hostDriver.Accept().

public void OnConnectPlayer()
{
    	Debug.Log("Player - Connecting to Host's client.");
    	// Sends a connection request to the Host Player.
    	clientConnection = playerDriver.Connect();
}

Sending messages

Now that the host player and player are connected, they can communicate by sending messages.

Sending messages as the host

The following code snippet has a function, OnHostSendMessage, that shows how a host player can send messages to its connected players. In this sample, the host broadcasts messages to all connected players.

public void OnHostSendMessage()
{
    	if (serverConnections.Length == 0)
    	{
        	Debug.LogError("No players connected to send messages to.");
        	return;
    	}

    	// Get message from the input field, or default to the placeholder text.
    	var msg = !String.IsNullOrEmpty(HostMessageInput.text) ? HostMessageInput.text : HostMessageInput.placeholder.GetComponent<Text>().text;

    	// In this sample, we will simply broadcast a message to all connected clients.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	if (hostDriver.BeginSend(serverConnections[i], out var writer) == 0)
        	{
            	// Send the message. Aside from FixedString32, many different types can be used.
            	writer.WriteFixedString32(msg);
            	hostDriver.EndSend(writer);
        	}
    	}
}

Sending messages as a player

The following code snippet has a function, OnPlayerSendMessage, that shows how a player can send a message to the host player.

public void OnPlayerSendMessage()
{
    	if (!clientConnection.IsCreated)
    	{
        	Debug.LogError("Player isn't connected. No Host client to send message to.");
        	return;
    	}

    	// Get message from the input field, or default to the placeholder text.
    	var msg = !String.IsNullOrEmpty(PlayerMessageInput.text) ? PlayerMessageInput.text : PlayerMessageInput.placeholder.GetComponent<Text>().text;
    	if (playerDriver.BeginSend(clientConnection, out var writer) == 0)
    	{
        	// Send the message. Aside from FixedString32, many different types can be used.
        	writer.WriteFixedString32(msg);
        	playerDriver.EndSend(writer);
    	}
}

Disconnecting

After a player finishes, they can disconnect from the host. A host player can also forcefully disconnect a connected player.

Disconnecting a player as the host

The following code snippet has a function, OnDisconnectPlayers, that shows how a host player can forcefully disconnect a connected player. In this sample, the host player disconnects all connected players.

public void OnDisconnectPlayers()
{
    	if (serverConnections.Length == 0)
    	{
        	Debug.LogError("No players connected to disconnect.");
        	return;
    	}

    	// In this sample, we will simply disconnect all connected clients.
    	for (int i = 0; i < serverConnections.Length; i++)
    	{
        	// This sends a disconnect event to the destination client,
        	// letting them know they're disconnected from the Host.
        	hostDriver.Disconnect(serverConnections[i]);

        	// Here, we set the destination client's NetworkConnection to the default value.
        	// It will be recognized in the Host's Update loop as a stale connection, and be removed.
        	serverConnections[i] = default(NetworkConnection);
    	}
}

Disconnecting as a player

The following code snippet has a function, OnDisconnect, that shows how a player can disconnect from the host player.

public void OnDisconnect()
{
    	// This sends a disconnect event to the Host client,
    	// letting them know they're disconnecting.
    	playerDriver.Disconnect(clientConnection);

    	// We remove the reference to the current connection by overriding it.
    	clientConnection = default(NetworkConnection);
}