65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2021年11月25日

CPOL

6分钟阅读

viewsIcon

10645

在本文中,我们将把我们的两个游戏组件组合在一起,使玩家能够登录并开始匹配。

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,以节省开发时间,并将精力集中在使您的游戏独一无二的功能上。

© . All rights reserved.