Last Updated: 8/15/2022, 4:23:44 PM

# Matchmaking

# Overview

Matchmaking allows parties to be paired to compete against each other in your game. With AccelByte matchmaking services, you can create your own matchmaking rules (MMR) for how parties are matched. MMR are used to determine how parties are matched up under ideal conditions. You can also create flex rules, which dictate how to match parties when ideal matches aren’t possible. These flex rules usually include time limits for the matchmaking process, to ensure that players don’t have to wait too long to play your game.

Like our Party services, Matchmaking uses WebSocket to ensure real-time updates for players. Here’s the basic flow of our matchmaking service:

  1. Start Matchmaking A party leader can make a request to initiate the matchmaking process.
  2. Confirm Players are Ready During the matchmaking process, each player needs to confirm they are ready to play. When all of the players are ready to start, the game will begin.
    • Re-matchmaking Matchmaking will restart if any players in the matchup aren’t ready to play.
    • Cancel Matchmaking Party leaders can cancel the matchmaking process before it’s completed.

Players need to be in a party to use matchmaking. The Matchmaking service supports parties with single users or multiple users, and also supports matching parties with an asymmetrical number of users, e.g. a party with 1 user vs. a party with 3 users. You can configure all of the matchmaking rules in our Admin Portal.

# Prerequisites

Before implementing the Matchmaking service, you need to define the Statistics that will be used by the Matchmaking service to match players together.

# Set Up the Matchmaking Channel in the Admin Portal

  1. In the Admin Portal, choose the target game namespace.

    matchmaking

  2. In the Game Management section, open the Lobby and Matchmaking section and click Matchmaking Ruleset.

    matchmaking

  3. On the Matchmaking page, click Add Configuration to create a new matchmaking channel.

    matchmaking

  4. The Create Ruleset wizard will appear. Fill in the required information on each page.

    • Configuration

      matchmaking

      • Input a name for the game mode in the Game Mode field.
      • Input a description of the game mode in the Game Mode Description field.
      • Input the amount of time, in seconds, that the game client should spend finding a match before timing out in the Find Match Timeout (sec) field.
      • Input the amount of time, in milliseconds, the game server will wait to assign a Match ID to a newly initiated match in the Maximum Delay Time field.

      TIP

      The Maximum Delay Time can be used to help players be matched together in any situation where player count is low, such as during testing.

      • If you want your matchmaking to be joinable, select the Use Joinable Session option.
        • If you select Use Joinable Session, input how long, in seconds, a match should be joinable before timing out in the Joinable Session Timeout (sec) field.

      IMPORTANT

      IMPORTANT Before you can implement joinable sessions, you must grant the following permission to your game server’s IAM Client

      Usage Resource Action
      Joinable Session NAMESPACE:{namespace}:SESSION Create, Read, Update, Delete

      After filling in all of the fields in the Configuration section, click Next.

    • Dedicated Server

      matchmaking

      • If you want to use Armada, select the Dedicated Server option.
        • Choose the Dedicated Server Deployment. If the deployment is left empty, Armada will spawn the default deployment.

      After filling the fields in the Dedicated Server section, click Next.

    • Ruleset

      • Choose Game Mode Only if your game only has one game mode.

        matchmaking

        • Input the maximum number of teams allowed in a match into the Max Teams field.
        • Input the minimum number of teams required for a match into the Min Teams field.
        • Input the maximum number of players allowed in a party into the Max Players field.
        • Input the minimum number of players required to form a party into the Min Players field.
      • Choose Sub Game Mode if your game hsa more than one game mode.

        matchmaking

        • Click Add Sub-Game Mode to add a ruleset for one of your game modes.

        • The Add Ruleset form appears. Fill in the required fields, as seen below:

          matchmaking

          • Input a name for the game mode in the Sub-Game Mode Name field.
          • Input the maximum number of teams allowed in a match into the Max Teams field.
          • Input the minimum number of teams required for a match into the Min Teams field.
          • Input the maximum number of players allowed in a party into the Max Players field.
          • Input the minimum number of players required to form a party into the Min Players field.
        • After filling the Add Ruleset form, click the Add button.

        Repeat this step, creating a ruleset for each of your different game modes. When you’re done, click Next.

    • Summary

      The summary of your configuration appears. Click Next to create the configuration.

      matchmaking

# Add Flexing Match Teams

After configuring your Match Team, you can add a Flexing Match Team rule to prevent players from timing out when not enough players are available for a standard Match Team. In the Flexing Match Team, you can define the rules and player numbers that apply after a set duration of time.

  1. In the Admin Portal, go to the Matchmaking Ruleset page. Choose the Game Mode you wish to manage. In the Action column, click the ellipsis button and select View. You will be directed to the Matchmaking Configuration page.

    matchmaking

  2. Open the Match Teams tab, scroll down to the Flexing Match Teams section, and click the Add Flexing Team button.

    matchmaking

  3. Fill in the Add Flexing Match Team Rule box with the required fields:

    matchmaking

    • Input the minimum number of teams required for a match into the Min Teams field.
    • Input the maximum number of teams allowed in a match into the Max Teams field.
    • Input the minimum number of players required to form a party into the Min Players field.
    • Input the maximum number of players allowed in a party into the Max Players field.
    • Input the duration (in seconds) for the Flexing Team Rule to take effect into the Duration (Sec) field.

Once completed, click the Add button and the Flexing Match Team will be added to the list.

matchmaking

# Matching Rules

In the Matching Rules, there are two types of attributes/rules. The main match rule will be used to find an available match, and the Flexing Match Rule will be used when the matchmaking service cannot find an available match based on the main match rules.

# Add Matching Rules

  1. In the Admin Portal, go to the Matchmaking page. Choose the to which Game Mode you want to add matchmaking rules. In the Action column, click the Ellipsis button and select View. You will be directed to the Matchmaking Detail page.

    matchmaking

  2. In the Matching Rule section, click the Add button.

    matchmaking

  3. Fill in the required information in the form that appears.

    matchmaking

    • Choose the player attribute. You can choose Stat Code if you’ve already created the related statistic configuration, or choose Custom if you want to use a custom attribute that doesn’t have a stat code. For this example, we’re going to choose stat code.
    • Input the player Attribute field with the statcode you used when you configured the related statistic, e.g. match-point. You do not need to fill in this field if you selected Custom above.
    • Input the Criteria for the matchmaking. Currently, the only supported value for criteria is Distance, which refers to the difference between players’ attributes. Matchmaking will match players based on their attribute distance.
    • Input the Reference point or number used for the criteria. As an example, if the distance reference is 1000 and the player’s attribute value is 2000, the matchmaking service will match players with attribute values between 1000 and 3000.
  4. Click Add to continue. The matching rule will be added to the list.

# Add Flexing Match Rules

  1. On the Matchmaking Detail page, switch to the Match Attributes tab. In the Flexing Match Attributes section, click the Add Flexing Attribute button.

matchmaking

  1. The Add Flexing Attribute form will appear. Fill in the required fields.

matchmaking

  • Choose the player Attribute type. You can choose Stat Code if you’ve already created the related statistic configuration, or choose Custom if you want to use a custom attribute that doesn’t have a Stat code. Input the Attribute. If you chose Stat Code above, then you can choose the pre-created stat code from the dropdown. If you chose Custom, enter the new attribute’s name.
  • Input the Criteria for matchmaking. Currently, the only supported value for criteria is Distance, which refers to the numerical difference between players’ attributes.
  • Input the Reference point or number used for the criteria. The reference for the flexing rule should be higher than the matching rule since it’s used to broaden the search for matchmaking. For example, if the matching rule distance value is 1000, then the flexing rule should be 2000, so it can widen the search for available players.
  • Input the Duration of time it will take for the flexing rules to take effect if no matches are found under the matching rules.
  1. Once completed, click Add. The new flexing attribute will be added to the list.

# Add Optional Parameters

You can add optional parameters to your matchmaking rules by first defining the parameters you want, such as region or player level, and then define whether players should be matched when all of the chosen parameters match, when any of them match, or when none of them match. To add optional parameters to your matchmaking rules, follow the steps below:

  1. On the Matchmaking Ruleset page of the Admin Portal, scroll to the Optional Parameters section at the bottom of the page and click Add Optional Parameter to add a new parameter.

    matchmaking

  2. The Add Optional Parameters form appears. Fill it in with the required information.

    matchmaking

    • Input the desired parameter in the Parameter field. This can be Region, Language, Player Level, or anything else.
    • Select the desired Matching Option from the choices below:
      • Select Disabled if you want to turn off optional parameters.
      • Select All if you want players to be matched when all of the defined parameters match.
      • Select Any if you want players to be matched when any single parameter is a match.
      • Select Unique if you want players to be matched when none of the defined parameters match.

    When you’re done, click Add Parameter to add the new parameter.

  3. The parameter you created will appear in the Optional Parameters list in the Matching Rules panel at the bottom of the page. From here you can edit or delete any parameter.

    matchmaking

# Matchmaking Statistics

From this panel you can see all the detailed information about matchmaking. This is real-time data, so you need to let the player start matchmaking, and then you can see the details of information on the panel.

  1. In the matchmaking detail page, find the Stats tab section and click the Stats tab button to go to the matchmaking stats. Here you can see by default all the panel of the matchmaking statistics is something like this If no one player has been in a match through your game.

    matchmaking

  2. Now if you can see it here. There is some information about the party information, such as party ID, user ID, and some user info. You can click the user id to see the detailed information about the player. This matchmaking statistics only happens when the party starts the matchmaking and the current situation is in a queue of matchmaking. If matchmaking is found, then the matchmaking stats or the counter data here will disappear.

    matchmaking

# Export and Import Matchmaking Configurations

# Export a Matchmaking Configuration

Follow the steps below to export a Matchmaking Configuration from a game namespace.

  1. In the Admin Portal, open the game namespace that contains the configuration you want to export. On the Matchmaking page, open the Add Configuration dropdown menu and select Export Configuration.

    matchmaking

  2. Your matchmaking configuration will be exported in a JSON file. Here is an example of the contents:

    IMPORTANT

    If you use our Dedicated Server Manager Armada, don’t forget to also export your Dedicated Server Configuration. You will need both configurations if you want to use them in a different game namespace.

# Import a Matchmaking Configuration

Follow the steps below to import a Matchmaking Configuration into a game namespace.

  1. In the Admin Portal, go to the game namespace where you want to import the configuration. On the Matchmaking page, open the Add Configuration dropdown menu and select Import Configuration.

    matchmaking

  2. The Import Configuration form appears. Here, select the configuration file you want to import and choose the import method from the options below:

    • Choose Replace if you want to completely replace the old configuration data for this namespace with the new data. Existing configurations with the same key will be replaced. New configurations with different keys will be added.
    • Choose Leave Out if you only want to add new data and leave the existing data as is. Existing configurations with the same key will not be changed. A new configurations with different keys will be added to the existing data.

    matchmaking

  3. A confirmation window appears. To confirm your selection, type IMPORT in the text box, then click the Import button.

    matchmaking

  4. Here you can see the configuration has been added to the namespace.

    matchmaking

    IMPORTANT

    If you use our Dedicated Server Manager Armada, don’t forget to also import your Dedicated Server Configuration. You will need both configurations in this namespace to use Armada.

# Implement Matchmaking using SDK

# Start the Matchmaking Process

The matchmaking can be started after the party leader invites another player to the party. The number of party members should match the configuration used when creating the matchmaking channel.

# Matchmaking Notifications

Once the matchmaking is finished, the matchmaking service will send a notification to the players to inform them that they've been matched. To implement this feature, use the function below.

# Cancel Matchmaking

The party leader can cancel matchmaking before a match is found. The players will also be notified that the matchmaking process is cancelled. To implement this feature, refer to the function below.

After the matchmaking process is completed, every player in each party needs to confirm their readiness to start the game. To implement this feature, refer to the function below.

# Restart Matchmaking

If a player doesn’t confirm that they are ready to play after 20 seconds, they will receive a notification. To implement this feature, use the function below.

# Joinable Sessions

# Start Matchmaking for a Joinable Session

The following function is used to start matchmaking for a joinable session.

# Check a Joinable Session’s Status

When a server is claimed, it will query the session details from the Matchmaking service. The game server will determine whether the session is joinable or not by calling the following function:

# Enqueue a Session

When receiving a joinable session, the game server will enqueue the session to the Matchmaking service as soon as it is ready to accept more players. To enqueue a session, use the following function:

After the game server enqueues a session, players who are already matched into the session can connect to the Lobby.

# Update a Session

To ensure that the session in the queue is accurate, any time a player leaves during the Lobby phase, the server will update the session in the queue. To make this update, use the function below:

# Dequeue a Session

When the game is about to start, the game server will dequeue the session to prevent the Matchmaking service from matching a new player into it. To dequeue a session, use the following function:

# Sub-Game Modes

# Matchmake into a Sub-Game Mode

You can use sub-game modes to allow players to choose from multiple game modes within your game. The following function is used to matchmake a player into a sub-game mode.

# Set Up the Matchmaking Channel in the API

You can configure the matchmaking channel using our API. Follow the steps below to perform a request:

  1. Use the Create a Channel: POST /matchmaking/namespaces/{namespace}/channels endpoint. Make sure you have the bearer access token for authorization.

  2. Input the desired Namespace.

  3. Fill out the Request Body.

    • Input a Description of the game mode.
    • Input the name of the game mode into the game_mode field. Possible game modes include duel and battle royale.
    • Define the ruleset of the game by filling in the team, matchmaking rule, and flexing rules.

    Fill in the attributes for the team to define how many teams will be matched.

    • Input the minimum number of teams required for the match into the min_number field.
    • Input the maximum number of teams allowed for the match into the max_number field.
    • Input the minimum number of players required in the team into the player_min_number field.
    • Input the maximum number of players allowed in a party into the player_max_number field.

    Fill in the attributes for the flexing rule. Flexing rules are used when the matchmaking service cannot find a match based on the matching rule. Flexing rules should be the same as matching rules, but with a higher reference point to allow for more possible matches.

    • Input the player Attribute field with the statcode you used when you configured the related statistic, e.g. match-point.
    • Input the Criteria for the matchmaking. Currently the only supported value for criteria is Distance, which refers to the difference between players’ attributes.
    • Input the Reference point or number used for the criteria. The reference for the flexing rule should be higher than the matching rule, since it’s used to broaden the search for matchmaking. For example, if the matching rule distance value is 1000, then the flexing rule should be 2000, so it can widen the search for available players.
    • Input the Duration of time it will take for the flexing rules to take effect if no matches are found under the matching rules.

    Fill in the attributes of the matching rule.

    • Input the player Attribute field with the statcode you used when you configured the related statistic, e.g. match-point.
    • Input the Criteria for the matchmaking. Currently, the only supported value for criteria is Distance, which refers to the difference between players’ attributes. Matchmaking will match players based on their attribute distance.
    • Input the Reference point or number used for the criteria.

# 1v1 Configuration

Here is an example of a channel configuration for a 1 vs 1 match. Two players will be matched together with this configuration.

{
   "slug":"namspace:string",
   "ruleSet":{
      "alliance":{
         "minNumber":2,
         "maxNumber":2,
         "playerMinNumber":1,
         "playerMaxNumber":1
      },
      "matchingRules":[
         {
            "attribute":"string",
            "criteria":"distance",
            "reference":0
         }
      ],
      "flexingRules":[
         {
            "duration":0,
            "attribute":"string",
            "criteria":"distance",
            "reference":0
         }
      ],
      "match_options":{
         "options":null
      },
      "sub_game_modes":{

      }
   },
   "namespace":"namespace",
   "gameMode":"string",
   "description":"string",
   "findMatchTimeoutSeconds":0,
   "sessionQueueTimeoutSeconds":0,
   "joinable":true,
   "socialMatchmaking":false,
   "use_sub_gamemode":false,
   "deployment":"-",
   "max_delay_ms":0
}

# 3v3v3 Configuration

This is a channel configuration for a 3v3v3 match where 9 individual players will be matched and play together.

{
   "slug":"namspace:string",
   "ruleSet":{
      "alliance":{
         "minNumber":3,
         "maxNumber":3,
         "playerMinNumber":3,
         "playerMaxNumber":1
      },
      "matchingRules":[
         {
            "attribute":"string",
            "criteria":"distance",
            "reference":0
         }
      ],
      "flexingRules":[
         {
            "duration":0,
            "attribute":"string",
            "criteria":"distance",
            "reference":0
         }
      ],
      "match_options":{
         "options":null
      },
      "sub_game_modes":{

      }
   },
   "namespace":"namespace",
   "gameMode":"string",
   "description":"string",
   "findMatchTimeoutSeconds":0,
   "sessionQueueTimeoutSeconds":0,
   "joinable":true,
   "socialMatchmaking":false,
   "use_sub_gamemode":false,
   "deployment":"-",
   "max_delay_ms":0
}

# Connecting Custom Services to the Matchmaking service using the Server SDK

# SDK Initialization

Before using the Matchmaking service from the SDK, you will need to initialize your server-side SDK to make you authorized and able to perform create, read, update, and delete actions.

# Golang SDK Initialization

Before using the Matchmaking service from the Golang SDK, you will need to initialize the SDK by following the steps below:

Once completed, you can use the Golang SDK to create, read, update, or delete the Matchmaking (opens new window) service from your serverless app.

# Python SDK Initialization

Before using the Matchmaking service from the Python SDK, you will need to initialize the SDK by following the steps below:

Once completed, you can use the Python SDK to create, read, update, or delete the Matchmaking (opens new window) service from your serverless app.

# .NET (C#) SDK Initialization

Before using the Matchmaking service, you will need to set some permissions. Use the following .NET namespaces:

using AccelByte.Sdk.Api.Matchmaking.Model;
using AccelByte.Sdk.Api.Matchmaking.Operation;
using AccelByte.Sdk.Api.Matchmaking.Wrapper;

# Java SDK Initialization

Before using the Matchmaking service, you will need to set some permissions. Initialize the Matchmaking wrapper from the Matchmaking service using the following code:

Matchmaking wMatchmaking = new Matchmaking(sdk);

Once completed, you can use the SDK to create, read, update, or delete matchmaking.

# Add a Player to the Session in a Channel

Use the following function to add a player to the session in a channel (opens new window):

# Remove a Player from the Session in a Channel

Use the following function to remove a player from the session in a channel (opens new window):

# Create a Matchmaking Channel

Use the following function to create a matchmaking channel (opens new window):

# Retrieve a Matchmaking Channel

Use the following function to retrieve a matchmaking channel (opens new window):

# Retrieve Sessions in a Matchmaking Channel

Use the following function to retrieve sessions in a matchmaking channel (opens new window):

# Update a Matchmaking Channel

Use the following function to update a matchmaking channel (opens new window):

# Delete a Matchmaking Channel

Use the following function to delete a matchmaking channel (opens new window):

# Implementing Matchmaking using the Unity SDK

# Quick Reference

References
using AccelByte.Api;
using AccelByte.Core;
using AccelByte.Models;
Matchmaking Notification Events
AccelBytePlugin.GetLobby().MatchmakingCompleted += result => {};
AccelBytePlugin.GetLobby().ReadyForMatchConfirmed += result => {};
AccelBytePlugin.GetLobby().DSUpdated += result =>{};
AccelBytePlugin.GetLobby().RematchmakingNotif += result =>{};
Start Matchmaking
string gameMode;

AccelBytePlugin.GetLobby().StartMatchmaking(gameMode, result =>
{
    //Check this is not an error
    if (!result.IsError)
    {
        Debug.Log("Started matchmaking is successful");
    }
});
Cancel Matchmaking
string gameMode;

AccelBytePlugin.GetLobby().CancelMatchmaking(gameMode, result =>
{
    //Check this is not an error
    if (!result.IsError)
    {
        Debug.Log("Canceled matchmaking is successful");
    }
});
Ready Matchmaking
string matchId;

AccelBytePlugin.GetLobby().ConfirmReadyForMatch(matchId, result =>
{
    //Check this is not an error
    if (!result.IsError)
    {
        Debug.Log("Ready for match is successful");
    }
});

# Quickstart Guide

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.

  1. 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.
  2. Create a new script called MatchmakingHandler.cs and attach it to the AccelByteHandler gameObject.
  3. Add the following AccelByte libraries to the top of the script:
using AccelByte.Api;
using AccelByte.Models;
using AccelByte.Core;
  1. 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";
  1. 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");
            }
    });
}
  1. 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 (opens new window).

# Step by Step Guide

UI Implementation
  1. 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:

matchmaking

  • Find Match button

matchmaking

  1. Create a new panel or scene for Matchmaking and add the following matchmaking-related panels and UI elements:
  2. Find Match panel
  • Header text
  • Loading image/bar (or any other object to indicate loading progress)
  • Count Up (Time Elapsed) text
  • Cancel button

matchmaking

  1. Ready Match panel
  • Exit button (optional)
  • Header text
  • Player Box panel with Username text. The maximum number of players is 10, so duplicate this object until you have created 10.
  • Countdown text
  • Ready button

matchmaking

  1. 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.

matchmaking

Code Implementation

Now that you have some basic Matchmaking functionalities, you can implement these with your UI.

  1. 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 (opens new window) to add or adjust your own Matchmaking Ruleset.
  2. 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
}
  1. 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;
        }
    }
}
  1. 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);
}

  1. 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;
}

  1. 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;
  1. Change the gameMode variable’s default value to gameMode (null).
// Current selected game mode
private GameMode gameMode;
  1. 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;
  1. 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
  1. 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;
}
  1. 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
}
  1. 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;
        }
    }
}
  1. 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;
}
  1. 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;
}
  1. 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;
...
  1. 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);
}
  1. 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
    ...
});

  1. 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
  1. 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
  1. 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;
}
...
  1. In the Unity Editor, on the AccelByteHandler gameobject, drag the necessary objects to the exposed variables of the script component.

matchmaking

Expand the MatchmakingPanel and drag the objects to the remaining exposed variables:

matchmaking

In the **Script **component, expand the Username list and drag the objects to their corresponding variables:

matchmaking

  1. 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;
  1. Add the following UI references and local variables to hold temporary data:
[SerializeField]
private Image loadingImage;

[SerializeField]
private Text countUpText;

[SerializeField]
private Text countDownText;

private bool isWaiting;
private float deltaTime;
  1. 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;
}
  1. 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();
}
  1. 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;
        }
    }
}
  1. In the Unity Editor, in your scene, on the MatchmakingPanel gameObject, drag the necessary objects to the exposed variables.

matchmaking

  1. 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;
  1. Add the following UI references and local variables to hold temporary data:
[SerializeField]
private Text UsernameText;
[SerializeField]
private Image UsernameFrameImage;
  1. 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;
}
  1. In the Unity Editor, open the PlayerBox object and drag the following objects to their variables:

matchmaking

TIP: 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 (opens new window).

# Full Code

MatchmakingHandler.cs
// 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;
    }
}
MatchmakingManagementPanel.cs
// 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();
    }
}
MatchmakingUsernamePanel.cs
// 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;
    }
}
LobbyHandler.cs
// 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);
    }
}
MenuHandler.cs
// 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);
        });
    }  
}
NotificationHandler.cs
// 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
}