Creating a Basic Custom Service #

To demonstrate the basic steps to create a custom service, we’ll be building a simple Echo service that receives a payload containing a message and responds with that same message.

This tutorial uses Pragma Engine 0.4.0.

Build Pragma Engine #

You must have Pragma Engine and its associated protos A format for efficiently serializing structured data that's cross-platform and compatible with several languages. built.

Run the following commands in the terminal from the platform directory:

./pragma build engine -s
./pragma build

Define service calls with protobufs #

Before writing any business logic, we’ll define the structure of the service calls for our simple service. Because our Echo service only receives a message and echos it back, it only has one request and one response.

Define protos for an Echo service #

  1. Create a echoRpc.proto file at pragma-engine/platform/<PROJECT>/<PROJECT>-protos/src/main/proto/demo/echo/echoRpc.proto. You will need to create the directories for demo and echo.

  2. Add the following code into echoRpc.proto:

    syntax = "proto3";
    
    package demo.echo;
    option csharp_namespace = "Demo.Echo";
    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;
    }
    
Method overview

In this code block, we define two messages: EchoV1Request and EchoV1Response. This is a naming convention for service calls that we adhere to throughout Pragma Engine. The service call is named Echo and is followed by the version of the service call. In this case, we’re dealing with a service call sent by a client, so we have a Request and a corresponding Response.

The two option lines in each message define which entity the service call is for and the type of the message. For example, EchoV1Request has a pragma.pragma_session_type of PLAYER and a pragma.pragma_message_type of REQUEST. This is how Pragma Engine recognizes that this call is sent by a player client to the service.

Because we want our Echo service to receive and echo back a string, we expect the payload for both the request and the response to simply be a string. That’s exactly what we see in both the EchoV1Request and EchoV1Response messages.

Build protos #

We now need to compile the new proto before we author our business logic. Run the following command in the terminal from the platform directory:

./pragma build project-protos

Create the Echo service #

Now we’ll author the code that constitutes our custom Echo service.

  1. Create a EchoService.kt Kotlin file at pragma-engine/platform/<PROJECT>/<PROJECT>-lib/src/main/kotlin/demo/echo/EchoService.kt. You will need to create the directories for demo and echo.
You may have to mark the Kotlin directory as a sources root first.
  1. Add the following code to EchoService.kt. Import classes as necessary.

    @PragmaService(backendTypes = [BackendType.GAME])
    class EchoService(pragmaNode: PragmaNode, instanceId: UUID) :
        DistributedService(pragmaNode, instanceId),
        ConfigHandler<EchoServiceConfig> {
        private lateinit var config: EchoServiceConfig
    
        override suspend fun onConfigChanged(serviceConfig: EchoServiceConfig) {
            config = serviceConfig
        }
    
        @PragmaRPC(SessionType.PLAYER, RoutingMethod.SESSION_PRAGMA_ID)
        suspend fun echoV1(
            session: PlayerSession,
            request: EchoV1Request
        ): EchoV1Response {
            val message = request.message
            return EchoV1Response.newBuilder()
                .setResponseMessage("response: $message")
                .build()
        }
    }
    
Method overview

The @PragmaService annotation is how Pragma Engine recognizes that the code we’re authoring is a custom service.

Pragma Engine provides two backend types: game and social. We’re building this Echo service against the game backend, hence the backendTypes = [BackendType.GAME] line.

In our constructor, we pass in a PragmaNode and UUID. The PragmaNode gives our service access to all of Pragma Engine’s features, while the UUID allows us to uniquely identify each running instance of the Echo service.

ConfigHandler<EchoServiceConfig> allows us to add configuration to our service; we’ll be creating a config file in the next step.

The actual business logic is contained in echoV1 and is fairly simple. We pull the message from the request and respond with that message (with the text response: prepended to the message).

  1. Create a configuration file EchoServiceConfig.kt in the same directory as EchoService.kt.

  2. Add the following code to EchoServiceConfig.kt. Import classes as necessary.

    class EchoServiceConfig private constructor(type: BackendType) :
        ServiceConfig<EchoServiceConfig>(type) {
        override val description = "Configuration for the Echo Service"
    
        companion object : ConfigBackendModeFactory<EchoServiceConfig> {
    
            override fun getFor(type: BackendType): EchoServiceConfig {
                return EchoServiceConfig(type)
            }
        }
    }
    
Method overview
The description value contains human-readable text describing the purpose of this configuration file. Our Echo service does not contain much configuration due to its simplicity.

Build and run Pragma Engine #

Now that you’ve finished the Echo service, rebuild and run Pragma:

./pragma build engine -s

./pragma build

./pragma run

Once Pragma Engine successfully starts, you’ll see EchoService listed under the running services in your terminal:

Echo Service Running

Test the Echo service #

You can test the Echo service using Postman.

  1. Open Postman. If necessary, import the latest Postman collection and environment from the pragma-engine/platform/devenv/postman directory.

  2. Send PragmaDev ➨ Public ➨ GetInQueuev1 to enter the login queue.

  3. Log in as a player by sending Player - AuthenticateOrCreateV2.

  4. Navigate to PragmaDev ➨ Game ➨ RPC - Player ➨ Telemetry. Duplicate the PlayerEventV1 service call and name it EchoV1.

    If you duplicate the PlayerEventV1 service call outside the Telemetry folder, additional setup is required to make the EchoV1 service call work:

    1. In the new EchoV1 service call, click the Auth tab and change the type to Bearer Token. In the Token field, insert {{test01PragmaPlayerGameToken}}.

    2. In the Headers tab, uncheck the Accept key. Create a new Accept key and insert the value application/json.

  5. Open the EchoV1 call and replace its body with the following JSON:

    {
        "requestId": 1,
        "type": "EchoRpc.EchoV1Request",
        "payload": {
            "message": "Hello there!"
        }
    }
    
  6. Click the send button. You should receive a response that looks like the following:

    {
        "sequenceNumber": 0,
        "response": {
            "requestId": 1,
            "type": "EchoRpc.EchoV1Response",
            "payload": {
                "responseMessage": "response: Hello there!"
            }
        }
    }
    

Appendix #

You can view the completed source code files for this section here.

PROJECT-protos/src/main/proto/demo/echo/echoRpc.proto
syntax = "proto3";

package demo.echo;
option csharp_namespace = "Demo.Echo";
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;
}
PROJECT-lib/src/main/kotlin/demo/echo/EchoService.kt
package demo.echo

import demo.echo.EchoRpc.EchoV1Request
import demo.echo.EchoRpc.EchoV1Response
import java.util.UUID
import pragma.PlayerSession
import pragma.PragmaNode
import pragma.config.ConfigHandler
import pragma.rpcs.PragmaRPC
import pragma.rpcs.RoutingMethod
import pragma.rpcs.SessionType
import pragma.services.DistributedService
import pragma.services.PragmaService
import pragma.settings.BackendType

@Suppress("UNUSED_PARAMETER", "RedundantSuspendModifier")
@PragmaService(backendTypes = [BackendType.GAME])
class EchoService(pragmaNode: PragmaNode, instanceId: UUID) :
    DistributedService(pragmaNode, instanceId),
    ConfigHandler<EchoServiceConfig> {
    private lateinit var config: EchoServiceConfig

    override suspend fun onConfigChanged(serviceConfig: EchoServiceConfig) {
        config = serviceConfig
    }

    @PragmaRPC(SessionType.PLAYER, RoutingMethod.SESSION_PRAGMA_ID)
    suspend fun echoV1(
        session: PlayerSession,
        request: EchoV1Request
    ): EchoV1Response {
        val message = request.message
        return EchoV1Response.newBuilder()
            .setResponseMessage("response: $message")
            .build()
    }
}
PROJECT-lib/src/main/kotlin/demo/echo/EchoServiceConfig.kt
package demo.echo

import pragma.config.ConfigBackendModeFactory
import pragma.config.ServiceConfig
import pragma.settings.BackendType

class EchoServiceConfig private constructor(type: BackendType) :
    ServiceConfig<EchoServiceConfig>(type) {
    override val description = "Configuration for the Echo Service"

    companion object : ConfigBackendModeFactory<EchoServiceConfig> {

        override fun getFor(type: BackendType): EchoServiceConfig {
            return EchoServiceConfig(type)
        }
    }
}