Creating a Custom Service #

To create the skeleton of a basic custom service, there are three key steps:

  1. Making the protos and generating them for Unreal/Unity
  2. Building the service on the server-side
  3. Building the corresponding structure on the client-side

Make and generate the protos #

Protos define the shape of every call we’ll be making with the custom service, which means we’ll need to build out the structure of our requests and responses. Once that’s done, we generate the protos for use in Unreal or Unity.

Proto creation #

The proto file should be created in the 5-ext/ext-protos/src/main/proto/ directory.

This file will need to contain:

  • A package line with the desired Kotlin namespace that the generated protos are available under.
  • Namespace lines used when generating protos for the SDK
    • Unity3D = csharp_namespace
    • Unreal = pragma.unreal_namespace
  • Request and response messages with versioned names so breaking changes can be handled in a backwards-compatible way.
  • An option that specifies the message type (REQUEST or RESPONSE).
  • An option that specifies the session type.
session typedefinitionexamples
PLAYERSDK or game client requestsgetting or modifying player data
PARTNERgame server or third-party requestsgranting items, match end processes
OPERATORadministration tasksbans, account management, customer support
SERVICEcommunications between Pragma Engine service instancesquerying for a player data feature, like a quest
After adding these new proto messages, you must run make protos on the command line so Pragma Engine can access them.
MyCustom example
syntax = "proto3";

package myproject.mycustom;

import "pragma/types.proto";
import "pragmaOptions.proto";

option csharp_namespace = "MyCustom";
option (pragma.unreal_namespace) = "MyCustom";

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

    pragma.Fixed128 custom_id = 1;
    string custom_data = 2;
}

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

    string action_response = 1;
}

Generate for Unity/Unreal #

Once the protos have been created, they need to be generated for the SDKs. When setting up Unreal/Unity, you configured the update-pragma-sdk.sh scripts for your game project. To generate the protos, run this script.

Once the SDK types are generated from the protos, new files will appear in the dto folders that represent the protos in the relevant engine language.

  • Unity: PragmaSDK/src/dto

  • Unreal: PragmaSDK/Source/PragmaSDK/Public/Dto

Build the service on the server-side #

Now that we have proto structure in place, the custom service itself can be created as a Kotlin file in the 5-ext/ext/src/main/kotlin/ directory, within folders that reflect the namespace.

MyCustom example path
For the MyCustom project, the full directory path would look like this: 5-ext/ext/src/main/kotlin/myproject/mycustom/. The service file would be called MyCustomService.kt.

Service file structure:

  • @Suppress and @PragmaService annotations and the class definition
  • @PragmaRPC annotation and suspend function

Custom services must have the @PragmaService annotation which includes the backend types the service supports (GAME and/or SOCIAL), and any dependencies that should be injected. It can also specify the dependencies to be injected on instantiation of the service.

Due to the way services are called within Kotlin code, incorrect IDE errors will need to be suppressed using the @Suppress annotation.

There are several options for extending the custom service from existing service classes:

servicedefinitionexamples
DistributedServicefor scalable systems, can be enabled/disabled across different Pragma NodesPlayer Data, Accounts
AlwaysStartedNodeServiceservices common across all nodesLoggingNodeService, MetricsNodeService
The default service should be extended from DistributedService. Only use the node service in the rare occasion it’s necessary, such as when all running services on a single node use one instance of the same resource.
MyCustom example

In this example, we’re setting the MyCustom service to support the GAME backend, and we’re using @Suppress to prevent erroneous IDE error messages.

We also inject a MyCustomDaoNodeService for a database, and the class extends from the existing DistributedService.

@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
@PragmaService(
    backendTypes = [BackendType.GAME],
    dependencies = [MyCustomDaoNodeService::class]
)
class MyCustomService(pragmaNode: PragmaNode, instanceId: UUID):
    DistributedService(pragmaNode, instanceId)
{

The service functions can be configured as externally accessible RPC endpoints, but must follow these rules:

  • have the @PragmaRPC annotation
  • be a suspend function
  • match names of established request/response proto messages

The @PragmaRPC annotation has a few properties you can set. The first is the sessionType, which follows the same pragma_session_type as defined in the protos (PLAYER, PARTNER, OPERATOR, SERVICE).

The second is the routingMethod, which can define different rules for how RPC requests are routed to which service instances. By default, we recommend using SESSION_PRAGMA_ID for player-facing services.

routing methoddefinition
RANDOMany service instance, randomly distributed
REQUEST_FIELDprotobuf field used to determine service instance to hit
SINGLETONalways route to a single service instance
DIRECTroute to a specified service instance
GAME_SESSION_PARAMuses game session attribute to route
SOCIAL_SESSION_PARAMuses social session attribute to route
SESSION_PRAGMA_IDuses a player’s PragmaId to route
If you specify REQUEST_FIELD, the annotation requires another property, protoRoutingFieldName, which is the name of the field in the proto used to determine which service instance to route to.
MyCustom example

For the MyCustom service, we define the @PragmaRPC session type to match the protos (PLAYER), and set the routing method to RANDOM. The suspend function matches our proto message names and defines the request and response.

@PragmaRPC(
        sessionType = SessionType.PLAYER,
        routingMethod = RoutingMethod.RANDOM,
    )
    suspend fun myCustomActionV1(
        session: PlayerSession,
        request: MyCustomRpc.MyCustomActionV1Request
    ): MyCustomRpc.MyCustomActionV1Response {

Scaling and coroutine-safe design #

A distributed service may exist as a single instance, as multiple instances on a single server, or as multiple instances on multiple servers. Because of this, RPC endpoints and service behavior must take into account the intended scaling approach. This is an advanced topic and might not be needed for a simple service. Visit the Concurrency page for more information.

Build the corresponding structure on the client-side #

A raw service file will need to be created to interface directly with RPC requests. A best practice is to separate the API method definitions and generated DTOs from the game logic and structs. This isolates Pragma SDK changes from your game logic, making it easier to handle SDK updates, especially in the case where you want to support two versions of an API at once but have them operate the same in your game code. The [ServiceName]ServiceRaw and [ServiceName]Service classes mentioned earlier are an example of this structure.

Note: The raw service file is generated automatically and registered with the appropriate session in Unreal. These can be found in the PragmaSDK/Source/PragmaSDK/Public/Dto folder alongside the RPC DTOs.

The custom service must be registered with the Player or Server objects (depending on which session it is available for) which can be done with the RegisterApi() method. We recommend creating a registration method on your custom service to attach to the service properly.

The custom service can then be retrieved from the session when the [ServiceName]Service class needs to communicate with the SDK by using the Api() method. As RPC endpoints are added to the service, methods will need to be added to [ServiceName]Service and [ServiceName]ServiceRaw in the SDK.

MyCustom example

In the MyCustomPragmaSDK directory under the SDK Assets or Content folder, create a new raw service file named MyCustomServiceRaw. The MyCustomService class handles responses and notifications.

public static void AttachToSession(Player session)
{
    var myCustomServiceRaw = new MyCustomServiceRaw();
    session.RegisterApi(myCustomServiceRaw);

    session.RegisterApi(new MyCustomService(myCustomServiceRaw));
}

Api() function call:

var myCustomService = session.Api<MyCustomService>();