Top level header for Creating a Custom Service.

This article was written on Pragma Engine version 0.0.94.

Creating a Custom Service #

In the second article of this series, we’ll demonstrate how to create a custom service by going over how to construct RPC protobufs, generate RPCs for Unity and Unreal, and build custom service logic in Kotlin for a custom Echo service, which simply echoes back strings to players.

For more information, check out the Creating a Basic Custom Service tutorial to learn more about implementing a custom Echo service.

Construct RPC protobufs #

The protobuf IDL is used throughout Pragma Engine to define how data is saved to the database and the data template of RPCs. When making a custom service, you’ll use protos for defining your custom service’s RPCs. To define your RPCs, you’ll need to create proto messages for the request and response ends of the calls.


A flowchart showcasing how to create RPCs for a custom service in protos.

How to construct RPC protos in Pragma Engine.

For an introduction on how we use protos in Pragma Engine, check out the Protocol Buffers section in the Content Data series.

All protos are created and modified in 5-ext/ext-protos/src/main/proto. This directory contains proto definitions for content and other types of data. If you’d like to separate your custom services protos from your Pragma Engine-provided content data protos, make sure you organize and create new directories to structure your different types of content.


The folder location for proto definitions in Pragma Engine.

Before we commence building the protos… #

First you’ll want to get your engine built and your 5-ext directory created. Remember that 5-ext is where you define all custom content.

Build the engine by running the following command in the terminal from the platform directory:

make skip-tests protos engine

Now run the next command to create a 5-ext folder (if you don’t have one already):

make ext

Proto Creation #

Defining the prerequisites #

You’ll need to use the first line of the file to define the proto’s syntax version, which will always be proto3 in Pragma Engine.

syntax = "proto3";

Then, define the necessary packages, options, and imports for your protos file.

  • package specifies instructions on where to generate the JSON packaged code from your proto definitions. You’ll use a package’s destination to import files and code logic when defining your services and plugins in Kotlin.
Pragma-Provided Content PackageCustom Content Package
package pragma.inventory.ext;package techblog.demoservice;
  • option is an individual declaration that Pragma Engine uses to specify how protos generate files in different languages for the PragmaSDK. Since the PragmaSDK uses Unreal and Unity, you’ll need to make two different option declarations for those SDKs’ respective languages.
UnityUnreal
option csharp_namespace = "Techblog.Demoservice";option (pragma.unreal_namespace) = "Techblog";
  • import is used to reference any other protos that your definitions depend on (such as if you section off the way you organize and write your protos definitions). You’re always going to need to add an import declaration for pragmaOptions.proto, but it depends on your protos and file structure if you’ll need to add any additional imports.
Required import for every protoAdditional import example
import "pragmaOptions.proto";import "pragma/types.proto";

Writing proto logic for custom services #

Once you’ve defined all your proto’s prerequisites, we can get started on defining the custom service’s RPC templates!

Below your packages, options, and imports, create two separate messages: one for the RPC’s request, and the other for the RPC’s response. Name these messages with version indicators, so any errors with the calls can be handled in a backwards-compatible and efficient way. Naming them without a version indicator will also break conformance tests.

Below is an example of what that message might look like as a request:

message EchoV1Request {}

Inside each message’s curly brackets, add the following: an option to specify the call’s session type, an option to specify whether the call is a response or request, and the data-types included in the specific RPC’s code logic.

The option to specify the call’s session type can be defined using four separate session categories. Below is an example of how to implement the session type option with PLAYER as the chosen session type. You’ll also see a table below detailing how each of the four session categories operates in Pragma Engine’s RPC infrastructure.

    option (pragma.pragma_session_type) = PLAYER;
Session typeDescription
PLAYERSDK or game client related, and involves RPCs directly initiated by players. This type of session is trusted as little as possible, and involves acquiring inventory data, activating a login scenario, exchanging items, and other player initiated calls.
PARTNERInvolves game server systems and trusted third party RPCs, which are handy for granting items to a player’s inventory, provider entitlements, match end processes, and anything involving a third party system’s integration. One of the most trusted session types.
OPERATORAdministrative tasks such as bans, account management, customer support, inventory deletion, game title and shard creations, and more.

Many Operator calls use this session type (such as updateItemsOperatorV1 for updating items).
SERVICECompletely internal, this session type is for communicative calls with service instances in Pragma Engine, such as queries for inventory and content data, data cache, authentication steps, and other instance data. It is frequently used by plugins.

Make sure you know exactly what information you’re trying to send and receive when making a service RPC, as you’ll need to find and solve errors.

Once the session type has been defined, create another option definition for the RPC’s message type, which will either be a request or a response. The code block below demonstrates how to implement the message type option with REQUEST as the chosen session type.

    option (pragma.pragma_message_type) = REQUEST;
Don’t forget to always make two separate RPC messages for the call’s REQUEST and RESPONSE operations.

After both of the message’s options are implemented, define the data fields attached to your RPC message, if any. Sometimes these data types are strings or integers, while other times they can return specific custom content already authored in your protos template.

All of the actions above are demonstrated in the code block below using a custom Echo service.

syntax = "proto3";

package techblog.echo;
option csharp_namespace = "Techblog.Echoservice";
option (pragma.unreal_namespace) = "Echo";

import "pragmaOptions.proto";

message EchoV1Request {
    option (pragma.pragma_session_type) = PLAYER;
    option (pragma.pragma_message_type) = REQUEST;

    string message = 1;
}

message EchoV1Response {
    option (pragma.pragma_session_type) = PLAYER;
    option (pragma.pragma_message_type) = RESPONSE;

    string response_message = 1;
}

Making our proto definitions in 5-ext #

Whenever you define new or edit existing content in protos (such as RPC messages), you’ll need to do the following make command using platform as the working directory.

make ext-protos

By running the make command above, our protos are built into 5-ext so they can be accessed by the rest of Pragma Engine.

Generate the RPCs for Unity and Unreal #

When you create RPC protobufs for a custom service, you’ll also need to generate those protos for your PragmaSDK. These RPC definitions will work just fine for internal Pragma Engine processes, but integrating a custom service fully with your SDK requires the RPCs be generated with the relevant engine language.

All you’ll need to do to generate the protos for your SDK is run the same update-pragma-sdk.sh scripts configured for your game project. This will automatically create new files in data transfer object (DTO) folders, which represent new custom RPCs in the relevant engine language.

The DTO folders are located in the directories below for Unity and Unreal.

  • Unity: PragmaSDK/src/dto
  • Unreal: PragmaSDK/Source/PragmaSDK/Public/Dto

A flowchart of the generated DTO folders in the PragmaSDK.

Locations of DTO folders after running update-pragma-sdk.sh.

Once the RPCs are defined in Protos and generated for your SDK, we can get started on building the custom service itself in Kotlin!

Build the custom service in Kotlin #

What your custom service does–and all the service’s code logic–resides in a 5-ext directory and in authored Kotlin files. A custom service can be completely independent or fully integrated with other Pragma Engine systems; you’ll define just how dependent or independent your custom service is using this Kotlin file.


A flowchart showcasing how to create a custom service in Kotlin.

The workflow for creating a custom service in Kotlin.

The next sections in this article will go over all the pieces that need to be defined in Kotlin to create a custom service:

  • The directory structure that houses the Kotlin file for the custom service
  • The prerequisites required for the service to run
  • The custom service’s class which holds the business logic
  • The functions and logic that defines externally accessible RPC endpoints using protobufs

Directory #

All custom services, alongside any plugins in Pragma Engine, must be created as a Kotlin file in the 5-ext/ext/src/main/kotlin/ directory.


A graphic showcasing a custom service’s location in Kotlin.

For example, if we were making a custom service called TechBlogEchoCustomService, we’d create the custom service’s Kotlin file in 5-ext/ext/src/main/kotlin/techblog/echo/ with a file name of EchoService.kt.

Prerequisites #

Before defining the class for your custom service, you’ll need to define the file’s necessary prerequisites.

These prerequisite lines of code will be the custom service’s package–the collection or directory of Kotlin files grouped in with your service–and any required file imports that your custom service’s code logic will use. Usually these imports will be from other kotlin or java packages.


A graphic showcasing a custom service’s prerequisite files.

Once all the file’s prerequisites are met, we can start structuring and defining our custom service’s logic.

All custom services must have the following two annotations at the beginning of the service:

  • @Suppress prevents erroneous IDE error messages that may occur when the file is called to run.
  • @PragmaService defines what backend type the custom service is used for–GAME and/or SOCIAL–and any dependencies that should be injected into the file at the start and middle of the service’s runtime.

The @Suppress annotation is written by including any incorrect IDE errors, or errors about variance conflict in the annotations parameters (usually pertaining to a language feature). For example, "UNUSED_PARAMETER" and "RedundantSuspendModifier" are fairly common errors that will need to be ignored using @Suppress.

When defining the @PragmaService, there are two parameters available for defining the backend types and dependencies you want your service to support:

  • backendTypes defines whether the custom service is part of Pragma’s GAME or SOCIAL features.
  • dependencies is used to register any required classes, services, and other dependencies required. Usually these are specific NodeServices authored by the user.

Below is an example of @Suppress and @PragmaService in action before a defined custom service class; the custom service uses BackendType.GAME and requires a custom TechBlogDaoNodeService class as defined in the parameters of @PragmaService.

@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
@PragmaService(
    backendTypes = [BackendType.GAME],
    dependencies = [TechBlogDaoNodeService::class]
)

Now that we have the custom service’s prerequisites written, we can define the service’s class and functions.

Class #

To start building your custom service’s logic, define the custom service’s class with a pragmaNode and an instanceId parameter. Then, classify the custom service from two existing service classes:

  • DistributedServices are used for scalable systems and can be enabled and disabled across many different Pragma Nodes and instances. Most GAME and SOCIAL services fall under DistributedServices. These include examples such as the InventoryService, AccountService, MatchmakingService, PartyService, GameDataService, and many, many more.
  • AlwaysStartedNodeServices are common across all nodes in the engine and aren’t restricted to one specific instance. Two services that operate like this are LoggingNodeService and MetricsNodeService, as they access all sorts of nodes and data throughout the engine.

A graphic demonstrating the two different existing service classes to choose from.

Choosing the two service classes for a custom service.

By default, you’ll usually extend your custom service from the DistributedService due to its practicality and common use cases. However, on rare occasions, the AlwaysStartedNodeService is the more practical and necessary solution–such as if you’re running services and data on a single node use and require using one instance of the same resource.

It’s important to note that DistributedService uses pragmaNode and instanceId as its parameters, while AlwaysStartedNodeService only uses pragmaNode due to its focus on running data on and from a single node.

Below is an example of a custom service class defined with an extension from the DistributedService class. Remember that before this class definition we’ll have the @Suppress() and @PragmaService() annotations with their respective parameters and definitions.

class TechBlogEchoCustomService(pragmaNode: PragmaNode, instanceId: UUID):
    DistributedService(pragmaNode, instanceId)
{}

Functions and Logic #

Once you’ve defined the custom service’s class, we’ll write all the service’s functions within it. These functions will be defined as externally accessible RPC endpoints using the protobufs in the previous section. Once these RPCs are defined in our custom service’s class, they’ll be externally accessible by other services and your PragmaSDK.


A graphic demonstrating how to write the functions and logic for a custom service.

When defining your custom service’s RPC endpoints, you must start with the @PragmaRPC annotation. Then, inside @PragmaRPC, you’ll define two important properties for the RPC: sessionType, which uses the same 4 session types used when defining our RPCs in protos, and routingMethod.

routingMethod defines the rules for how RPC requests are routed to the desired service instance. There are seven different routing methods available, though by default, we recommend using SESSION_PRAGMA_ID for all RPCs for player-facing services.

Routing MethodDefinition
RANDOMAcquires any service instance or session database; randomly chosen and distributed.

Example: simulateRewardsV2 uses this routing method to choose a random reward for players from a select database.
REQUEST_FIELDDetermines the service instance to use based on a user-defined protobuf field. This annotation requires another property called protoRoutingFieldName, which is the name of the specific field used in the RPC protos to determine which service instance to route to.

Example: MatchEndV4 uses this routing method to request player_Ids to acquire MatchEnd data.
SINGLETONAlways routes to a single, specific service instance every time.

Example: enterMatchmakingV2 uses this routing method, as everyone needs to get into the same matchmaking queue when starting matchmaking.
DIRECTRoutes directly to any service instance, usually used when knowing specific session instances on the same service.

This routing method is very uncommon.
GAME_SESSION_PARAMUtilizes game session attributes for routing.

Example: many RPCs in the Party service use GAME_SESSION_PARAM to route players in separate game sessions into the same server.
SOCIAL_SESSION_PARAMUtilizes social session attributes for routing.

Example: the Party service uses this routing method to look into players’ separate social sessions and route them into the same server.
SESSION_PRAGMA_IDSimilar to REQUEST_FIELD, as it routes using a player’s PragmaId. It’s useful for keeping a player in the same pragma session when using a particular call or service.

Example: the Inventory service frequently uses this routing method to keep players in the same pragma session when dealing with their inventory, items, and store purchases.

Once you’ve fully fleshed out @PragmaRPC with sessionType and routingMethod, use a suspend fun() with the exact name used from your protobuf RPC and exact proto message fields for the function’s parameters. Then define the rest of the RPCs logic for both the request or response call.

To see a custom service’s RPC logic defined using the instructions above, check out the code block below for a custom Echo service. It defines an RPC that uses the PLAYER sessionType, SESSION_PRAGMA_ID as the routingMethod, as well as the suspend fun() detailing the call’s unique logic.

    @PragmaRPC(
        SessionType.PLAYER,
        RoutingMethod.SESSION_PRAGMA_ID
    )

    suspend fun echoV1(
        session: PlayerSession,
        request: Echoservice.EchoV1Request
    ): Echoservice.EchoV1Response {
        val message = request.message
        return Echoservice.EchoV1Response.newBuilder().setResponseMessage("response:$message").build()
        }

Make ext with the custom plugin #

Once you’ve fully built out the logic of your custom service, you’ll need to run the following make command to get your 5-ext directory created with the custom service in place.

make ext

And that’s how you fully build out the logic of your custom service! All that’s left is to define the corresponding structure on the client-side.

Before we move on, there’s one thing that you should always consider when creating your own services in Pragma Engine.

Writing custom services with scaling in mind #

Distributed services are varied and diverse, and can exist as a single instance, as multiple instances on a single server, or as multiple instances on multiple servers. Because of this, you might initially think of your custom service existing as something singular, but in reality it may need to be updated for future and more complex service behavior. In other words, when making your own custom distributed service, make sure you take into account your intended scaling approach.

This topic greatly relies on how you intend to use your Pragma Engine and custom services, and can be very advanced or unnecessary for simpler custom services. To learn more about some good common practices when writing your own services, features, and data, check out our Concurrency page for more information.

Now that a custom service is built on the server side–in protos and Kotlin–we can create the necessary client-side structures to interact with our custom service and RPCs via the PragmaSDK.

Create the corresponding structure on the client-side #

Before we implement a custom service on the client-side, let’s explain how provided services in Pragma Engine operate in the SDK. We’ll be using code snippet examples from a MyCustomTechBlogService to demonstrate what an integrated custom service looks like in the PragmaSDK. Whenever you create your own custom service, we recommend creating the methods and logic for your custom services and its RPCs similarly to Pragma’s provided ones.


A graphic showcasing the two class files involved for a custom service in the PragmaSDK.

The two class files for a custom service in the PragmaSDK.

In the PragmaSDK, all provided Pragma Engine services come with two important class files: [ServiceName]Service and [ServiceName]ServiceRaw.

  • [ServiceName]Service is the rich service layer that creates a domain boundary between your game code and the APIs. This class houses most of the service’s business logic for your SDK. It includes functionality like caching states and automatic delta applications.
  • [ServiceName]ServiceRaw houses raw, direct API calls to Pragma Engine for provided Pragma Engine services.

When creating the methods and logic that will interact with your custom service and its RPCs on the client-side, it’s best practice to separate API and DTO definitions from your game’s application logic, similarly to how we have a Service and a ServiceRaw class for provided services. This way, when applying new SDK updates, any errors caused by new changes won’t cause cascading breakages across your game’s code.

To separate your API method definitions from your game’s application logic, create and house all your custom service’s API and DTO logic in a Raw service class in your dto directory. If your custom services require, we recommend creating a separate [ServiceName]Service file to house the rest of your service’s business logic in the SDK.

If you’re using Unreal Engine, then all your Raw service files are generated automatically! They can be found in the PragmaSDK/Source/PragmaSDK/Public/Dto directory alongside the RPC DTOs. If you’re using Unity, you’ll need to generate or author the Raw file manually.

Below is an example of a newly created MyCustomTechBlogServiceRaw.cs file written in C# for Unity. It includes a single RPC called MyCustomRpc and a single notification called MyCustomNotification.

public class MyCustomTechBlogServiceRaw : Service
{
    // RPC Calls.
    
    public virtual void MyCustomRpc(MyCustomRpcV1Request request, Protocol.OnComplete<MyCustomRpcV1Response> callback)
    {
        Connection.Game.SendMessage(request, callback);
    }


    // Alternative format if you prefer using futures.

    public RpcFuture<MyCustomRpcV1Response> MyCustomRpc(MyCustomRpcV1Request request)
    {
        return Connection.Game.SendMessage<MyCustomRpcV1Request, MyCustomRpcV1Response>(request);
    }

    // Notifications.

    public event Protocol.OnNotification<MyCustomNotification> OnMyCustomNotification;

    protected override void OnConnectionInitialized()
    {
        Connection.Game.AddNotificationHandler<MyCustomNotification>((notification, metadata) =>
            OnMyCustomNotification?.Invoke(notification, metadata));
    }
}

Registering a custom service #

A custom service’s [ServiceName]Service and [ServiceName]ServiceRaw classes must be registered in the SDK using the RegisterService() method. You can use this method with the client’s Runtime or register it with the player or server session objects. We highly recommend using Runtime to register your custom service, since whenever you create a player or server, it’s through the runtime.

For custom services, define the registration method for both the [ServiceName]Service and [ServiceName]ServiceRaw classes to attach the service properly.

The line of code below demonstrates a registered MyCustomTechBlogServiceRaw class once through Runtime and once not through Runtime. Remember that if you don’t register your service through Runtime, you’ll need to register the two custom service classes with the player or server object.

// registering through Runtime

Runtime.Get().RegisterService<MyCustomTechBlogServiceRaw>();

// registering not through Runtime

player.RegisterService<MyCustomTechBlogServiceRaw>();

Retrieving a custom service #

Using the GetService() method, a registered service can be retrieved anywhere from the service’s session when the [ServiceName]Service class needs to communicate with the SDK. You can also use the GetService() method to get a registered service of any type, such as Pragma-provided ones like the AccountService and InventoryService.

Below is an example of a myCustomTechBlogService calling the MyCustomTechBlogServiceRaw file using the GetService() method:

var myCustomTechBlogService = session.GetService<MyCustomTechBlogServiceRaw>();
Any time you update your custom service’s RPCs, you’ll also need to add and update the methods in your [ServiceName]Service and [ServiceName]ServiceRaw class in the SDK.

Utilizing the custom service #

Once you’ve registered your custom service, you can retrieve it, make RPC calls, and bind it to notification events however you’d like in your game code.

And that’s how you fully define your custom service in protos, Kotlin, and your PragmaSDK. There are plenty of additional options available to you when designing your custom service, which we’ll get into in the next article by enabling database storage for custom services.


For more information, check out the rest of the articles in this series:

Part I: Introduction to Custom Services
Part II: Creating a Custom Service (this article)
Part III: Using Database Storage for Custom Services
Part IV: Integrating Custom Services with Provided Services



Posted by Patrick Olszewski on May 11, 2023