Search Unity

Question How to Connect a Client to Unity Game Server Hosting Online?

Discussion in 'Netcode for GameObjects' started by donbonbon, Mar 27, 2024.

  1. donbonbon

    donbonbon

    Joined:
    May 28, 2023
    Posts:
    29
    I'm currently facing a challenge in connecting a client to a game hosted on Unity's Game Server Hosting (Multiplay) over the internet. I've managed to create a dedicated server build and successfully uploaded it to Multiplay. The allocation test was positive, and I can connect to the game server locally using the Unity Editor. However, transitioning this process to work online has been confusing and unsuccessful so far.

    Here's a summary of what I've tried and the obstacles I've encountered:

    1. Direct Connection Attempt: Attempting to connect to the game server using its address and port directly hasn't worked. It seems that some form of Unity-specific client input is necessary. Do I need to create a specific client build for this purpose?

    2. WebGL Build: I've also experimented with a WebGL build but failed to establish any connection to the server. I haven't configured any WebSockets, which might be the issue if the dedicated server build doesn't set these up by default.

    3. Linux Build: Considering the server's Linux environment, I attempted a Linux build for the client. While this theoretically matches the server's environment, I couldn't make an online connection, even after bypassing graphics dependencies using the -batchmode and -nographics flags. My command line was as follows:

      Code (CSharp):
      1. ./executable.x86_64 -batchmode -nographics -logFile ./logFile.txt -ip "server-game-address" -port "9000"
    I'm at a loss for how to proceed and would greatly appreciate any guidance, advice, or resources you could share. How can I successfully connect a client to our game server hosted online through Unity's Game Server Hosting?

    Thank you in advance for your help!
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,021
    Post your code. ;)

    WebGL requires WebSockets enabled in the client build. The server does nothing to set this up.
     
  3. donbonbon

    donbonbon

    Joined:
    May 28, 2023
    Posts:
    29
    Here's the code related two the Game Server Hosting and the matchmaking. Though so far, the problem I face, it that I cannot establish a connection to the server, i.e. with webgl and linux builds. And I don't really get what it the standard structure to apply in this case.
    Code (CSharp):
    1. using System;
    2. public class ServerStartUp : MonoBehaviour
    3. {
    4.     public static event System.Action ClientInstance;
    5.     private const string InternalServerIP = "0.0.0.0";
    6.     private string _externalServerIP = "0.0.0.0";
    7.     private ushort _serverPort = 7777;
    8.     private string _externalConnectionString =>$"{_externalServerIP}:{_serverPort}";
    9.  
    10.     private IMultiplayService _multiplayService;
    11.     const int _multiplayServiceTimeout = 20000;
    12.  
    13.     private string _allocationId;
    14.     private MultiplayEventCallbacks _serverCallbacks;
    15.     private IServerEvents _serverEvents;
    16.  
    17.     private BackfillTicket _localBackFillTicket;
    18.     private CreateBackfillTicketOptions _createBackfillTickerOptions;
    19.     private const int _tickerCheckMs = 1000;
    20.     private MatchmakingResults _matchmakingPayload;
    21.     private bool _backfilling = false;
    22.     async void Start()
    23.     {
    24.         bool server = false;
    25.         var args = System.Environment.GetCommandLineArgs();
    26.         for (int i = 0; i < args.Length; i++)
    27.         {
    28.             if (args[i] == "-dedicatedServer")
    29.             {
    30.                 server = true;
    31.             }
    32.             if (args[i] == "-port" && (i + 1 < args.Length))
    33.             {
    34.                 _serverPort = (ushort)int.Parse(args[i +1 ]);
    35.  
    36.             }
    37.  
    38.             if (args[i] == "-ip" && (i + 1 < args.Length))
    39.             {
    40.                 _externalServerIP = args[i +1];
    41.             }
    42.         }
    43.  
    44.         if (server)
    45.         {
    46.             StartServer();
    47.             await StartServerServices();
    48.         }
    49.         else
    50.         {
    51.             ClientInstance?.Invoke();
    52.         }
    53.        
    54.     }
    55.  
    56.     private void StartServer()
    57.     {
    58.         NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(InternalServerIP, _serverPort);
    59.         NetworkManager.Singleton.StartServer();
    60.         NetworkManager.Singleton.OnClientDisconnectCallback += ClientDisconnected;
    61.     }
    62.  
    63.     async Task StartServerServices()
    64.     {
    65.         await UnityServices.InitializeAsync();
    66.         try
    67.         {
    68.             _multiplayService = MultiplayService.Instance;
    69.             await _multiplayService.StartServerQueryHandlerAsync(
    70.                 (ushort)ConnectionApprovalHandler.MaxPlayers,
    71.                 serverName: "n/a",
    72.                 gameType: "n/a",
    73.                 buildId: "0",
    74.                 map: "n/a"
    75.             );
    76.         }
    77.         catch (Exception ex)
    78.         {
    79.             Debug.LogWarning($"Something went wrong trying to set up the SWP Service:\n{ex}");
    80.         }
    81.  
    82.         try
    83.         {
    84.             _matchmakingPayload = await GetMatchmakerPayload(_multiplayServiceTimeout);
    85.             if (_matchmakingPayload != null)
    86.             {
    87.                 Debug.Log($"Got payload: {_matchmakingPayload}");
    88.                 await StartBackfill(_matchmakingPayload);
    89.             }
    90.             else
    91.             {
    92.                 Debug.LogWarning($"Getting the MatchMaker Payload out, Starting with Defaults.");
    93.             }
    94.         }
    95.         catch (Exception ex)
    96.         {
    97.             Debug.LogWarning($"Something went wrong trying to set up the Allocation & Backfill services:\n{ex}");
    98.         }
    99.     }
    100.  
    101.     private async Task<MatchmakingResults> GetMatchmakerPayload(int timeout)
    102.     {
    103.         var matchmakerPayloadTask = SubscribeAndAwaitMatchMakerAllocation();
    104.         if (await Task.WhenAny(matchmakerPayloadTask, Task.Delay(timeout))==matchmakerPayloadTask)
    105.         {
    106.             return matchmakerPayloadTask.Result;
    107.         }
    108.  
    109.         return null;
    110.     }
    111.  
    112.     private async Task<MatchmakingResults> SubscribeAndAwaitMatchMakerAllocation()
    113.     {
    114.         if (_multiplayService == null) return null;
    115.         _allocationId = null;
    116.         _serverCallbacks = new MultiplayEventCallbacks();
    117.         _serverCallbacks.Allocate += OnMultiplayAllocation;
    118.         _serverEvents = await _multiplayService.SubscribeToServerEventsAsync(_serverCallbacks);
    119.         _allocationId = await AwaitAllocationId(); //this tells us when server was allocted, so we can ge info now about the match
    120.         var mmPayload = await GetMatchmakerAllocationPayloadAsync();
    121.         return mmPayload;
    122.     }
    123.  
    124.     private void OnMultiplayAllocation(MultiplayAllocation allocation)
    125.     {
    126.         Debug.Log($"OnAllocation: {allocation.AllocationId}");
    127.         if (string.IsNullOrEmpty(allocation.AllocationId)) return;
    128.         _allocationId = allocation.AllocationId;
    129.     }
    130.  
    131.     private async Task<string> AwaitAllocationId()
    132.     {
    133.         var config = _multiplayService.ServerConfig;
    134.         Debug.Log("Awaiting allocation. Server config is:\n" +
    135.             $" -ServerId:{config.ServerId}\n" +
    136.             $" -AllocationId: {config.AllocationId}\n" +
    137.             $" -Port: {config.Port}\n" +
    138.             $" -QPort: {config.QueryPort}\n" +
    139.             $" -logs: {config.ServerLogDirectory}\n"
    140.             );
    141.  
    142.         while (string.IsNullOrEmpty(_allocationId))
    143.         {
    144.             var configId = config.AllocationId;
    145.             if (!string.IsNullOrEmpty(configId) && string.IsNullOrEmpty(_allocationId)) //if we found one but haven't allocation it yet
    146.             {
    147.                 _allocationId = configId;
    148.                 break;
    149.             }
    150.  
    151.             await Task.Delay(100);
    152.         }
    153.  
    154.         return _allocationId;
    155.     }
    156.  
    157.     private async Task<MatchmakingResults> GetMatchmakerAllocationPayloadAsync()
    158.     {
    159.         try
    160.         {
    161.             var payloadAllocation = await MultiplayService.Instance.GetPayloadAllocationFromJsonAs<MatchmakingResults>();
    162.             var modelAsJson = JsonConvert.SerializeObject(payloadAllocation, Formatting.Indented);
    163.             Debug.Log($"{nameof(GetMatchmakerAllocationPayloadAsync)}:\n{modelAsJson}");
    164.             return payloadAllocation;
    165.         }
    166.         catch (Exception ex)
    167.         {
    168.             Debug.LogWarning($"Something went wrong trying to get the MatchMaker Payload in GetMatchmakerAllocationPayloadAsync:\n{ex}");
    169.         }
    170.  
    171.         return null;
    172.     }
    173.  
    174.     private async Task StartBackfill(MatchmakingResults payload)
    175.     {
    176.         var backfillProperties = new BackfillTicketProperties(payload.MatchProperties);
    177.         new BackfillTicket { Id = payload.MatchProperties.BackfillTicketId, Properties = backfillProperties};
    178.         await BeginBackfilling(payload);
    179.  
    180.     }
    181.  
    182.     private async Task BeginBackfilling(MatchmakingResults payload)
    183.     {
    184.         var matchProperties = payload.MatchProperties;
    185.        
    186.         if (string.IsNullOrEmpty(_localBackFillTicket.Id))
    187.         {
    188.             _createBackfillTickerOptions = new CreateBackfillTicketOptions
    189.         {
    190.             Connection = _externalConnectionString ,
    191.             QueueName = payload.QueueName,
    192.             Properties = new BackfillTicketProperties(matchProperties)
    193.         };
    194.             _localBackFillTicket.Id = await MatchmakerService.Instance.CreateBackfillTicketAsync(_createBackfillTickerOptions);
    195.         }
    196.         _backfilling = true;
    197.         # pragma warning disable 4014
    198.         BackfillLoop();
    199.         # pragma warning restore 4014
    200.     }
    201.  
    202.     private async Task BackfillLoop()
    203.     {
    204.         while (_backfilling && NeedsPlayers())
    205.         {
    206.             if (!NeedsPlayers())
    207.             {
    208.                 _localBackFillTicket = await MatchmakerService.Instance.ApproveBackfillTicketAsync(_localBackFillTicket.Id);
    209.                 _localBackFillTicket.Id = null;
    210.                 _backfilling = false;
    211.                 return;
    212.             }
    213.  
    214.             await Task.Delay(_tickerCheckMs);
    215.         }
    216.  
    217.         _backfilling = false;
    218.     }
    219.  
    220.     private void ClientDisconnected(ulong clientId)
    221.     {
    222.         if (!_backfilling && NetworkManager.Singleton.ConnectedClients.Count > 0 && NeedsPlayers())
    223.         {
    224.             BeginBackfilling(_matchmakingPayload);
    225.         }
    226.     }
    227.  
    228.     private bool NeedsPlayers()
    229.     {
    230.         return NetworkManager.Singleton.ConnectedClients.Count < ConnectionApprovalHandler.MaxPlayers;
    231.     }
    232.  
    233.     private void Dispose()
    234.     {
    235.         _serverCallbacks.Allocate -= OnMultiplayAllocation;
    236.         _serverEvents?.UnsubscribeAsync();
    237.     }
    238. }
    239.  
    Code (CSharp):
    1. public class MatchmakerClient : MonoBehaviour
    2. {
    3.     private string _ticketId;
    4.     // Start is called before the first frame update
    5.     private void OnEnable()
    6.     {
    7.         ServerStartUp.ClientInstance += SignIn;
    8.     }
    9.  
    10.     private void OnDisable()
    11.     {
    12.         ServerStartUp.ClientInstance -= SignIn;  
    13.     }
    14.  
    15.     private async void SignIn()
    16.     {
    17.         await ClientSignIn("QuartetsPlayer");
    18.         await AuthenticationService.Instance.SignInAnonymouslyAsync();
    19.     }
    20.  
    21.     private async Task ClientSignIn(string serviceProfileName = null)
    22.     {
    23.         if (serviceProfileName != null)
    24.         {
    25.             #if UNITY_EDITOR
    26.             serviceProfileName = $"{serviceProfileName}{GetCloneNumberSuffix()}";
    27.             #endif
    28.             var initOptions = new InitializationOptions();
    29.             initOptions.SetProfile(serviceProfileName);
    30.             await UnityServices.InitializeAsync(initOptions);
    31.         }
    32.         else
    33.         {
    34.             await UnityServices.InitializeAsync();
    35.         }
    36.  
    37.         Debug.Log($"Signed In Anonymously as {serviceProfileName}({PlayerID()})");
    38.     }
    39.  
    40.     private string PlayerID()
    41.     {
    42.         return AuthenticationService.Instance.PlayerId;
    43.     }
    44.  
    45.     #if UNITY_EDITOR
    46.     private string GetCloneNumberSuffix()
    47.     {
    48.         {
    49.             string projectPath = ClonesManager.GetCurrentProjectPath();
    50.             int lastUnderscore = projectPath.LastIndexOf("_");
    51.             string projectCloneSuffix = projectPath.Substring(lastUnderscore + 1);
    52.             if (projectCloneSuffix.Length != 1)
    53.                 projectCloneSuffix = "";
    54.             return projectCloneSuffix;
    55.         }
    56.     }
    57.     #endif
    58.  
    59.     public void StartClient()
    60.     {
    61.         CreateATicket();
    62.     }
    63.  
    64.     private async void CreateATicket()
    65.     {
    66.         var options = new CreateTicketOptions(queueName: "QuartetsMode");
    67.  
    68.         var players = new List<Unity.Services.Matchmaker.Models.Player>
    69.         {
    70.             new Unity.Services.Matchmaker.Models.Player(
    71.                 PlayerID(),
    72.                 new Dictionary<string, object>
    73.                 {
    74.                     {"Skill", 100}
    75.                 })
    76.         };
    77.  
    78.         try
    79.         {
    80.             var ticketResponse = await MatchmakerService.Instance.CreateTicketAsync(players, options);
    81.             _ticketId = ticketResponse.Id;
    82.             Debug.Log($"Ticket ID: {_ticketId}");
    83.             PollTicketStatus();
    84.         }
    85.         catch (Exception e)
    86.         {
    87.             Debug.LogError($"Failed to create a matchmaking ticket: {e.Message}");
    88.         }
    89.     }
    90.  
    91.  
    92.     [Serializable]
    93.     public class MacthmakingPlayerData
    94.     {
    95.         public int Skill;
    96.     }
    97.  
    98.     private async void PollTicketStatus()
    99.     {
    100.         MultiplayAssignment multiplayAssignment = null;
    101.         bool gotAssignment = false;
    102.         do
    103.         {
    104.             await Task.Delay(TimeSpan.FromSeconds(1f));
    105.             var ticketStatus = await MatchmakerService.Instance.GetTicketAsync(_ticketId);
    106.             if (ticketStatus == null) continue;
    107.             if (ticketStatus.Type == typeof(MultiplayAssignment))
    108.             {
    109.                 multiplayAssignment = ticketStatus.Value as MultiplayAssignment;
    110.             }
    111.             switch (multiplayAssignment.Status)
    112.             {
    113.                 case StatusOptions.Found:
    114.                     gotAssignment = true;
    115.                     TicketAssigned(multiplayAssignment);
    116.                     break;
    117.                 case StatusOptions.InProgress:
    118.                     break;
    119.                 case StatusOptions.Failed:
    120.                     gotAssignment = true;
    121.                     Debug.LogError($"Failed to get ticket Status. Error: {multiplayAssignment.Message}");
    122.                     break;
    123.                 case StatusOptions.Timeout:
    124.                     gotAssignment = true;
    125.                     Debug.LogError("Failed to get ticket Status. Ticket timed out");
    126.                     break;
    127.                 default:
    128.                     throw new InvalidOperationException();
    129.             }
    130.         } while(!gotAssignment);
    131.     }
    132.  
    133.     private void TicketAssigned(MultiplayAssignment assignment)
    134.     {
    135.         Debug.Log($"Ticket Assigned: {assignment.Ip}:{assignment.Port}");
    136.         NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(assignment.Ip, (ushort)assignment.Port);
    137.         NetworkManager.Singleton.StartClient();
    138.     }
    139. }
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,021
    Third parameter is missing, should also be 0.0.0.0. That's the server "listen" address to make it listen to any incoming connections. Not sure if this is required or what the default is, so it may not make a difference.

    What's the failure you are getting, a time out?
    Try logging the server process, how to access logs should be in the manual. Just be sure it's running and what parameters. Then try to ping that address to make sure it's accessible from remote.