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:
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?
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.
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:
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?
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.
using System;
public class ServerStartUp : MonoBehaviour
{
public static event System.Action ClientInstance;
private const string InternalServerIP = "0.0.0.0";
private string _externalServerIP = "0.0.0.0";
private ushort _serverPort = 7777;
private string _externalConnectionString =>$"{_externalServerIP}:{_serverPort}";
private IMultiplayService _multiplayService;
const int _multiplayServiceTimeout = 20000;
private string _allocationId;
private MultiplayEventCallbacks _serverCallbacks;
private IServerEvents _serverEvents;
private BackfillTicket _localBackFillTicket;
private CreateBackfillTicketOptions _createBackfillTickerOptions;
private const int _tickerCheckMs = 1000;
private MatchmakingResults _matchmakingPayload;
private bool _backfilling = false;
async void Start()
{
bool server = false;
var args = System.Environment.GetCommandLineArgs();
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "-dedicatedServer")
{
server = true;
}
if (args[i] == "-port" && (i + 1 < args.Length))
{
_serverPort = (ushort)int.Parse(args[i +1 ]);
}
if (args[i] == "-ip" && (i + 1 < args.Length))
{
_externalServerIP = args[i +1];
}
}
if (server)
{
StartServer();
await StartServerServices();
}
else
{
ClientInstance?.Invoke();
}
}
private void StartServer()
{
NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(InternalServerIP, _serverPort);
NetworkManager.Singleton.StartServer();
NetworkManager.Singleton.OnClientDisconnectCallback += ClientDisconnected;
}
async Task StartServerServices()
{
await UnityServices.InitializeAsync();
try
{
_multiplayService = MultiplayService.Instance;
await _multiplayService.StartServerQueryHandlerAsync(
(ushort)ConnectionApprovalHandler.MaxPlayers,
serverName: "n/a",
gameType: "n/a",
buildId: "0",
map: "n/a"
);
}
catch (Exception ex)
{
Debug.LogWarning($"Something went wrong trying to set up the SWP Service:\n{ex}");
}
try
{
_matchmakingPayload = await GetMatchmakerPayload(_multiplayServiceTimeout);
if (_matchmakingPayload != null)
{
Debug.Log($"Got payload: {_matchmakingPayload}");
await StartBackfill(_matchmakingPayload);
}
else
{
Debug.LogWarning($"Getting the MatchMaker Payload out, Starting with Defaults.");
}
}
catch (Exception ex)
{
Debug.LogWarning($"Something went wrong trying to set up the Allocation & Backfill services:\n{ex}");
}
}
private async Task<MatchmakingResults> GetMatchmakerPayload(int timeout)
{
var matchmakerPayloadTask = SubscribeAndAwaitMatchMakerAllocation();
if (await Task.WhenAny(matchmakerPayloadTask, Task.Delay(timeout))==matchmakerPayloadTask)
{
return matchmakerPayloadTask.Result;
}
return null;
}
private async Task<MatchmakingResults> SubscribeAndAwaitMatchMakerAllocation()
{
if (_multiplayService == null) return null;
_allocationId = null;
_serverCallbacks = new MultiplayEventCallbacks();
_serverCallbacks.Allocate += OnMultiplayAllocation;
_serverEvents = await _multiplayService.SubscribeToServerEventsAsync(_serverCallbacks);
_allocationId = await AwaitAllocationId(); //this tells us when server was allocted, so we can ge info now about the match
var mmPayload = await GetMatchmakerAllocationPayloadAsync();
return mmPayload;
}
private void OnMultiplayAllocation(MultiplayAllocation allocation)
{
Debug.Log($"OnAllocation: {allocation.AllocationId}");
if (string.IsNullOrEmpty(allocation.AllocationId)) return;
_allocationId = allocation.AllocationId;
}
private async Task<string> AwaitAllocationId()
{
var config = _multiplayService.ServerConfig;
Debug.Log("Awaiting allocation. Server config is:\n" +
$" -ServerId:{config.ServerId}\n" +
$" -AllocationId: {config.AllocationId}\n" +
$" -Port: {config.Port}\n" +
$" -QPort: {config.QueryPort}\n" +
$" -logs: {config.ServerLogDirectory}\n"
);
while (string.IsNullOrEmpty(_allocationId))
{
var configId = config.AllocationId;
if (!string.IsNullOrEmpty(configId) && string.IsNullOrEmpty(_allocationId)) //if we found one but haven't allocation it yet
{
_allocationId = configId;
break;
}
await Task.Delay(100);
}
return _allocationId;
}
private async Task<MatchmakingResults> GetMatchmakerAllocationPayloadAsync()
{
try
{
var payloadAllocation = await MultiplayService.Instance.GetPayloadAllocationFromJsonAs<MatchmakingResults>();
var modelAsJson = JsonConvert.SerializeObject(payloadAllocation, Formatting.Indented);
Debug.Log($"{nameof(GetMatchmakerAllocationPayloadAsync)}:\n{modelAsJson}");
return payloadAllocation;
}
catch (Exception ex)
{
Debug.LogWarning($"Something went wrong trying to get the MatchMaker Payload in GetMatchmakerAllocationPayloadAsync:\n{ex}");
}
return null;
}
private async Task StartBackfill(MatchmakingResults payload)
{
var backfillProperties = new BackfillTicketProperties(payload.MatchProperties);
new BackfillTicket { Id = payload.MatchProperties.BackfillTicketId, Properties = backfillProperties};
await BeginBackfilling(payload);
}
private async Task BeginBackfilling(MatchmakingResults payload)
{
var matchProperties = payload.MatchProperties;
if (string.IsNullOrEmpty(_localBackFillTicket.Id))
{
_createBackfillTickerOptions = new CreateBackfillTicketOptions
{
Connection = _externalConnectionString ,
QueueName = payload.QueueName,
Properties = new BackfillTicketProperties(matchProperties)
};
_localBackFillTicket.Id = await MatchmakerService.Instance.CreateBackfillTicketAsync(_createBackfillTickerOptions);
}
_backfilling = true;
# pragma warning disable 4014
BackfillLoop();
# pragma warning restore 4014
}
private async Task BackfillLoop()
{
while (_backfilling && NeedsPlayers())
{
if (!NeedsPlayers())
{
_localBackFillTicket = await MatchmakerService.Instance.ApproveBackfillTicketAsync(_localBackFillTicket.Id);
_localBackFillTicket.Id = null;
_backfilling = false;
return;
}
await Task.Delay(_tickerCheckMs);
}
_backfilling = false;
}
private void ClientDisconnected(ulong clientId)
{
if (!_backfilling && NetworkManager.Singleton.ConnectedClients.Count > 0 && NeedsPlayers())
{
BeginBackfilling(_matchmakingPayload);
}
}
private bool NeedsPlayers()
{
return NetworkManager.Singleton.ConnectedClients.Count < ConnectionApprovalHandler.MaxPlayers;
}
private void Dispose()
{
_serverCallbacks.Allocate -= OnMultiplayAllocation;
_serverEvents?.UnsubscribeAsync();
}
}
public class MatchmakerClient : MonoBehaviour
{
private string _ticketId;
// Start is called before the first frame update
private void OnEnable()
{
ServerStartUp.ClientInstance += SignIn;
}
private void OnDisable()
{
ServerStartUp.ClientInstance -= SignIn;
}
private async void SignIn()
{
await ClientSignIn("QuartetsPlayer");
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
private async Task ClientSignIn(string serviceProfileName = null)
{
if (serviceProfileName != null)
{
#if UNITY_EDITOR
serviceProfileName = $"{serviceProfileName}{GetCloneNumberSuffix()}";
#endif
var initOptions = new InitializationOptions();
initOptions.SetProfile(serviceProfileName);
await UnityServices.InitializeAsync(initOptions);
}
else
{
await UnityServices.InitializeAsync();
}
Debug.Log($"Signed In Anonymously as {serviceProfileName}({PlayerID()})");
}
private string PlayerID()
{
return AuthenticationService.Instance.PlayerId;
}
#if UNITY_EDITOR
private string GetCloneNumberSuffix()
{
{
string projectPath = ClonesManager.GetCurrentProjectPath();
int lastUnderscore = projectPath.LastIndexOf("_");
string projectCloneSuffix = projectPath.Substring(lastUnderscore + 1);
if (projectCloneSuffix.Length != 1)
projectCloneSuffix = "";
return projectCloneSuffix;
}
}
#endif
public void StartClient()
{
CreateATicket();
}
private async void CreateATicket()
{
var options = new CreateTicketOptions(queueName: "QuartetsMode");
var players = new List<Unity.Services.Matchmaker.Models.Player>
{
new Unity.Services.Matchmaker.Models.Player(
PlayerID(),
new Dictionary<string, object>
{
{"Skill", 100}
})
};
try
{
var ticketResponse = await MatchmakerService.Instance.CreateTicketAsync(players, options);
_ticketId = ticketResponse.Id;
Debug.Log($"Ticket ID: {_ticketId}");
PollTicketStatus();
}
catch (Exception e)
{
Debug.LogError($"Failed to create a matchmaking ticket: {e.Message}");
}
}
[Serializable]
public class MacthmakingPlayerData
{
public int Skill;
}
private async void PollTicketStatus()
{
MultiplayAssignment multiplayAssignment = null;
bool gotAssignment = false;
do
{
await Task.Delay(TimeSpan.FromSeconds(1f));
var ticketStatus = await MatchmakerService.Instance.GetTicketAsync(_ticketId);
if (ticketStatus == null) continue;
if (ticketStatus.Type == typeof(MultiplayAssignment))
{
multiplayAssignment = ticketStatus.Value as MultiplayAssignment;
}
switch (multiplayAssignment.Status)
{
case StatusOptions.Found:
gotAssignment = true;
TicketAssigned(multiplayAssignment);
break;
case StatusOptions.InProgress:
break;
case StatusOptions.Failed:
gotAssignment = true;
Debug.LogError($"Failed to get ticket Status. Error: {multiplayAssignment.Message}");
break;
case StatusOptions.Timeout:
gotAssignment = true;
Debug.LogError("Failed to get ticket Status. Ticket timed out");
break;
default:
throw new InvalidOperationException();
}
} while(!gotAssignment);
}
private void TicketAssigned(MultiplayAssignment assignment)
{
Debug.Log($"Ticket Assigned: {assignment.Ip}:{assignment.Port}");
NetworkManager.Singleton.GetComponent<UnityTransport>().SetConnectionData(assignment.Ip, (ushort)assignment.Port);
NetworkManager.Singleton.StartClient();
}
}
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.