This article was written on Pragma Engine version 0.0.95.
Parties in the Platform: Enabling Backend Party Services #
In this article, we’re going to overview why party features are crucial for online games. We’ll go into detail on how we’ve built a scalable and customizable party service for Pragma Engine and cover the following topics:
- the function of a party service in online games
- how parties are involved in matchmaking, player data, and more
- where the party lives in the backend platform
- what a customizable and scalable party service look like
- how to enable parties and matchmaking for cross-platform functionality
Where’s the Party? #
The way players interact with each other in and out of matches is incredibly important, both on the frontend (in a virtual lobby or chatroom) and on the backend (the platform topology and hosted game servers). To make this player-to-player interaction as seamless, intuitive, and fun to use as possible, party features enable friends and strangers alike to get together and play.
In theory, this sounds simple to implement–just create an invite and grouping feature. However, in practice, this task can be daunting, complicated, and hard to execute. In order to produce an efficient party feature, we need to delve into the purpose of a party service and why it’s complicated to implement.
The Purpose of a Party Service #
A backend’s party service functions as an entry point into multiplayer, assembling players into groups so they can initialize matchmaking together. A party service handles communication from player clients to the platform topology via intricately designed connections, as well as with other services and features in the platform.
A party service must handle, collect, and cache important data that only players can provide, such as direct and indirect player invites, notifications, leader functionality, voting features, and inventory data. Some of this data can be shared with the entire party for things like loadout and map selection, while other times this data should be private or hidden altogether such as hidden skill ranks or MMR rating.
Note that these are just the foundations of a party service–every game has unique party and grouping functionality and can have a different combination of features than those described above.
Parties contain leaders (crown), invites and join requests (letters), and player data (item-filled backpack).
Things get even more complicated if you want players to be able to play with all their friends regardless of the game client’s platform. Cross-platform play enables you to unlock your playerbase into one massive audience. Though this topic involves more than just parties (especially matchmaking and player accounts), the purpose of cross-platform compatibility goes hand in hand with the purpose of a party service: to let players play with their friends!
Lastly, a party service should allow for the seamless addition of new party related content and scaling. This means making sure your party service has rich and intuitive features for creating and altering party data. For scalability, this simply means making sure party data is easily accessible by players and platform services without relying on the service itself to do all the methods and decision making.
The next section in this article will delve into the details on building a party service’s infrastructure, and the “do’s and don’ts” of party functionality.
The Fundamentals of a Good Party #
Party services are usually finely tuned to the type of game that they’re built for because of different matchmaking and gameplay requirements. For Pragma Engine, we needed to figure out the fundamentals of what makes a good party service and how to easily customize such a feature to fit any type of game.
Customization #
Pragma Engine’s services utilize customization as a core feature of the product. Our strategy is to build a solid foundation for backend services and allow the developer to customize the backend’s features for their game’s unique design.
To fully enable developer customization, Pragma Engine’s Party service is built with two main data classes:
- The
Party
class tracks data related to the whole party, such as the selected game mode or queue selection. It contains objects for the invite code to the party, max player count, list of all the players in the party, and thegameServerVersion
. - The
PartyPlayer
class tracks data specific to each player in the party, such as custom data for character loadout and skin selection, inventory snapshots, ready states, party leaders, each player’s game client version, and a map of player ping zones. This class is a part of every Party object so theParty
class knows the map ofplayerIds
for eachPartyPlayer
.
In Pragma Engine, plugins and ext
data are the avenues through which developers can create their own custom logic and data against existing services. ext
data, which is defined using protobuf types, contains data values for public and hidden custom content, and plugins build and utilize that data through methods authored by the developer. In other words, custom data in the form of ext
s and plugins allow the developer to enforce specific rules on a platform service. For parties, this means enforcing rules based on the party’s makeup, employing ext
data specifically for custom public, private, and hidden data, and adding custom party logic that Pragma Engine doesn’t natively support.
For example, a party service might have ext
data for game mode selection, queuing ranked or unranked, and match difficulty. You can see an example of such custom data for party selections written in protos below.
// ext field for party state
message ExtPartySelections {
bool can_queue_ranked = 1;
pragma.inventory.ext.GameMode game_mode = 2;
party.AdventureModeDifficulty adventure_mode_difficulty = 3;
bool skip_finding_teammates = 4;
}
Then, the strongly typed protobuf data can be utilized by an authored plugin. Below is an example of the Party Plugin using the custom ext
data so parties can decide the game mode and the difficulty of their matches. Remember that these data fields are stored across the Party
and PartyPlayer
classes, and can even be passed to other plugins and services in the engine.
override suspend fun buildMatchmakingKey(party: Party): ExtMatchmakingKey {
val extPartySelections: ExtPartySelections = party.extPartySelections
return ExtMatchmakingKey.newBuilder()
.setGameMode(extPartySelections.gameMode)
.setAdventureModeDifficulty(extPartySelections.adventureModeDifficulty)
.build()
}
Both the Party
and PartyPlayer
classes can handle hidden player data like player skill ratings for matchmaking. Hidden player data allows developers to keep data that is important for a plugin close to each player, without exposing that data to the player. For a party service, hidden player data helps the matchmaking service take into account an entire party’s makeup, such as collective party skill rating calculated from each individual PartyPlayer
’s skill rating. Some of this hidden data could also just be concealed for each individual player so they don’t notify the entire party–things like character or item skin selection could fall into this category.
Sometimes players in a party are running on different gameServerVersions
and gameClientVersions
. These objects are used to identify different operating systems, patches, or regions built for your game. The Party
class needs to handle these different versions from PartyPlayers
so everyone in the party can play together. For example, if a player on gameClientVersion1
tries to join a party with someone on gameClientVersion2
, the platform needs to have configurations for what gameClientVersions
can support one another.
Pragma Engine has a plugin called gameServerVersionCompatabilityPlugin
that allows developers to configure what versions of their game support each other. In short, this allows the Party
class to own the most current game compatibility version from custom logic authored in the gameserverVersionCompatabilityPlugin
. You can also override and ignore these compatibility options in the Party Plugin if you don’t want to keep your version up to date in development.
Real-Time Communication #
How a party service communicates with other backend services, the player’s SDK client, and the platform topology as a whole is crucial in a live production environment. It’s important to minimize the time between the server understanding party state changes and the player clients processing party information and state changes. It’s also crucial that you reduce the number of messages handled by the server in a party service.
If this communication workflow is at all sluggish or slow to respond, this will affect players’ experiences on the front-end, due to other backend services often relying on party requests, responses, and data.
The solution to this problem is ensuring that parties can act in an independent manner–but what exactly does that look like?
Long-Lived Sockets Are a Must #
Party services often start out communicating with platform states via polling because it’s easy to set up and get going, especially for initial platform testing purposes. However, polling constantly adds noise to the platform because of the constant notifications sent back and forth from the game client. This especially becomes an issue when the platform’s load increases, more services get added to the backend, and delays and duplications in polling notifications need to be managed. In short, polling doesn’t scale very well, but there is a better solution.
Instead of having a party service constantly send and receive various platform states, parties need to have a push model. This is why we utilize bidirectional communication via a long-lived socket for Pragma Engine.
Although this takes more time and investment to implement, the server is able to immediately send exactly one message to the platform and game clients when something needs to be updated. This also enables continuous connection between the platform and the game client.
Security #
It’s generally bad practice to have any platform service infrastructure run on the player client side (not managed by the platform), especially for a service that is built around communication. That’s why we have all party information cached on the backend platform and not on individual player clients.
One major reason why we have our Party service on the platform is so players can’t cheat and start matches based on party data and selections. For example, since party data can contain matchmaking rank and skill-based matchmaking data, a party service run on player clients would give cheaters the ability to manipulate local files on a player and party’s rank or skill rating.
Matchmaking Persistent Parties #
How online games handle matches or sessions of multiplayer gameplay varies from game to game. Some games are purely match-based, with set match starts and match ends for all players within the game. Others, like battle royales, extraction shooters, and drop-in/drop-out coop experiences all fall in the camp of requiring some type of persistent matchmaking. To support all types of online gameplay fully, parties must persist before, during, and after matches.
In earlier iterations of Pragma Engine, we had parties exist only outside of matchmaking. This meant that parties were essentially disbanded once matchmaking started and recreated when matchmaking ended. While this worked for purely match-based games, this was ineffective and a hassle for any type of gameflow requiring party interaction inside the matchmaking service (during matches).
The solution was long-lived parties which persist in and out of matchmaking. Because parties now exist as independent entities and persist inside of matchmaking, the Party service is also capable of sending peer to peer information and connections before, during, and out of matches. The example below showcasing the Matchmaking Plugin in Arbiter (our demo game) contains custom data utilized in the party service to matchmake specific game modes from party selections.
override fun initialize(queueKey: MatchmakingQueueKey, party: Matchmaking.Party): NewGameInstance? {
val ext: ExtMatchmakingKey = queueKey.extMatchmakingKey
when (ext.gameMode.id) {
ArbiterAdventure.gameModeId -> return adventure.initialize(queueKey, party)
ArbiterDuel.gameModeId -> return duel.initialize(party)
ArbiterCustomGame.gameModeId -> return custom.initialize(party)
ArbiterCasual3v3.gameModeId -> casual3v3.initialize(party)
ArbiterRanked3v3.gameModeId -> ranked3v3.initialize(party)
ArbiterArena.gameModeId -> arena.initialize(config, party)
else -> throw ExtException(ExtError.ArbiterMatchmaking_InvalidGameMode)
}
return null
}
Cross-Platform Capability #
To fully let players play with all their friends regardless of the platform they bought your game with, you need to have some type of cross-platform capability on your backend platform.
Cross-platform play is typically really challenging to implement because many backend platforms use first-party hosting services (PlayStation, Xbox, etc.) for player accounts and multiplayer functionality. Although using first-party hosting services is an easy solution for quick development in the beginning, it locks your ability to have cross-compatibility, since all of your players are isolated to the platform they bought the game with.
For Pragma Engine, our Accounts and Party services enable cross-platform play. The Accounts service connects players into the platform via pragmaIds
linked to each player’s account, and the Matchmaking and Party services use pragmaIds
to group and match players together. Additionally, the Party service handles the compatible gameClientVersions
and gameServerVersions
when deciding what players can group together. Instead of having players match via a first-party account ID, they use Pragma which works for all console and game client platforms.
Parties That Can Scale #
It’s normal for platform scaling issues to occur when you’re trying to have each backend service handle more load. However, depending on how the backend was designed and built, there may be a limit to how much you can scale before you need to go back to the drawing board and start from scratch.
Sometimes vertical scaling works, but oftentimes you’ll hit the limit of what backend services are capable of that way. With a party service especially, it’s important to avoid having every party in-game rely on one another and the service as a whole, or else the service will become too busy and bog down the rest of the platform.
Instead of scaling backend services vertically, you should scale them horizontally. For a party service, this means enabling every initialized party with in-memory caching through deterministic routing.
The Party class is cached in memory to represent the party state at any given moment and to keep interactions with the class’s objects as fast as possible. We cache this memory rather than using database backing because the Party service utilizes deterministic routing for platform services, which guarantees the server has the correctly cached party for the service’s requests. Additionally, this cached data–a collection of all the Party objects managed by a server–is held in the Party service, and the service uses partyIds
to retrieve and send cached data for each party.
Pragma Engine’s routing layer allows for different routing methods for platform services, and services in Pragma Engine that have agnostic routing can not use in memory caching because they aren’t always guaranteed the exact data cached to the server.
We use in-memory caching for party data because the data doesn’t have to exist forever on the platform, since parties are only created when players group up for matchmaking. This means that the data is temporary and isn’t always persisted on the backend, which in turn means less expensive database calls.
Having all party data be temporary through in-memory caching also means that important data that needs to persist can exist outside of parties, in the case of a server error or shutdown. In the case of a node failure, Party data can be rebuilt easily from the player clients, and any lost party data won’t affect any persistent data loss in a player’s inventory or from the platform. Party services are then easily rebuildable and scalable because no major data losses can occur on the service side.
To learn more about Pragma Engine’s scalability, check out our load testing article on Load Testing a Backend for Launch.
Why Parties Matter #
Our goal at Pragma is to build backend features that fundamentally support and encourage online communities and online play. One of the main reasons why people play online games is to play with their friends and make some new ones along the way. That’s why a well-built, customizable, and scalable party service matters: how players play with one another can’t be overlooked or underestimated.
A well-built party service needs to be directly tied to matchmaking, player data, and accounts. It needs to communicate directly with the backend platform via notifications–Pragma Engine utilizes RPC calls–and avoid the use of any polling structure that hinders future scaling efforts. Potential load risks for parties should also be addressed by targeting specific users for updates to the shared platform state, rather than sending out broad notifications to all. And most importantly, they need to enable the developer to customize how the service works for their game’s unique features.
Every party service works differently in every kind of game, but the fundamentals of what makes a good party service always stays the same.
To learn more about how our Party service can support your game’s backend platform, check out our Party Concepts documentation.