跳到主要内容

典型用户案例

组队、房间、匹配和战斗会话共同构成了战斗服务。组队、房间和匹配提供了聚集玩家的方法,而战斗会话则为他们分配专用服务器以进行战斗。

1. 与好友组队开始对战

1.1 创建组队并邀请好友

玩家可以通过组队聚集在一起,队长可以邀请好友加入、相互发送文字消息或带领队员进入对战。

当玩家加入或离开组队、发送文字消息或组队解散时,每个客户端都会收到通知。

开发人员可以在网页端查看所有已创建的组队信息,包括组队的基本信息和状态变更记录:

1605498590504

组队成功后,队长可以选择通过房间或匹配开始对战。

1.2 从房间开始对战

组队服务与房间服务是相互独立的。按照惯例,组队中的队长负责创建房间并成为房主,然后邀请队员加入房间。 房间按照队伍对玩家进行分组,每个队伍包含有限数量的位置。房主可以更改房间的可见性,使其他玩家能否在房间搜索结果中看到该房间。房主还可以设置密码以防止不需要的玩家加入。

房间持有一个KV数据结构并将其同步到所有客户端。开发者可以使用它来临时存储设置,例如玩家选择的游戏地图,或每个玩家的角色信息等。

房主开始对战前需要所有房间成员准备就绪,之后房间状态将从"等待中"切换为"游戏中",并分配一个对战会话ID。客户端可以使用对战会话ID查询基本信息以进入对战并连接到专用服务器。

通过以下可在网页门户上找到的房间创建记录,更容易理解房间的整体概念:

image-20210628151624510

1.3 从匹配开始对战

与房间服务让玩家手动加入并在每场对战中保持相同的玩家构成不同,匹配服务会根据开发者编写的特定规则自动将玩家分组组成对战。

玩家可以单独发起匹配请求,也可以与其他玩家一起进行匹配。在后一种情况下,他们将在最终的匹配结果中被分在同一队。

匹配是一个异步过程,可能需要几十秒的时间。客户端可以使用在开始匹配请求时分配的TicketID来追踪匹配进度。

在接下来的示例中,将演示复杂的规则集以满足不同需求。

2. 匹配MMR值相近的玩家

匹配评级(Matchmaking Rating,简称MMR)是用于确定每个玩家技能水平的数值。匹配服务最常见的需求是将MMR值相近的玩家匹配在一起,这对双方玩家都是公平的。

2.1 定义玩家数据模板

玩家数据是与玩家关联的一组key-value数据对。在检查玩家数据之前,需要在PGOS门户上为游戏自定义数据模板

在本例中,您需要在玩家模板表中定义mmr字段并指定一个数值。

image-20210628200744678

mmr 字段将用于以下匹配规则集,并演示云函数的使用方法。

2.2 编写规则集

本示例说明如何使用distanceRulecomparisonRule来设置两支MMR值相近的队伍,并为另一个字符串属性设置限制。

  • 创建两支玩家队伍。
  • 包含以下玩家属性。在开始匹配请求时需要将这些属性传递给匹配服务。
    • 玩家的mmr值(如未提供则默认为1)。请注意,由于MMR已在后端定义和存储,客户端无需向匹配服务传递MMR。只需在playerAttribute结构的末尾添加"key": "mmr",匹配服务就会自动从玩家数据服务中读取MMR属性。
    • 玩家的vip状态(如未提供则默认为"no")。请注意,VIP属性未在玩家模板中定义,因此客户端需要向匹配服务传递VIP值,否则将使用默认值。
  • 编写两条规则,必须同时满足这两条规则才能提出匹配结果:
    • 根据玩家的技能水平是否与其他玩家相近来选择玩家。确保两支队伍的平均mmr相差在50分以内。
    • 选择vip状态为"yes"的玩家
{
"version": "v1.0",
"playerAttributes": [
{
"name": "mmr",
"type": "number",
"default": 1,
"key": "mmr"
},
{
"name": "vip",
"type": "string",
"default": "no"
}
],
"teams": [
{
"name": "red",
"maxPlayers": 3,
"minPlayers": 3
},
{
"name": "green",
"maxPlayers": 3,
"minPlayers": 3
}
],
"rules": [
{
"name": "mmrRule",
"type": "distanceRule",
"measurements": [
"flatten(teams[*].players.playerAttributes[mmr])"
],
"referenceValue": "avg(flatten(teams[*].players.playerAttributes[mmr]))",
"maxDistance": 50
},
{
"name": "vipRule",
"type": "comparisonRule",
"measurements": [
"flatten(teams[*].players.playerAttributes[vip])"
],
"referenceValue": "yes",
"operation": "="
}
],
"expansions": []
}

2.3 更新 MMR 数据

MMR 值通常在对战结束并产生胜负结果时进行更新。

MMR 不是 PGOS 中的概念,但使用云函数服务很容易实现 MMR 的计算,这类似于 AWS Lambda 和 Azure Functions 等常见的云脚本服务。

开发者可以订阅对战会话终止事件,并设置云函数来更新 MMR 值。

image-20210628195147997

3. 匹配选择不同阵营的玩家

本示例说明如何使用 comparisonRule 将选择不同地图的玩家分组到同一地图中。

  • 创建两支玩家队伍。
    • 包含以下玩家属性:
    • 玩家的等级(如未提供,默认为0)。
    • 玩家的阵营("进攻方"或"防守方")。
  • 编写以下规则,必须全部满足才能提出匹配结果:
    • 根据玩家等级与其他玩家的相似程度来选择玩家。确保两支队伍的平均玩家等级相差不超过3级。
    • side_binding 规则限制同一队伍中的玩家必须选择相同的阵营。
    • side_binding_red 规则限制红队全部为进攻方。
    • side_binding_blue 规则限制蓝队全部为防守方。
{
"version": "v1.0",
"playerAttributes": [
{
"name": "level",
"type": "number"
},
{
"name": "side",
"type": "string"
}
],
"teams": [
{
"name": "red",
"maxPlayers": 4,
"minPlayers": 4
},
{
"name": "blue",
"maxPlayers": 4,
"minPlayers": 4
}
],
"rules": [
{
"name": "mmr_calc",
"type": "distanceRule",
"measurements": [
"flatten(teams[*].players.playerAttributes[level])"
],
"referenceValue": "avg(flatten(teams[*].players.playerAttributes[level]))",
"minDistance": 0,
"maxDistance": 3
},
{
"name": "side_binding",
"type": "comparisonRule",
"measurements": [
"teams[*].players.playerAttributes[side]"
],
"operation": "="
},
{
"name": "side_binding_red",
"type": "comparisonRule",
"measurements": [
"flatten(teams[red].players.playerAttributes[side])"
],
"referenceValue": "attacker",
"operation": "="
},
{
"name": "side_binding_blue",
"type": "comparisonRule",
"measurements": [
"flatten(teams[blue].players.playerAttributes[side])"
],
"referenceValue": "defender",
"operation": "="
}
],
"expansions": []
}

4. 将在短时间内发起匹配的玩家匹配到同一场对战中

本示例说明如何使用expansion功能将在短时间内开始匹配的玩家分组到同一场对战中。

  • 此规则集要求每场对战1~6名玩家
  • 限制团队至少需要6名玩家
  • 逐步将限制从6人扩展到1人
提示

如果在不使用expansion功能的情况下将minPlayers配置设置为1,匹配算法会立即启动并为第一个发起匹配的玩家生成一个满足minPlayers要求的游戏会话。即使尝试通过回填将后续玩家添加到游戏会话中,也会倾向于生成新的游戏会话并回填旧的会话,这无法保证所有新玩家都会被回填到较早的游戏会话中。

通过使用expansion功能,首先发起匹配的玩家可以稍作等待,并与短时间内发起匹配的其他玩家一起开始游戏会话。

{
"version": "v1.0",
"playerAttributes": [],
"teams": [
{
"name": "SoloTeam",
"minPlayers": 6,
"maxPlayers": 6,
"minQuantity": 1,
"maxQuantity": 1
}
],
"rules": [],
"expansions": [
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 1,
"value": 5
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 2,
"value": 4
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 3,
"value": 3
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 4,
"value": 2
}
]
},
{
"target": "teams[SoloTeam].minPlayers",
"steps": [
{
"waitTimeSeconds": 5,
"value": 1
}
]
}
]
}

5. 重连对战会话案例

一个典型的场景是,当玩家刚经历了游戏崩溃或电脑异常重启后,需要找到正在进行的游戏并重新连接到 DS。

  • 如果 DS 想要踢出断线的玩家
    • PGOS 会在对战会话中保存玩家的会话信息,直到游戏通过 DS SDK 中的 UPgosHostingAPI::RemovePlayerBattleSession 接口将该玩家从对战会话中移除。
  • 如果客户端想要重连到 DS 以继续之前的对战会话
    • 开发者可以通过调用客户端 SDK 中的 UPgosBattleAPI::DescribeBattleSessions 接口来查找某个玩家正在进行的对战会话。该接口的返回数据中包含了对战会话的连接地址(IP 和端口)。

6. 案例 避免编写矛盾规则

矛盾的规则会导致所有匹配请求100%超时。 造成冲突的方式不止一种。因此请仔细检查规则集的正确性,避免编写矛盾的规则。 在以下案例中,第一条规则将MMR差值范围限制在[0, 0.5]内,而第二条规则将其限制在[0.6, 1]内。

{
"version": "v1.0",
"playerAttributes": [
{
"name": "mmr",
"type": "number"
}
],
"teams": [
{
"name": "team1",
"maxPlayers": 3,
"minPlayers": 3
},
{
"name": "team2",
"maxPlayers": 3,
"minPlayers": 3
},
{
"name": "team3",
"maxPlayers": 3,
"minPlayers": 3
}
],
"rules": [
{
"name": "mmr_calc",
"type": "distanceRule",
"measurements": [
"flatten(teams[*].players.playerAttributes[mmr])"
],
"referenceValue": "avg(flatten(teams[*].players.playerAttributes[mmr]))",
"minDistance": 0,
"maxDistance": 0.5
},
{
"name": "mmr_calc2",
"type": "distanceRule",
"measurements": [
"flatten(teams[*].players.playerAttributes[mmr])"
],
"referenceValue": "avg(flatten(teams[*].players.playerAttributes[mmr]))",
"minDistance": 0.6,
"maxDistance": 1
}
],
"expansions": []
}

7. 使用对战数据服务构建对战历史记录

7.1 对战历史记录的数据示例

我们的目标是从游戏客户端获取玩家参与过的对战历史数据。格式如下。

DateUptimeGame ModeMy KDA
(Kill / Death / Assist)
2022-2-330min 30sImba0/5/3
2022-2-215min 15sLadder10/5/3
2022-2-159min 59sLadder2/0/15

7.2 定义战斗历史记录的数据结构

我们使用对战数据中的键值来描述上表中的标题。我们有4个键值:

  • data
  • uptime
  • game_mode
  • players_kda

需要注意的是,即使只显示当前玩家的kda数据,也需要保存所有玩家的KDA数据以用于对战历史记录。 因此,请用JSON格式描述对战记录项的结构。

{
"date": "",
"uptime": "",
"game_mode": "",
"players_kda": [
"player_id": "",
"kda": ""
]
}

1646125069904

7.3 战斗历史记录

我们建议将战斗历史数据作为战斗会话流程的最后一环进行写入,并确保在调用ProcessEnding接口之前完成所有操作。

首先,调用UpdateBattleData接口来写入一场战斗会话的战斗数据。示例代码如下:

#include "PgosServerAPI.h"

void OnBattleEnd() {
...
TMap<FString, FString> record_data;
record_data["date"] = "fake_date_2022_02_03";
record_data["uptime"] = "fake_uptime_1830";
record_data["game_mode"] = "Imba";
TArray<TMap> players_kda;

foreach (auto player: in battle_players) {
auto player_kda_data;
player_kda_data["player_id"] = player.id;
}

record_data["players_kda"] = players_kda;
auto battle = IPgosSDKCpp::Get().GetServerBattleAPI();
battle->UpdateBattleData("fake_battle_session_id",
record_data,
[](const FPgosResult& Ret){})
}

然后,调用UpdateBattleRecord将战斗数据绑定到指定的玩家列表,只有列表中的玩家才能在客户端访问这些数据。示例代码如下:

#include "PgosServerAPI.h"

void OnBattleEnd() {
...
TArray<FString> player_ids;
foreach (auto player: in battle_players) {
auto player_kda_data;
player_ids.append(player.id);
}
tags;
auto battle = IPgosSDKCpp::Get().GetServerBattleAPI();
battle->UpdateBattleRecord("fake_battle_session_id",
player_ids,
tags,
[](const FPgosResult& Ret){})
}

通过以上两个步骤,我们完成了为指定玩家添加战斗历史记录的工作。

玩家进行多场战斗后,可以拉取他们的战斗历史数据。

7.4 从游戏客户端获取战斗历史数据

游戏结束后,从游戏客户端调用GetBattleRecordList来获取当前玩家的战斗历史数据。

#include "PgosSDKCpp.h"
#include "Core/PgosErrorCode.h"

void SomeUObjectClass::GetBattleRecordList()
{
auto battle = IPgosSDKCpp::Get().GetClientBattleAPI();
if (battle)
{
FString PlayerID = "fake_my_player_id";
TArray<FString> Tags;
TArray<FString> Keys;
int32 Offset = 0;
int32 Count = 20;
battle->GetBattleRecordList(PlayerID, Tags, Keys, Offset, Count, [](const FPgosResult& Ret, const FClientBattleListInfo* Data) {
if (Ret.err_code == (int32)Pgos::PgosErrCode::kSuccess) {
UE_LOG(LogTemp, Log, TEXT("GetBattleRecordListSuccess"));
} else {
UE_LOG(LogTemp, Log, TEXT("GetBattleRecordListFailed: err_code=%d, err_msg=%s"), Ret.err_code, *Ret.msg);
}
});
}
}

8. 独立集成匹配服务

通常情况下,匹配服务和DS托管服务是配套使用的,因为在匹配完成后会创建对战会话,而DS托管可以找到合适的DS来运行对战会话。

然而,如果开发者只想使用匹配服务,并在自己的DS托管平台上管理对战,可以按照以下步骤进行操作。

image-20231124114222612

  • 首先,在 MatchConfig 的 placer 选项中选择 NoPlacer。这样,在匹配完成后,DS 托管服务就不会被自动调用来部署对局。

image-20231124110241784

  • 接下来,在事件列表中找到 event_matchmaking_completed,并将其与 WebHook云函数关联。

image-20231124111511526

  • 最后,尝试使用客户端发起匹配。匹配成功后,匹配服务会将结果(包括匹配的队伍和成员、成员属性信息以及 BattleProperties 信息)打包成事件,并触发您的 Webhook 或云函数。如果您的云函数记录了事件信息,您可以查看日志以验证事件详情是否符合预期,然后继续进行后续的逻辑处理和开发。
提示

要创建响应匹配完成事件的云函数,您需要处理 URI 为 /pgos_event 的 HTTP 请求。 如果您使用 Golang、Python 或 NodeJS 进行开发,可以参考此处.)提供的示例代码。