Show me an example of how to keep statistics of an object in c++ unreal
**TL;DR:** Add a reusable `UStatsComponent` to any Actor. It tracks counters/timers, replicates in multiplayer, and can persist via `USaveGame`. Use `Increment*`, `Add*`, and `ExportToJson()` to read/write. Hook into gameplay events.
### Plan (pseudocode)
– Create `FTrackedStat` (name, value, last updated time).
– Create `FObjectStats` (maps for Int/Float, uptime seconds).
– Make `UStatsComponent`:
– `UPROPERTY(ReplicatedUsing=OnRep_Stats)` for replication.
– Ticks uptime (server-only).
– Public API: `IncrementInt`, `AddFloat`, `GetInt`, `GetFloat`, `ExportToJson`, `ImportFromJson`, `ResetAll`.
– Delegates: `OnStatChanged`.
– Save/Load with a `UStatsSaveGame` slot.
– Multiplayer:
– Server mutates; clients request via `Server*` RPCs (optional example).
– `OnRep_Stats` fires delegate on clients.
– Example usage in a pawn/actor (`AMyHero`).
– Edge cases:
– Null owner/world, missing slot, first-time save.
– Clamping, negative inputs.
– Time tracked only while component is active.
“`cpp
// File: Source/YourGame/Public/Stats/StatsComponent.h
#pragma once
#include “CoreMinimal.h”
#include “Components/ActorComponent.h”
#include “StatsComponent.generated.h”
// Why: Notify Blueprints/C++ when a stat changes (UI updates, achievements).
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnStatChanged, FName, StatName, float, NewValue, float, Delta);
USTRUCT(BlueprintType)
struct FTrackedStat
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=”Stats”)
float Value = 0.f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=”Stats”)
FDateTime LastUpdatedUtc = FDateTime(0);
};
USTRUCT(BlueprintType)
struct FObjectStats
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=”Stats”)
TMap IntStats;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=”Stats”)
TMap FloatStats;
/** Accumulated alive/active seconds (server-auth). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=”Stats”)
float UptimeSeconds = 0.f;
};
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class YOURGAME_API UStatsComponent : public UActorComponent
{
GENERATED_BODY()
public:
UStatsComponent();
// —– Delegates —–
UPROPERTY(BlueprintAssignable, Category=”Stats”)
FOnStatChanged OnStatChanged;
// —– Query API —–
UFUNCTION(BlueprintCallable, Category=”Stats”)
int32 GetInt(FName Name, bool& bFound) const;
UFUNCTION(BlueprintCallable, Category=”Stats”)
float GetFloat(FName Name, bool& bFound) const;
UFUNCTION(BlueprintPure, Category=”Stats”)
float GetUptimeSeconds() const { return ReplicatedStats.UptimeSeconds; }
// —– Mutation API (Authority or via RPC) —–
UFUNCTION(BlueprintCallable, Category=”Stats”)
void IncrementInt(FName Name, int32 Amount = 1);
UFUNCTION(BlueprintCallable, Category=”Stats”)
void AddFloat(FName Name, float Amount);
UFUNCTION(BlueprintCallable, Category=”Stats”)
void SetFloat(FName Name, float NewValue);
UFUNCTION(BlueprintCallable, Category=”Stats”)
void ResetAll();
// —– Serialization helpers —–
UFUNCTION(BlueprintCallable, Category=”Stats”)
FString ExportToJson(bool bPretty = true) const;
UFUNCTION(BlueprintCallable, Category=”Stats”)
bool ImportFromJson(const FString& Json);
// —– Persistence (SaveGame) —–
UFUNCTION(BlueprintCallable, Category=”Stats|Save”)
bool SaveToSlot(const FString& SlotName, int32 UserIndex = 0);
UFUNCTION(BlueprintCallable, Category=”Stats|Save”)
bool LoadFromSlot(const FString& SlotName, int32 UserIndex = 0);
// —– Networking —–
virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override;
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
/** Server is source of truth for time & mutations. */
UPROPERTY(ReplicatedUsing=OnRep_Stats, VisibleAnywhere, Category=”Stats”)
FObjectStats ReplicatedStats;
UFUNCTION()
void OnRep_Stats();
// Why: buffer stat changes while applying to broadcast once.
void BroadcastChange(const FName Name, float NewValue, float Delta);
// Server-only mutators
void IncrementInt_Server(const FName Name, const int32 Amount);
void AddFloat_Server(const FName Name, const float Amount);
void SetFloat_Server(const FName Name, const float NewValue);
// RPCs for clients to request mutations in multiplayer.
UFUNCTION(Server, Reliable)
void Server_IncrementInt(FName Name, int32 Amount);
UFUNCTION(Server, Reliable)
void Server_AddFloat(FName Name, float Amount);
UFUNCTION(Server, Reliable)
void Server_SetFloat(FName Name, float NewValue);
// Helpers
static void Touch(FTrackedStat& Stat);
static float GetCurrentValue(const TMap& Map, const FName& Name, bool& bFound);
};
// File: Source/YourGame/Private/Stats/StatsComponent.cpp
#include “StatsComponent.h”
#include “Net/UnrealNetwork.h”
#include “Serialization/JsonWriter.h”
#include “Serialization/JsonReader.h”
#include “Serialization/JsonSerializer.h”
#include “Kismet/GameplayStatics.h”
#include “StatsSaveGame.h”
UStatsComponent::UStatsComponent()
{
PrimaryComponentTick.bCanEverTick = true;
SetIsReplicatedByDefault(true);
}
void UStatsComponent::BeginPlay()
{
Super::BeginPlay();
}
void UStatsComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}
void UStatsComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
// Server tracks uptime only.
if (GetOwner() && GetOwner()->HasAuthority())
{
ReplicatedStats.UptimeSeconds += FMath::Max(0.f, DeltaTime);
}
}
void UStatsComponent::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(UStatsComponent, ReplicatedStats);
}
void UStatsComponent::OnRep_Stats()
{
// Clients could refresh UI here; fine-grained diffs omitted for simplicity.
}
void UStatsComponent::Touch(FTrackedStat& Stat)
{
Stat.LastUpdatedUtc = FDateTime::UtcNow();
}
float UStatsComponent::GetCurrentValue(const TMap& Map, const FName& Name, bool& bFound)
{
if (const FTrackedStat* Found = Map.Find(Name))
{
bFound = true;
return Found->Value;
}
bFound = false;
return 0.f;
}
int32 UStatsComponent::GetInt(FName Name, bool& bFound) const
{
return static_cast(GetCurrentValue(ReplicatedStats.IntStats, Name, bFound));
}
float UStatsComponent::GetFloat(FName Name, bool& bFound) const
{
return GetCurrentValue(ReplicatedStats.FloatStats, Name, bFound);
}
void UStatsComponent::BroadcastChange(const FName Name, float NewValue, float Delta)
{
OnStatChanged.Broadcast(Name, NewValue, Delta);
}
void UStatsComponent::IncrementInt(FName Name, int32 Amount)
{
if (!GetOwner()) return;
if (GetOwner()->HasAuthority())
{
IncrementInt_Server(Name, Amount);
}
else
{
Server_IncrementInt(Name, Amount);
}
}
void UStatsComponent::AddFloat(FName Name, float Amount)
{
if (!GetOwner()) return;
if (GetOwner()->HasAuthority())
{
AddFloat_Server(Name, Amount);
}
else
{
Server_AddFloat(Name, Amount);
}
}
void UStatsComponent::SetFloat(FName Name, float NewValue)
{
if (!GetOwner()) return;
if (GetOwner()->HasAuthority())
{
SetFloat_Server(Name, NewValue);
}
else
{
Server_SetFloat(Name, NewValue);
}
}
void UStatsComponent::IncrementInt_Server(const FName Name, const int32 Amount)
{
FTrackedStat& Stat = ReplicatedStats.IntStats.FindOrAdd(Name);
const float Before = Stat.Value;
Stat.Value = FMath::Clamp(Stat.Value + Amount, -2147483648.f, 2147483647.f);
Touch(Stat);
BroadcastChange(Name, Stat.Value, Stat.Value – Before);
}
void UStatsComponent::AddFloat_Server(const FName Name, const float Amount)
{
FTrackedStat& Stat = ReplicatedStats.FloatStats.FindOrAdd(Name);
const float Before = Stat.Value;
Stat.Value += Amount;
Touch(Stat);
BroadcastChange(Name, Stat.Value, Stat.Value – Before);
}
void UStatsComponent::SetFloat_Server(const FName Name, const float NewValue)
{
FTrackedStat& Stat = ReplicatedStats.FloatStats.FindOrAdd(Name);
const float Before = Stat.Value;
Stat.Value = NewValue;
Touch(Stat);
BroadcastChange(Name, Stat.Value, Stat.Value – Before);
}
void UStatsComponent::Server_IncrementInt_Implementation(FName Name, int32 Amount)
{
IncrementInt_Server(Name, Amount);
}
void UStatsComponent::Server_AddFloat_Implementation(FName Name, float Amount)
{
AddFloat_Server(Name, Amount);
}
void UStatsComponent::Server_SetFloat_Implementation(FName Name, float NewValue)
{
SetFloat_Server(Name, NewValue);
}
void UStatsComponent::ResetAll()
{
if (!GetOwner() || !GetOwner()->HasAuthority()) return;
ReplicatedStats.IntStats.Reset();
ReplicatedStats.FloatStats.Reset();
ReplicatedStats.UptimeSeconds = 0.f;
OnStatChanged.Broadcast(“ResetAll”, 0.f, 0.f);
}
FString UStatsComponent::ExportToJson(bool bPretty) const
{
TSharedRef Root = MakeShared();
Root->SetNumberField(TEXT(“uptimeSeconds”), ReplicatedStats.UptimeSeconds);
// Ints
{
TSharedRef Ints = MakeShared();
for (const auto& Kv : ReplicatedStats.IntStats)
{
Ints->SetNumberField(Kv.Key.ToString(), Kv.Value.Value);
}
Root->SetObjectField(TEXT(“ints”), Ints);
}
// Floats
{
TSharedRef Floats = MakeShared();
for (const auto& Kv : ReplicatedStats.FloatStats)
{
Floats->SetNumberField(Kv.Key.ToString(), Kv.Value.Value);
}
Root->SetObjectField(TEXT(“floats”), Floats);
}
FString Out;
if (bPretty)
{
TSharedRef<TJsonWriter> Writer = TJsonWriterFactory::Create(&Out);
FJsonSerializer::Serialize(Root, Writer);
}
else
{
TSharedRef<TJsonWriter<UTF8CHAR, TCondensedJsonPrintPolicy>> Writer =
TJsonWriterFactory<UTF8CHAR, TCondensedJsonPrintPolicy>::Create(&Out);
FJsonSerializer::Serialize(Root, Writer);
}
return Out;
}
bool UStatsComponent::ImportFromJson(const FString& Json)
{
TSharedPtr Root;
const TSharedRef<TJsonReader> Reader = TJsonReaderFactory::Create(Json);
if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid()) return false;
if (GetOwner() && !GetOwner()->HasAuthority()) return false; // Why: prevent clients desyncing server.
ReplicatedStats.UptimeSeconds = Root->GetNumberField(TEXT(“uptimeSeconds”));
auto LoadMap = [&](const FString& Field, TMap& Target)
{
Target.Reset();
const TSharedPtr* ObjPtr;
if (Root->TryGetObjectField(Field, ObjPtr) && ObjPtr && ObjPtr->IsValid())
{
for (const auto& Pair : (*ObjPtr)->Values)
{
FTrackedStat S;
S.Value = static_cast(Pair.Value->AsNumber());
Touch(S);
Target.Add(FName(Pair.Key), S);
}
}
};
LoadMap(TEXT(“ints”), ReplicatedStats.IntStats);
LoadMap(TEXT(“floats”), ReplicatedStats.FloatStats);
OnStatChanged.Broadcast(“ImportFromJson”, 0.f, 0.f);
return true;
}
bool UStatsComponent::SaveToSlot(const FString& SlotName, int32 UserIndex)
{
if (SlotName.IsEmpty()) return false;
UStatsSaveGame* SaveObj = Cast(UGameplayStatics::CreateSaveGameObject(UStatsSaveGame::StaticClass()));
if (!SaveObj) return false;
SaveObj->Stats = ReplicatedStats;
return UGameplayStatics::SaveGameToSlot(SaveObj, SlotName, UserIndex);
}
bool UStatsComponent::LoadFromSlot(const FString& SlotName, int32 UserIndex)
{
if (SlotName.IsEmpty()) return false;
if (USaveGame* Raw = UGameplayStatics::LoadGameFromSlot(SlotName, UserIndex))
{
if (UStatsSaveGame* SaveObj = Cast(Raw))
{
if (GetOwner() && !GetOwner()->HasAuthority()) return false;
ReplicatedStats = SaveObj->Stats;
OnStatChanged.Broadcast(“LoadFromSlot”, 0.f, 0.f);
return true;
}
}
return false;
}
// File: Source/YourGame/Public/Stats/StatsSaveGame.h
#pragma once
#include “CoreMinimal.h”
#include “GameFramework/SaveGame.h”
#include “StatsComponent.h”
#include “StatsSaveGame.generated.h”
UCLASS()
class YOURGAME_API UStatsSaveGame : public USaveGame
{
GENERATED_BODY()
public:
// Why: Mark SaveGame so these fields persist automatically.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=”Stats”, SaveGame)
FObjectStats Stats;
};
// File: Source/YourGame/Private/Stats/StatsSaveGame.cpp
#include “StatsSaveGame.h”
// Intentionally empty.
// File: Source/YourGame/Public/Characters/MyHero.h
#pragma once
#include “CoreMinimal.h”
#include “GameFramework/Character.h”
#include “MyHero.generated.h”
class UStatsComponent;
UCLASS()
class YOURGAME_API AMyHero : public ACharacter
{
GENERATED_BODY()
public:
AMyHero();
UFUNCTION(BlueprintCallable, Category=”Demo”)
void OnDealtDamage(float Amount);
UFUNCTION(BlueprintCallable, Category=”Demo”)
void OnKillEnemy();
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category=”Stats”)
UStatsComponent* StatsComp;
};
// File: Source/YourGame/Private/Characters/MyHero.cpp
#include “MyHero.h”
#include “StatsComponent.h”
AMyHero::AMyHero()
{
StatsComp = CreateDefaultSubobject(TEXT(“StatsComp”));
}
void AMyHero::BeginPlay()
{
Super::BeginPlay();
if (StatsComp)
{
StatsComp->OnStatChanged.AddDynamic(this, [](FName Name, float NewValue, float Delta)
{
// Why: Minimal inline lambda to demo UI hooks; replace with proper handler.
UE_LOG(LogTemp, Log, TEXT(“[StatChanged] %s -> %f (Δ%f)”), *Name.ToString(), NewValue, Delta);
});
}
}
void AMyHero::OnDealtDamage(float Amount)
{
if (!StatsComp) return;
StatsComp->AddFloat(“DamageDealt”, Amount);
StatsComp->IncrementInt(“HitsLanded”, 1);
}
void AMyHero::OnKillEnemy()
{
if (!StatsComp) return;
StatsComp->IncrementInt(“Kills”, 1);
// Example: grant achievement when kills reach threshold.
bool bFound=false;
if (StatsComp->GetInt(“Kills”, bFound) >= 10 && bFound)
{
UE_LOG(LogTemp, Log, TEXT(“Achievement: Slayer”));
}
}
// File: Source/YourGame/Private/Example/StatsUsageExample.cpp
// Quick demonstration (e.g., called from a cheat manager/console)
#include “StatsComponent.h”
#include “GameFramework/Actor.h”
static void Demo_Stats(UStatsComponent* Comp)
{
if (!Comp) return;
Comp->IncrementInt(“Pickups”, 1);
Comp->AddFloat(“MetersTraveled”, 2.5f);
const FString Json = Comp->ExportToJson(true);
UE_LOG(LogTemp, Log, TEXT(“Stats JSON:n%s”), *Json);
Comp->SaveToSlot(“PlayerStats”);
}
“`
**Usage tips**
– Add `UStatsComponent` to any Actor (Character, Weapon, Pickup).
– Call `IncrementInt/AddFloat` from gameplay events (`OnHit`, `OnKilled`, `OnPickup`).
– For UI, bind to `OnStatChanged` or poll getters.
– In MP, only the Server should mutate; clients call the provided RPCs.
**a.** Want a variant that also tracks rolling time windows (last 5/60 mins) and percentile metrics?
**b.** Generate a minimal UMG widget to render these stats with sorting/filtering?