Integrate a matchmaker with zeuz

A matchmaker is a service you create, to match game clients to your game server. As part of your game development you might want to integrate your matchmaker with zeuz.

Matchmaking is the process of connecting players together for game playing sessions. See Wikipedia: Matchmaking for more information.

This page provides advice on what to incorporate into your matchmaker, to best integrate your game with zeuz.

Before you begin

Before you develop your matchmaker, we recommend that you perform these tasks:

  • Follow the zeuz Get Started tutorial. It ​​guides you through setting up hosting for a session-based game with zeuz server hardware orchestration tools, and takes approximately 60-90 minutes to complete.

  • Set up hosting for your own session-based game project with zeuz server hosting orchestration tools. For instructions, refer to the Set up a project guide.

Overview

The following image shows the zeuz components, your game components, and the matchmaking interactions between them:

Image: zeuz matchmaking overview.

zeuz provides the following components:

  • zeuz SDK: A package of files which contains an SDK in Go, zeuz API wrappers for Unreal and Unity, and other resources that you can use when you set up your game with zeuz.

    Note: You download the SDK as part of the Get Started tutorial. See the README.txt file in the unzipped package for a full description of the SDK contents.

  • The zeuz API.

  • The hosting platform for your game servers.

zeuz SDK in Go

In the zeuz SDK download, we provide the SDK in Go. We recommend that you use it to develop your matchmaker. The SDK in Go provides the following:

  • Native representations of zeuz objects such as allocations, payloads, and machines.

  • Methods for serializing and de-serializing objects in compliance with the zeuz API.

  • Methods to correctly attach the sign-hash to requests you make to the zeuz API.

    See Sign-hash generation for more information.

  • Helper functions.

You can use a different language to develop a matchmaker, but you will need to implement the above features yourself. The examples in this document use the zeuz SDK in Go.

Note: The Unreal and Unity wrappers in the zeuz SDK are designed to integrate directly with a game client and server you create with Unreal or Unity. You can’t use these wrappers to develop a matchmaker.

Refer to your game development software documentation for more information on how to develop a matchmaker for your platform.

Connection flow

We recommend the following connection flow for your matchmaker, as shown in the image above:

  1. A game client requests a payload to connect to, from a matchmaking service.

  2. The matchmaker uses the zeuz SDK to communicate with the zeuz API and identify unreserved payloads.

  3. The matchmaker reserves a payload for game clients to connect to.

  4. The matchmaker ensures that the payload is ready to accept connections.

    Note: A payload is “ready” when its initialization is complete. See Payload readiness below for more information.

  5. The matchmaker passes connection details to the game client (IP address and port number).

  6. The game client uses the connection details to connect to the payload.

Authentication

To interact with the zeuz API, your matchmaker must first authenticate with it. Before you can authenticate, you need:

  • An API key and API secret.

    See API keys for information on how to generate these.

    Note: In the zeuz control panel (ZCP) the API secret is called the API key password.

  • Your zeuz project ID (visible in the left pane of the ZCP).

  • Your zeuz environment ID (visible in the left pane of the ZCP).

Note: Your matchmaker can’t authenticate directly with zeuz using the zeuz tool CLI. We don’t recommend using zeuz tool programmatically and not all zeuz API endpoints are supported by zeuz tool.

In your code, use the API key and API secret to produce a session ID and a session key. Use the session ID and session key, together with your project ID and environment ID, to make requests to the zeuz API.

It is important to do the following:

  • Incorporate the authentication that the zeuz base API requires. See API authentication for more information.

  • Use a globally cached session.

  • Track session validity and renew the session as needed. See Error handling for more information.

Example: Use the zeuz SDK to authenticate to the ZCP and make requests to the zeuz API (Go)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
import (
   "encoding/base64"
   "log"
   "net/url"
   "strconv"

   // Point to a local copy of the zeuz SDK in your project's
   // go.mod directory
   "github.com/improbable/zeuz-olympuz/sdk/golang/zeuzsdk"
   "golang.org/x/crypto/scrypt"
   "golang.org/x/text/unicode/norm"
}

// Provide the required information to authenticate with the ZCP
// See the Get Started tutorial for more information:
// https://doc.zeuz.io/docs/get-started
type GamebackendConfig struct {
   ProjID    string
   EnvID     string
   APIKey    string
   APISecret string
}

// Once you authenticate with the ZCP, cache the session information
// for subsequent API calls
var session *zeuzsdk.Session

// Track the date/time when the session expires
var ValidThru zeuzsdk.Timestamp

// Call the GetCachedSessionClient function to get an
// authenticated client for the ZCP
// The rest of the SDK requires a zeuzsdk.Client object to make requests
func GetCachedSessionClient(gamebackend *GamebackendConfig) *zeuzsdk.Client {
   client := getNewClient()

   if !isCachedSessionValid() {
      auth(gamebackend.APIKey, gamebackend.APISecret)
      client.SessionID = session.ID
      client.SessionKey = session.SessionKey
   } else {
      client.SessionID = session.ID
      client.SessionKey = session.SessionKey
   }
   client.SetProject(zeuzsdk.ProjID(gamebackend.ProjID))
   client.SetEnv(zeuzsdk.EnvID(gamebackend.EnvID))
   return client
}

// The auth function uses the API key and API secret to create
// a new session with zeuz
func auth(apiKey string, apiSecret string) {
   loginParams := getLoginParams(apiKey, apiSecret)
   client := getNewClient()
   loginResult, err := zeuzsdk.APIAuthLogin(client, &loginParams)
   if err != nil {
      log.Fatal(err)
   }
   ValidThru = loginResult.ValidThru
   session = zeuzsdk.SessionFromAuth(loginResult, calcPWHash(apiKey, apiSecret))
}

// Create a zeuzsdk.AuthLoginIn object from the API key and API secret
func getLoginParams(apiKey string, apiSecret string) zeuzsdk.AuthLoginIn {
   nonce := zeuzsdk.IDGenerate(zeuzsdk.IDTypeInvalid)
   curTime := zeuzsdk.TSNow()
   sT := strconv.FormatInt(int64(curTime), 10)
   pwHash := calcPWHash(apiKey, apiSecret)
   loginParams := zeuzsdk.AuthLoginIn{
      Login:  apiKey,
      IsUser: false,
      IsApi:  true,
      Time:   curTime,
      Nonce:  nonce,
      Hash:   zeuzsdk.StringHash(nonce + sT + pwHash),
   }
   return loginParams

// Go implementation of the zeuz password hashing algorithm
// See https://doc.zeuz.io/docs/api-login#step-2---create-and-encode-a-password-hash
// for more information
func calcPWHash(apiKey string, apiSecret string) string {
   apiSecret = norm.NFKC.String(apiSecret)
   apiKey = norm.NFKC.String(apiKey)
   bytes, err := scrypt.Key([]byte(apiSecret), []byte("zeuz"+apiKey), 1024, 8, 1, 32)
   if err != nil {
      panic(err)
   }
   return "a" + base64.StdEncoding.EncodeToString(bytes)
}

// Check if the cached session is valid
func isCachedSessionValid() bool {
   return session != nil && ValidThru > zeuzsdk.TSNow()
}

// Get an instance of the HTTP client to communicate with zeuz
// The root URL is fixed because you only need to communicate with the base API
func getNewClient() *zeuzsdk.Client {
   zeuzURL, err := url.Parse("https://zcp.zeuz.io/api/v1")
   if err != nil {
      log.Fatal(err)
   }
   return zeuzsdk.NewClient(*zeuzURL)
}

Payload scaling

We recommend that you let zeuz handle the scaling of your payloads. Ideally, your matchmaker should not need to directly add or remove a payload or machine. Your matchmaker should only reserve payloads.

Scaling up

zeuz ensures that the minimum number of unreserved payloads you specify in your allocation configuration are available to your game. This means that once you reserve enough payloads, zeuz automatically spins up new ones when needed.

Example: Use the zeuz SDK to reserve a payload (Go)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import "github.com/improbable/zeuz-olympuz/sdk/golang/zeuzsdk"

// Reserve a specified payload
// Returns a boolean flag indicating whether or not the specified payload is reserved
func EnsureReserved(payloadID string, gamebackend *GamebackendConfig) bool {
   // In this example we use the function from the code example in the 
   // Authentication session above to get an authenticated client for the ZCP
   // See: https://doc.zeuz.io/docs/integrate-a-matchmaker#authentication for details
   client := GetCachedSessionClient(gamebackend)

   // Find all payloads for a specified allocation
   payloads, err := zeuzsdk.APIPayloadGet(client, &zeuzsdk.PayloadGetIn{PayloadIDs: []zeuzsdk.PayloadID{zeuzsdk.PayloadID(payloadID)}})
   if err != nil {
      fmt.Println(err)
      return false
   }

   if len(payloads.Items) == 0 {
      fmt.Printf("Payload does not exist %s\n", payloadID)
      return false
   }

   if payloads.Items[0].Reserved {
      fmt.Printf("Already reserved %s\n", payloadID)
      return true
   }

   _, err = zeuzsdk.APIPayloadReserve(client, zeuzsdk.PayloadID(payloadID))
   if err != nil {
      // Report an error if the payload is already reserved
      // The code above checks whether the payload is reserved, so this error should
      // only occur if there's a race condition
      fmt.Println(err)
      return false
   }

   fmt.Printf("Successfully reserved %s\n", payloadID)
   return true
}

Scaling down

We recommend that you set a payload to stop running when a game session ends. This automatically frees up server hardware resources and saves you from manually unreserving the payload.

To enable this, add your API key and API key password to your allocation’s payload definition. See the information on automatic payload release for more details.

Payload readiness

Even if a payload is reserved, it might not be ready to accept player connections. Incorporate steps into your matchmaker to validate that a payload is ready to accept connections, before passing its connection details to game clients. This helps to ensure that connections are seamless and error-free.

To determine whether a payload is ready, use a utility such as netcat (Wikipedia: netcat) to check whether the payload’s port is occupied. If the port is occupied, the payload is ready.

Use a command similar to the following:

nc -vz <your.game.address> <port number>

For example:

nc -vz my.game.com 9000

Note: On a UDP server, the port to check is 9001.

For more information on payload scaling, see:


2021-aug-23 Page added with editorial review.


Last edited on: October 14, 2021 (9f176cbe)