Access
Connect cross-platform accounts & identity management
using AccelByte.Api;
using AccelByte.Core;
using AccelByte.Models;
AccelBytePlugin.GetLobby().MatchmakingCompleted += result => {};
AccelBytePlugin.GetLobby().ReadyForMatchConfirmed += result => {};
AccelBytePlugin.GetLobby().DSUpdated += result =>{};
AccelBytePlugin.GetLobby().RematchmakingNotif += result =>{};
string gameMode;
AccelBytePlugin.GetLobby().StartMatchmaking(gameMode, result =>
{
//Check this is not an error
if (!result.IsError)
{
Debug.Log("Started matchmaking is successful");
}
});
string gameMode;
AccelBytePlugin.GetLobby().CancelMatchmaking(gameMode, result =>
{
//Check this is not an error
if (!result.IsError)
{
Debug.Log("Canceled matchmaking is successful");
}
});
string matchId;
AccelBytePlugin.GetLobby().ConfirmReadyForMatch(matchId, result =>
{
//Check this is not an error
if (!result.IsError)
{
Debug.Log("Ready for match is successful");
}
});
In this tutorial, you will learn how to use the Matchmaking services. This guide assumes that you already implemented the Lobby (opens new window), Friends (opens new window), and Party (opens new window) services.
Since Matchmaking implementation can vary for each game, you can familiarize yourself with other concepts and classes in the LobbyModels.cs file inside the plugin SDK.
We will start by adding simple matchmaking logic into the game.
Ensure you already have a Matchmaking Ruleset set up in the Admin Portal, or follow this guide (opens new window) to make a Matchmaking Ruleset now.
Create a new script called MatchmakingHandler.cs and attach it to the AccelByteHandler gameObject.
Add the following AccelByte libraries to the top of the script:
using AccelByte.Api;
using AccelByte.Models;
using AccelByte.Core;
Store the game mode’s name based on the existing Matchmaking Ruleset. In this tutorial, we will use 1vs1.
// Current selected game mode
private string gameMode = "1vs1";
Add some basic Matchmaking functionalities in MatchmakingHandler.cs:
Start Matchmaking
This function requires gameMode to start matchmaking. Ensure you have already created a party with at least one player before adding this function.
private void FindMatch()
{
AccelBytePlugin.GetLobby().StartMatchmaking(gameMode, result =>
{
// Check this is an error
if (result.IsError)
{
Debug.Log($"Unable to start matchmaking: error code: {result.Error.Code} message: {result.Error.Message}");
}
else
{
Debug.Log("Started matchmaking is successful");
}
});
}
Cancel Matchmaking
This function also requires gameMode to cancel the current matchmaking action.
private void CancelMatchmaking()
{
AccelBytePlugin.GetLobby().CancelMatchmaking(gameMode, result =>
{
//Check this is an error
if (result.IsError)
{
Debug.Log($"Unable to cancel matchmaking: error code: {result.Error.Code} message: {result.Error.Message}");
}
else
{
Debug.Log("Canceled matchmaking is successful");
}
});
}
Ready Matchmaking
This function requires matchId to confirm if a player is ready for a match. You can find matchId in the MatchmakingCompleted notification event.
private void ReadyMatchmaking()
{
AccelBytePlugin.GetLobby().ConfirmReadyForMatch(matchId, result =>
{
//Check this is an error
if (result.IsError)
{
Debug.Log($"Unable to ready for match: error code: {result.Error.Code} message: {result.Error.Message}");
}
else
{
Debug.Log("Ready for match is successful");
}
});
}
To update any matchmaking changes, use the Lobby service notification events. In this section, we will add events in LobbyHandler.cs on the ConnectToLobby() function, and add a Debug.Log to confirm that everything is functional. Each event is explained in more detail below.
Matchmaking Completed
This notification event occurs when the player finds a match, or the party leader starts or cancels matchmaking. You can differentiate these events by using the matchmaking status, such as in the following example:
```
_lobby.MatchmakingCompleted += result =>
{
// Called when party leader start a match
if (result.Value.status == "start")
{
Debug.Log($"Party leader start matchmaking.");
}
// Called when party leader cancel a match
else if (result.Value.status == "cancel")
{
Debug.Log($"Party leader cancel matchmaking.");
}
// Called when found a match
else if (result.Value.status == "done")
{
Debug.Log($"Found a match. Match id: {result.Value.matchId}");
}
};
```
start: party members receive this notification event when the party leader starts matchmaking.
cancel: party members receive this notification event when the party leader cancels matchmaking.
done: party members receive this notification event when a match is found.
Ready for Match Confirmed
This notification event occurs when a player in the lobby declares they are ready to play the match. You can follow this to get the log when the player is ready to play.
```
`_lobby.ReadyForMatchConfirmed += result => { Debug.Log($"User {result.Value.userId} is ready!");};`
```
DSUpdated
This notification event occurs when all players are ready to play in the current match, and this prompts an update from the Dedicated Server. To connect to the server, you must wait until the status from the DSUpdated notification event becomes ready or busy, such as in the following code:
```
_lobby.DSUpdated += result =>
{
if (result.Value.status == "READY" || result.Value.status == "BUSY")
{
Debug.Log($"Game is started");
}
};
```
RematchmakingNotif
You may also want to retry matchmaking if the first matchmaking attempt fails. Use this event below to do this.
```
_lobby.RematchmakingNotif += result => { Debug.Log($"Find another match");};
```
The result data only contains ban durations. Based on this, there are two rematchmaking behaviors:
banDuration == 0 (seconds): matchmaking will restart automatically. The banDuration will be set to 0 if all the members in the party declared themselves ready to play during the last matchmaking attempt.
banDuration == 30 (seconds): the players will need to restart the matchmaking manually from the Lobby. This ban duration will take effect if one or more party members were not ready to play in the last matchmaking attempt.
Congratulations! You have learnt how to use the Matchmaking service!
Continue on for a step by step example of the UI and code implementation. Otherwise, you are now ready to move on to Armada.
Open your Lobby panel/scene and add the following UI objects to the same panel as your Friends Management button:
In the Game Mode dropdown, make sure the following items are listed:
Find Match button
Create a new panel or scene for Matchmaking and add the following matchmaking-related panels and UI elements:
Ready Match panel
Game panel
You can create an empty panel as this will be a temporary panel for now. You will change this to a real game scene later.
Now that you have some basic Matchmaking functionalities, you can implement these with your UI.
Before continuing, ensure you have the necessary Matchmaking Ruleset in place to accommodate two different game modes. In this tutorial, we use 1vs1 and 2vs2 game modes. Follow this guide to add or adjust your own Matchmaking Ruleset.
Prepare your gameMode. Create a separate script called GameModeHandler.cs and add the following enum to the top of the script:
public enum GameMode
{
None,
versusOne,
versusTwo
}
Create a new static class called GameModeHandler. Add the following static functions to handle anything related to Game Mode and its Total Players:
public static class GameModeHandler
{
// This string is based on mode in Admin Portal
private const string None = "None"; // This is default and not registered in AP
private const string VersusOne = "1vs1";
private const string VersusTwo = "2vs2";
/// <summary>
/// Parse Game Mode from enum to string
/// </summary>
/// <param name="mode"> Game Mode enum that will be parsed into string</param>
/// <returns></returns>
public static string GetString(this GameMode mode)
{
switch (mode)
{
case GameMode.versusOne:
return VersusOne;
case GameMode.versusTwo:
return VersusTwo;
case GameMode.None:
default:
return None;
}
}
/// <summary>
/// Parse Game Mode to return total players
/// </summary>
/// <param name="mode"> Game Mode enum that will be parsed into total players</param>
/// <returns></returns>
public static int GetTotalPlayers(this GameMode mode)
{
switch (mode)
{
case GameMode.versusOne:
return 2;
case GameMode.versusTwo:
return 4;
case GameMode.None:
default:
return 0;
}
}
/// <summary>
/// Parse Game Mode from string to GameMode
/// </summary>
/// <param name="mode"> Game Mode string that will be parsed into enum</param>
/// <returns></returns>
public static GameMode ToGameMode(this string mode)
{
switch (mode)
{
case VersusOne:
return GameMode.versusOne;
case VersusTwo:
return GameMode.versusTwo;
case None:
default:
return GameMode.None;
}
}
}
Create a script called ConnectionHandler.cs and add ip, port, and uport. Create this as a static class so you can get and set these variables from anywhere.
using System;
public static class ConnectionHandler
{
// Get/ set ip in string
public static string ip = "localhost";
// Get/ set port in integer
public static int port = 7777;
// Get port in ushort format
public static ushort uPort => Convert.ToUInt16(port);
}
Create a script to obtain a command line argument. You will use this later to switch between environments in order to test your game on your local PC or using AccelByte’s server.
public static bool GetLocalArgument()
{
bool isLocal = false;
// Get Local Argument from the system
// You can run local by adding -local when executing the game/ server
string[] args = System.Environment.GetCommandLineArgs();
foreach (var arg in args)
{
if (arg.Contains("local"))
{
isLocal = true;
break;
}
}
return isLocal;
}
Go to the MatchmakingHandler.cs script and add the following code after the libraries at the top of the script:
...
using UnityEngine.UI;
using UI = UnityEngine.UI;
Change the gameMode variable’s default value to gameMode (null).
// Current selected game mode
private GameMode gameMode;
Add the following variables. These will be used later to store Matchmaking-related data.
private const string DefaultCountUp = "Time Elapsed: 00:00";
// This default count down time must be similar with the Lobby Config in the Admin Portal
private const string DefaultCountDown = "20";
// Current active match id
private string matchId;
Add the following Matchmaking UI references:
#region UI
[SerializeField]
private GameObject matchmakingWindow;
[SerializeField]
private GameObject findMatchWindow;
[SerializeField]
private GameObject readyMatchWindow;
[SerializeField]
private GameObject gameWindow;
[SerializeField]
private Dropdown gameModeDropdown;
#region Button
[SerializeField]
private Button findMatchButton;
[SerializeField]
private Button readyButton;
[SerializeField]
private Button cancelButton;
[SerializeField]
private Button exitButton;
#endregion
[SerializeField]
private Text countUpText;
[SerializeField]
private Text countDownText;
[SerializeField]
private MatchmakingUsernamePanel[] usernameList;
#endregion
Add the following functions to reset the Username text, frame color, and all the timers:
/// Clean up the username text and reset frame color
/// After that, populate current party members to the username text
private void CleanAndPopulateUsernameText()
{
// Emptying username text in the matchmaking window
foreach (var username in usernameList)
{
username.SetUsernameText("");
username.SetUsernameFrameColor(Color.white);
}
// Populate party members
int count = 0;
foreach (var username in GetPartyHandler().partyMembers)
{
Debug.Log($"Populate party member with username: {username.Value}");
usernameList[count].SetUsernameText(username.Value);
count++;
}
}
/// Reset the count down and count up into default value
private void ResetTimerText()
{
countDownText.enabled = true;
countDownText.text = DefaultCountDown;
countUpText.text = DefaultCountUp;
}
Since the Matchmaking panel contains multiple windows (views), create an enum in MatchmakingHandler.cs to help change windows. Add this enum after the UI references:
private enum MatchmakingWindows
{
Lobby,
FindMatch,
Matchmaking,
Game
}
Create a function that states what will happen when you change into the selected window.
private MatchmakingWindows DisplayWindow
{
get => DisplayWindow;
set
{
switch (value)
{
case MatchmakingWindows.Lobby:
matchmakingWindow.SetActive(false);
findMatchWindow.SetActive(false);
readyMatchWindow.SetActive(false);
gameWindow.SetActive(false);
break;
case MatchmakingWindows.FindMatch:
ResetTimerText();
matchmakingWindow.SetActive(true);
findMatchWindow.SetActive(true);
readyMatchWindow.SetActive(false);
gameWindow.SetActive(false);
break;
case MatchmakingWindows.Matchmaking:
ResetTimerText();
CleanAndPopulateUsernameText();
matchmakingWindow.SetActive(true);
findMatchWindow.SetActive(false);
readyMatchWindow.SetActive(true);
gameWindow.SetActive(false);
break;
case MatchmakingWindows.Game:
matchmakingWindow.SetActive(true);
findMatchWindow.SetActive(false);
readyMatchWindow.SetActive(false);
gameWindow.SetActive(true);
break;
default:
break;
}
}
}
Since you will frequently need to call party data from PartyHandler.cs_ _ using the Lobby instance, add the following code into MatchmakingHandler.cs to simplify the call:
// Grab the current Party Handler
private PartyHandler GetPartyHandler()
{
return LobbyHandler.Instance.partyHandler;
}
Create a function that validates all the data needed for Matchmaking:
/// Validate empty party, game mode, and party members count based on game mode
private bool ValidateGameModeAndParty()
{
// Set game mode based on game mode selector dropdown
gameMode = gameModeDropdown.options[gameModeDropdown.value].text.ToGameMode();
// Avoid to choose default game mode
if (gameModeDropdown.value == 0)
{
LobbyHandler.Instance.WriteLogMessage("Choose the game mode", Color.red);
return false;
}
// Check if user is not in the party
if (GetPartyHandler().partyMembers == null || GetPartyHandler().partyMembers?.Count == 0)
{
LobbyHandler.Instance.WriteLogMessage("You are not in the party", Color.red);
return false;
}
// Avoid party members exceed from the game mode
if (gameMode == GameMode.versusOne && GetPartyHandler().partyMembers?.Count == 1 ||
gameMode == GameMode.versusTwo && GetPartyHandler().partyMembers?.Count <= 2)
{
return true;
}
LobbyHandler.Instance.WriteLogMessage("Party members are exceeding from the selected game mode", Color.red);
return false;
}
Now that you have set the preceding functions, you can update your basic functionalities. Earlier, you only added debugging to the output; now you can add some code to update the UIs.
Check the game mode and party. After it passes, start matchmaking
private void FindMatch()
{
// Check if the party is already created,
// game mode is not empty, and
// party /// members are not exceeding from the selected game mode
if (!ValidateGameModeAndParty())
{
return;
}
AccelBytePlugin.GetLobby().StartMatchmaking(gameMode.GetString()
, result =>
{
if (result.IsError)
...
else
{
...
DisplayWindow = MatchmakingWindows.FindMatch;
...
/// Cancel find match and back to the Lobby
private void CancelMatchmaking()
{
AccelBytePlugin.GetLobby().CancelMatchmaking(gameMode.GetString(), result =>
{
//Check this is not an error
if (result.IsError)
...
else
{
...
DisplayWindow = MatchmakingWindows.Lobby;
...
Create a new function to set up Matchmaking UIs when initializing.
/// Setup UI
public void SetupMatchmaking()
{
// Setup button listener
findMatchButton.onClick.AddListener(FindMatch);
readyButton.onClick.AddListener(ReadyMatchmaking);
cancelButton.onClick.AddListener(CancelMatchmaking);
exitButton.onClick.AddListener(() => DisplayWindow = MatchmakingWindows.Lobby);
}
Insert the SetupMatchmaking function into MenuHandler.cs. When a player navigates to the Lobby menu, this will trigger the setup.
LobbyButton.onClick.AddListener(() =>
{
GetComponent<MatchmakingHandler>().SetupMatchmaking();
// Other actions
...
});
Create some functions that will update your UIs in the notification events update in MatchmakingHandler.cs.
#region Notification
/// Called when party leader find a match, cancel match, or found a match
public void MatchmakingCompletedNotification(MatchmakingNotif result)
{
// Handle if match id is empty
if (string.IsNullOrEmpty(result.matchId))
{
// Called when party leader find a match
if (result.status == "start")
{
DisplayWindow = MatchmakingWindows.FindMatch;
}
// Called when party leader cancel a match
else if (result.status == "cancel")
{
DisplayWindow = MatchmakingWindows.Lobby;
}
return;
}
// Called when found a match
matchId = result.matchId;
Debug.Log($"Found a match. Match id: {matchId}");
DisplayWindow = MatchmakingWindows.Matchmaking;
}
// Called when there is a player who set ready to play
public void ReadyForMatchConfirmedNotification(ReadyForMatchConfirmation result)
{
// Display unknown player who ready in the current match into right panel
if (!GetPartyHandler().partyMembers.ContainsKey(result.userId))
{
for (int i = GetPartyHandler().partyMembers.Count; i < usernameList.Length; i++)
{
if (string.IsNullOrEmpty(usernameList[i].GetUsernameText()))
{
AccelBytePlugin.GetUser().GetUserByUserId(result.userId, getUserResult =>
{
usernameList[i].SetUsernameText(getUserResult.Value.displayName);
});
usernameList[i].SetUsernameFrameColor(Color.green);
break;
}
}
}
// Display party member who ready in the current match into left panel
else
{
for (int i = 0; i < GetPartyHandler().partyMembers.Count; i++)
{
if (usernameList[i].GetUsernameText() == GetPartyHandler().partyMembers[result.userId])
{
usernameList[i].SetUsernameFrameColor(Color.green);
break;
}
}
}
}
/// Called when all player in the current match is already ready to play
public void DSUpdatedNotification(DsNotif result)
{
countDownText.enabled = false;
ConnectionHandler.ip = result.ip;
ConnectionHandler.port = result.port;
if (result.status != "READY" && result.status != "BUSY") return;
Debug.Log($"Game is started");
DisplayWindow = MatchmakingWindows.Game;
}
/// Called when matchmaking is canceled when there are not enough players ready to play
public void RematchmakingNotif(RematchmakingNotification result)
{
// Find another match if the ban duration is zero
if (result.banDuration == 0)
{
Debug.Log($"Find another match");
DisplayWindow = MatchmakingWindows.FindMatch;
return;
}
// Display ban duration to party notification
LobbyHandler.Instance.WriteLogMessage($"You must wait for {result.banDuration} s to start matchmaking", Color.red);
DisplayWindow = MatchmakingWindows.Lobby;
}
#endregion
Create the following new functions in NotificationHandler.cs to call the functions that you created earlier:
// Collection of friend notifications
#region Matchmaking
/// Called when matchmaking is found
/// <param name="result"> Contains data of status and match id</param>
public void OnMatchmakingCompleted(Result<MatchmakingNotif> result)
{
GetComponent<MatchmakingHandler>().MatchmakingCompletedNotification(result.Value);
}
/// <summary>
/// Called when user send ready for match confirmation
/// </summary>
/// <param name="result"> Contains data of user id and match id</param>
public void OnReadyForMatchConfirmed(Result<ReadyForMatchConfirmation> result)
{
GetComponent<MatchmakingHandler>().ReadyForMatchConfirmedNotification(result.Value);
}
/// <summary>
/// Called when all user is already confirmed the readiness
/// </summary>
/// <param name="result"> Contains data of ds notification</param>
public void OnDSUpdated(Result<DsNotif> result)
{
GetComponent<MatchmakingHandler>().DSUpdatedNotification(result.Value);
}
/// <summary>
/// Called when there is user who not confirm the match
/// - The party that has a user who did not confirm the match will get banned and need to start matchmaking again
/// - The other party will start matchmaking automatically if ban duration is zero
/// </summary>
/// <param name="result"> Contains data of ban duration</param>
public void OnRematchmakingNotif(Result<RematchmakingNotification> result)
{
GetComponent<MatchmakingHandler>().RematchmakingNotif(result.Value);
}
#endregion
Update the notification events in LobbyHandler.cs:
public void ConnectToLobby()
{
...
//Matchmaking
_lobby.MatchmakingCompleted += notificationHandler.OnMatchmakingCompleted;
_lobby.ReadyForMatchConfirmed += notificationHandler.OnReadyForMatchConfirmed;
_lobby.RematchmakingNotif += notificationHandler.OnRematchmakingNotif;
_lobby.DSUpdated += notificationHandler.OnDSUpdated;
}
...
public void RemoveLobbyListeners()
{
...
//Matchmaking
_lobby.MatchmakingCompleted -= notificationHandler.OnMatchmakingCompleted;
_lobby.ReadyForMatchConfirmed -= notificationHandler.OnReadyForMatchConfirmed;
_lobby.RematchmakingNotif -= notificationHandler.OnRematchmakingNotif;
_lobby.DSUpdated -= notificationHandler.OnDSUpdated;
}
...
In the Unity Editor, on the AccelByteHandler gameobject, drag the necessary objects to the exposed variables of the script component.
Expand the MatchmakingPanel and drag the objects to the remaining exposed variables:
In the Script component, expand the Username list and drag the objects to their corresponding variables:
Create a new script called MatchmakingManagementPanel.cs. Attach it to the MatchmakingPanel gameObject and add the following UI library to the top of the script:
using UnityEngine.UI;
[SerializeField]
private Image loadingImage;
[SerializeField]
private Text countUpText;
[SerializeField]
private Text countDownText;
private bool isWaiting;
private float deltaTime;
Create a coroutine that will update the loading (timer) animation every 0.1 seconds.
/// Animate the loading bar each 0.1 seconds
private IEnumerator StartLoadingAnimation()
{
// Avoid the loading animation is being called when the Async is not finished yet
isWaiting = true;
// Animate the loading image
loadingImage.transform.Rotate(0, 0, -45);
// Wait 0.1 seconds before next animation is executed
yield return new WaitForSeconds(0.1f);
// Set boolean so it can be called again in the Update
isWaiting = false;
}
Add the following code to create some functions that will update the Find Match and Ready countdown timers.
/// Start count up timer to show time elapsed for waiting to find a match
private void StartCountup()
{
if (!countUpText.isActiveAndEnabled) return;
// Parse the time elapsed text into minutes and seconds (int)
string time = countUpText.text.Substring(countUpText.text.IndexOf(':') + 2);
int minutes = int.Parse(time.Substring(0, time.IndexOf(':')));
int seconds = int.Parse(time.Substring(time.IndexOf(':') + 1));
// Add 1 second increment
int timer = minutes * 60 + seconds + 1;
// Parse the time into text with format mm:ss
countUpText.text = $"Time Elapsed: {string.Format("{0:00}", (timer / 60))}:{string.Format("{0:00}", (timer % 60))}";
}
/// Start the countdown timer to show time remaining to set ready to play
private void StartCountdown()
{
// Do nothing if the count down text is not active
if (!countDownText.isActiveAndEnabled) return;
// Parse from text into time (int)
int timer = int.Parse(countDownText.text);
// Decrease 1 second and parse into text
countDownText.text = Mathf.Round(timer - 1).ToString();
}
Add OnEnable() and Update() into MatchmakingManagementPanel.cs after the UI references:
private void OnEnable()
{
// Reset boolean when object changes into enable
isWaiting = false;
}
private void Update()
{
// Start loading animation
if (!isWaiting)
{
StartCoroutine(StartLoadingAnimation());
}
// Start count down and count up timer
if (countUpText.isActiveAndEnabled || countDownText.isActiveAndEnabled)
{
if (deltaTime >= 1)
{
deltaTime = 0;
StartCountup();
StartCountdown();
}
else
{
deltaTime += Time.deltaTime;
}
}
}
In the Unity Editor, in your scene, on the MatchmakingPanel gameObject, drag the necessary objects to the exposed variables.
Create a new script called MatchmakingUsernamePanel.cs and attach it to all PlayerObject gameObjects.
Add the following UI library to the top of the MatchmakingUsernamePanel.cs script:
using UnityEngine.UI;
Add the following UI references and local variables to hold temporary data:
[SerializeField]
private Text UsernameText;
[SerializeField]
private Image UsernameFrameImage;
Add the following code to update the PlayerBox UI:
public string GetUsernameText()
{
return UsernameText.text;
}
public void SetUsernameText(string text)
{
if (UsernameText == null) return;
UsernameText.text = text;
}
public void SetUsernameFrameColor(Color color)
{
UsernameFrameImage.color = color;
}
In the Unity Editor, open the PlayerBox object and drag the following objects to their variables:
NOTE
Save time by attaching the MatchmakingUsernamePanel.cs script to a prefab and then duplicating it for each gameObject.
Congratulations! You have now fully implemented the Matchmaking service.
Proceed to the next section to learn how to implement Armada.
// Copyright (c) 2021 - 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
using AccelByte.Api;
using AccelByte.Models;
using AccelByte.Core;
using UnityEngine;
using UnityEngine.UI;
using UI = UnityEngine.UI;
public class MatchmakingHandler : MonoBehaviour
{
private const string DefaultCountUp = "Time Elapsed: 00:00";
// This default count down time must be similar with the Lobby Config in the Admin Portal
private const string DefaultCountDown = "20";
// Current selected game mode
private GameMode gameMode;
// Current active match id
private string matchId;
#region UI
[SerializeField]
private GameObject matchmakingWindow;
[SerializeField]
private GameObject findMatchWindow;
[SerializeField]
private GameObject readyMatchWindow;
[SerializeField]
private GameObject gameWindow;
[SerializeField]
private Dropdown gameModeDropdown;
#region Button
[SerializeField]
private Button findMatchButton;
[SerializeField]
private Button readyButton;
[SerializeField]
private Button cancelButton;
[SerializeField]
private Button exitButton;
#endregion
[SerializeField]
private Text countUpText;
[SerializeField]
private Text countDownText;
[SerializeField]
private UI.Image loadingImage;
[SerializeField]
private MatchmakingUsernamePanel[] usernameList;
#endregion
private enum MatchmakingWindows
{
Lobby,
FindMatch,
Matchmaking,
Game
}
private MatchmakingWindows DisplayWindow
{
get => DisplayWindow;
set
{
switch (value)
{
case MatchmakingWindows.Lobby:
matchmakingWindow.SetActive(false);
findMatchWindow.SetActive(false);
readyMatchWindow.SetActive(false);
gameWindow.SetActive(false);
break;
case MatchmakingWindows.FindMatch:
ResetTimerText();
matchmakingWindow.SetActive(true);
findMatchWindow.SetActive(true);
readyMatchWindow.SetActive(false);
gameWindow.SetActive(false);
break;
case MatchmakingWindows.Matchmaking:
ResetTimerText();
CleanAndPopulateUsernameText();
matchmakingWindow.SetActive(true);
findMatchWindow.SetActive(false);
readyMatchWindow.SetActive(true);
gameWindow.SetActive(false);
break;
case MatchmakingWindows.Game:
matchmakingWindow.SetActive(true);
findMatchWindow.SetActive(false);
readyMatchWindow.SetActive(false);
gameWindow.SetActive(true);
break;
default:
break;
}
}
}
/// <summary>
/// Grab the current Party Handler
/// </summary>
private PartyHandler GetPartyHandler()
{
return LobbyHandler.Instance.partyHandler;
}
/// <summary>
/// Setup UI
/// </summary>
public void SetupMatchmaking()
{
// Setup button listener
findMatchButton.onClick.AddListener(FindMatch);
readyButton.onClick.AddListener(ReadyMatchmaking);
cancelButton.onClick.AddListener(CancelMatchmaking);
exitButton.onClick.AddListener(() => DisplayWindow = MatchmakingWindows.Lobby);
}
/// <summary>
/// Check the game mode and party. After it passes, start matchmaking
/// </summary>
private void FindMatch()
{
// Check if the party is already created,
// game mode is not empty, and
// party members are not exceeding from the selected game mode
if (!ValidateGameModeAndParty())
{
return;
}
AccelBytePlugin.GetLobby().StartMatchmaking(gameMode, result =>
{
//Check this is not an error
if (result.IsError)
{
Debug.Log($"Unable to start matchmaking: error code: {result.Error.Code} message: {result.Error.Message}");
}
else
{
Debug.Log("Started matchmaking is successful");
DisplayWindow = MatchmakingWindows.FindMatch;
}
});
}
/// <summary>
/// Cancel find match and back to the Lobby
/// </summary>
private void CancelMatchmaking()
{
AccelBytePlugin.GetLobby().CancelMatchmaking(gameMode, result =>
{
//Check this is not an error
if (result.IsError)
{
Debug.Log($"Unable to cancel matchmaking: error code: {result.Error.Code} message: {result.Error.Message}");
}
else
{
Debug.Log("Canceled matchmaking is successful");
DisplayWindow = MatchmakingWindows.Lobby;
}
});
}
/// <summary>
/// Set ready to play when matchmaking
/// </summary>
private void ReadyMatchmaking()
{
AccelBytePlugin.GetLobby().ConfirmReadyForMatch(matchId, result =>
{
//Check this is not an error
if (result.IsError)
{
Debug.Log($"Unable to ready for match: error code: {result.Error.Code} message: {result.Error.Message}");
}
else
{
Debug.Log("Ready for match is successful");
}
});
}
#region Notification
/// <summary>
/// Called when party leader find a match, cancel match, or found a match
/// </summary>
/// <param name="result"> Contains data of status and match id</param>
public void MatchmakingCompletedNotification(MatchmakingNotif result)
{
// Handle if match id is empty
if (string.IsNullOrEmpty(result.matchId))
{
// Called when party leader find a match
if (result.status == "start")
{
DisplayWindow = MatchmakingWindows.FindMatch;
}
// Called when party leader cancel a match
else if (result.status == "cancel")
{
DisplayWindow = MatchmakingWindows.Lobby;
}
return;
}
// Called when found a match
matchId = result.matchId;
Debug.Log($"Found a match. Match id: {matchId}");
DisplayWindow = MatchmakingWindows.Matchmaking;
}
/// <summary>
/// Called when there is a player who set ready to play
/// </summary>
/// <param name="result"> Contains data of match id and user id</param>
public void ReadyForMatchConfirmedNotification(ReadyForMatchConfirmation result)
{
// Display unknown player who ready in the current match into right panel
if (!GetPartyHandler().partyMembers.ContainsKey(result.userId))
{
for (int i = GetPartyHandler().partyMembers.Count; i < usernameList.Length; i++)
{
if (string.IsNullOrEmpty(usernameList[i].GetUsernameText()))
{
AccelBytePlugin.GetUser().GetUserByUserId(result.userId, getUserResult =>
{
usernameList[i].SetUsernameText(getUserResult.Value.displayName);
});
usernameList[i].SetUsernameFrameColor(Color.green);
break;
}
}
}
// Display party member who ready in the current match into left panel
else
{
for (int i = 0; i < GetPartyHandler().partyMembers.Count; i++)
{
if (usernameList[i].GetUsernameText() == GetPartyHandler().partyMembers[result.userId])
{
usernameList[i].SetUsernameFrameColor(Color.green);
break;
}
}
}
}
/// <summary>
/// Called when all player in the current match is already ready to play
/// </summary>
/// <param name="result"> contains data of status, match id, ip, port, etc</param>
public void DSUpdatedNotification(DsNotif result)
{
countDownText.enabled = false;
Debug.Log($"Game is started");
DisplayWindow = MatchmakingWindows.Game;
}
/// <summary>
/// Called when matchmaking is canceled due to not enough players being ready to play
/// </summary>
/// <param name="result"> contains data of ban duration </param>
public void RematchmakingNotif(RematchmakingNotification result)
{
// Find another match if the ban duration is zero
if (result.banDuration == 0)
{
Debug.Log($"Find another match");
DisplayWindow = MatchmakingWindows.FindMatch;
return;
}
// Display ban duration to party notification
GetPartyHandler().WritePartyMessage($"[Matchmaking] You must wait for {result.banDuration} s to start matchmaking", Color.red);
DisplayWindow = MatchmakingWindows.Lobby;
}
#endregion
/// <summary>
/// Validate empty party, game mode, and party members count based on game mode
/// </summary>
/// <returns> Return true value if the validation is passed and vice versa</returns>
private bool ValidateGameModeAndParty()
{
// Set game mode based on game mode selector dropdown
gameMode = gameModeDropdown.options[gameModeDropdown.value].text.ToGameMode();
// Avoid to choose default game mode
if (gameModeDropdown.value == 0)
{
LobbyHandler.Instance.WriteLogMessage("Choose the game mode", Color.red);
return false;
}
// Check if user is not in the party
if (GetPartyHandler().partyMembers == null || GetPartyHandler().partyMembers?.Count == 0)
{
LobbyHandler.Instance.WriteLogMessage("You are not in the party", Color.red);
return false;
}
// Avoid party members exceed from the game mode
if (gameMode == GameMode.versusOne && GetPartyHandler().partyMembers?.Count == 1 ||
gameMode == GameMode.versusTwo && GetPartyHandler().partyMembers?.Count <= 2)
{
return true;
}
LobbyHandler.Instance.WriteLogMessage("Party members are exceeding from the selected game mode", Color.red);
return false;
}
/// <summary>
/// Clean up the username text and reset frame color
/// After that, populate current party members to the username text
/// </summary>
private void CleanAndPopulateUsernameText()
{
// Emptying username text in the matchmaking window
foreach (var username in usernameList)
{
username.SetUsernameText("");
username.SetUsernameFrameColor(Color.white);
}
// Populate party members
int count = 0;
foreach (var username in GetPartyHandler().partyMembers)
{
Debug.Log($"Populate party member with username: {username.Value}");
usernameList[count].SetUsernameText(username.Value);
count++;
}
}
/// <summary>
/// Reset the count down and count up into default value
/// </summary>
private void ResetTimerText()
{
countDownText.enabled = true;
countDownText.text = DefaultCountDown;
countUpText.text = DefaultCountUp;
}
}
// Copyright (c) 2021 - 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class MatchmakingManagementPanel : MonoBehaviour
{
[SerializeField]
private Image loadingImage;
[SerializeField]
private Text countUpText;
[SerializeField]
private Text countDownText;
private bool isWaiting;
private float deltaTime;
private void OnEnable()
{
// Reset boolean when object changes into enable
isWaiting = false;
}
private void Update()
{
// Start loading animation
if (!isWaiting)
{
StartCoroutine(StartLoadingAnimation());
}
// Start count down and count up timer
if (countUpText.isActiveAndEnabled || countDownText.isActiveAndEnabled)
{
if (deltaTime >= 1)
{
deltaTime = 0;
StartCountup();
StartCountdown();
}
else
{
deltaTime += Time.deltaTime;
}
}
}
/// <summary>
/// Animate the loading bar each 0.1 seconds
/// </summary>
private IEnumerator StartLoadingAnimation()
{
// Avoid the loading animation is being called when the Async is not finished yet
isWaiting = true;
// Animate the loading image
loadingImage.transform.Rotate(0, 0, -45);
// Wait 0.1 seconds before next animation is executed
yield return new WaitForSeconds(0.1f);
// Set boolean so it can be called again in the Update
isWaiting = false;
}
/// <summary>
/// Start count up timer to show time elapsed for waiting to find a match
/// </summary>
private void StartCountup()
{
if (!countUpText.isActiveAndEnabled) return;
// Parse the time elapsed text into minutes and seconds (int)
string time = countUpText.text.Substring(countUpText.text.IndexOf(':') + 2);
int minutes = int.Parse(time.Substring(0, time.IndexOf(':')));
int seconds = int.Parse(time.Substring(time.IndexOf(':') + 1));
// Add 1 second increment
int timer = minutes * 60 + seconds + 1;
// Parse the time into text with format mm:ss
countUpText.text = $"Time Elapsed: {string.Format("{0:00}", (timer / 60))}:{string.Format("{0:00}", (timer % 60))}";
}
/// <summary>
/// Start the count down timer to show time remaining to set ready to play
/// </summary>
private void StartCountdown()
{
// Do nothing if the count down text is not active
if (!countDownText.isActiveAndEnabled) return;
// Parse from text into time (int)
int timer = int.Parse(countDownText.text);
// Decrease 1 second and parse into text
countDownText.text = Mathf.Round(timer - 1).ToString();
}
}
// Copyright (c) 2021 - 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
using UnityEngine;
using UnityEngine.UI;
public class MatchmakingUsernamePanel : MonoBehaviour
{
[SerializeField]
private Text UsernameText;
[SerializeField]
private Image UsernameFrameImage;
public string GetUsernameText()
{
return UsernameText.text;
}
public void SetUsernameText(string text)
{
UsernameText.text = text;
}
public void SetUsernameFrameColor(Color color)
{
UsernameFrameImage.color = color;
}
}
// Copyright (c) 2021 - 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
using UnityEngine;
using AccelByte.Api;
public class LobbyHandler : MonoBehaviour
{
/// <summary>
/// Private Instance
/// </summary>
static LobbyHandler _instance;
/// <summary>
/// The Instance Getter
/// </summary>
public static LobbyHandler Instance => _instance;
/// <summary>
/// The Instance Getter
/// </summary>
private Lobby _lobby;
public GameObject LobbyWindow;
#region Notification Box
[Header("Notification Box")]
[SerializeField]
private Transform notificationBoxContentView;
[SerializeField]
private GameObject logMessagePrefab;
#endregion
[HideInInspector]
public NotificationHandler notificationHandler;
private void Awake()
{
// Check if another Instance is already created, and if so delete this one, otherwise destroy the object
if (_instance != null && _instance != this)
{
Destroy(this);
return;
}
else
{
_instance = this;
}
// Get the the object handler
notificationHandler = gameObject.GetComponent<NotificationHandler>();
}
/// <summary>
/// Connect to the <see cref="Lobby"/> and setup CallBacks
/// </summary>
public void ConnectToLobby()
{
//Get a reference to the instance of the Lobby
_lobby = AccelBytePlugin.GetLobby();
//Init menu handler
GetComponent<MenuHandler>().Create();
GetComponent<MenuHandler>().Menu.gameObject.SetActive(true);
//Connection
_lobby.Connected += notificationHandler.OnConnected;
_lobby.Disconnecting += notificationHandler.OnDisconnecting;
_lobby.Disconnected += notificationHandler.OnDisconnected;
//Friends
_lobby.FriendsStatusChanged += notificationHandler.OnFriendsStatusChanged;
_lobby.FriendRequestAccepted += notificationHandler.OnFriendRequestAccepted;
_lobby.OnIncomingFriendRequest += notificationHandler.OnIncomingFriendRequest;
_lobby.FriendRequestCanceled += notificationHandler.OnFriendRequestCanceled;
_lobby.FriendRequestRejected += notificationHandler.OnFriendRequestRejected;
_lobby.OnUnfriend += notificationHandler.OnUnfriend;
//Party
_lobby.InvitedToParty += notificationHandler.OnInvitedToParty;
_lobby.JoinedParty += notificationHandler.OnJoinedParty;
_lobby.KickedFromParty += notificationHandler.OnKickedFromParty;
_lobby.LeaveFromParty += notificationHandler.OnLeaveFromParty;
_lobby.RejectedPartyInvitation += notificationHandler.OnRejectedPartyInvitation;
_lobby.PartyDataUpdateNotif += notificationHandler.OnPartyDataUpdateNotif;
//Matchmaking
_lobby.MatchmakingCompleted += notificationHandler.OnMatchmakingCompleted;
_lobby.ReadyForMatchConfirmed += notificationHandler.OnReadyForMatchConfirmed;
_lobby.RematchmakingNotif += notificationHandler.OnRematchmakingNotif;
_lobby.DSUpdated += notificationHandler.OnDSUpdated;
//Connect to the Lobby
if (!_lobby.IsConnected)
{
_lobby.Connect();
}
}
public void RemoveLobbyListeners()
{
//Remove delegate from Lobby
//Connection
_lobby.Connected -= notificationHandler.OnConnected;
_lobby.Disconnecting -= notificationHandler.OnDisconnecting;
_lobby.Disconnected -= notificationHandler.OnDisconnected;
//Friends
_lobby.FriendsStatusChanged -= notificationHandler.OnFriendsStatusChanged;
_lobby.FriendRequestAccepted -= notificationHandler.OnFriendRequestAccepted;
_lobby.OnIncomingFriendRequest -= notificationHandler.OnIncomingFriendRequest;
_lobby.FriendRequestCanceled -= notificationHandler.OnFriendRequestCanceled;
_lobby.FriendRequestRejected -= notificationHandler.OnFriendRequestRejected;
_lobby.OnUnfriend -= notificationHandler.OnUnfriend;
//Party
_lobby.InvitedToParty -= notificationHandler.OnInvitedToParty;
_lobby.JoinedParty -= notificationHandler.OnJoinedParty;
_lobby.KickedFromParty -= notificationHandler.OnKickedFromParty;
_lobby.LeaveFromParty -= notificationHandler.OnLeaveFromParty;
_lobby.RejectedPartyInvitation -= notificationHandler.OnRejectedPartyInvitation;
_lobby.PartyDataUpdateNotif -= notificationHandler.OnPartyDataUpdateNotif;
//Matchmaking
_lobby.MatchmakingCompleted -= notificationHandler.OnMatchmakingCompleted;
_lobby.ReadyForMatchConfirmed -= notificationHandler.OnReadyForMatchConfirmed;
_lobby.RematchmakingNotif -= notificationHandler.OnRematchmakingNotif;
_lobby.DSUpdated -= notificationHandler.OnDSUpdated;
}
public void DisconnectFromLobby()
{
if (AccelBytePlugin.GetLobby().IsConnected)
{
AccelBytePlugin.GetLobby().Disconnect();
}
}
private void OnApplicationQuit()
{
// Attempt to Disconnect from the Lobby when the Game Quits
DisconnectFromLobby();
}
/// <summary>
/// Write the log message on the notification box
/// </summary>
/// <param name="text"> text that will be shown in the party notification</param>
public void WriteLogMessage(string text, Color color)
{
LogMessagePanel logPanel = Instantiate(logMessagePrefab, notificationBoxContentView).GetComponent<LogMessagePanel>();
logPanel.UpdateNotificationUI(text, color);
}
}
// Copyright (c) 2021 - 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class MenuHandler : MonoBehaviour
{
public Transform Menu;
public Button LobbyButton;
public Button FriendsButton;
private bool isInitialized = false;
public void Create()
{
if (isInitialized) return;
isInitialized = true;
LobbyButton.onClick.AddListener(() =>
{
GetComponent<PartyHandler>().SetupParty();
GetComponent<MatchmakingHandler>().SetupMatchmaking();
Menu.gameObject.SetActive(false);
GetComponent<LobbyHandler>().LobbyWindow.SetActive(true);
});
FriendsButton.onClick.AddListener(() =>
{
GetComponent<FriendsManagementHandler>().Setup(FriendsManagementHandler.ExitMode.Menu);
Menu.gameObject.SetActive(false);
GetComponent<FriendsManagementHandler>().FriendsManagementWindow.SetActive(true);
});
}
}
// Copyright (c) 2021 - 2022 AccelByte Inc. All Rights Reserved.
// This is licensed software from AccelByte Inc, for limitations
// and restrictions contact your company contract manager.
using UnityEngine;
using AccelByte.Models;
using AccelByte.Core;
public class NotificationHandler : MonoBehaviour
{
#region Notifications
// Collection of connection notifications
#region Connections
/// <summary>
/// Called when lobby is connected
/// </summary>
public void OnConnected()
{
Debug.Log("Lobby Connected");
}
/// <summary>
/// Called when connection is disconnecting
/// </summary>
/// <param name="result"> Contains data of message</param>
public void OnDisconnecting(Result<DisconnectNotif> result)
{
Debug.Log($"Lobby Disconnecting {result.Value.message}");
}
/// <summary>
/// Called when connection is being disconnected
/// </summary>
/// <param name="result"> Contains data of websocket close code</param>
public void OnDisconnected(WsCloseCode result)
{
Debug.Log($"Lobby Disconnected: {result}");
}
#endregion
// Collection of friend notifications
#region Friends
/// <summary>
/// Called when friend status is changed
/// </summary>
/// <param name="result"> Contains data of user id, availability, status, etc</param>
public void OnFriendsStatusChanged(Result<FriendsStatusNotif> result)
{
GetComponent<FriendsManagementHandler>().UpdateFriends(result.Value);
}
/// <summary>
/// Called when friend request is accepted
/// </summary>
/// <param name="result"> Contains data of friend's user id</param>
public void OnFriendRequestAccepted(Result<Friend> result)
{
Debug.Log($"Accepted a Friend Request from user {result.Value.friendId}");
}
/// <summary>
/// Called when there is incoming friend request
/// </summary>
/// <param name="result"> Contains data of friend's user id</param>
public void OnIncomingFriendRequest(Result<Friend> result)
{
Debug.Log($"Received a Friend Request from user {result.Value.friendId}");
}
/// <summary>
/// Called when friend is unfriend
/// </summary>
/// <param name="result"> Contains data of friend's user id</param>
public void OnUnfriend(Result<Friend> result)
{
Debug.Log($"Unfriended User {result.Value.friendId}");
}
/// <summary>
/// Called when friend request is canceled
/// </summary>
/// <param name="result"> Contains data of sender user id</param>
public void OnFriendRequestCanceled(Result<Acquaintance> result)
{
Debug.Log($"Cancelled a Friend Request from user {result.Value.userId}");
}
/// <summary>
/// Called when friend request is rejected
/// </summary>
/// <param name="result"> Contains data of rejector user id</param>
public void OnFriendRequestRejected(Result<Acquaintance> result)
{
Debug.Log($"Rejected a Friend Request from user {result.Value.userId}");
}
#endregion
// Collection of party notifications
#region Party
/// <summary>
/// Called when user gets party invitation
/// </summary>
/// <param name="result"> Contains data of inviter, party id, and invitation token</param>
public void OnInvitedToParty(Result<PartyInvitation> result)
{
GetComponent<PartyHandler>().InvitePartyNotification(result.Value);
}
/// <summary>
/// Called when user joins to the party
/// </summary>
/// <param name="result"> Contains data of joined user id</param>
public void OnJoinedParty(Result<JoinNotification> result)
{
GetComponent<PartyHandler>().JoinedPartyNotification(result.Value);
}
/// <summary>
/// Called when user is kicked by party leader
/// </summary>
/// <param name="result"> Contains data of party leader's user id, party id, and kicked user id</param>
public void OnKickedFromParty(Result<KickNotification> result)
{
GetComponent<PartyHandler>().KickPartyNotification();
}
/// <summary>
/// Called when user leaves from the party
/// </summary>
/// <param name="result"> Contains data of party leader's user id and leaver user id</param>
public void OnLeaveFromParty(Result<LeaveNotification> result)
{
GetComponent<PartyHandler>().LeavePartyNotification(result.Value);
}
/// <summary>
/// Called when user rejects party invitation
/// </summary>
/// <param name="result"> Contains data of party id, party leader's user id, and rejector user id</param>
public void OnRejectedPartyInvitation(Result<PartyRejectNotif> result)
{
Debug.Log("[Party-Notification] Invitee rejected a party invitation");
}
/// <summary>
/// Called when party data is updated
/// </summary>
/// <param name="result"> Contains data of updated party</param>
public void OnPartyDataUpdateNotif(Result<PartyDataUpdateNotif> result)
{
GetComponent<PartyHandler>().DisplayPartyData(result);
}
#endregion
// Collection of friend notifications
#region Matchmaking
/// <summary>
/// Called when matchmaking is found
/// </summary>
/// <param name="result"> Contains data of status and match id</param>
public void OnMatchmakingCompleted(Result<MatchmakingNotif> result)
{
GetComponent<MatchmakingHandler>().MatchmakingCompletedNotification(result.Value);
}
/// <summary>
/// Called when user send ready for match confirmation
/// </summary>
/// <param name="result"> Contains data of user id and match id</param>
public void OnReadyForMatchConfirmed(Result<ReadyForMatchConfirmation> result)
{
GetComponent<MatchmakingHandler>().ReadyForMatchConfirmedNotification(result.Value);
}
/// <summary>
/// Called when all user is already confirmed the readiness
/// </summary>
/// <param name="result"> Contains data of ds notification</param>
public void OnDSUpdated(Result<DsNotif> result)
{
GetComponent<MatchmakingHandler>().DSUpdatedNotification(result.Value);
}
/// <summary>
/// Called when there is user who not confirm the match
/// - The party that has a user who did not confirm the match will get banned and need to start matchmaking again
/// - The other party will start matchmaking automatically if ban duration is zero
/// </summary>
/// <param name="result"> Contains data of ban duration</param>
public void OnRematchmakingNotif(Result<RematchmakingNotification> result)
{
GetComponent<MatchmakingHandler>().RematchmakingNotif(result.Value);
}
#endregion
#endregion
}