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
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
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
Loading the interactive viewer.
Read achievement definitions
Loading the interactive viewer.
Set partial progress
Loading the interactive viewer.
Read win stats
Loading the interactive viewer.
Write stats from a server
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 ProgressionExamplesUse 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.