Skip to main content

Avatars

An avatar is a player-owned profile: an identity a player picks from and carries across sessions, holding small state other players can read and only the owner can write. It is not a live world actor and it is not server-checked game state; it is a stored profile the rest of the SDK reads to seed one, distinct from authoritative gameplay truth.

When you touch this​

When you build a profile or loadout screen, when a player changes a cosmetic or setting that should survive between sessions, or when you seed an entity's spawn state from a stored value the player already owns.

The record​

UCrowdyAvatars (Game Instance subsystem, category Crowdy SDK > Avatars) owns every avatar call. An avatar record, FCrowdyAvatar, holds:

FieldHolds
AvatarIdThe avatar's int64 id.
UserIdThe owning player's int64 id.
NameDisplay name.
PublicStateBase64 blob, readable by every player.
PrivateStateBase64 blob, readable by the owner only; stripped server-side on a non-owner read.
CreatedAtCreation timestamp.

Per-app data is a separate record, FCrowdyAppAvatarState, keyed by AppId and AvatarId: RawState (base64, empty string means not set), CreatedAt, UpdatedAt. This is the slot for cosmetics, settings, or any small struct your game defines; the avatar record's own fields do not hold arbitrary game data.

Only the owner may write an avatar, and the SDK does not check locally.

Every write, UpdateAvatar, UpdateAvatarState, UpdatePublicAvatarState, UpdatePrivateAvatarState, UpdateAvatarAppState, DeleteAvatar, is owner-only, enforced by the Game API against the caller's app-scoped token. UCrowdyAvatars performs no local ownership check: a write for an avatar the signed-in player does not own is sent over the wire and refused server-side, surfaced through OnError and usually classified Forbidden.

Reading avatars: cache versus network​

GetCachedMyAvatars, HasCachedAvatars, and GetMyAvatarById are synchronous, cache-only, and never populate themselves. Only GetMyAvatars (a network refresh) repopulates the cache and fires OnMyAvatarsCacheChanged (FOnMyAvatarsCacheChanged, one param, the refreshed array) so anything watching the cache updates without polling. A create, rename, delete or state write does not touch the cache, so call GetMyAvatars again after a write you want the cache to reflect.

GetAvatar reads any avatar by id, including one you do not own; GetUserAvatars lists another user's avatars. Neither is limited to the signed-in player.

Reading and writing per-app state​

GetAvatarAppState and GetAvatarAppStates (batch) read the calling app's slot for one or many avatars. UpdateAvatarAppState replaces it.

AppId always comes from project settings; there is no per-call override.

GetAvatarAppState, GetAvatarAppStates, and UpdateAvatarAppState resolve AppId from project settings, not a parameter. This subsystem cannot target a different app's slot.

UpdatePublicAvatarState and UpdatePrivateAvatarState each leave the other field untouched on the server; UpdateAvatarState replaces both public and private state atomically.

The combined write still round-trips an unchanged field.

Calling UpdateAvatarState with a field's old value does not clear it, it just resends it. Prefer the separate UpdatePublicAvatarState / UpdatePrivateAvatarState calls when only one field changes.

Serializing struct state​

State fields are base64 strings on the wire. SerializeToAvatarState and DeserializeFromAvatarState convert any USTRUCT to and from that string; GetAvatarAppStateAs and SetAvatarAppStateAs are latent wrappers that fetch-and-deserialize or serialize-and-write in one call.

The State pin is a wildcard: one struct, string or scalar, never an array.

SerializeToAvatarState, DeserializeFromAvatarState, GetAvatarAppStateAs, and SetAvatarAppStateAs are CustomThunk with a CustomStructureParam. The int32& in the C++ signature is a wildcard pin in Blueprint: connect a struct pin (or a single string, bool, integer or float) and the pin takes that type. An array, set or map pin is refused.

The lantern serializes its torch color as a small cosmetic struct, writes it to the app-state slot, and re-applies it on spawn. The example assumes the player already has an avatar (created with CreateAvatar, or Create Avatar in Blueprint); with none, AvatarId stays 0 and every call below returns early.

SetTorchColor writes the picked FLinearColor directly with FMemoryWriter, byte-identical to what SerializeToAvatarState would produce for a linear color; C++ cannot call that helper itself, since its struct pin is a CustomThunk wildcard. UpdateAvatarAppState sends the encoded bytes, and Torch only recolours once the write's OnSuccess handler, ApplyTorchColor, confirms it.

// LanternPlayer.h
UFUNCTION(BlueprintCallable, Category = "Lantern")
void SetTorchColor(FLinearColor NewColor);

// LanternPlayer.cpp
#include "Misc/Base64.h"
#include "Serialization/MemoryWriter.h"
#include "Subsystem/CrowdyAvatars.h"

void ALanternPlayer::SetTorchColor(FLinearColor NewColor)
{
UCrowdyAvatars* Avatars = GetGameInstance()->GetSubsystem<UCrowdyAvatars>();
if (!Avatars || AvatarId == 0)
{
return;
}
// App state is an opaque base64 blob; these 16 bytes match what Serialize Struct to Avatar State writes for a Linear Color.
TArray<uint8> Bytes;
FMemoryWriter Writer(Bytes);
Writer << NewColor;
FOnAppStateSuccess Saved;
Saved.BindDynamic(this, &ALanternPlayer::ApplyTorchColor);
Avatars->UpdateAvatarAppState(AvatarId, FBase64::Encode(Bytes), Saved, FOnAvatarError());
}

LoadTorchColor calls GetAvatarAppState; the handler ApplyTorchColor, shared with the write above, base64-decodes, guards on 16 bytes, and reads the color with FMemoryReader straight into Torch. The latent GetAvatarAppStateAs is not used here for the same wildcard-thunk reason as above.

// LanternPlayer.h
#include "Subsystem/CrowdyAvatars.h"

void LoadTorchColor();

UFUNCTION()
void ApplyTorchColor(FCrowdyAppAvatarState AppState);

// LanternPlayer.cpp
#include "Misc/Base64.h"
#include "Serialization/MemoryReader.h"

void ALanternPlayer::LoadTorchColor()
{
UCrowdyAvatars* Avatars = GetGameInstance()->GetSubsystem<UCrowdyAvatars>();
if (!Avatars || AvatarId == 0)
{
return;
}
FOnAppStateSuccess Loaded;
Loaded.BindDynamic(this, &ALanternPlayer::ApplyTorchColor);
Avatars->GetAvatarAppState(AvatarId, Loaded, FOnAvatarError());
}

void ALanternPlayer::ApplyTorchColor(FCrowdyAppAvatarState AppState)
{
// An empty Raw State is an avatar that never saved a color; anything but a Linear Color's 16 bytes is ignored too.
TArray<uint8> Bytes;
if (!FBase64::Decode(AppState.RawState, Bytes) || Bytes.Num() != static_cast<int32>(sizeof(FLinearColor)))
{
return;
}
FLinearColor Color;
FMemoryReader Reader(Bytes);
Reader << Color;
Torch->SetLightColor(Color);
}

WatchAvatars, bound at BeginPlay, picks up AvatarId from the first entry of a cache refresh and calls LoadTorchColor, so a cache change elsewhere still lands the stored color. The handler takes const TArray<FCrowdyAvatar>&.

// LanternPlayer.h
#include "Subsystem/CrowdyAvatars.h"

// The keeper this player plays as: their first avatar, learned when the cache answers.
UPROPERTY(BlueprintReadOnly, Category = "Lantern")
int64 AvatarId = 0;

void WatchAvatars();

UFUNCTION()
void HandleAvatarsChanged(const TArray<FCrowdyAvatar>& Avatars);

// LanternPlayer.cpp
void ALanternPlayer::WatchAvatars()
{
UCrowdyAvatars* Avatars = GetGameInstance()->GetSubsystem<UCrowdyAvatars>();
Avatars->OnMyAvatarsCacheChanged.AddDynamic(this, &ALanternPlayer::HandleAvatarsChanged);
Avatars->GetMyAvatars(FOnAvatarsSuccess(), FOnAvatarError());
}

void ALanternPlayer::HandleAvatarsChanged(const TArray<FCrowdyAvatar>& Avatars)
{
if (Avatars.Num() == 0)
{
return;
}
AvatarId = Avatars[0].AvatarId;
LoadTorchColor();
}

Async action nodes​

Every network call above also exists as a Blueprint async action node with the same name and, as its underlying class, the subsystem name plus the call (Get My Avatars UCrowdyAvatars_GetMyAvatars, Get Avatar UCrowdyAvatars_GetAvatar, Get User Avatars UCrowdyAvatars_GetUserAvatars, Get Avatar App State UCrowdyAvatars_GetAvatarAppState, Get Avatar App States UCrowdyAvatars_GetAvatarAppStates, Create Avatar UCrowdyAvatars_CreateAvatar, Update Avatar UCrowdyAvatars_UpdateAvatar, Delete Avatar UCrowdyAvatars_DeleteAvatar, Update Public Avatar State UCrowdyAvatars_UpdatePublicAvatarState, Update Private Avatar State UCrowdyAvatars_UpdatePrivateAvatarState, Update Avatar App State UCrowdyAvatars_UpdateAvatarAppState), each with its own On Success and On Error exec pins instead of a bound delegate. On Success and On Error share one set of data pins: the result (Avatar, Result or AppState; Delete Avatar has none), then Error and Message. On On Success, Error and Message are empty. On On Error the result is empty, Error is an FCrowdyAvatarError (break it to read its Code), and Message says what went wrong, so a graph can show or branch on the failure. The pin types are the multicast FAvatarAsyncResult (Get Avatar, Create Avatar, Update Avatar, Update Public Avatar State, Update Private Avatar State), FAvatarsAsyncResult (Get My Avatars, Get User Avatars), FAppStateAsyncResult (Get Avatar App State, Update Avatar App State), FAppStatesAsyncResult (Get Avatar App States) and FAvatarsAsyncOnError (Delete Avatar); the subsystem's per-call FOnAvatarSuccess, FOnAvatarsSuccess, FOnAppStateSuccess, FOnAppStatesSuccess, FOnAvatarVoidSuccess and FOnAvatarError are a separate family the async nodes never expose. TArray results arrive wrapped, FCrowdyAvatarList for a list of avatars and FCrowdyAppAvatarStateList for a list of app states, so the result is one struct pin. UpdateAvatarState (the combined public-and-private write), GetMyAvatarById, HasCachedAvatars, GetCachedMyAvatars, SerializeToAvatarState, DeserializeFromAvatarState, GetAvatarAppStateAs, and SetAvatarAppStateAs have no async action node; a Blueprint reaches those only through the subsystem functions shown above.

Creating and removing avatars​

CreateAvatar(Name, OnSuccess, OnError) makes a new avatar for the signed-in player; UpdateAvatar(AvatarId, Name, ...) renames one; DeleteAvatar(AvatarId, OnVoidSuccess, OnError) removes one. All three are owner-only, enforced the same way as the state writes above.

Error codes​

FCrowdyAvatarError carries a Code (ECrowdyAvatarErrorCode: Unknown, NotFound, Forbidden, NetworkError, ServerError) and a Message. Bind FOnAvatarError to a UFUNCTION with the signature (FCrowdyAvatarError Error, FString Message); Message repeats Error.Message. On an async node, On Error shows the same two values as its Error and Message pins.

The error code is a client-side guess, not a server code.

ECrowdyAvatarErrorCode is classified from the error message text by substring match, not a code the server sends. Branch on it for a coarse UI response; do not treat it as a stable server contract.

Cosmetic selections and progression flags belong here; currency balances and match results do not. Avatar state, public, private, or app, is profile data, not server-authoritative gameplay truth, which belongs in your server code.

Gotchas​

  • GetCachedMyAvatars, HasCachedAvatars, and GetMyAvatarById never populate themselves; only GetMyAvatars does. A create, rename, delete or state write leaves the cache as it was.
  • Every write is owner-only and enforced server-side; a non-owner's write is sent, not blocked locally.
  • AppId for the per-app state calls always comes from project settings; there is no per-call override.
  • The serialization helpers take a wildcard pin: one struct, string or scalar. An array, set or map pin is refused.
  • ECrowdyAvatarErrorCode is a client-side text classification, not a server-issued code.
  • Teams: a sibling service on the same subsystem pattern.
  • The Two Planes: the contrast; avatar state is profile data, not server-checked truth.
  • Entities and spawning: seeding an entity's spawn state from an avatar's public part.