Gaming

How to develop an SBMM solution with PubNub

SBM-1200.png

When thinking about building a game that keeps gamers engaged and coming back for more, matchmaking is one of the first challenges that comes to mind. Matchmaking that ensures players are matched with opponents of similar skill levels is crucial for creating a fair and enjoyable experience. This is where Skill-Based Matchmaking (SBMM) shines. By pairing players based on their abilities, SBMM increases engagement, achieves fair competition, and improves player retention.

However, architecting/implementing a matchmaking system can be complex. In this blog, we’ll explore how you can leverage PubNub’s Chat SDK and portal to build and manage an SBMM solution while exploring its many benefits. Next, we will check PubNub’s most popular SDKs to see how the system can integrate with popular gaming engines, such as Unreal Engine and Unity.

Everything discussed in this blog can be found on the SBMM Engine GitHub, or you can use the demo by navigating to the SBMM demo.

Why Use PubNub for Matchmaking?

Matchmaking involves multiple moving parts, from tracking player skill ratings and latency to ensuring a smooth and timely connection between matched players. Here’s why PubNub is the ideal solution:

  1. It enables instant communication between players and the server, ensuring that matchmaking decisions based on player status, Elo ratings, latency, and region occur in real time without any delays.
  2. The globally distributed network allows the SBMM system to handle millions of concurrent users across different regions, ensuring low latency and seamless player connections worldwide. With PubNub AppContext, you can trust that player data is processed and transmitted reliably, leading to uninterrupted gameplay experiences.
  3. Customize your matchmaking logic using the PubNub Platform with tools like PubNub Illuminate. These tools allow you to fine-tune and adapt matchmaking algorithms on the fly, using live data to dynamically adjust Elo ranges, latency filters, or regional preferences.
  4. PubNub Access Manager enables secure messaging to ensure that player data remains private and protected, a critical feature of competitive gaming environments.

Architecture Overview: How It Works

Here’s a high-level overview of the SBMM architecture built with PubNub:

  • Queue System: Players join a matchmaking queue, sending their skill level, region, and other metadata to a PubNub channel.
  • Skill Buckets: Players are grouped into predefined Elo ranges (e.g., 0–999, 1000–1499), making matchmaking efficient. Latency Mapping: PubNub’s signal feature calculates players’ latencies and uses them as matchmaking criteria.
  • Pre-Lobby Confirmation: Once paired, players join a pre-lobby channel to confirm their readiness.
  • Game Lobby Creation: Confirmed players are moved to a game lobby, and their Elo ratings are adjusted post-game based on the match outcome.

Step-by-step Guide: Building SBMM with PubNub

Before enabling the ChatSDK in your repository, we need to install the ChatSDK using npm. Refer to the documentation for enabling the ChatSDK in Unreal Engine, Unity or JavaScript.

1 npm i @pubnub/chat

After we have installed the SD, we can create a Chat instance using our Publish and Subscribe Keys.

1 2 3 4 5 6 7 import { Chat } from '@pubnub/chat'; const chat: Chat = await Chat.init({ publishKey: process.env.PUBLISH_KEY as string, subscribeKey: process.env.SUBSCRIBE_KEY as string, userId: userId });

Setting up the Matchmaking Queue

The matchmaking queue starts with players joining. This is handled by subscribing to a PubNub channel that tracks incoming players. Each listener should listen to its respective region in this case, we will be using us-east-1. This will allow us to host the game server in the ideal geolocation for both players once they are matched.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let regionChannel = await chat.getChannel("matchmaking-us-east-1"); // Add a message listener for the matchmaking channel regionChannel.join(async (message) => { const userId = message.userId; const messageContent = message.content.text; // Add the user to the matchmaking queue if not already queued if (!matchmakingQueue.has(userId)) { matchmakingQueue.set(userId, { userId, message: messageContent }); const usersToProcess = Array.from(matchmakingQueue.values()); const userIds = usersToProcess.map((entry) => entry.userId); await notifyTestingClientUsersMatchmaking(userIds); } });

The notifyTestingClientUsersMatchmaking function will notify our dashboard that the user has been processed and they are officially in the queue.

1 2 3 4 5 6 7 8 9 10 11 12 async function notifyTestingClientUsersMatchmaking(userIds: string[]){ let channel = await chat.getChannel(`Matchmaking-In-Progress-Client-Testing`); if(channel){ // Convert the object to a JSON string const jsonString = JSON.stringify({ message: "Joining", matchedUsers: userIds }); // Notify the client that their matchmaking request is being processed await channel.sendText(jsonString); }

Pair Players by Skill and Latency

Use a pairing algorithm that prioritizes skill similarity and latency. PubNub’s Chat SDK can fetch metadata to determine each player’s skill level and region. We first need to enable App Context on our keyset to utilize the metadata.

Navigate to your keyset and, under App Context, enable User Metadata Events. This will allow you to update the User object when using the ChatSDK. When a user is retrieved, you can also retrieve any metadata that might be stored.

We can pair users by skill and latency, assuming this data is stored within App Context. The following section will show you how to update the App Context metadata using the ChatSDK.

After receiving a message using the event listener described above, we can get the UserId from the message. Once we have the user, we can retrieve the metadata using the custom object embedded in the User object.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** * Calculate the score between two users based solely on their ELO difference * * @param userA - First user * @param userB - Second user * @returns A score representing the cost of pairing these two users based on skill */ function calculateScore(userA: User, userB: User): number { const eloA = userA.custom?.elo ?? 0; const eloB = userB.custom?.elo ?? 0; const eloDifference = Math.abs(eloA - eloB); // Use only skill difference as the score return eloDifference; }

In this case, we are accessing the custom Elo metadata uploaded to AppContext. If you are having issues accessing the custom metadata that you have previously uploaded, we can double-check this by navigating to BizOps Workspace under User Management. Click on the user you are trying to access or the corresponding UserId, and check if the attribute is there under Custom data fields. In this case, we can see the elo is 220, so we should be able to grab that value using User.custom.elo.

We can now manage our players from this portal by checking their latencies, lobbies, and elo ratings. We can update this data from the portal at any time.

Creating Pre-lobby Confirmations

The pre-lobby stage ensures both players are ready to play. This uses PubNub’s Chat SDK to create a shared channel for communication and updates between the players. We are trying to achieve the functionality that all players must be ready to play after being matched based on their corresponding skill levels.

As seen in the image above, there are a few things to note. We need a timer for how long everyone has in the pre-lobby to accept the match. We must also tell the corresponding user how many people have received the game, represented in the image by 7/10. In this example, we will be hosting games that are 1-1.

Let us create the pre-lobby listener and listen for updates from the respective clients.

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 // Set up a timeout to cancel confirmation after 30 seconds const confirmationTimeout = new Promise((resolve) => { setTimeout(() => resolve('timeout'), 30000); }); // Listen for confirmation from both players const confirmationPromise = new Promise((resolve) => { preLobbyChannel.join(async (message: Message) => { try { if (message.content.text === "match_confirmed") { const { userId } = message; await notifyTestingClientofUserConfirmed(userId); // Update player confirmation status if (userId === player1.id) player1Confirmed = true; if (userId === player2.id) player2Confirmed = true; if (player1Confirmed && player2Confirmed) resolve('confirmed'); } } catch (error) { console.error("Error processing player confirmation:", error); } }); }); // Wait for both confirmations or timeout const result = await Promise.race([confirmationPromise, confirmationTimeout]);

Here, we are creating a race condition to see which function gets resolved first: the timer or the pre-lobby event listener. Once both players have accepted the match, we can create the game lobby. If they have not received the match, I have made a function to punish the corresponding player by updating the player's metadata.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 await updatePlayerMetadata (player, { punished: true, searching: false, confirmed: false }); async function updatePlayerMetadata(user: User, newCustomData: any) { // Fetch the user's existing metadata const userMetadata = user.custom; // Merge existing custom data with new custom fields const updatedCustomData = { ...userMetadata, // Existing data ...newCustomData // New data to update }; // Update the user's metadata await user.update({ custom: updatedCustomData, // Merged data }); }

This function ensures no other metadata corresponding to the player is overwritten while being updated. Game Lobby Creating and Elo Adjustment Next, we must create a game lobby for the respective players. For this, we will create a game lobby channel, and the players will be able to join these lobbies on the client side.

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 /** * Create the actual game lobby once both players confirm the match * * This function creates a new game lobby channel after both players agree to the match in the pre-lobby. * * @param player1 - The first player object. * @param player2 - The second player object. * @param preLobbyChannel - The pre-lobby channel where confirmation occurred. */ async function createChannelLobby(player1: User, player2: User, preLobbyChannel: Channel) { try { const chat = getPubNubChatInstance(serverID); const gameLobbyChannelID = `game-lobby-${player1.id}-${player2.id}`; // Get or create the game lobby channel with retries let gameLobbyChannel = getOrCreateChannel(chat, gameLobbyChannelID), 3, 1000); // Notify both players that the game lobby has been created await preLobbyChannel.sendText(`game-lobby-${player1.id}-${player2.id}`); // Update the database to mark players as done searching await updatePlayerMetadataWithRetry(player1, { searching: false }); await updatePlayerMetadataWithRetry(player2, { searching: false }); } catch (error) { console.error("Error in createChannelLobby:", error); } }

The getOrCreateChannel function will create the game channel for both players.

1 2 3 4 5 6 7 8 9 10 export async function getOrCreateChannel(chat: Chat, channelId: string) { try { let channel = await chat.getChannel(channelId); if (!channel) { channel = await chat.createPublicConversation({ channelId }); } return channel; } catch (error) { console.log(error); }

On the client side, users can listen to the pre-lobby channel receiving the message to join the game-lobby channel.

1 2 3 4 5 6 7 8 9 10 11 12 13 async function joinPreLobby(preLobbyChannel: Channel) { await preLobbyChannel.join(async (message: Message) => { if (message.content.text.startsWith("game-lobby-")) { gameLobbyChannelID = message.content.text; // Join the game lobby await joinGameLobby(gameLobbyChannelID, chat); await updateMatchesFormed(chat, gameLobbyChannelID, channelTracker); preLobbyChannel.leave(); } }); }

One topic we have yet to discuss is the use of PubNub Memberships. When you join a channel, the user will have a membership created for them, which comes in handy when managing event listeners. The best part is that Memberships can store metadata as well. In this case, we have to ensure the User leaves the pre-lobby channel, as in this code, you can not be in two pre-lobbies at the same time. After receiving the message from the preLobbyChannel, we can finally join the game lobby.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * Simulate joining a game lobby after leaving the pre-lobby * * This function logs the user joining the game lobby. * * @param gameLobbyID - The ID of the game lobby the user will join * @param userID - The ID of the user joining the game lobby */ async function joinGameLobby(gameLobbyID: string, chat: Chat) { const gameLobbyChannel: Channel | undefined = await getOrCreateChannel(chat, gameLobbyID); // Fetch thet gamelobby if (!gameLobbyChannel) { throw new Error("Failed to find matchmaking channel"); // Throw an error if the channel is not found } await gameLobbyChannel.join((_) => {}); }

We can manage user memberships by using the BizOps workspace to check if a user is in a pre-lobby or game-lobby. This allows you to have admin control over your player base. Deleting a user's membership from a game lobby or pre-lobby will kick them out of the game or chat, depending on the architecture. We can find these memberships by navigating the Channel Management tab under the BizOps workspace. This will show you all the channels or game/pre-lobbies created.

By clicking on a specific game lobby, we can now navigate to View Memberships and view the memberships on individual lobbies. The second, or easier, way to manage a user is to navigate to the User Management tab, select an individual user, and select View Memberships.

Viewing their Channel Memberships, we see this user in a game.

More Resources

When it comes to creating an online game that requires skill-based matchmaking, it can be intense. With all the features with queues, pre-lobby, and game-lobby channels, the skill part of skill-based matchmaking can be forgotten. This is where PubNub shines. PubNub enables you to build out these real-time features, whether SBMM, in-game chat, or even real-time user metadata updates and allows developers to stay focused on the experience of the game they are making.

For more resources about SBM, check the links below:

To start with PubNub, sign up for our admin portal and create your first keyset for free. Here, you can implement all the logic in the demo and test the real-time functionality for yourself. Regardless of the platform you are using, PubNub has you covered.

Get started with PubNub by signing up for a free account right now.

Feel free to contact the DevRel team at devrel@pubnub.com or our Support team for help with any aspect of your PubNub development.