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

Track achievements and stats

Record player progress and display unlocked achievements.

Create matching achievement and stat IDs in the providers you enable under Progression. Sign in before reading or writing progress.

Unlock an achievement

Unlock an achievement
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: UCIKCore, BetideCore.

#include "Engine/GameInstance.h"
#include "Progression/UCIKProgressionSubsystem.h"
#include "UCIKCore.h"

void UnlockFirstWin(UGameInstance &GameInstance)
{
    auto *Service = GameInstance.GetSubsystem<UUCIKProgressionSubsystem>();
    if (!Service)
        return;

    Service->UnlockAchievement(
        TEXT("FIRST_WIN"),
        FBetideCallback<TMap<FBetideProviderId, bool>>::CreateWeakLambda(
            &GameInstance, [](const TBetideResult<TMap<FBetideProviderId, bool>> &Result) {
                if (!Result.bOk)
                {
                    UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                    return;
                }
                for (const auto &Pair : Result.GetValue())
                {
                    UE_LOG(LogUCIK, Log, TEXT("%s: %s"), *Pair.Key.ToString(),
                           Pair.Value ? TEXT("updated") : TEXT("not updated"));
                }
            }));
}

Write a stat

Write a stat
Preparing your graph…

Loading the interactive viewer.

Game module dependencies: UCIKCore, BetideCore.

#include "Engine/GameInstance.h"
#include "Progression/UCIKProgressionSubsystem.h"
#include "UCIKCore.h"

void RecordWin(UGameInstance &GameInstance)
{
    auto *Service = GameInstance.GetSubsystem<UUCIKProgressionSubsystem>();
    if (!Service)
        return;

    TMap<FString, FUCIK_StatValue> Stats;
    Stats.Add(TEXT("Wins"), FUCIK_StatValue::FromInteger(1));
    Service->WriteStats(
        Stats, FBetideCallback<TMap<FBetideProviderId, bool>>::CreateWeakLambda(
                   &GameInstance, [](const TBetideResult<TMap<FBetideProviderId, bool>> &Result) {
                       if (!Result.bOk)
                       {
                           UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                           return;
                       }
                       for (const auto &Pair : Result.GetValue())
                       {
                           UE_LOG(LogUCIK, Log, TEXT("%s: %s"), *Pair.Key.ToString(),
                                  Pair.Value ? TEXT("updated") : TEXT("not updated"));
                       }
                   }));
}

For an EOS SUM stat, 1 adds one win. Steam stats set a stored value. With an aggregating authority first in the provider policy, UCIK reads back the stored total and sends that to mirrors. Do not retry a SUM write blindly: it can count twice.

Read progress and definitions

Read achievement progress
Read achievement progress
Preparing your graph…

Loading the interactive viewer.

Read achievement definitions
Read achievement definitions
Preparing your graph…

Loading the interactive viewer.

Set partial progress
Set partial progress
Preparing your graph…

Loading the interactive viewer.

Read win stats
Read win stats
Preparing your graph…

Loading the interactive viewer.

Write stats from a server
Write stats from a server
Preparing your graph…

Loading the interactive viewer.

Call Callback value
QueryAchievements({}, Callback) TArray<FUCIK_Achievement>
QueryAchievementDefinitions({}, Callback) TArray<FUCIK_Achievement>
SetAchievementProgress(Id, Current, Max, Callback) TMap<FBetideProviderId, bool>
ReadStats(Player, Names, Callback) TArray<FUCIK_UserStats>
WriteStatsForPlayer(Player, Stats, Callback) bool
UnlockAchievementForPlayer(Player, Id, Callback) bool
OnAchievementUnlocked / OnAchievementUnlockedNative event: FUCIK_PlayerHandle, achievement id
#include "Engine/GameInstance.h"
#include "Engine/World.h"
#include "Progression/UCIKProgressionSubsystem.h"
#include "UCIKCore.h"

namespace ProgressionExamples
{
UUCIKProgressionSubsystem *Service(UGameInstance &Game)
{
    auto *Progression = Game.GetSubsystem<UUCIKProgressionSubsystem>();
    if (!Progression)
        UE_LOG(LogUCIK, Warning, TEXT("Progression subsystem is unavailable"));
    return Progression;
}

void ReadAchievementRows(UGameInstance &Game, bool bDefinitions)
{
    auto *Progression = Service(Game);
    if (!Progression)
        return;
    auto Callback = FBetideCallback<TArray<FUCIK_Achievement>>::CreateWeakLambda(
        &Game, [bDefinitions](const TBetideResult<TArray<FUCIK_Achievement>> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                return;
            }
            for (const FUCIK_Achievement &Achievement : Result.GetValue())
            {
                if (bDefinitions)
                    UE_LOG(LogUCIK, Log, TEXT("%s: %s"), *Achievement.DisplayName,
                           *Achievement.Description);
                else
                    UE_LOG(LogUCIK, Log, TEXT("%s: %f"), *Achievement.DisplayName,
                           Achievement.Progress);
            }
        });
    if (bDefinitions)
        Progression->QueryAchievementDefinitions({}, MoveTemp(Callback));
    else
        Progression->QueryAchievements({}, MoveTemp(Callback));
}

void RefreshAchievements(UGameInstance &Game)
{
    ReadAchievementRows(Game, false);
}
void ReadAchievementDefinitions(UGameInstance &Game)
{
    ReadAchievementRows(Game, true);
}

void UpdateWinProgress(UGameInstance &Game, int32 Wins)
{
    auto *Progression = Service(Game);
    if (!Progression)
        return;
    Progression->SetAchievementProgress(
        TEXT("WIN_TEN_MATCHES"), Wins, 10,
        FBetideCallback<TMap<FBetideProviderId, bool>>::CreateWeakLambda(
            &Game, [](const TBetideResult<TMap<FBetideProviderId, bool>> &Result) {
                if (!Result.bOk)
                {
                    UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                    return;
                }
                for (const auto &Pair : Result.GetValue())
                    UE_LOG(LogUCIK, Log, TEXT("%s: %s"), *Pair.Key.ToString(),
                           Pair.Value ? TEXT("updated") : TEXT("not updated"));
            }));
}

void ReadWins(UGameInstance &Game, const FUCIK_PlayerHandle &Player)
{
    auto *Progression = Service(Game);
    if (!Progression)
        return;
    Progression->ReadStats(
        Player, {TEXT("Wins")},
        FBetideCallback<TArray<FUCIK_UserStats>>::CreateWeakLambda(
            &Game, [](const TBetideResult<TArray<FUCIK_UserStats>> &Result) {
                if (!Result.bOk)
                {
                    UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                    return;
                }
                for (const FUCIK_UserStats &Row : Result.GetValue())
                {
                    if (const FUCIK_StatValue *Wins = Row.Stats.Find(TEXT("Wins")))
                        UE_LOG(LogUCIK, Log, TEXT("Wins: %lld"), Wins->IntegerValue);
                    else
                        UE_LOG(LogUCIK, Warning, TEXT("Wins was not returned by this provider"));
                }
            }));
}

void RecordServerWin(UGameInstance &Game, const FUCIK_PlayerHandle &Player)
{
    if (!Game.GetWorld() || Game.GetWorld()->GetNetMode() != NM_DedicatedServer)
    {
        UE_LOG(LogUCIK, Warning, TEXT("Run this on your trusted dedicated server"));
        return;
    }
    auto *Progression = Service(Game);
    if (!Progression)
        return;
    TMap<FString, FUCIK_StatValue> Stats;
    Stats.Add(TEXT("Wins"), FUCIK_StatValue::FromInteger(1));
    Progression->WriteStatsForPlayer(
        Player, Stats,
        FBetideCallback<bool>::CreateWeakLambda(&Game, [](const TBetideResult<bool> &Result) {
            if (!Result.bOk)
            {
                UE_LOG(LogUCIK, Warning, TEXT("%s"), *Result.Error.GetLogString());
                return;
            }
            UE_LOG(LogUCIK, Log, TEXT("Server stat write completed"));
        }));
}
} // namespace ProgressionExamples

Use an integer Wins stat and your configured WIN_TEN_MATCHES achievement ID. An empty Provider combines matching achievement IDs into one row with the highest progress; Provider Progress keeps each store’s value. Match definitions and progress by achievement ID.

Progress and partial results

EOS derives partial achievement progress from stats; direct Set Crossplay Achievement Progress is not supported there. Its stat adapter uses 32-bit integers, rounding floating-point input.

Server stat writes require a trusted server and a provider with that capability. Client-side success is not proof of an authoritative game result.

On Achievement Unlocked on the progression subsystem fires whenever a provider confirms an unlock: the kit’s own Unlock Crossplay Achievement, a server unlock, or an unlock the service applied itself from stats or progress. It carries the player’s handle on that provider and the achievement id, so a crossplay unlock that reaches two stores fires once per store.

Write Crossplay Stats (Server) and Unlock Crossplay Achievement (Server) take the target’s player handle. On EOS they use the server client policy. On Steam they use the game-server API when the process is a logged-on Steam game server that authenticated the player, and otherwise the publisher Web API with the key read from the SIK_PUBLISHER_WEB_API_KEY environment variable (or the per-developer editor ini). Without either route the Steam provider does not declare the server capabilities and the nodes fail with a not-supported error naming what is missing.

Write results list responding providers; failed providers can be absent. Check the returned provider IDs against the stores you expected. Overall success does not promise every store was updated. If an authority write or readback fails, mirrors can receive the original samples instead of a stored total. Re-read progress before claiming stores are synchronized.

Was this page helpful?