Getting Started in Player Data #
This guide will get you started on how to use Pragma Engine’s Player Data service to create a key-value store for your game client.
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 sdk.
Setup #
Run the following command in a terminal from the platform
directory to start with a clean build of your project.
./pragma build project -s
Create the Kotlin packages: add a [game-studio].keyvaluestore
package under [project-name]/[project-name]-player-data/player-data/src/main/kotlin
Define the persisted data structure #
Add a new file called KeyValueData.kt
at [project-name]/[project-name]-player-data/player-data/src/main/kotlin/[game-studio]/keyvaluestore/KeyValueData.kt
@SerializedName("[timestamp-epoch]")
data class PersistedData(
@FieldNumber(1) var value: String,
): Component
Author the API #
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 {
context.snapshot.deleteEntity(
context.snapshot.getOrCreateUniqueEntity(request.key)
)
return DeleteDataResponse()
}
}
Build the project and regenerate the sdk.
./pragma build project -s
Generate the API for the Game Client #
Unreal #
From you Unreal project Plugins folder, run the Pragma SDK update script to incorporate the new APIs into your game project.
update-pragma-sdk.sh
Below is an example of calling the AddOrUpdateData
operation through the Pragma SDK and then accessing the data from the client side cache.
// some 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 saved the data for the player to the db ====="));
if (PragmaResult.IsFailure()) {
return; // check failure type and handle
}
const FString StringDataFromCache = GetValueFromPlayerDataCache(Player, "LoadOut").Value;
// ready to turn this data back into the LoadOut struct
}));
}
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 {};
}