Implement wrapper to connect to custom backend service - Game client integration - (Unity module)
Unwrap the wrapper
This tutorial shows how to connect the game client to the sample matchmaking backend service via WebSocket connection. In Byte Wars, there is a completed wrapper defined in the CustomMatchmakingWrapper_Starter
class. In this tutorial, you use the starter version of that wrapper to integrate the functionalities from scratch.
What's in the Starter Pack
To follow this tutorial, you use the starter wrapper class called CustomMatchmakingWrapper_Starter
. This wrapper is defined in the file below:
- CS file:
Assets/Resources/Modules/CustomMatchmaking/Scripts/CustomMatchmakingWrapper_Starter.cs
There is also a model class defined in the file below:
- CS file:
Assets/Resources/Modules/CustomMatchmaking/Scripts/CustomMatchmakingModels.cs
The CustomMatchmakingModels
class contains predefined constants, struct, and helper function below:
String to display message based on matchmaking states.
public static readonly string TravelingMessage = "Traveling to Server";
public static readonly string FindingMatchMessage = "Finding Match";
public static readonly string RequestMatchmakingMessage = "Requesting";
public static readonly string CancelMatchmakingMessage = "Canceling";
public static readonly string MatchmakingCanceledErrorMessage = "Canceled";
public static readonly string MatchmakingErrorMessage = "Connection failed. Make sure the matchmaking server is running, reachable, and the address and port is set properly.";
public static readonly string MatchmakingInvalidPayloadErrorMessage = "Received invalid payload format from matchmaking server. Make sure you are running a compatible version.";A struct and enum to parse the matchmaking payload.
public enum MatchmakerPayloadType
{
OnFindingMatch,
OnMatchFound,
OnServerReady,
OnServerClaimFailed
}
public class MatchmakerPayload
{
[JsonProperty(Required = Required.Always)]
public MatchmakerPayloadType type { get; set; }
[JsonProperty(Required = Required.Always)]
public string message { get; set; }
}A helper function to get the matchmaker URL. You can override the URL using
-CustomMatchmakingUrl=<your_url>
launch parameter. The default value is localhostws://127.0.0.1:8080
.public static string GetMatchmakerUrl()
{
// Get from launch parameter first.
string customMatchmakerUrl = TutorialModuleUtil.GetLaunchParamValue($"-{CustomMatchmakerConfigKey}=");
// Get from config file if laucnh params are empty.
if (ConfigurationReader.Config != null)
{
if (string.IsNullOrEmpty(customMatchmakerUrl))
{
customMatchmakerUrl = ConfigurationReader.Config.AMSModuleConfiguration.customMatchmakingUrl;
}
}
// Parse localhost if any, because it is not supported by Unity's networking.
return Utilities.TryParseLocalHostUrl(customMatchmakerUrl);
}
Implement custom matchmaking
Since Byte Wars is build to support both desktop and WebGL, we are using two types of WebSocket. For desktop, we use .NET NativeWebSocket.WebSocket
. As for WebGL, since Unity does not officially support WebSocket for WebGL, we are using Netcode.Transports.WebSocket.IWebSocketClient
from Unity Netcode WebSocket by community contribution on official Unity's GitHub repository.
Open the
CustomMatchmakingWrapper_Starter
class and declare the WebSocket variable below.#if !UNITY_WEBGL
// Use native WebSocket.
private WebSocket nativeWebSocket;
#else
// Use Unity Netcode's WebSocket that supports WebGL.
private IWebSocketClient netcodeWebSocket;
#endifDeclare the delegates below to broadcast the matchmaking events. You will use this delegate with the UI later.
public Action OnMatchmakingStarted;
public Action<WebSocketCloseCode /*closeCode*/, string /*closeMessage*/> OnMatchmakingStopped;
public Action<CustomMatchmakingModels.MatchmakerPayload /*payload*/> OnMatchmakingPayload;
public Action<string /*serverIp*/, ushort /*serverPort*/> OnMatchmakingServerReady;
public Action<string /*errorMessage*/> OnMatchmakingError;Next, declare the helper variable below to track matchmaking WebSocket close message before broadcasting it through the delegates you declared previously.
private string pendingCloseMessage = string.Empty;
Now, create a new function to start the matchmaking by connecting to the sample matchmaking backend service via WebSocket. In the code below, it switch between
NativeWebSocket.WebSocket
andNetcode.Transports.WebSocket.IWebSocketClient
according the platform. Notice that the WebSocket callback functions are not defined yet, you will create them later.public void StartMatchmaking()
{
BytewarsLogger.Log("Start matchmaking.");
pendingCloseMessage = string.Empty;
string matchmakerUrl = CustomMatchmakingModels.GetMatchmakerUrl();
#if !UNITY_WEBGL
nativeWebSocket = new WebSocket(matchmakerUrl);
nativeWebSocket.OnOpen += OnMatchmakerOpen;
nativeWebSocket.OnClose += (int closeCode) =>
{
OnMatchmakerClosed(
Enum.IsDefined(typeof(WebSocketCloseCode), closeCode) ?
(WebSocketCloseCode)closeCode :
WebSocketCloseCode.Undefined);
};
nativeWebSocket.OnMessage += (byte[] data) =>
{
OnMatchmakerPayload(data);
};
nativeWebSocket.OnError += OnMatchmakerError;
nativeWebSocket.Connect();
#else
netcodeWebSocket = WebSocketClientFactory.Create(
useSecureConnection: false,
url: matchmakerUrl,
username: string.Empty,
password: string.Empty);
netcodeWebSocket.Connect();
#endif
}Next, create a new function to cancel the matchmaking by closing the WebSocket connection from the sample matchmaking backend service. Similarly, the code below switch between
NativeWebSocket.WebSocket
andNetcode.Transports.WebSocket.IWebSocketClient
according the platform.public void CancelMatchmaking(bool isIntentional)
{
// Use generic error message if the cancelation is intentional.
if (isIntentional)
{
pendingCloseMessage = CustomMatchmakingModels.MatchmakingCanceledErrorMessage;
}
#if !UNITY_WEBGL
if (nativeWebSocket == null)
{
BytewarsLogger.LogWarning("Cannot cancel matchmaking. WebSocket to matchmaker is null.");
OnMatchmakerClosed(WebSocketCloseCode.Normal);
return;
}
#else
if (netcodeWebSocket == null)
{
BytewarsLogger.LogWarning("Cannot cancel matchmaking. WebSocket to matchmaker is null.");
OnMatchmakerClosed(WebSocketCloseCode.Normal);
return;
}
#endif
BytewarsLogger.Log("Cancel matchmaking.");
#if !UNITY_WEBGL
nativeWebSocket.Close();
#else
netcodeWebSocket.Close();
#endif
}Then, create a new function to poll the WebSocket event received from the sample matchmaking backend service. Similarly, the code below switch between
NativeWebSocket.WebSocket
andNetcode.Transports.WebSocket.IWebSocketClient
according the platform. Notice that the WebSocket callback functions are not defined yet, you will create them later.private void PollMatchmakerEvent()
{
#if !UNITY_WEBGL
if (nativeWebSocket == null)
{
return;
}
nativeWebSocket.DispatchMessageQueue();
#else
if (netcodeWebSocket == null)
{
return;
}
WebSocketEvent pollEvent = netcodeWebSocket.Poll();
if (pollEvent != null)
{
switch (pollEvent.Type)
{
case WebSocketEventType.Open:
OnMatchmakerOpen();
break;
case WebSocketEventType.Close:
OnMatchmakerClosed(
Enum.IsDefined(typeof(WebSocketCloseCode), (int)pollEvent.CloseCode) ?
(WebSocketCloseCode)pollEvent.CloseCode :
WebSocketCloseCode.Undefined);
break;
case WebSocketEventType.Payload:
OnMatchmakerPayload(pollEvent.Payload);
break;
case WebSocketEventType.Error:
OnMatchmakerError(pollEvent.Error);
break;
}
}
#endif
}Next, create the Unity's
Update()
function and call thePollMatchmakerEvent()
function to poll the WebSocket event on every frame.private void Update()
{
PollMatchmakerEvent();
}Create a new callback function to handle when the sample matchmaking backend service WebSocket connection open. This function simply broadcasts the delegate you created earlier.
private void OnMatchmakerOpen()
{
BytewarsLogger.Log("Connected to matchmaker.");
OnMatchmakingStarted?.Invoke();
}Next, create a new callback function to handle when the sample matchmaking backend service WebSocket connection closed. This function reset the WebSocket variables and broadcasts the delegate with appropriate WebSocket close message you created earlier.
private void OnMatchmakerClosed(WebSocketCloseCode closeCode)
{
#if !UNITY_WEBGL
nativeWebSocket = null;
#else
netcodeWebSocket = null;
#endif
// Store and clear the pending close message.
string closeMessage = pendingCloseMessage;
pendingCloseMessage = string.Empty;
/* Handle WebSocket close conditions:
* If closed normally with a close message, set the close code to undefined.
* If closed abnormally, use the generic error message.
* Otherwise, retain the WebSocket's native close code and message. */
if (closeCode == WebSocketCloseCode.Normal && !string.IsNullOrEmpty(closeMessage))
{
closeCode = WebSocketCloseCode.Undefined;
}
else if (closeCode != WebSocketCloseCode.Normal)
{
closeMessage = CustomMatchmakingModels.MatchmakingErrorMessage;
}
BytewarsLogger.Log($"Disconnected from matchmaker. Close code: {closeCode}. Close message: {closeMessage}");
OnMatchmakingStopped?.Invoke(closeCode, closeMessage);
}Then, create a new callback function to handle when the sample matchmaking backend service WebSocket connection error. This function resets the WebSocket variables, stores the WebSocket error message to the close message variable, and broadcasts the appropriate delegate.
private void OnMatchmakerError(string errorMessage)
{
BytewarsLogger.Log($"Connection to matchmaker error. Error: {errorMessage}");
#if !UNITY_WEBGL
nativeWebSocket = null;
#else
netcodeWebSocket = null;
#endif
pendingCloseMessage = errorMessage;
OnMatchmakingError?.Invoke(errorMessage);
}Now, create a new callback function to handle when the game receives a payload from the sample matchmaking backend service. This function checks whether the payload is valid. If the payload is valid, the function broadcasts it via the delegate. If the payload is invalid, the function cancels the matchmaking process. Additionally, if the payload indicates that the server is ready, the function calls another function to connect the game client to the game server, which you will create later.
private void OnMatchmakerPayload(byte[] bytes)
{
// Get the payload.
string payloadStr = System.Text.Encoding.UTF8.GetString(bytes);
if (string.IsNullOrEmpty(payloadStr))
{
BytewarsLogger.LogWarning("Cannot handle matchmaker payload. Payload is null.");
return;
}
// Try parse the payload.
CustomMatchmakingModels.MatchmakerPayload payload = null;
try
{
/* Configure settings to prevent enum deserialization from integer values.
* The enum value must match the string representation defined in the class.*/
JsonSerializerSettings settings = new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new StringEnumConverter { AllowIntegerValues = false } },
NullValueHandling = NullValueHandling.Ignore
};
payload = JsonConvert.DeserializeObject<CustomMatchmakingModels.MatchmakerPayload>(payloadStr, settings);
}
catch (Exception e)
{
BytewarsLogger.LogWarning($"Cannot handle matchmaker payload. Unable to parse payload. Error: {e.Message}");
payload = null;
}
// Abort matchmaking if payload is invalid.
if (payload == null)
{
BytewarsLogger.LogWarning("Cannot handle matchmaker payload. Matchmaker payload is null.");
pendingCloseMessage = CustomMatchmakingModels.MatchmakingInvalidPayloadErrorMessage;
CancelMatchmaking(isIntentional: false);
return;
}
// Broadcast the payload.
BytewarsLogger.Log($"Received payload from matchmaker: {payloadStr}");
OnMatchmakingPayload?.Invoke(payload);
// Travel to the server.
if (payload.type == CustomMatchmakingModels.MatchmakerPayloadType.OnServerReady)
{
OnMatchmakerServerReady(payload.message);
}
}When the server is ready, the sample matchmaking backend service sends a payload to the game client containing server info in
IPaddress:port
format. Thus, you need to parse this message by creating a new helper function below:private bool TryParseServerInfo(
string serverInfoToParse,
out string serverIp,
out ushort serverPort)
{
serverIp = null;
serverPort = 0;
if (string.IsNullOrWhiteSpace(serverInfoToParse))
{
BytewarsLogger.LogWarning("Server info is empty.");
return false;
}
// Try to split server Ip and port.
string[] serverAddressParts = serverInfoToParse.Split(':');
if (serverAddressParts.Length != 2)
{
BytewarsLogger.LogWarning("Server info does not contain Ip address and port.");
return false;
}
// Try to parse server Ip.
if (!IPAddress.TryParse(serverAddressParts[0], out _) &&
Uri.CheckHostName(serverAddressParts[0]) == UriHostNameType.Unknown)
{
BytewarsLogger.LogWarning("Server info has invalid Ip address.");
return false;
}
// Try to parse server port.
if (!ushort.TryParse(serverAddressParts[1], out serverPort))
{
BytewarsLogger.LogWarning("Server info has invalid port.");
return false;
}
// Parse localhost if any, because it is not supported by Unity's networking.
serverIp = Utilities.TryParseLocalHostUrl(serverAddressParts[0]);
return true;
}Finally, create a new function to handle the on-server ready payload. This function parses the server info and connects the game client to the game server using the server's IP address and port.
private void OnMatchmakerServerReady(string serverInfo)
{
// Travel to the server if the server info is valid.
if (TryParseServerInfo(serverInfo, out string serverIp, out ushort serverPort))
{
BytewarsLogger.Log($"Server info found. Start traveling to {serverIp}:{serverPort}.");
OnMatchmakingServerReady?.Invoke(serverIp, serverPort);
GameManager.Instance.ShowTravelingLoading(() =>
{
GameManager.Instance.StartAsClient(serverIp, serverPort, CustomMatchmakingModels.DefaultGameMode);
},
CustomMatchmakingModels.TravelingMessage);
}
else
{
BytewarsLogger.LogWarning($"Cannot travel to server. Unable to parse server info Ip address and port.");
pendingCloseMessage = CustomMatchmakingModels.MatchmakingInvalidPayloadErrorMessage;
CancelMatchmaking(isIntentional: false);
}
}
Resources
The files used in this tutorial section are available in the Unity Byte Wars GitHub repository.