Creating a Custom Service #
Let’s create a simple Echo service that receives a message and responds with that same message.
This tutorial was authored on Pragma Engine 0.6.0.
Unreal example project #
This tutorial is based on an example Unreal project using the Third Person Unreal C++ project template. The example project is named ‘Unicorn’, update references accordingly.
This tutorial was built and tested using Unreal 5.3.X, see the SDK overview for the full sdk compatibility chart.
This guide builds on the example project started in the Unreal SDK setup guide, which can be referenced regarding assumptions about project setup.
Relative paths #
Relative paths start from the pragma project directory or game engine project directory, respectively.
unicorn-lib\...
->~\[pragma_repo_root_dir]\pragma-engine\platform\unicorn\unicorn-lib\...
Plugins\PragmaSDK
->~\[game_repo_root_dir]\Unicorn\Plugins\PragmaSDK
Overview #
Steps for authoring the Echo service:
- Define the Echo service
- Define the echoV1 RPC
- Build and run Pragma with our new service
- Test the echoV1 endpoint via the load-simulator
- Perform the SDK Generation step to generate bindings for the new service and RPC.
- Invoke the echoV1 RPC via from the game client via the SDK.
1. Define the Echo service #
- Create
EchoService.kt
atunicorn-lib\src\main\kotlin\unicorn\echo\EchoService.kt
- Add the following:
EchoService.kt
package unicorn.echo
import java.util.UUID
import pragma.PragmaNode
import pragma.services.DistributedService
import pragma.services.PragmaService
import pragma.settings.BackendType
@Suppress("UNUSED_PARAMETER") // suppress warning caused by reflection based invocation of Pragma RPCs
@PragmaService(backendTypes = [BackendType.GAME])
class EchoService(pragmaNode: PragmaNode, instanceId: UUID) : DistributedService(pragmaNode, instanceId) {
}
The engine identifies the @PragmaService
annotation in order to initialize and register it. Services can be registered as GAME
or SOCIAL
types, which will dictate which Pragma nodes the service will run on.
2. Define the echoV1 RPC #
Define request and response protos #
- Create
echoRpc.proto
atunicorn-protos\src\main\proto\unicorn\echo\echoRpc.proto
- Add the following:
syntax = "proto3";
package unicorn.echo;
option csharp_namespace = "Unicorn.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;
}
Build protos #
We’ll run the protobuf generation to make the request and response objects available to our service code.
- Run in git-bash from the
platform
directory:./pragma build project-protos
Define the RPC Handler #
- Add the echoV1 method to handle the RPC request in the service:
EchoService.kt
package unicorn.echo
// ...
import pragma.PlayerSession
import pragma.rpcs.PragmaRPC
import pragma.rpcs.RoutingMethod
import pragma.rpcs.SessionType
import unicorn.echo.EchoRpc.EchoV1Request
import unicorn.echo.EchoRpc.EchoV1Response
@PragmaService(/**/)
class EchoService(/**/) {
@PragmaRPC(SessionType.PLAYER, RoutingMethod.SESSION_PRAGMA_ID)
suspend fun echoV1(session: PlayerSession, request: EchoV1Request): EchoV1Response {
val response = EchoV1Response.newBuilder()
response.setResponseMessage(request.message)
return response.build()
}
}
Run the conformance tests #
To confirm your proto and RPC handler are correct, run the conformance tests for a quick feedback loop.
- Open
ConformanceTest.kt
located atunicorn-lib\src\test\kotlin\conformance\ConformanceTest.kt
- Run the tests via the green play button in the editor next to
class ConformanceTest
- The example code above is correct, but if you’re developing your own APIs, the error messages from the conformance tests will help identify any mismatches / missing conventions.
Run Pragma #
- Start Pragma with the Intellij
run-pragma
run configuration available in the drop-down in the upper right of the IDE.
Once Pragma Engine successfully starts, you’ll see EchoService
listed under the running services in the Game node
startup output:
Test the Echo service #
Let’s test our service via the load simulator. The load simulator is a powerful tool for validating, testing, and exercising workflows and features within Pragma.
Open LoadSimulatorMain.kt
at unicorn-load-simulator\src\main\kotlin\LoadSimulatorMain.kt
We’ll add a call to our new RPC from within the example step provided within the file.
LoadSimulatorMain.kt
// ...
import unicorn.echo.EchoRpc.EchoV1Request
import unicorn.echo.EchoRpc.EchoV1Response
class ExampleStep(/**/) : Step(/**/) {
override suspend fun runStep(/**/) {
// ... existing example code
val echoRequest = EchoV1Request.newBuilder().setMessage("hello world!").build()
val echoResponse = leader.game.sendOrThrow(echoRequest, EchoV1Response::class, tracker)
println("EchoV1 - ${leader.displayName.displayName}: ${echoResponse.responseMessage}")
}
}
- Start the load simulator via the
run-load-simulator
Intellij run configuration (located in a dropdown in the top right) - Observe the output of the echoV1 response streaming during the simulation run:
EchoV1 - LoadSimulatorPlayer1: hello world!
- Success!
Call echoV1 from the game client #
Now that we know our EchoV1 RPC is working, let’s call it from the game client.
Switch to a new git-bash terminal in your game code. If using Rider, you can launch this terminal window within the IDE.
# new git-bash terminal
cd ~/Unreal/Unicorn
Run update-pragma-sdk.sh
in git bash from the game engine plugins directory
cd Plugins
# this will run the sdk generation step and then copy the updated plugin sources to the plugin dir
./update-pragma-sdk.sh
- Now that our new EchoV1 RPC is available in the sdk, let’s add an exec function to call it!
Source\Unicorn\UnicornPlayerController.h
// ...
class UNICORN_API AUnicornPlayerController : public APlayerController
{
// ...
public:
UFUNCTION(Exec)
void Echo(const FString& Message);
// ...
}
Source\Unicorn\UnicornPlayerController.cpp
// ...
#include "Dto/PragmaEchoRpcDto.h"
#include "Dto/UnicornEchoServiceRaw.h"
// ...
void AUnicornPlayerController::Echo(const FString& Message)
{
const FPragma_Echo_EchoV1Request request = FPragma_Echo_EchoV1Request {
{ Message }
};
Player->Api<UUnicornEchoServiceRaw>().EchoV1(request, UUnicornEchoServiceRaw::FEchoV1Delegate::CreateWeakLambda(
this, [this](const TPragmaResult<FPragma_Echo_EchoV1Response>& EchoResult, const FPragmaMessageMetadata&)
{
if (EchoResult.IsSuccessful())
{
UE_LOG(LogTemp
, Display
, TEXT("Pragma -- EchoV1 response: %s")
, *EchoResult.Payload<FPragma_Echo_EchoV1Response>().ResponseMessage);
} else
{
UE_LOG(LogTemp, Display, TEXT("Pragma -- EchoV1 something went wrong: %s"), *EchoResult.GetErrorAsString());
}
})
);
}
Run the game #
- Ensure Pragma is running via
./pragma run
or the Intellijrun-pragma
run configuration. - Run the game
- Call the
Echo
exec function- Open the console with backtick in the running game window and type
Echo hello world!
- Open the console with backtick in the running game window and type
- Confirm the echo response in the log
Pragma -- EchoV1 response: hello world!
- Success!