Unity Matchmaker

This guide will help you create a Unity project using Edgegap's Advanced Matchmaker system to automate deployments and connections between players.

This guide will use the open-source sample project Tanks, available in the Mirror sample under Assets/Mirror/Examples/Tanks. The final sample can be found on our GitHub.

Matchmaker setup

Create the components

To begin, you will need to create your 3 Matchmaker components. For this project, you may follow along with our basic component tutorial here.

Release

Local

If you want to test your Matchmaker in a local environment, simply apply your Kubernetes settings as mentionned at the end of the basic component tutorial. You can then skip ahead to the project setup.

With Edgegap

If you instead want to test the Matchmaker in an online environment with Edgegap, you will need to slightly modify your components from the previous tutorial. You will be using the injected variables that are added during the release instead of some hardcoded values. The changes should be as follows:

Director

...
using System;

namespace director
{
  public static class Constant
  {
    static Constant()
    {
        OpenMatchMatchFunctionHost = Environment.GetEnvironmentVariable("OM_MMF_HOST");
        OpenMatchMatchFunctionPort = Int32.Parse(Environment.GetEnvironmentVariable("OM_MMF_HTTP_PORT"));
        OpenMatchBackendService = Environment.GetEnvironmentVariable("OM_BACKEND_HOST") + ':' + Int32.Parse(Environment.GetEnvironmentVariable("OM_BACKEND_HTTP_PORT"));
    }

    public static readonly string OpenMatchMatchFunctionHost;
    public static readonly int OpenMatchMatchFunctionPort;
    public static readonly string OpenMatchBackendService;

    ...
  }

  ...
}

Frontend

...

namespace front_end
{
  // All the constants used in the program
  public static class Constant
  {
    static Constant()
    {
        OpenMatchFrontendService = Environment.GetEnvironmentVariable("OM_FRONTEND_HOST") + ':' + Int32.Parse(Environment.GetEnvironmentVariable("OM_FRONTEND_HTTP_PORT"));
    }

    public static readonly string OpenMatchFrontendService;
  }

  ...
}

Match Function

...

namespace match_function
{
  // All the constants used in the program
  public static class Constant
  {
    static Constant()
    {
        OpenMatchQueryService = Environment.GetEnvironmentVariable("OM_QUERY_HOST") + ':' + Int32.Parse(Environment.GetEnvironmentVariable("OM_QUERY_HTTP_PORT"));
    }

    public static readonly string OpenMatchQueryService;
    public const string MatchName = "basics-match-function";
  }

  ...
}

Once this is done, upload them to Edgegap, then create and release your Matchmaker on the Edgegap dashboard.

Project setup

Next, we want to modify the base Tank project so that the Matchmaker is being used to connect the players together. For the purposes of this sample, we want our players to choose what mode of match they want to play before creating a Matchmaker ticket, so that the Matchmaker will automatically connect 2 players who chose the same one.

With the basic Matchmaker components created with the previous tutorial, the available modes are mode.casual, mode.ranked, and mode.private.

In the project, open the scene located under Assets/Mirror/Examples/Tanks/Scenes. Disable the Network Manager HUD component of the NetworkManager gameObject, then create your own HUD to allow the players to enter a game mode, create and delete a ticket when they are not currently in a match, and exit their current match when they are.

Offline HUD

Online HUD

Then, you will need to create new scripts inside your project to manage what requests are sent to the Matchmaker according to what button the player clicks. Your code for managing the requests themselves should have a similar structure to this:

Functions

public class MatchmakerManager : ScriptableObject
{
  //when testing the Matchmaker locally, this value should be http://localhost:51504
  //when testing the Matchmaker's online release, you do not need to specify the port at the end of the URL
  public const string MATCHMAKER_URL = "<FRONTEND_COMPONENT_URL>";
  private readonly HttpClient _httpClient = new();
  public KcpTransport transport = (KcpTransport)NetworkManager.singleton.transport;

  /// <summary>
  /// Get a ticket's data from the Matchmaker
  /// </summary>
  /// <param name="ticketId">Ticket's ID</param>
  /// <returns>Ticket data</returns>
  public async Task<Ticket> GetTicket(string ticketId)
  {
    var response = await _httpClient.GetAsync($"{MATCHMAKER_URL}/v1/tickets/{ticketId}");

    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException($"Error code: {response.StatusCode}");
    }

    var responseContent = await response.Content.ReadAsStringAsync();
    Ticket content = JsonConvert.DeserializeObject<Ticket>(responseContent);

    return content;
  }

  /// <summary>
  /// Create a new ticket for the Matchmaker
  /// </summary>
  /// <param name="modeTag">What game mode the players wants to be in</param>
  /// <returns>Ticket data</returns>
  public async Task<Ticket> CreateTicket(string modeTag)
  {
    CreateTicketPayload objectToSerialize = new()
    {
        mode = modeTag
    };

    var jsonContent = new StringContent(JsonConvert.SerializeObject(objectToSerialize), Encoding.UTF8, "application/json");
    var response = await _httpClient.PostAsync($"{MATCHMAKER_URL}/v1/tickets", jsonContent);

    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException($"Error code: {response.StatusCode}");
    }

    var responseContent = await response.Content.ReadAsStringAsync();
    Ticket content = JsonConvert.DeserializeObject<Ticket>(responseContent);

    return content;
  }

  /// <summary>
  /// Delete a ticket from the Matchmaker
  /// </summary>
  /// <param name="ticketId">Ticket's ID</param>
  public async Task DeleteTicket(string ticketId)
  {
    var response = await _httpClient.DeleteAsync($"{MATCHMAKER_URL}/v1/tickets/{ticketId}");

    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException($"Error code: {response.StatusCode}");
    }
  }

  /// <summary>
  /// Connect the player to a match
  /// </summary>
  /// <param name="assignment">Ticket's assignment data</param>
  public void ConnectPlayer(Assignment assignment)
  {
    string[] networkComponents = assignment.connection.Split(':');
    NetworkManager.singleton.networkAddress = networkComponents[0];

    if (ushort.TryParse(networkComponents[1], out ushort port))
    {
        transport.port = port;
    }
    else
    {
        throw new Exception("port couldn't be parsed");
    }

    NetworkManager.singleton.StartClient();
  }

  /// <summary>
  /// Disconnect the player from a match
  /// </summary>
  public void DisconnectPlayer()
  {
    NetworkManager.singleton.StopClient();
  }
}

Classes

public class CreateTicketPayload
{
    public string mode { get; set; }
}
public class Ticket
{
    public string id { get; set; }
    public Assignment assignment { get; set; }
    public SearchFields search_fields { get; set; }
    public Dictionary<string, Extension> extensions { get; set; }
    public Dictionary<string, Extension> persistent_field { get; set; }
    public string create_time { get; set; }
}
public class SearchFields
{
    public string[] tags { get; set; }
}
public class Extension
{
    public string @type { get; set; }
    public byte[] value { get; set; }
}
public class Assignment
{
    public string connection { get; set; }
    public Dictionary<string, Extension> extensions { get; set; }
}

However you choose to manage your HUD, make sure to create your MatchmakerManager instance in your Start() function to ensure the code works properly.

Once the player has created a ticket, you will need to periodically check its status in order to get the assignment information as soon as 2 tickets have been matched, because the ticket will be deleted a few minutes after this value has been set. Once you have the assignment, you can connect the player to the match.

Build the game server & Containerizing

Once you have finished adding the modifications from the previous step to the project, you will need to build your game server and containerize it. First, make sure the NetworkManager gameObject has the following settings:

  • Enable Auto Start Server Build in the NetworkManager component;

  • Set the port of the KcpTransport component to the same value you used for your Matchmaker's GameServerPort in the Director component.

In this example, we used `7777` for both values.

Once this is done, head to the Build screen of the Unity Editor, under File -> Build Settings in the top menus. Make sure to select the right presets depending on your version of Unity.

  • Prior to version 2021.2:

    • Set Target Platform to Linux;

    • Set Architecture to x86_64;

    • Check the Server Build option.

  • Otherwise:

    • Set Platform to Dedicated Server;

    • Set Target Platform to Linux.

Then press build and select a new empty folder named linux_server as the file destination. Transfer the linux_server folder to a second empty folder, which will be refered as the [SERVER BUILD] folder in this document. Add the following Dockerfile and boot.sh file to the [SERVER BUILD] folder:

Dockerfile

FROM ubuntu:bionic
MAINTAINER edgegap

ARG debian_frontend=noninteractive
ARG docker_version=17.06.0-ce

RUN apt-get update && \
apt-get install -y libglu1 xvfb libxcursor1

# Update root CA to ensure outbound HTTPS requests don't fail
RUN apt-get update && \
    apt-get install -y ca-certificates && \
    update-ca-certificates && \
    apt-get clean

EXPOSE 3389/TCP
EXPOSE [GAME PORT]/TCP
EXPOSE [GAME PORT]/UDP

COPY linux_server/ /root/linux_server/
COPY boot.sh /boot.sh

WORKDIR /root/
ENTRYPOINT ["/bin/bash", "/boot.sh"]

Make sure to replace the [GAME PORT] placeholders with the same port value as before.

boot.sh

xvfb-run --auto-servernum --server-args='-screen 0 640X480X24:32' /root/build/[YOUR GAME].x86_64 -batchmode -nographics

Make sure to replace the [YOUR GAME] placeholder with the name of the generated file.

Then, start a command prompt in the [SERVER BUILD] folder; Run the following Docker commands to create an image of your build and push it to a private registry. See this doc if you want to use Edgegap's private registry.

Using Linux

# build the image
docker build . -t <IMAGE_NAME>:<IMAGE_TAG>

# login, a prompt will ask the password
docker login -u '<REGISTRY_USERNAME>' <REGISTRY_URL>

# add another tag to your image corresponding to the registry
docker image tag <IMAGE_NAME>:<IMAGE_TAG> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

#push the image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

Using cmd

# build the image
docker build . -t <IMAGE_NAME>:<IMAGE_TAG>

# login, a prompt will ask the password
docker login -u <REGISTRY_USERNAME> <REGISTRY_URL>

# add another tag to your image corresponding to the registry
docker image tag <IMAGE_NAME>:<IMAGE_TAG> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

#push the image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

Using Powershell

# build the image
docker build . -t <IMAGE_NAME>:<IMAGE_TAG>

# login, a prompt will ask the password
docker login -u '<REGISTRY_USERNAME>' <REGISTRY_URL>

# add another tag to your image corresponding to the registry
docker image tag <IMAGE_NAME>:<IMAGE_TAG> <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

#push the image
docker push <REGISTRY_URL>/<PROJECT_NAME>/<IMAGE_NAME>:<IMAGE_TAG>

Deploying to Edgegap

Create a new application on the Edgegap dashboard with the following settings:

  • Application name : Make sure that it's the same name that you used for the AppName in your Matchmaker's Director component. Must be in lowercase.

  • Image : Can be any specific picture you want to use to easily recognize your application among others.

  • Version name : Make sure that it's the same version that you used for the AppVersion in your Matchmaker's Director component. Examples may be “demo”, “production”, “v1”, “v2”

  • Container :

    • Registry : “[URL]”, where [URL] is the value from the credentials you can display on the Container Repository page.

    • Image repository : “[PROJECT]/[YOUR GAME]”, where [PROJECT] and [YOUR GAME] are the values you used earlier when pushing the docker image.

    • Tag : “[TAG]”, where [TAG] is the value you used earlier when pushing the docker image.

    • Tick “Using a private repository”

    • Private registry username : “[USERNAME]”, where [USERNAME] is the value from your credentials.

    • Private registry token : “[TOKEN]”, where [TOKEN] is the value from your credentials.

    • Requirements : Left as is.

    • Ports :

      • Click the + Add port link to add a new port, and add the following entries :

        • [GAME PORT] - TCP/UDP - disable Verifications

        • 3389 - TCP - disable Verifications

We recommend setting a Game Max Duration in your application as well, so that your deployments self-terminate after a few minutes since the Matchmaker will automatically create new ones when matching tickets.

This is to make sure that you do not have any empty deployments running for too long.

Testing with client builds

In order to see if the Matchmaker works as intended, you will need the launch the game on 2 separate clients instances and have your Matchmaker release running as well.

Before creating a client build of the game, make sure to uncheck the Auto Start Server Build option of the NetworkManager gameObject. Then, depending on what kind of Matchmaker release you are testing, change the MATCHMAKER_URL to the following:

  • Local : change it to "http://localhost:51504"

  • With Edgegap : change it to the value shown on the Edgegap dashboard after enabling the release. You do not need to specify the port in this case.

Then, head back to the Build screen of the Unity Editor, under File -> Build Settings in the top menus. Select the Windows, Mac, Linux platform and the correct target platform for your environment. Then, press build and select a new empty folder named client_build as the file destination.

Once you launch both clients (either two windows of your client build, or one build window and the Unity editor), have them both select the same game mode and create their tickets. After a short while, both clients will be connected to a newly deployed server, which you can see the status of on the Edgegap dashboard.

Last updated

Was this helpful?