集成 zeuz 和匹配器

匹配器将游戏客户端匹配到您的游戏服务器。在游戏开发的过程中,您可能需要将 zeuz 和您的匹配器进行集成。

匹配是将玩家连接到游戏会话的过程。更多信息,请参阅 维基百科:匹配

本文档就向您的匹配器纳入哪些内容提供了建议,以实现 zeuz 和您游戏的最佳集成。

前期准备

在开发匹配器前,我们建议您执行下列操作:

  • 完成 zeuz 入门教程,指导您使用 zeuz 服务器硬件编排工具为对局游戏设置托管。该教程预计需要 60-90 分钟。

  • 参阅 设置项目 指南,使用 zeuz 服务器托管编排工具为您的对局游戏项目设置托管。

概览

下图显示了 zeuz 组件、您的游戏组件以及匹配过程中二者之间的交互:

图像:zeuz 匹配概览

zeuz 提供下列组件:

  • zeuz SDK:用于设置 zeuz 和您游戏的 文件包,其中包含 Go SDK、虚幻引擎和 Unity 的 zeuz API 封装以及其他资源。

    注意:下载 SDK 是 入门 教程的一部分。请参阅解压包中的 README.txt 文件了解 SDK 中包含的全部内容。

  • zeuz API。

  • 为您的游戏服务器提供的托管平台。

zeuz Go SDK

我们建议您使用 zeuz SDK 下载中的 Go SDK 来开发匹配器。Go SDK 提供下列内容:

  • zeuz 对象的原生表现,例如配载、负载和机器。

  • 遵照 zeuz API 序列化和反序列化对象的方法。

  • 将签名哈希正确地附加到 zeuz API 请求的方法。

    更多信息,请参阅 签名哈希生成

  • 辅助函数。

本文档的示例使用的是 zeuz Go SDK。您可以使用其他语言来开发匹配器,但您需要自行实现上述功能。

注意:zeuz SDK 中提供的虚幻引擎封装和 Unity 引擎封装,用于直接集成这两种游戏引擎创建的服务器和客户端,不能用于开发匹配器。

关于如何为您的平台开发匹配器,请参阅您自己的游戏开发软件文档。

连接流程

我们建议您的匹配器采用下列连接流程 (如上图所示):

  1. 游戏客户端向匹配服务发送接入负载的请求。

  2. 匹配器接收到请求后,通过 zeuz SDK 调用 zeuz API 以识别待分配负载。

  3. 匹配器将负载分配给游戏客户端,供其接入。

  4. 匹配器确保负载已就绪,可被客户端连接。

    注意:负载“已就绪”意味着负载的初始化已完成。更多信息,请参阅下方的 负载准备

  5. 匹配器将接入详情 (IP 地址和端口号) 传递给游戏客户端。

  6. 游戏客户端使用接入详情连接到负载。

身份认证

如需调用 zeuz API,您的匹配器必须首先完成认证。在进行认证前,您需要获取以下信息:

  • API 密钥和 API 密码。

    关于如何生成 API 密钥和 API 密码,请参阅 API 密钥

    注意:在 zeuz 控制面板 中,API 密码即为 API Keys (API 密钥) 页面的 Password (密码)。

  • 项目 ID (在 zeuz 控制面板的左侧可见)。

  • 环境 ID (在 zeuz 控制面板的左侧可见)。

注意:您的匹配器不能直接通过 zeuz 命令行工具 (CLI) 进行 zeuz 认证。我们建议您不要将 zeuz 工具嵌入代码中,且仅部分 API 端点支持通过 zeuz 工具调用。

在您的代码中,使用 API 密码和 API 密钥生成会话 ID (SessionID) 和会话密钥 (SessionKey),再使用会话 ID、会话密钥、项目 ID (ProjID) 和环境 ID (EnvID),向 zeuz API 发送请求。

您的代码需要满足以下几点:

  • 纳入 zeuz 基础 API 要求的认证。 更多信息,请参阅 API 认证

  • 全局缓存会话信息。

  • 追踪会话有效性,在会话过期失效时重新登录。 更多信息,请参阅 错误处理

示例:使用 zeuz Go SDK 进行身份认证以访问 zeuz 控制面板,并通过 Go SDK 发送 zeuz API 请求。

  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"

   // 指向您项目中 zeuz SDK 本地副本的 go.mod 目录
   "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
}

// 您与 zeuz 控制面板的认证一经完成,
// 即刻开始为接下来的 API 调用缓存会话信息。
var session *zeuzsdk.Session

// 追踪会话过期的日期 / 时间。
var ValidThru zeuzsdk.Timestamp

// 调用 GetCachedSessionClient 函数,
// 以获取一个经过认证的 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
}

// auth 函数使用 API 密钥和 API 密钥密码,
// 创建一个新的 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))
}

// 使用 API 密钥和 API 密钥密码,
// 创建一个 zeuzsdk.AuthLoginIn 对象。
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

// zeuz 密码哈希算法的 Go 实现。
// 如需更多信息,请参阅
// https://doc.zeuz.io/docs/api-login#step-2---create-and-encode-a-password-hash
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)
}

// 检查缓存的会话是否有效。
func isCachedSessionValid() bool {
   return session != nil && ValidThru > zeuzsdk.TSNow()
}

// 获取一个 HTTP 客户端实例,以便与 zeuz 进行通信。
// 根 URL 是固定的,因为您只需要与基础 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)
}

负载伸缩

我们建议您使用 zeuz 来处理负载伸缩。理想状况下,匹配器无需直接参与调整负载和机器的数量,仅用于 分配 负载。

扩容

zeuz 确保将配载配置中指定的最低待分配负载数应用于您的游戏。这意味着一旦您分配了一定数量的负载,为满足最低待分配负载数的设置,zeuz 会自动启动新的负载。

示例:使用 zeuz Go SDK 来分配负载。

 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"

// 分配指定负载。
// 返回一个布尔标志,表明指定负载是否被分配。
func EnsureReserved(payloadID string, gamebackend *GamebackendConfig) bool {
   // 我们使用代码示例里的函数,
   // 在上述的认证会话中获取经过认证的 ZCP 客户端。
   // 详情请参阅:https://doc.zeuz.io/docs/integrate-a-matchmaker#身份认证。
   client := GetCachedSessionClient(gamebackend)

   // 查找指定配载的全部负载。
   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 {
      // 如果负载已被分配,报错。
      // 上述代码检查了负载是否被分配,
      // 因此该错误仅在竞态条件下发生。
      fmt.Println(err)
      return false
   }

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

缩容

我们建议您设置在游戏会话结束时自动停用负载。这样既可以避免资源浪费,还可以让您免于手动操作。

如需启用该功能,请在配载的负载定义部分添加 API 密钥和 API 密码。详情请参阅 负载自动释放

负载准备

负载被分配并不等于准备好被玩家接入。因此,请在您的匹配器中添加步骤,验证其是否准备好接入玩家,然后再将接入详情传递给游戏客户端,以确保连接无缝无误。

如需确定负载是否就绪,您可以使用 netcat (请参阅 维基百科:netcat) 等工具来检查负载端口是否被占用。如果端口被占用,则说明负载已就绪。

请使用以下命令格式:

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

请参考以下命令示例:

nc -vz my.game.com 9000

注意:在 UDP 服务器上,要检查的端口号为 9001。

关于负载伸缩的更多信息,请参阅下列文档:


2021年8月23日 该文档已创建并通过审校


最近更新时间: October 20, 2021 (5d3ab3d7)