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
Requirements for Components
  • A unique value within the @SerializedName(val name: String) annotation

    • Values must never change
    • Values must be unique across the project
    • We recommend using a UTC timestamp in EPOCH seconds
  • The @FieldNumber annotation must be a unique integer for each property

    • Use @ReservedFieldNumbers([retired-value-1],[retired-value-2]) when you remove properties from a component to ensure these values are not accidentally reused

Supported Data Types:
#

  • String

  • Numeric Types: Int, UInt, Byte, UByte, Short, UShort, Long, ULong, Boolean, Float, and Double

  • UUID

  • Map and MutableMap

    • Keys must be Int or String
  • List and MutableList

  • A class with properties using the above data types

  • All basic types and nested classes can be marked nullable.

    • Example: Int?, String?, UUID?, MyCustomObject?
    • Nullable markers are not supported with collection types.

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()
    }
}
Operation Requirements
  • The sessionTypes parameter controls which session types can invoke the operation

    • Supported values:

      • SessionType.PLAYER
      • SessionType.OPERATOR
      • SessionType.PARTNER
      • SessionType.SERVICE
  • The function’s name must be unique across all defined Operations

  • PlayerDataSubService classes must provide a constructor that accepts a PlayerDataContentLibrary

  • Each operation requires its own PlayerDataRequest and PlayerDataResponse object

    • The names of these objects must be unique
    • Each Operation requires its own object; they cannot be reused across multiple Operations

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 {};
}