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:
| Field | Holds |
|---|---|
AvatarId | The avatar's int64 id. |
UserId | The owning player's int64 id. |
Name | Display name. |
PublicState | Base64 blob, readable by every player. |
PrivateState | Base64 blob, readable by the owner only; stripped server-side on a non-owner read. |
CreatedAt | Creation 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.
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.
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.
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.
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.
- C++
- Blueprint
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();
}
Variables the Blueprint needs: Torch (a UPointLightComponent reference) and AvatarId (int64). Set AvatarId from a Get My Avatars result (the first entry's AvatarId) or from the cache-changed event described below; the two graphs read it and do nothing while it is 0.
Saving the color: OnTorchColorPicked (custom event, NewColor) runs Serialize Struct to Avatar State on NewColor, reads AvatarId, and passes the result to the Update Avatar App State async node; its OnSuccess pin calls Set Light Color on Torch with NewColor.
Loading it on spawn: Event BeginPlay gets the Crowdy Avatars subsystem, reads AvatarId, and calls the latent Get Avatar App State As, whose wildcard output feeds NewLightColor; a Branch on its bSuccess output calls Set Light Color on Torch.
Following cache changes: no figure is shown for this step. Add a custom event with one input, Avatars (array of Crowdy Avatar), bind it with Bind Event to On My Avatars Cache Changed at Begin Play, set AvatarId from the first entry in its body, then run the loading path above; the C++ tab's WatchAvatars is the same step.
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.
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, andGetMyAvatarByIdnever populate themselves; onlyGetMyAvatarsdoes. 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.
AppIdfor 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.
ECrowdyAvatarErrorCodeis a client-side text classification, not a server-issued code.
Related
- 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.