Create a client Key Value Store #
This guide will introduce you to the basics of the player data service.
A key-value store is useful for early development to allow rapid iteration by the design team; however, it is strongly encouraged that more formal data models and APIs are developed for production. Key-value patterns scale poorly and make content management in production extremely difficult.
By the end of this guide, you’ll have:
- An endpoint for the client to store, update, and delete persisted data.
- Access the persisted data through the client side cache. All retrieval and syncing is handled automatically by the Pragma SDK.
Setup #
Start with a clean build of your project.
./pragma build -s
Define the persisted data structure #
Add a new file at:
[project]/[project]-player-data/player-data/src/main/kotlin/[game-studio]/keyvalue/KeyValue.kt
Add the definition below:
package documentation
import pragma.playerdata.Component
import pragma.playerdata.FieldNumber
import pragma.types.SerializedName
@SerializedName("[timestamp-epoch]")
data class PersistedData(
@FieldNumber(1) var value: String,
): Component
Author the APIs #
To create an API for the game client, we need to author a PlayerDataOperation.
Add the following to KeyValue.kt
package documentation
import pragma.playerdata.Context
import pragma.playerdata.PlayerDataContentLibrary
import pragma.playerdata.PlayerDataOperation
import pragma.playerdata.PlayerDataRequest
import pragma.playerdata.PlayerDataResponse
import pragma.playerdata.PlayerDataSubService
import pragma.rpcs.SessionType
data class AddOrUpdate(
val key: String,
val value: String
): PlayerDataRequest
class AddOrUpdateResponse: PlayerDataResponse
data class DeleteData(
val key: String,
): PlayerDataRequest
class DeleteDataResponse: PlayerDataResponse
// Pragma Engine constructs this class through reflection
// so we suppress the unused warning.
@Suppress("unused")
class KeyValueStore(
contentLibrary: PlayerDataContentLibrary
): PlayerDataSubService(contentLibrary) {
@PlayerDataOperation(sessionTypes = [SessionType.PLAYER])
fun addOrUpdate(request: AddOrUpdate, context: Context): AddOrUpdateResponse {
val entity = context.snapshot.getOrCreateUniqueEntity(request.key) {
listOf(PersistedData(""))
}
val component = entity.getComponentsByType<PersistedData>().single()
component.value = request.value
return AddOrUpdateResponse()
}
@PlayerDataOperation(sessionTypes = [SessionType.PLAYER])
fun delete(request: DeleteData, context: Context): DeleteDataResponse {
val entity = context.snapshot.getOrCreateUniqueEntity(request.key)
context.snapshot.deleteEntity(entity.instancedId)
return DeleteDataResponse()
}
}
Build the project to run player data code generation and run tests.
./pragma build
Adding and accessing data from the client #
Unreal #
Below is an example of calling the KeyValueStore.AddOrUpdateData operation through the Pragma SDK and then accessing the data from the client side cache.
This Unreal example codes extends from the Unreal SDK Setup tutorial.
From you Unreal project, run the Pragma SDK update script to incorporate the new APIs.
update-pragma-sdk.sh
Header file: Source\Unicorn\UnicornPlayerController.h
#pragma once
#include "CoreMinimal.h"
#include "Dto/CodeGen_PlayerData_Documentation.h"
#include "GameFramework/PlayerController.h"
#include "Services/PragmaPlayerDataCache.h"
#include "PragmaPtr.h"
#include "UnicornPlayerController.generated.h"
PRAGMA_FWD(FPlayer);
// example data to add to key-value store
USTRUCT()
struct FLoadOutData {
GENERATED_BODY()
UPROPERTY()
FString SelectedHero;
};
UCLASS()
class UNICORN_API AUnicornPlayerController : public APlayerController
{
GENERATED_BODY()
public:
// ...
UFUNCTION(Exec)
void AddOrUpdateData(const FLoadOutData& LoadOutData);
UFUNCTION(Exec)
FPragma_PlayerData_PersistedDataProto
GetValueFromPlayerDataCache(const FString &EntityName);
// ...
};
Source\Unicorn\UnicornPlayerController.cpp
void AUnicornPlayerController::AddOrUpdateData(
const FLoadOutData &LoadOutData
) {
FString JsonString;
FJsonObjectConverter::UStructToJsonObjectString<FLoadOutData>(
LoadOutData,
JsonString
);
Player->PlayerDataService().KeyValueStore().AddOrUpdate(
"LoadOut", JsonString,
FOnKeyValueStoreAddOrUpdateDelegate::CreateLambda(
[this](TPragmaResult<FPragma_PlayerData_AddOrUpdateResponseProto>
PragmaResult) {
if (PragmaResult.IsFailure()) {
return; // check failure type and handle.
}
// The client cache has been updated
const FString StringDataFromCache =
GetValueFromPlayerDataCache("LoadOut").Value;
}));
}
// example show structure of cache
FPragma_PlayerData_PersistedDataProto
AUnicornPlayerController::GetValueFromPlayerDataCache(
const FString &EntityName) {
if (!Player->PlayerDataService().GetCache().IsValid()) {
return {};
}
const TSharedPtr<FPlayerDataCache> PlayerDataCache =
Player->PlayerDataService().GetCache().Pin();
const TWeakPtr<FPragmaEntity> EntitySaved =
PlayerDataCache->GetUniqueEntityByName(EntityName);
if (!EntitySaved.IsValid()) {
return {};
}
const TSharedPtr<FPragmaEntity> Entity = EntitySaved.Pin();
const TMap<FString, FPragmaComponent> &Components = Entity->Components;
for (const auto &Component : Components) {
if (Component.Value.Is<FPragma_PlayerData_PersistedDataProto>()) {
return Component.Value
.ReadValue<FPragma_PlayerData_PersistedDataProto>();
}
}
return {};
}