Create a Key Value Store #
This guide will get you started on how to use Pragma Engine’s 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-name]/[project-name]-player-data/player-data/src/main/kotlin/[game-studio]/keyvaluestore/KeyValueData.kt
Add the definition below:
package [game-studio].keyvaluestore
@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 KeyValueData.kt
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
Using the Key Value Store #
Unreal #
From you Unreal project, run the Pragma SDK update script to incorporate the new APIs.
update-pragma-sdk.sh
Below is an example of calling the KeyValueStore.AddOrUpdateData
operation through the Pragma SDK and then accessing the data from the client side cache.
// data you would like to persist
USTRUCT()
struct FLoadOutData {
GENERATED_BODY()
UPROPERTY()
FString SelectedHero;
};
FPragma_PlayerData_PersistedDataProto GetValueFromPlayerDataCache(Pragma::FPlayerPtr Player, const FString& EntityName);
TMap<FString, FPragmaComponent> GetComponents(Pragma::FPlayerPtr Player, const FString& EntityName);
void AddOrUpdateData(Pragma::FPlayerPtr Player, const FLoadOutData& LoadOutData) {
FString JsonString;
FJsonObjectConverter::UStructToJsonObjectString<FLoadOutData>(LoadOutData, JsonString);
// add or updade an Entity named "LoadOut" that will contain the LoadOut struct as a string
Player->PlayerDataService().KeyValueStore().AddOrUpdate(
"LoadOut", JsonString,
FOnKeyValueStoreAddOrUpdateDelegate::CreateLambda(
[Player, this](
TPragmaResult<FPragma_PlayerData_AddOrUpdateResponseProto> PragmaResult) {
UE_LOG(LogTemp, Display, TEXT("===== success - we called the player data operation ====="));
if (PragmaResult.IsFailure()) {
return; // check failure type and handle. Note in a failure case, data is not persisted
}
// Note - the cache is updated before calling the delegate.
const FString StringDataFromCache = GetValueFromPlayerDataCache(Player, "LoadOut").Value;
// ready to turn this data back into the FLoadOutData struct and use it.
}));
}
TMap<FString, FPragmaComponent> GetComponents(Pragma::FPlayerPtr Player, const FString& EntityName) {
if (!Player->PlayerDataService().GetCache().IsValid()) {
return {};
}
const TSharedPtr<FPlayerDataCache> PlayerDataCache = Player->PlayerDataService().GetCache().Pin();
if (const TWeakPtr<FPragmaEntity> EntitySaved = PlayerDataCache->GetUniqueEntityByName(EntityName); EntitySaved.IsValid())
{
return EntitySaved.Pin()->Components;
}
return {};
}
FPragma_PlayerData_PersistedDataProto GetValueFromPlayerDataCache(Pragma::FPlayerPtr Player, const FString& EntityName
) {
TMap<FString, FPragmaComponent> Components = GetComponents(Player, EntityName);
for (TTuple<FString, FPragmaComponent> Component : Components) {
if (Component.Value.Is<FPragma_PlayerData_PersistedDataProto>()) {
return Component.Value.ReadValue<FPragma_PlayerData_PersistedDataProto>();
}
}
return {};
}