Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Don’t Build Games from Scratch Part 3: Adding Multiplayer Matchmaking with Azure PlayFab

0.00/5 (No votes)
25 Nov 2021 1  
In this article, we’ll put our two game components together to enable players to log in and start matchmaking.
Here we’ll build on the previous two parts of this series by adding multiplayer matchmaking to a game server using an actual, fully functional multiplayer game. We’ll deploy it to PlayFab Multiplayer Servers, and then PlayFab Matchmaking APIs will enable various players to connect to the same, dynamically-assigned server to play the snake game together.

Azure PlayFab enables game developers and studios to quickly and easily set up a gaming backend. With the time they save, developers can focus on developing new games and features to keep their players happily gaming.

In the previous two articles of this three-part series, we wrapped and deployed a snake game to PlayFab where the servers scale automatically to meet demand. Then, we created a web interface to connect to PlayFab’s user authentication service, enabling our players to log in without hosting our own backend or database.

It’s great that our players can now log in and play our multiplayer game. But randomly matching players isn’t always the best idea — and isn’t necessarily what the players want.

A more sophisticated matchmaking system should pair players by skill level and other preferences for the best user experience. Building a matchmaking system like this from scratch is, of course, time-consuming. So PlayFab offers this ready-made service, too.

In this article, we’ll build on the previous two parts of this series by adding multiplayer matchmaking to a game server using an actual, fully functional multiplayer game. We’ll deploy it to PlayFab Multiplayer Servers. PlayFab Matchmaking APIs will enable various players to connect to the same, dynamically-assigned server to play the snake game together.

You just need to know some Java to follow along, and I’ll walk you through how to use the PlayFab Matchmaking APIs. You should read through parts one and two of this series before continuing with this final article.

Requirements

To follow along with this guide, you need a PlayFab account and the following software installed on your computer:

Updating the Snake Game for On-Demand Matches

We built and deployed the snake game to a PlayFab virtual machine (VM) to be ready on standby in part one.

Before we start matchmaking the players and assigning them to servers, we need to update the code to exit automatically when a multiplayer match has concluded. That way, PlayFab can reuse the VM for future games.

First, open the snake game project directory in Visual Studio Code. Then, open app\controllers\game-controller.js, which contains the snake game’s server-side logic.

Next, find the runGameCycle() function. It should be near line 83. Inside the first if-statement, which checks when all players have left the game, update the code to exit the process before the Game Paused message log:

JavaScript
console.log('Game Paused');
// Exit the server!
process.exit();

This adjustment is the only change the snake game requires for multiplayer matchmaking. Now run build.ps1 to build and package the project again. Upload the new build to PlayFab using the PlayFab dashboard.

If your previous build from part one of this series is still running, you may need to rename gameassets.zip before uploading to avoid an asset name conflict. It’s also acceptable to delete any previous builds, as they’re no longer necessary.

Creating a Matchmaking Queue

Next, we need to configure a matchmaking queue for the game in the PlayFab dashboard.

You can create a queue based on various rules and attributes, such as skill level, region, or team size.

In the PlayFab dashboard, navigate to the Build > Multiplayer page and select the Matchmaking tab.

Click New queue and set a name for the queue, such as "testqueue." You’ll need this queue name later.

Next, set the Match Size to a Min of 2 and Max of 4. Also, check the Enable server allocation option, which indicates that PlayFab hosts and automatically allocates our game server.

Next, select your uploaded Build for multiplayer server in the dropdown box. Then, click + Add Rule and choose a Region selection rule. Specify "latencies" as the text inside the Attribute path and set a value such as 200 for maximum latency. Finally, click Create queue to finish.

Starting Matchmaking in Your Web App

We’re ready to start using the PlayFab matchmaking service!

Let’s start with our authentication-ready frontend from part two.

First, duplicate index.html to a new file named matchmaking.html. Define your queue name and a matchmaking expiration time below the game title ID variable:

JavaScript
const titleId = "YOUR-TITLE-ID";
const queueName = "YOUR-QUEUE-NAME";
const matchmakingExpiration = 120; // 120 seconds


The three matchmaking APIs we’ll use are:

  • CreateMatchmakingTicket, which submits a matchmaking request from each player.
  • GetMatchmakingTicket, which checks the ticket status with a polling rate limit of ten per minute or once every six seconds.
  • GetMatch, which gets the detailed information of a fulfilled matchmaking ticket.

Creating the Helper Functions

Create the following helper functions to call these APIs using your game title ID:

JavaScript
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() );
}

Creating the startMatchmaking() Function

Let’s create one more function named startMatchmaking(). This function starts the matchmaking process and polls its status every six seconds until PlayFab finds a match or someone cancels the ticket. Once PlayFab finds a match, we'll redirect the players to the matched server’s address and port, so they connect to the same server.

In our case, we redirect the player to a new page on finding a match. Keep in mind that, generally, the game client only connects to the matched server and communicates with its APIs.

JavaScript
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 );
    }
}

Updating the onPlayFabResponse Handler

Finally, we can update the onPlayFabResponse handler to start the matchmaking process when a player logs in.

Because we only have one region, we can hard-code a dummy latency value in the player’s attributes for simplicity. However, usually, we should compute these values in the code by pinging PlayFab’s Quality-of-Service beacons to determine the lowest latency.

JavaScript
// 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 );
}

Just like that, you’ve set up multiplayer matchmaking — with no backend code required.

The full code for this matchmaking page should look something like this:

HTML
<!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>

Playing a Matchmade Snake Game

With everything configured and ready, you should now be able to open two browser windows to the webpage and play the game with a friend.

Open http://localhost:8080/matchmaking.html and log in or register as a different account in each window. You’ll be able to join the same snake game as two different players playing against each other, fully hosted and matchmade on PlayFab!

What’s Next

In this series, you experienced how integrating an out-of-the-box solution like PlayFab can significantly speed up multiplayer game development and easily support sophisticated features like authentication and matchmaking.

I hope you enjoyed following along and seeing how quickly you can create compelling real-time multiplayer game experiences using PlayFab and the Microsoft Game Stack.

This series is just an introduction to PlayFab multiplayer servers. Explore some other features now that you hold this multiplayer game development power in your hands:

  • Usernames in the game session: Modify the snake game code to display the player’s PlayFab username instead of a randomly-generated number
  • Matchmaking with more than two players: Add matchmaking for four players
  • Skill-level matchmaking attributes: Use player attributes and set skill level data as a factor in the matchmaking criteria
  • HTTPS for servers - Secure your connection to the multiplayer servers using SSL certificates

Try Azure PlayFab for yourself to save development time and focus instead on the features that make your game unique.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here