不要从头开始构建游戏,第 3 部分:使用 Azure PlayFab 添加多人匹配





5.00/5 (2投票s)
在本文中,我们将把我们的两个游戏组件组合在一起,使玩家能够登录并开始匹配。
Azure PlayFab 使游戏开发者和工作室能够快速轻松地设置游戏后端。通过节省的时间,开发人员可以专注于开发新游戏和功能,以让他们的玩家愉快地游戏。
在本系列文章的前两篇中,我们将贪吃蛇游戏打包并部署到 PlayFab,服务器会根据需求自动扩展。然后,我们创建了一个 Web 界面来连接到 PlayFab 的用户身份验证服务,使我们的玩家无需托管自己的后端或数据库即可登录。
很高兴我们的玩家现在可以登录并玩我们的多人游戏了。但随机匹配玩家并不总是最好的主意——也不一定是玩家想要的。
一个更复杂的匹配系统应该根据技能水平和其他偏好来匹配玩家,以获得最佳的用户体验。当然,从头开始构建这样的匹配系统非常耗时。所以 PlayFab 也提供了这个现成的服务。
在本文中,我们将基于本系列的前两部分,使用一个实际的、功能齐全的多人游戏,为游戏服务器添加多人匹配功能。我们将将其部署到 PlayFab 多人服务器。PlayFab 匹配 API 将允许不同的玩家连接到同一个、动态分配的服务器,以一起玩贪吃蛇游戏。
您只需要了解一些 Java 知识即可跟上,我将引导您了解如何使用 PlayFab 匹配 API。在继续阅读这最后一部分之前,您应该阅读本系列的第一部分和第二部分。
要求
要学习本指南,您需要一个 PlayFab 帐户,并在您的计算机上安装以下软件
更新贪吃蛇游戏以支持按需匹配
我们在第一部分中构建并部署了贪吃蛇游戏到 PlayFab 虚拟机 (VM),以备待机。
在开始匹配玩家并将他们分配到服务器之前,我们需要更新代码,以便在多人比赛结束后自动退出。这样,PlayFab 就可以将 VM 重用于未来的游戏。
首先,在 Visual Studio Code 中打开贪吃蛇游戏项目目录。然后,打开包含贪吃蛇游戏服务器端逻辑的 _app\controllers\game-controller.js_。
接下来,找到 runGameCycle()
函数。它应该接近 **第 83 行**。在检查所有玩家是否已离开游戏的第一个 if 语句中,更新代码以在 **Game Paused** 消息日志之前退出进程。
console.log('Game Paused');
// Exit the server!
process.exit();
此调整是贪吃蛇游戏进行多人匹配所需的唯一更改。现在运行 _build.ps1_ 来构建和打包项目。使用 **PlayFab 控制面板**将新构建上传到 PlayFab。
如果您本系列第一部分的先前构建仍在运行,您可能需要重命名 _gameassets.zip_,然后再上传以避免资产名称冲突。删除任何先前构建也是可以的,因为它们不再是必需的。
创建匹配队列
接下来,我们需要在 **PlayFab 控制面板**中为游戏配置匹配队列。
您可以根据各种规则和属性创建队列,例如技能水平、地区或团队规模。
在 **PlayFab 控制面板**中,导航到 **Build** > **Multiplayer** 页面,然后选择 **Matchmaking** 选项卡。
单击 **New queue** 并为队列设置一个名称,例如 **“testqueue”**。您稍后将需要此队列名称。
接下来,将 **Match Size** 设置为 **Min** 2,**Max** 4。另外,勾选 **Enable server allocation** 选项,这表示 PlayFab 会托管并自动分配我们的游戏服务器。
接下来,在下拉框中选择您上传的 **Build for multiplayer server**。然后,单击 **+ Add Rule** 并选择一个 **Region** 选择规则。指定“**latencies**”作为 **Attribute path** 中的文本,并为 **maximum latency** 设置一个值,例如 **200**。最后,单击 **Create queue** 完成。
在您的 Web 应用中启动匹配
我们已准备好开始使用 PlayFab 匹配服务!
让我们从第二部分中准备好的身份验证前端开始。
首先,将 _index.html_ 复制到一个名为 _matchmaking.html_ 的新文件中。在游戏标题 ID 变量下方定义您的队列名称和匹配过期时间。
const titleId = "YOUR-TITLE-ID";
const queueName = "YOUR-QUEUE-NAME";
const matchmakingExpiration = 120; // 120 seconds
我们将使用的三个匹配 API 是:
CreateMatchmakingTicket
,它提交来自每个玩家的匹配请求。GetMatchmakingTicket
,它以每分钟十次或每六秒一次的轮询速率限制来检查票证状态。GetMatch
,它获取已完成的匹配票证的详细信息。
创建辅助函数
创建以下辅助函数,使用您的游戏标题 ID 调用这些 API。
async function getMatch( entityToken, matchId, queueName ) {
return await fetch( `https://${titleId}.playfabapi.com/Match/GetMatch`, {
method: "POST",
headers: {
"X-EntityToken": entityToken,
"Content-Type": "application/json"
},
body: JSON.stringify( {
EscapeObject: false,
MatchId: matchId,
QueueName: queueName,
ReturnMemberAttributes: true,
})
} ).then( r => r.json() );
}
async function getMatchmakingTicket( entityToken, queueName, ticketId ) {
return await fetch( `https://${titleId}.playfabapi.com/Match/GetMatchmakingTicket`, {
method: "POST",
headers: {
"X-EntityToken": entityToken,
"Content-Type": "application/json"
},
body: JSON.stringify( {
EscapeObject: false,
QueueName: queueName,
TicketId: ticketId,
})
} ).then( r => r.json() );
}
async function createMatchmakingTicket( entityToken, creator, expiration, queueName, tags = {} ) {
return await fetch( `https://${titleId}.playfabapi.com/Match/CreateMatchmakingTicket`, {
method: "POST",
headers: {
"X-EntityToken": entityToken,
"Content-Type": "application/json"
},
body: JSON.stringify( {
Creator: creator,
GiveUpAfterSeconds: expiration,
QueueName: queueName,
CustomTags: tags,
})
} ).then( r => r.json() );
}
创建 startMatchmaking() 函数
让我们再创建一个名为 startMatchmaking()
的函数。此函数启动匹配过程,并每六秒轮询其状态,直到 PlayFab 找到匹配或有人取消票证。一旦 PlayFab 找到匹配,我们将玩家重定向到匹配服务器的地址和端口,以便他们连接到同一服务器。
在我们的例子中,我们在找到匹配项时将玩家重定向到一个新页面。请记住,通常情况下,游戏客户端仅连接到匹配的服务器并与其 API 进行通信。
async function startMatchmaking( entityToken, creator ) {
const result = await createMatchmakingTicket( entityToken, creator, matchmakingExpiration, queueName );
logLine( "Matchmaking Ticket: " + JSON.stringify( result ) );
if( result.code === 200 ) {
// Success!
logLine("Matching...");
const ticketId = result.data.TicketId;
const matchmakingTimer = setInterval( async () => {
const ticket = await getMatchmakingTicket( entityToken, queueName, ticketId );
logLine( "Ticket: " + JSON.stringify( ticket ) );
if( ticket.code !== 200 ) {
logLine( "Error!" );
clearInterval( matchmakingTimer );
return;
}
switch( ticket.data.Status ) {
case "Canceled":
logLine( "Canceling!" );
clearInterval( matchmakingTimer );
break;
case "Matched":
const matchId = ticket.data.MatchId;
logLine( "Matched: " + matchId );
clearInterval( matchmakingTimer );
const match = await getMatch( entityToken, matchId, queueName );
logLine( "Match: " + JSON.stringify( match ) );
const serverDetails = match.data[ "ServerDetails" ];
// Redirect to the matched server
// NOTE: Normally, games will connect to the server rather than redirect to it
location.href = `http://${serverDetails[ "Fqdn" ]}:${serverDetails[ "Ports" ][ 0 ][ "Num" ]}`;
break;
case "WaitingForServer":
logLine( "Waiting for server..." );
break;
case "WaitingForMatch":
logLine( "Waiting for match..." );
break;
default:
logLine( "Status: " + ticket.data.Status );
break;
}
}, 6000 );
}
}
更新 onPlayFabResponse 处理程序
最后,我们可以更新 onPlayFabResponse
处理程序,以便在玩家登录时启动匹配过程。
由于我们只有一个区域,为了简单起见,我们可以硬编码一个虚拟延迟值作为玩家的属性。但是,通常情况下,我们应该通过ping PlayFab 的服务质量信标来确定最低延迟,并在代码中计算这些值。
// Handles response from playfab.
function onPlayFabResponse(response, error) {
if( response ) {
logLine( "Response: " + JSON.stringify( response ) );
}
if( error ) {
logLine( "Error: " + JSON.stringify( error ) );
}
const entityToken = response.data[ "EntityToken" ][ "EntityToken" ];
logLine( "EntityToken: " + entityToken );
const creator = {
Entity: response.data[ "EntityToken" ][ "Entity" ],
Attributes: {
DataObject: {
latencies: [ // Note: This should be normally calculated by pinging PlayFab QoS Servers
{
region: "EastUs",
latency: 100,
},
]
},
}
};
startMatchmaking( entityToken, creator );
}
就这样,您已经设置了多人匹配——无需后端代码。
此匹配页面的完整代码应如下所示:
<!DOCTYPE html>
<html>
<head>
<!-- Load PlayFab Client JavaScript SDK -->
<script src="https://download.playfab.com/PlayFabClientApi.js"></script>
</head>
<body>
<p>PlayFab Player Matchmaking Example</p>
<input type="text" id="email" />
<input type="password" id="password" />
<button onclick="loginBtn();">Login</button>
<button onclick="registerBtn();">Register</button>
<script>
const titleId = "YOUR-TITLE-ID";
const queueName = "YOUR-QUEUE-NAME";
const matchmakingExpiration = 120; // 120 seconds
// Handles response from playfab.
function onPlayFabResponse(response, error) {
if( response ) {
logLine( "Response: " + JSON.stringify( response ) );
}
if( error ) {
logLine( "Error: " + JSON.stringify( error ) );
}
const entityToken = response.data[ "EntityToken" ][ "EntityToken" ];
logLine( "EntityToken: " + entityToken );
const creator = {
Entity: response.data[ "EntityToken" ][ "Entity" ],
Attributes: {
DataObject: {
latencies: [ // Note: This should be normally calculated by pinging PlayFab QoS Servers
{
region: "EastUs",
latency: 100,
},
]
},
}
};
startMatchmaking( entityToken, creator );
}
function logLine( message ) {
var textnode = document.createTextNode( message );
document.body.appendChild( textnode );
var br = document.createElement( "br" );
document.body.appendChild( br );
}
function loginBtn() {
logLine( "Attempting PlayFab Sign-in" );
PlayFabClientSDK.LoginWithEmailAddress({
TitleId: titleId,
Email: document.getElementById( "email" ).value,
Password: document.getElementById( "password" ).value,
RequireBothUsernameAndEmail: false,
}, onPlayFabResponse);
}
function registerBtn() {
logLine( "Attempting PlayFab Registration" );
PlayFabClientSDK.RegisterPlayFabUser({
TitleId: titleId,
Email: document.getElementById( "email" ).value,
Password: document.getElementById( "password" ).value,
RequireBothUsernameAndEmail: false,
}, onPlayFabResponse);
}
async function getMatch( entityToken, matchId, queueName ) {
return await fetch( `https://${titleId}.playfabapi.com/Match/GetMatch`, {
method: "POST",
headers: {
"X-EntityToken": entityToken,
"Content-Type": "application/json"
},
body: JSON.stringify( {
EscapeObject: false,
MatchId: matchId,
QueueName: queueName,
ReturnMemberAttributes: true,
})
} ).then( r => r.json() );
}
async function getMatchmakingTicket( entityToken, queueName, ticketId ) {
return await fetch( `https://${titleId}.playfabapi.com/Match/GetMatchmakingTicket`, {
method: "POST",
headers: {
"X-EntityToken": entityToken,
"Content-Type": "application/json"
},
body: JSON.stringify( {
EscapeObject: false,
QueueName: queueName,
TicketId: ticketId,
})
} ).then( r => r.json() );
}
async function createMatchmakingTicket( entityToken, creator, expiration, queueName, tags = {} ) {
return await fetch( `https://${titleId}.playfabapi.com/Match/CreateMatchmakingTicket`, {
method: "POST",
headers: {
"X-EntityToken": entityToken,
"Content-Type": "application/json"
},
body: JSON.stringify( {
Creator: creator,
GiveUpAfterSeconds: expiration,
QueueName: queueName,
CustomTags: tags,
})
} ).then( r => r.json() );
}
async function startMatchmaking( entityToken, creator ) {
const result = await createMatchmakingTicket( entityToken, creator, matchmakingExpiration, queueName );
logLine( "Matchmaking Ticket: " + JSON.stringify( result ) );
if( result.code === 200 ) {
// Success!
logLine("Matching...");
const ticketId = result.data.TicketId;
const matchmakingTimer = setInterval( async () => {
const ticket = await getMatchmakingTicket( entityToken, queueName, ticketId );
logLine( "Ticket: " + JSON.stringify( ticket ) );
if( ticket.code !== 200 ) {
logLine( "Error!" );
clearInterval( matchmakingTimer );
return;
}
switch( ticket.data.Status ) {
case "Canceled":
logLine( "Canceling!" );
clearInterval( matchmakingTimer );
break;
case "Matched":
const matchId = ticket.data.MatchId;
logLine( "Matched: " + matchId );
clearInterval( matchmakingTimer );
const match = await getMatch( entityToken, matchId, queueName );
logLine( "Match: " + JSON.stringify( match ) );
const serverDetails = match.data[ "ServerDetails" ];
// Redirect to the matched server
// NOTE: Normally, games will connect to the server rather than redirect to it
location.href = `http://${serverDetails[ "Fqdn" ]}:${serverDetails[ "Ports" ][ 0 ][ "Num" ]}`;
break;
case "WaitingForServer":
logLine( "Waiting for server..." );
break;
case "WaitingForMatch":
logLine( "Waiting for match..." );
break;
default:
logLine( "Status: " + ticket.data.Status );
break;
}
}, 6000 );
}
}
</script>
</body>
</html>
玩匹配好的贪吃蛇游戏
一切配置就绪后,您现在应该能够打开两个浏览器窗口访问网页,并与朋友一起玩游戏。
打开 https://:8080/matchmaking.html 并在每个窗口中以不同的帐户登录或注册。您将能够作为两个不同的玩家加入同一个贪吃蛇游戏,这些游戏完全托管和匹配在 PlayFab 上!
下一步
在本系列中,您体验了集成像 PlayFab 这样的现成解决方案如何能显著加速多人游戏开发,并轻松支持身份验证和匹配等复杂功能。
我希望您喜欢这次的跟随学习,并看到使用 PlayFab 和Microsoft Game Stack 可以多么快速地创建引人入胜的实时多人游戏体验。
本系列只是 PlayFab 多人服务器的入门介绍。现在您掌握了多人游戏开发的力量,可以探索一些其他功能:
- 游戏会话中的用户名:修改贪吃蛇游戏代码,以显示玩家的 PlayFab 用户名而不是随机生成的数字。
- 多人匹配(超过两人):添加四人匹配。
- 技能水平匹配属性:使用玩家属性并将技能水平数据设置为匹配标准的一个因素。
- 服务器的 HTTPS:使用 SSL 证书保护您的多人服务器连接。
亲身体验Azure PlayFab,以节省开发时间,并将精力集中在使您的游戏独一无二的功能上。