Lobby, Relay and Netcode for entities

Hi everyone, we are developing our game using DOTS and Netcode for Entities. We have an issue related to the configuration of the ServerWorld. We want to use Relay and Lobby for our players. Unfortunately, I’m having a problem with the setup. I’ll share what I’ve written. The result is that the client does not connect to the server. I’m sure I’ve made some mistakes, but I don’t understand how to make the relay direct the client traffic to my server. Can anyone help me or suggest some documentation that explains how to use RelayAllocation for server and client configurations?
At the moment server is listeninng on 0.0.0.0:0 and client is connecting to the relay Ip and relay port…remaining in connecting till the timeout

public async Task SetupClientServer(string relayJoinCode)
{
    // Set the role as ServerClient
    role = Role.ServerClient;
    // Create server and client worlds
    ServerWorld = ClientServerBootstrap.CreateServerWorld("ServerWorld");
    ClientWorld = ClientServerBootstrap.CreateClientWorld("ClientWorld");
    // Destroy all previous worlds
    DestroyAllWorld();
    // Set the injection world for GameObjects
    World.DefaultGameObjectInjectionWorld = ServerWorld;
    World.DefaultGameObjectInjectionWorld = ClientWorld;
    // Get the relay allocation using the join code
    JoinAllocation relayAllocation = await JoinRelay(relayJoinCode);
    // Check if the relay allocation is null
    if (relayAllocation == null)
    {
        Debug.LogError("Failed to join relay. Allocation is null.");
        return;
    }
    // Set up the Relay server data
    var relayServerData = GetRelayServerData(relayAllocation);
    // Configure the network driver for the server world to listen on any IP
    using var queryServer = ServerWorld.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>());
    queryServer.GetSingletonRW<NetworkStreamDriver>().ValueRW.Listen(NetworkEndpoint.AnyIpv4);
    // Configure the network driver for the client world to connect to the Relay endpoint
    using var queryClient = ClientWorld.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>());
    queryClient.GetSingletonRW<NetworkStreamDriver>().ValueRW.Connect(ClientWorld.EntityManager, relayServerData.Endpoint);
    // Invoke the event to indicate the completion of the client-server setup
    OnSetupClientServerComplete?.Invoke();
}

public async Task<JoinAllocation> JoinRelay(string joinCode)
{
    try
    {
        // Request the join allocation using the join code
        JoinAllocation joinAllocation = await RelayService.Instance.JoinAllocationAsync(joinCode);
        return joinAllocation;
    }
    catch (RelayServiceException e)
    {
        // Log the error if the join request fails
        WriteLog($"Failed to join relay with join code {joinCode}: {e.Message}");
        return null;
    }
}

static RelayServerData GetRelayServerData(JoinAllocation allocation, string connectionType = "dtls")
{
    // Find the endpoint based on the desired connection type
    var endpoint = allocation.ServerEndpoints.Find(e => e.ConnectionType == connectionType);
    if (endpoint == null)
    {
        throw new Exception($"Endpoint for connectionType {connectionType} not found");
    }
    // Prepare the server endpoint using the Relay server's IP and port
    var serverEndpoint = NetworkEndpoint.Parse(endpoint.Host, (ushort)endpoint.Port);
    // Convert byte arrays for allocation
    var allocationIdBytes = RelayAllocationId.FromByteArray(allocation.AllocationIdBytes);
    var connectionData = RelayConnectionData.FromByteArray(allocation.ConnectionData);
    var hostConnectionData = RelayConnectionData.FromByteArray(allocation.HostConnectionData);
    var key = RelayHMACKey.FromByteArray(allocation.Key);
    // Prepare the Relay server data
    var relayServerData = new RelayServerData(ref serverEndpoint, 0, ref allocationIdBytes, ref connectionData,
        ref hostConnectionData, ref key, connectionType == "dtls");
    return relayServerData;
}

You mean host, right?
If it’s a dedicated server you shouldn’t use Relay, everyone can connect directly to a dedicated server hosted on the Internet.

The server will abort (return) here since the server won’t have a valid relay join code - the server is supposed to request that.
Split this method in two, you need to have a SetupServer and a SetupClient method - you can’t combine the two (not easily anyway and it makes for much harder debugging).

Speaking of which, did you debug? Set a breakpoint and step through.

Hi @CodeSmile ,
thanks for the reply.
No, I’m running a client-server connection. However, the result seems like the server is waiting on all ports, and the client is sending a message to the relay while staying in the “connecting” state.
I’m attaching the screenshot. I’m definitely making a mistake in the relay configuration, but I don’t understand…
I’ve searched everywhere, and my code represents the result of my research… perhaps wrong.

You need to confirm that. Debug, logs, and definitely (I always recommend that) subscribe to all events and log those. You want to see as much as possible what’s happening and what isn’t.

And the code above definitely needs refactoring into client-only and server-only parts.

Another common oversight (and not well documented) is the server listen address in transport. If this is unset (in Inspector for NGO it’s called “allow remote connections”), it defaults to 127.0.0.1 and allows only local connections. It also needs to be 0.0.0.0 to allow incoming connections from any IP.

From the debug, everything seems fine. I’m not using NGO but Netcode for Entities, and I don’t have any transport to configure.

Netcode doesn’t work without a transport. I haven’t used NfE but I expect it to have a configurable transport. Most likely it’s this line:

var serverEndpoint = NetworkEndpoint.Parse(endpoint.Host, (ushort)endpoint.Port);

In NGO you have a third optional parameter. Check if you can do this:

var serverEndpoint = NetworkEndpoint.Parse(endpoint.Host, (ushort)endpoint.Port, "0.0.0.0");

At the end the best way is to follow this example from Unity:

But it’s not easy

Yeah, I know. Their example is a mess. Separated in several separate classes, using systems instead of handling it all locally. It might be good practice to write code like that but HELL is it hard to figure out as an example.

I have previously adapted their example for my own project and got something working. Here is the code I use for using Relay with Netcode for Entities (I have not added Lobby just yet). This is the same sample code from example project but adapted to use coroutines instead of state machines and for all the action to happen in the same class. IMO much more understandable.

using System;
using System.Collections;
using System.Threading.Tasks;
using Unity.Entities;
using Unity.NetCode;
using Unity.Networking.Transport;
using Unity.Networking.Transport.Relay;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Relay;
using UnityEngine;
using Extension;

namespace Managers {
    public class RelayInitializer : MonoBehaviour {
        private static RelayServerData? _relayServerData, _relayClientData;
        private static Action OnConnectionComplete;
        public static string _joinCode;
    
        // Singleton (without duplicate destroy logic) for access to the class instance from within the static methods
        private static RelayInitializer Instance { get; set; }
        protected void Awake() => Instance = this;
        
        // HOSTING

        public static void StartHost() => Instance.StartCoroutine(InitializeHost());

        private static IEnumerator InitializeHost() {
            var initializeTask = UnityServices.InitializeAsync();
            while (!initializeTask.IsCompleted)
                yield return null;
            if (ProcessTaskFail(initializeTask, nameof(initializeTask)))
                yield break;

            var signInTask = Task.CompletedTask;
            if (!AuthenticationService.Instance.IsSignedIn) {
                signInTask = AuthenticationService.Instance.SignInAnonymouslyAsync();
                while (!signInTask.IsCompleted)
                    yield return null; 
            }
            if (ProcessTaskFail(signInTask, nameof(signInTask)))
                yield break;

            var allocationTask = RelayService.Instance.CreateAllocationAsync(5);
            while (!allocationTask.IsCompleted)
                yield return null;
            if (ProcessTaskFail(allocationTask, nameof(allocationTask)))
                yield break;

            var joinCodeTask = RelayService.Instance.GetJoinCodeAsync(allocationTask.Result.AllocationId);
            while (!joinCodeTask.IsCompleted)
                yield return null;
            if (ProcessTaskFail(joinCodeTask, nameof(joinCodeTask)))
                yield break;

            _joinCode = joinCodeTask.Result;

            try {
                Debug.Log("Hosting relay data");
                _relayServerData = RelayServerDataHelper.RelayData(allocationTask.Result);
            } catch (Exception e) {
                Debug.LogException(e);
                _relayServerData = null;
                yield break;
            }

            Debug.Log("Success, players may now connect");
            while (_relayServerData == null || (!_relayServerData?.Endpoint.IsValid ?? false))
                yield return null;
                
            yield return JoinUsingCode(_joinCode);
            yield return WaitRelayConnection();
            SetupRelayHostedServerAndConnect();
        }

        private static void SetupRelayHostedServerAndConnect() {
            if (ClientServerBootstrap.RequestedPlayType != ClientServerBootstrap.PlayType.ClientAndServer) {
                UnityEngine.Debug.LogError(
                    $"Creating client/server worlds is not allowed if playmode is set to {ClientServerBootstrap.RequestedPlayType}");
                return;
            }

            var relayServerData = _relayServerData.GetValueOrDefault();
            var relayClientData = _relayClientData.GetValueOrDefault();

            var oldConstructor = NetworkStreamReceiveSystem.DriverConstructor;
            NetworkStreamReceiveSystem.DriverConstructor = new RelayDriverConstructor(relayServerData, relayClientData);
            var server = ClientServerBootstrap.CreateServerWorld("ServerWorld");
            WorldManager.RegisterServerWorld(server);
            var client = ClientServerBootstrap.CreateClientWorld("ClientWorld");
            WorldManager.RegisterClientWorld(client);
            NetworkStreamReceiveSystem.DriverConstructor = oldConstructor;

            

            WorldManager.DestroyLocalSimulationWorld();
            World.DefaultGameObjectInjectionWorld ??= server;

            // Load scene here if you want to.
            
            Debug.Log(_joinCode);

            var networkStreamEntity =
                server.EntityManager.CreateEntity(ComponentType.ReadWrite<NetworkStreamRequestListen>());
            server.EntityManager.SetName(networkStreamEntity, "NetworkStreamRequestListen");
            server.EntityManager.SetComponentData(networkStreamEntity,
                new NetworkStreamRequestListen { Endpoint = NetworkEndpoint.AnyIpv4 });

            networkStreamEntity =
                client.EntityManager.CreateEntity(ComponentType.ReadWrite<NetworkStreamRequestConnect>());
            client.EntityManager.SetName(networkStreamEntity, "NetworkStreamRequestConnect");
            client.EntityManager.SetComponentData(networkStreamEntity,
                new NetworkStreamRequestConnect { Endpoint = relayClientData.Endpoint });
            
            ProcessConnectionComplete();
        }

        // CONNECTING
    
        public static void ConnectByCode(string joinCode) => Instance.StartCoroutine(ProcessCodeConnection(joinCode));
    
        private static IEnumerator ProcessCodeConnection(string joinCode) {
            Instance.StartCoroutine(JoinExternalServer(joinCode));
            yield return WaitRelayConnection();
            ConnectToRelayServer();
        }

        private static IEnumerator WaitRelayConnection() {
            while (_relayClientData == null || (!_relayClientData?.Endpoint.IsValid ?? false))
                yield return null;
        }
        
        private static IEnumerator JoinExternalServer(string joinCode) {
            Debug.Log("Waiting for relay response");
            var setupTask = UnityServices.InitializeAsync();

            while (!setupTask.IsCompleted)
                yield return null;
            
            var signInTask = Task.CompletedTask;
            if (!AuthenticationService.Instance.IsSignedIn) {
                signInTask = AuthenticationService.Instance.SignInAnonymouslyAsync();
                while (!signInTask.IsCompleted)
                    yield return null;
            }
            if (ProcessTaskFail(signInTask, nameof(signInTask)))
                yield break;

            yield return JoinUsingCode(joinCode);
        }

        private static IEnumerator JoinUsingCode(string joinCode) {
            // Send the join request to the Relay service
            var joinTask = RelayService.Instance.JoinAllocationAsync(joinCode);
            while (!joinTask.IsCompleted)
                yield return null;
            
            if (ProcessTaskFail(joinTask, nameof(joinTask)))
                yield break;
            
            // Format the server data, based on desired connectionType
            try {
                _relayClientData = RelayServerDataHelper.RelayData(joinTask.Result);
            } catch (Exception e) {
                Debug.LogException(e);
                _relayClientData = null;
            }

            _joinCode = joinCode;
        }
    
        private static void ConnectToRelayServer() {
            var relayClientData = _relayClientData.GetValueOrDefault();
            
            var oldConstructor = NetworkStreamReceiveSystem.DriverConstructor;
            NetworkStreamReceiveSystem.DriverConstructor =
                new RelayDriverConstructor(new RelayServerData(), relayClientData);
            var client = ClientServerBootstrap.CreateClientWorld("ClientWorld");
            WorldManager.RegisterClientWorld(client);
            NetworkStreamReceiveSystem.DriverConstructor = oldConstructor;

            WorldManager.DestroyLocalSimulationWorld();
            World.DefaultGameObjectInjectionWorld ??= client;
        
            var networkStreamEntity =
                client.EntityManager.CreateEntity(ComponentType.ReadWrite<NetworkStreamRequestConnect>());
            client.EntityManager.SetName(networkStreamEntity, "NetworkStreamRequestConnect");
            client.EntityManager.SetComponentData(networkStreamEntity,
                new NetworkStreamRequestConnect { Endpoint = relayClientData.Endpoint });
            ProcessConnectionComplete();
        }

        // COMMON
    
        private static bool ProcessTaskFail(Task task, string taskName) {
            if (!task.IsFaulted) return false;
            Debug.LogError($"Task {taskName} failed.");
            Debug.LogException(task.Exception);
            return true;
        }

        public static void SubscribeToConnectionComplete(Action handler) => OnConnectionComplete += handler;

        public static void ProcessConnectionComplete() {
            if (OnConnectionComplete == null)
                return;
            OnConnectionComplete?.Invoke();
            foreach (var handler in OnConnectionComplete?.GetInvocationList()!)
                OnConnectionComplete -= (Action)handler;
        }
    }

    public class RelayDriverConstructor : INetworkStreamDriverConstructor {
        private RelayServerData _relayServerData, _relayClientData;
    
        public RelayDriverConstructor(RelayServerData serverData, RelayServerData clientData) {
            _relayServerData = serverData;
            _relayClientData = clientData;
        }

        public void CreateClientDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug) {
            var settings = DefaultDriverBuilder.GetNetworkSettings();
            settings.WithRelayParameters(ref _relayClientData);
            DefaultDriverBuilder.RegisterClientDriver(world, ref driverStore, netDebug, settings);
        }

        public void CreateServerDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug) =>
            DefaultDriverBuilder.RegisterServerDriver(world, ref driverStore, netDebug, ref _relayServerData);
    }
}

RelayServerDataHelper class:

using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Networking.Transport;
using Unity.Networking.Transport.Relay;
using Unity.Services.Relay.Models;

namespace Extension {
	public static class RelayServerDataHelper {
		private static RelayServerData GetRelayData(List<RelayServerEndpoint> endpoints, byte[] allocationIdBytes, 
			byte[] connectionDataBytes, byte[] hostConnectionDataBytes, byte[] keyBytes) {
			var endpoint = endpoints.FirstOrDefault(e => e.ConnectionType == "dtls")
			               ?? throw new InvalidOperationException($"endpoint for connectionType dtls not found");
       
			var server = NetworkEndpoint.Parse(endpoint.Host, (ushort)endpoint.Port);
    
			var allocationId = RelayAllocationId.FromByteArray(allocationIdBytes);
			var connData = RelayConnectionData.FromByteArray(connectionDataBytes);
			var hostData = RelayConnectionData.FromByteArray(hostConnectionDataBytes);
			var key = RelayHMACKey.FromByteArray(keyBytes);

			return new RelayServerData(ref server, 0, ref allocationId, ref connData, ref hostData, ref key, true); 
		}

		public static RelayServerData RelayData(JoinAllocation a) =>
			GetRelayData(a.ServerEndpoints, a.AllocationIdBytes, a.ConnectionData, a.HostConnectionData, a.Key);

		public static RelayServerData RelayData(Allocation a) =>
			GetRelayData(a.ServerEndpoints, a.AllocationIdBytes, a.ConnectionData, a.ConnectionData, a.Key);
	}
}

This also refers to WorldManager which is just a global way to access the client and server worlds. You probably already have something like this but here is mine:

using System.Diagnostics.CodeAnalysis;
using Unity.Entities;

namespace Managers {
	public static class WorldManager {
		private static World _clientWorld, _serverWorld;
		
		[SuppressMessage("ReSharper", "ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator")]
		public static void DestroyLocalSimulationWorld() {
			foreach (var world in World.All) {
				if (world.Flags != WorldFlags.Game) continue;
				world.Dispose();
				break;
			}
		}
		
		public static void RegisterServerWorld(World world) => _serverWorld = world;
		public static void RegisterClientWorld(World world) => _clientWorld = world;

		public static World GetServerWorld() => _serverWorld;
		public static World GetClientWorld() => _clientWorld;
	}
}

To use this code in your project, place a RelayInitializer in your scene and then simply call RelayInitializer.StartHost() to start host and RelayInitializer.ConnectByCode(joinCode) to connect with joinCode. If you want a method to be called once host is started or client is connected, you can use RelayInitializer.SubscribeToConnectionComplete(Method) with the delegate to which method to call once the player is fully connected.

Hope that is useful to others cause I surely spent a lot of time navigating through the hell of NFE + Relay sample project.

Hi,

We recently released our new Multiplayer Services package to streamline the usage of the Lobby, Relay, Matchmaker and Multiplay SDKs. Please have a look at Build a session with Netcode for Entities (unity.com).

Upgrading from the standalone SDK to the Multiplayer SDK mostly requires you to remove the old SDK and add the Multiplayer SDK from the package manager. Even if you don’t want to use the sessions, the same namespaces and functionalities as the standalone SDKs are available within the Multiplayer SDK.

We are also eager for feedback both about the documentation and in the SDK ease of use.

Hi @ArthurAtUnity ,
I’m very happy about this, though I found it strange that there weren’t any official guidelines on how to use this package, which, from everything I’ve read, will take our development possibilities to a really top level. Keep in mind that our core business is Meta VR, so we are used to having to carefully manage resources. DOTS would be a game-changer, and Netcode would be the ideal solution.
I will definitely give you feedback on your documentation. Let me know if you’d prefer to receive it privately or as a comment on this thread.

@MatveiKharlamov Thank you for your example code. You greatly simplified that web of complexity from the GitHub example. However, after a first look at the documentation for the new Multiplayer release, I believe we should follow this new approach.

Hi @Lothar_BreakingStudio ,
Thank you for sharing more of your context !

If you feel like the feedback would be relevant for the topic of this thread, feel free to share it here :+1:
Otherwise feel free to DM me about it …

EDIT: the step by step migration has now been published: Migrating from the standalone SDKs to the Multiplayer SDK