Seeking Advice on WebSockets Configuration for WebGL Client and Unity NGO Server

I’m trying to establish a WebSocket connection between a WebGL client and a Unity Services NGO game server but am encountering issues.

My Setup:

  • Unity Editor (local client) connects to an NGO Linux server (WebSockets inactive).
  • WebGL client connects to a local Unity Editor server (WebSockets active).

Actions Taken:

  • Enabled “Use Websockets” in Unity Transport.
  • Built and deployed a Linux server to NGO and a WebGL client.

Problem:

After setting up the connection, the WebGL client receives the server’s IP and port, but the connection fails. I’ve tried using WebSocketNetworkInterface as suggested in the Unity Transport 2.1 documentation, but it results in an error indicating the interface cannot be found.

Request for Advice:

  • Is there a specific WebSocket API or external library I should use for the WebGL client to connect with the server via Transport WebSocket?
  • Are there additional WebSocket configurations required for this setup in a multiplayer NGO context?

Any insights or recommendations would be highly appreciated.

Here are my current scripts. Please advise of any further info is needed.

using System.Collections;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Services.Core;
#if !UNITY_WEBGL
using Unity.Services.Multiplay;
#endif
using UnityEngine;
using Unity.Networking.Transport; // Required for NetworkDriver

public class Server : MonoBehaviour
{
    void Start()
    {
        DontDestroyOnLoad(gameObject);

        #if UNITY_EDITOR
        StartCoroutine(StartLocalServer());
        #else
        StartCoroutine(StartServer());
        #if !UNITY_WEBGL
        StartCoroutine(ApproveBackfillTicketEverySecond());
        #endif
        #endif
    }

    IEnumerator StartLocalServer()
    {
        Debug.Log("Starting local server...");

        var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();

        #if UNITY_WEBGL
        var networkDriver = NetworkDriver.Create(new WebSocketNetworkInterface());
        #else
        var networkDriver = NetworkDriver.Create(); // Default UDP
        #endif

        transport.SetConnectionData("127.0.0.1", 7777); // Localhost for testing

        if (!NetworkManager.Singleton.StartServer())
        {
            Debug.LogError("Failed to start local server");
            yield break;
        }

        NetworkManager.Singleton.SceneManager.LoadScene("GameLogic", LoadSceneMode.Single);

        yield return null;
    }

    async Task StartServer()
    {
        await UnityServices.InitializeAsync();

        #if !UNITY_WEBGL
        var server = MultiplayService.Instance.ServerConfig;
        var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();

        transport.SetConnectionData("0.0.0.0", server.Port); // Bind to server port
        #endif

        if (!NetworkManager.Singleton.StartServer())
        {
            Debug.LogError("Failed to start server");
            throw new Exception("Failed to start server");
        }

        NetworkManager.Singleton.SceneManager.LoadScene("GameLogic", LoadSceneMode.Single);

        #if !UNITY_WEBGL
        var callbacks = new MultiplayEventCallbacks();
        callbacks.Allocate += OnAllocate;
        callbacks.Deallocate += OnDeallocate;

        while (MultiplayService.Instance == null)
        {
            await Task.Yield();
        }

        var events = await MultiplayService.Instance.SubscribeToServerEventsAsync(callbacks);
        await CreateBackfillTicket();
        #endif
    }

    #if !UNITY_WEBGL
    async void OnAllocate(MultiplayAllocation allocation)
    {
        await MultiplayService.Instance.ReadyServerForPlayersAsync();
    }

    async Task CreateBackfillTicket()
    {
        var results = await MultiplayService.Instance.GetPayloadAllocationFromJsonAs<MatchmakingResults>();

        var options = new CreateBackfillTicketOptions("DedicatedServerQueue",
            MultiplayService.Instance.ServerConfig.IpAddress + ":" + MultiplayService.Instance.ServerConfig.Port,
            new Dictionary<string, object>(),
            new BackfillTicketProperties(results.MatchProperties));

        await MatchmakerService.Instance.CreateBackfillTicketAsync(options);
    }

    IEnumerator ApproveBackfillTicketEverySecond()
    {
        yield return new WaitForSeconds(4);

        while (true)
        {
            yield return new WaitForSeconds(1f);
            // Approve backfill ticket logic...
        }
    }
    #endif
}

using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using Unity.Services.Authentication;
using Unity.Services.Core;
using Unity.Services.Matchmaker;
using UnityEngine;
using Unity.Networking.Transport; // Required for NetworkDriver

public class Client : MonoBehaviour
{
    static bool initialized;

    async void Start()
    {
        if (!initialized)
        {
            await UnityServices.InitializeAsync();
            await AuthenticationService.Instance.SignInAnonymouslyAsync();
            initialized = true;
        }

        if (LoadClientButton.connectToLocalServer)
        {
            ConnectToLocalServer();
        }
        else
        {
            await ConnectToNGOServer();
        }
    }

    void ConnectToLocalServer()
    {
        var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();

        #if UNITY_WEBGL
        var networkDriver = NetworkDriver.Create(new WebSocketNetworkInterface());
        #else
        var networkDriver = NetworkDriver.Create(); // Default UDP
        #endif

        transport.SetConnectionData("127.0.0.1", 7777); // Localhost

        if (!NetworkManager.Singleton.StartClient())
        {
            Debug.LogError("Failed to connect to local server");
        }
    }

    async Task ConnectToNGOServer()
    {
        var players = new List<Player> { new Player(AuthenticationService.Instance.PlayerId, new Dictionary<string, object>()) };
        var options = new CreateTicketOptions("DedicatedServerQueue", new Dictionary<string, object>());

        while (!await FindMatch(players, options))
        {
            await Task.Delay(1000);
        }
    }

    async Task<bool> FindMatch(List<Player> players, CreateTicketOptions options)
    {
        var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();

        #if UNITY_WEBGL
        var networkDriver = NetworkDriver.Create(new WebSocketNetworkInterface());
        #else
        var networkDriver = NetworkDriver.Create(); // Default UDP
        #endif

        var ticketResponse = await MatchmakerService.Instance.CreateTicketAsync(players, options);

        while (true)
        {
            await Task.Delay(1000);
            var ticketStatusResponse = await MatchmakerService.Instance.GetTicketAsync(ticketResponse.Id);
            
            if (ticketStatusResponse?.Value is MultiplayAssignment assignment)
            {
                if (assignment.Status == MultiplayAssignment.StatusOptions.Found && assignment.Port.HasValue)
                {
                    transport.SetConnectionData(assignment.Ip, (ushort)assignment.Port);

                    return NetworkManager.Singleton.StartClient();
                }
            }
        }
    }
}

Just to be sure: WebSockets is enabled for BOTH the web client AND the server? Because the server must use WebSockets too, otherwise the web clients can’t communicate with that server.

If you continue to use #if … in this manner (inline) you will create an unmanagable, undebuggable mess. :wink:

Make these methods that returns the driver to encapsulate the #if. And then don’t copy-paste that same code but instead call the method whenever you need it (at least the driver create has a duplicate).

Whenever possible, avoid the #if and instead prefer to use Application.platform or Application.isEditor conditionals - this is just a simple bool check but it avoids parts of the code not being compiled until you switch platforms, at which point you will likely get compile errors every now and then. And quite possible far later than when you made that code change which makes it harder to reason about why that won’t compile.

Instead of coroutines you should rather enable/disable the whole component based on what platform you work with, ideally even using separate components for each platform so that platform-specific code isn’t intermingled with other platform specific code, plus all the generic code.

All (!) service calls can throw exceptions so you absolutely have to try/catch all of them. AND handle them appropriately! (eg log them, return to main menu, show a popup error message, or whatever)

This will only allow connections from localhost, too. To accept all incoming connections from anywhere add the third parameter:

transport.SetConnectionData("127.0.0.1", 7777, "0.0.0.0");

Thanks for the reply.

Yes both use WebSockets.

You are totally right. Thanks for the reminder. Here, it’s only for testing purposes, as the entire project is design to test only websockets flow.

This is only for local testing, using 127.0.0.1. For remote servers, the code use 0.0.0.0 to bind the server. Ex: connecting a win or linux client build, with websockets disabled, works as expected and the client gets the remote ip and port.