Skip to content
UCIK Docs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Data with DynamoDB

Read and write structured game data in DynamoDB.

Finish AWS setup. For the player-data helper, create a table with string partition key PlayerId and string sort key RecordKey, then set DynamoDB → Player Data Table. Restrict access by authenticated identity on the service side.

Read a player record

Use your table name in the request. Profile Key Json uses DynamoDB’s typed JSON:

{"PlayerId":{"S":"player-demo"},"RecordKey":{"S":"Profile"}}
Read a player record
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: AWSIKDynamoDB, BetideCore, Engine.

#include "Engine/GameInstance.h"
#include "Generated/Items/Nodes/AWSIKDynamoDBItemsNodes02.h"

void ReadPlayerProfile(UGameInstance &GameInstance, const FString &ProfileKeyJson,
                       const FScriptDelegate &OnComplete)
{
    FAWSIKDynamoDBGetItemRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.Key.Json = ProfileKeyJson;
    Request.bSetConsistentRead = true;
    Request.ConsistentRead = true;
    auto *Action = UAWSIKDynamoDBGetItem::GetItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}

Bind OnComplete to a UFUNCTION taking const FAWSIKDynamoDBGetItemResult& and const FAWSIKDynamoDBError&. Check Error.IsError() first; display Error.Common.Message on failure or read Result.Item.Json on success.

On Success includes missing records. Check the returned item for your key before using its data; bSetItem does not indicate existence. Keep Consistent Read enabled for a strongly consistent table read. Raw requests use Table Name, not the Player Data Table setting.

The player-data helper stores JSON in Data and a numeric Version. These examples use the same layout. Build dynamic keys with a JSON serializer so quotes and backslashes are escaped correctly.

Write with a version check

These examples use player-demo / Profile in PlayerData. Replace those values with your own. Create stores {"nickname":"Rookie"} at version 1; save changes the nickname to Veteran and increments the version from your last read.

Create a profile once
Create a profile once
Preparing your graph…

Loading the interactive viewer.

Save a profile with its last version
Save a profile with its last version
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: AWSIKDynamoDB, BetideCore, Engine.

#include "Engine/GameInstance.h"
#include "AWSIKDynamoDB.h"
#include "Generated/Items/Nodes/AWSIKDynamoDBItemsNodes02.h"

FAWSIKDynamoDBAttributeValue DynamoAttribute(FString Json)
{
    FAWSIKDynamoDBAttributeValue Value;
    Value.Json = MoveTemp(Json);
    return Value;
}

FAWSIKDynamoDBPutItemRequest MakeDemoProfile(int64 Version, bool bCreate)
{
    FAWSIKDynamoDBPutItemRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.Item.Add(TEXT("PlayerId"), DynamoAttribute(TEXT(R"({"S":"player-demo"})")));
    Request.Item.Add(TEXT("RecordKey"), DynamoAttribute(TEXT(R"({"S":"Profile"})")));
    Request.Item.Add(TEXT("Data"),
                     DynamoAttribute(bCreate ? TEXT(R"({"S":"{\"nickname\":\"Rookie\"}"})")
                                             : TEXT(R"({"S":"{\"nickname\":\"Veteran\"}"})")));
    Request.Item.Add(TEXT("Version"),
                     DynamoAttribute(FString::Printf(TEXT("{\"N\":\"%lld\"}"), Version)));
    Request.bSetConditionExpression = true;
    Request.bSetExpressionAttributeNames = true;
    return Request;
}

void CreatePlayerProfile(UGameInstance &GameInstance, const FScriptDelegate &OnComplete)
{
    auto Request = MakeDemoProfile(1, true);
    Request.ConditionExpression = TEXT("attribute_not_exists(#player_id)");
    Request.ExpressionAttributeNames.Add(TEXT("#player_id"), TEXT("PlayerId"));
    auto *Action = UAWSIKDynamoDBPutItem::PutItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}

void SavePlayerProfile(UGameInstance &GameInstance, int64 ExpectedVersion,
                       const FScriptDelegate &OnComplete)
{
    if (ExpectedVersion <= 0 || ExpectedVersion == MAX_int64)
    {
        UE_LOG(LogAWSIKDynamoDB, Warning,
               TEXT("Expected Version must be positive and below the int64 maximum"));
        return;
    }
    auto Request = MakeDemoProfile(ExpectedVersion + 1, false);
    Request.ConditionExpression = TEXT("#version = :expected");
    Request.ExpressionAttributeNames.Add(TEXT("#version"), TEXT("Version"));
    Request.bSetExpressionAttributeValues = true;
    Request.ExpressionAttributeValues.Add(
        TEXT(":expected"),
        DynamoAttribute(FString::Printf(TEXT("{\"N\":\"%lld\"}"), ExpectedVersion)));
    auto *Action = UAWSIKDynamoDBPutItem::PutItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}

Bind OnComplete to a UFUNCTION taking const FAWSIKDynamoDBPutItemResult& and const FAWSIKDynamoDBError&. Check Error.IsError() and show Error.Common.Message on failure. On success, the saved version is 1 for create or ExpectedVersion + 1 for save; Put Item does not return the new item.

A condition failure means the record already exists, was deleted, or has a different version. Read again and resolve the conflict before retrying. Keep the expression fields enabled on the Make Request nodes; C++ sets their bSet... flags.

Put Item replaces the whole item, including removing attributes you omit. Use Update Item for partial changes. Put Item behavior.

Raw requests, stats, and leaderboards

GetPlayerDataProvider()->GetRecord and SetRecord offer the same record layout through C++ callbacks. For SetRecord, pass -1 to create an absent record or the version from your last read to replace one. These helpers use Player Data Table in settings.

The helper’s IncrementStat(PlayerId, StatName, Amount, Callback) uses a service-side update. A retry of a successful increment can count twice. Raw Amazon DynamoDB Update Item supports your own update expressions; Amazon DynamoDB Query reads key ranges. Follow pagination tokens for raw queries.

The separate leaderboard helper expects LeaderboardId and a sortable string ScoreKey in Leaderboard Table, plus PlayerId, numeric Score, and Metadata fields. Your backend owns score validation and sortable-key creation. GetLeaderboardProvider()->QueryLeaderboard(Id, Limit, Callback) reads 1–100 entries; it does not write or maintain a ranking automatically.

Update and query records

The increment adds one win to player-demo / stat:Wins, creating the stat if needed. The query loads that player’s stat: records one page at a time. Use a Boolean StatsQueryInProgress, default false, for the query graph.

Increment a player win
Increment a player win
Preparing your graph…

Loading the interactive viewer.

Load every stat page
Load every stat page
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: AWSIKDynamoDB, BetideCore, Engine.

Increment wins
#include "Engine/GameInstance.h"
#include "Generated/Items/Nodes/AWSIKDynamoDBItemsNodes02.h"

void RecordPlayerWin(UGameInstance &GameInstance, const FScriptDelegate &OnComplete)
{
    FAWSIKDynamoDBUpdateItemRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.Key.Json = TEXT(R"({"PlayerId":{"S":"player-demo"},"RecordKey":{"S":"stat:Wins"}})");
    Request.bSetUpdateExpression = true;
    Request.UpdateExpression =
        TEXT("ADD #value :one SET #version = if_not_exists(#version, :zero) + :one");
    Request.bSetReturnValues = true;
    Request.ReturnValues = EAWSIKDynamoDBReturnValue::ALL_NEW;
    Request.bSetExpressionAttributeNames = true;
    Request.ExpressionAttributeNames.Add(TEXT("#value"), TEXT("Value"));
    Request.ExpressionAttributeNames.Add(TEXT("#version"), TEXT("Version"));
    Request.bSetExpressionAttributeValues = true;
    FAWSIKDynamoDBAttributeValue One;
    One.Json = TEXT(R"({"N":"1"})");
    FAWSIKDynamoDBAttributeValue Zero;
    Zero.Json = TEXT(R"({"N":"0"})");
    Request.ExpressionAttributeValues.Add(TEXT(":one"), MoveTemp(One));
    Request.ExpressionAttributeValues.Add(TEXT(":zero"), MoveTemp(Zero));
    auto *Action = UAWSIKDynamoDBUpdateItem::UpdateItem(&GameInstance, Request);
    Action->OnSuccess.Add(OnComplete);
    Action->OnFailure.Add(OnComplete);
    Action->Activate();
}

Bind OnComplete to a UFUNCTION taking const FAWSIKDynamoDBUpdateItemResult& and const FAWSIKDynamoDBError&. Check Error.IsError() first, then read Result.Attributes.Json for the updated stat.

Read every stat page

Add these members to your Game Instance class, or use this class as the project’s Game Instance.

// DynamoStatsGameInstance.h
#pragma once

#include "Engine/GameInstance.h"
#include "Generated/Tables/Nodes/AWSIKDynamoDBTablesNodes02.h"
#include "DynamoStatsGameInstance.generated.h"

UCLASS()
class UDynamoStatsGameInstance : public UGameInstance
{
    GENERATED_BODY()
  public:
    UFUNCTION(BlueprintCallable)
    void LoadPlayerStats();

  private:
    bool bStatsQueryInProgress = false;
    void ReadStatsPage(const FString &StartKeyJson);
    UFUNCTION()
    void OnStatsPage(const FAWSIKDynamoDBQueryResult &Result, const FAWSIKDynamoDBError &Error);
};
// DynamoStatsGameInstance.cpp
#include "DynamoStatsGameInstance.h"
#include "AWSIKDynamoDB.h"

void UDynamoStatsGameInstance::LoadPlayerStats()
{
    if (bStatsQueryInProgress)
    {
        UE_LOG(LogAWSIKDynamoDB, Warning, TEXT("Stats query already running"));
        return;
    }
    bStatsQueryInProgress = true;
    ReadStatsPage(TEXT("{}"));
}

void UDynamoStatsGameInstance::ReadStatsPage(const FString &StartKeyJson)
{
    FAWSIKDynamoDBQueryRequest Request;
    Request.TableName = TEXT("PlayerData");
    Request.bSetLimit = true;
    Request.Limit = 25;
    Request.bSetExclusiveStartKey = true;
    Request.ExclusiveStartKey.Json = StartKeyJson;
    Request.bSetKeyConditionExpression = true;
    Request.KeyConditionExpression = TEXT("#player = :player AND begins_with(#record, :prefix)");
    Request.bSetExpressionAttributeNames = true;
    Request.ExpressionAttributeNames.Add(TEXT("#player"), TEXT("PlayerId"));
    Request.ExpressionAttributeNames.Add(TEXT("#record"), TEXT("RecordKey"));
    Request.bSetExpressionAttributeValues = true;
    FAWSIKDynamoDBAttributeValue Player;
    Player.Json = TEXT(R"({"S":"player-demo"})");
    FAWSIKDynamoDBAttributeValue Prefix;
    Prefix.Json = TEXT(R"({"S":"stat:"})");
    Request.ExpressionAttributeValues.Add(TEXT(":player"), MoveTemp(Player));
    Request.ExpressionAttributeValues.Add(TEXT(":prefix"), MoveTemp(Prefix));
    auto *Action = UAWSIKDynamoDBQuery::Query(this, Request);
    Action->OnSuccess.AddDynamic(this, &UDynamoStatsGameInstance::OnStatsPage);
    Action->OnFailure.AddDynamic(this, &UDynamoStatsGameInstance::OnStatsPage);
    Action->Activate();
}

void UDynamoStatsGameInstance::OnStatsPage(const FAWSIKDynamoDBQueryResult &Result,
                                           const FAWSIKDynamoDBError &Error)
{
    if (Error.IsError())
    {
        bStatsQueryInProgress = false;
        UE_LOG(LogAWSIKDynamoDB, Warning, TEXT("%s"), *Error.Common.Message);
        return;
    }
    UE_LOG(LogAWSIKDynamoDB, Log, TEXT("%s"), *Result.Items.Json);
    if (Result.LastEvaluatedKey.Json.Contains(TEXT("\"PlayerId\":"), ESearchCase::CaseSensitive))
    {
        ReadStatsPage(Result.LastEvaluatedKey.Json);
        return;
    }
    bStatsQueryInProgress = false;
    UE_LOG(LogAWSIKDynamoDB, Log, TEXT("All stat pages loaded"));
}

An increment is atomic, but repeating it can count a win twice. For rewards that must happen once, use a backend operation with duplicate detection. Atomic counter behavior.

The query checks for "PlayerId": in the plugin’s compact Last Evaluated Key Json; change that check if you rename the partition key. bSetLastEvaluatedKey is always set by the current converter, so it cannot tell you whether another page exists. Pass the entire returned key unchanged. An empty item page can still have a continuation key. Query pagination.

Queries are eventually consistent by default and pages are not a snapshot. Change the sample player ID in both examples and enforce access in your runtime role or backend.

Was this page helpful?