Player State & User Data: the profile fields brainCloud already tracks, plus a free-form attribute bag for the rest
Authentication is usually what triggers these calls, right after login or when a player attaches a real identity. Entity Storage is the article to reach for once your data outgrows a flat key-value bag.
Every brainCloud user has a small set of built-in profile fields: a name, a contact email, a picture URL, a language and country preference, a tester flag. None of that needs modeling. Player State reads and writes those fields directly: no entity, no cloud-code script, no schema to design. bitBuddies, Marketplace, and bcchat all call the same handful of Player State methods for this, right after login or whenever a player edits their profile.
Player State has a second job: UpdateAttributes and GetAttributes give every user a flat, free-form key-value bag, no ACL, no entity type, no query, just a single JSON object of string values attached directly to the account. Marketplace uses it to persist a couple of timestamps for its mock in-app-purchase subscriptions. It's the right tool for a handful of small settings, not the place to reach once data grows past a few fields; that's what Entity Storage (Article 4) is for.
How it works
- UpdateUserName and UpdateContactEmail write the two most common profile fields. bitBuddies calls both right after login, syncing brainCloud's copy with whatever it has locally, and bcchat calls updateUserName straight from its settings screen.
- UpdateUserPictureUrl points the profile picture field at wherever the image is actually stored. bcchat calls it right after its file-upload flow (Article 9) hands back a downloadUrl; the two features chain together directly.
- UpdateAttributes takes a single-layer JSON object where every value is a string, plus a wipeExisting flag that clears everything else first instead of merging. Marketplace passes false here because it's adding a couple of keys to an existing bag, not replacing it.
- GetAttributes reads that same bag back, and Marketplace uses it on cold start to check whether a mock subscription's renewal timing survived from a previous session, since there's nothing else on the client holding that state at launch.
- Attributes and Entity storage solve different problems. Attributes are a flat bag of strings scoped to the account itself, useful for the kind of small setting that would otherwise end up in local storage. Entity storage is structured, typed JSON with its own ACL and query model. Attributes fit a handful of fields; Entity storage fits anything with more structure than a flat string map.
Code
var username = data["playerName"] as string;
if (username.IsNullOrEmpty() && !CurrentUserInfo.Username.IsNullOrEmpty())
{
Wrapper.PlayerStateService.UpdateUserName(CurrentUserInfo.Username);
}
else if (!username.IsNullOrEmpty())
{
CurrentUserInfo.UpdateUsername(username);
}
var email = data["emailAddress"] as string;
if (email.IsNullOrEmpty() && !CurrentUserInfo.Email.IsNullOrEmpty())
{
Wrapper.PlayerStateService.UpdateContactEmail(CurrentUserInfo.Email);
IsEmailAuthenticated = true;
}handleName(name)
{
let fullName = name.firstname + " " + name.lastname;
this.bcWrapper.playerState.updateUserName(fullName, result =>
{
if (result.status === 200)
{
this.onLoggedIn();
}
else
{
this.dieWithMessage("Failed to update username to brainCloud");
}
});
}this.bcWrapper.playerState.updateUserPictureUrl(file.downloadUrl, result =>
{
if (result.status !== 200)
{
this.dieWithMessage("Failed to updateUserPictureUrl");
return;
}
// ...
});BCManager.Instance.BCWrapper.PlayerStateService.UpdateAttributes(
JsonWriter.Serialize(new Dictionary<string, object>
{
{ $"mockSubStart_{productId}", entry.start.ToString() },
{ $"mockSubAutoRenew_{productId}", "true" },
{ $"mockSubFinalExpiry_{productId}", "0" }
}),
false,
(string _, object __) => Debug.Log($"[Mock Sub] Persisted '{productId}' timing to user attributes."),
(int _, int __, string err, object ___) => Debug.LogError($"[Mock Sub] Failed to persist timing: {err}"));BCManager.Instance.BCWrapper.PlayerStateService.GetAttributes(
(string attrJson, object _) =>
{
var attrData = (JsonReader.Deserialize<Dictionary<string, object>>(attrJson)["data"]
as Dictionary<string, object>)["attributes"] as Dictionary<string, object>;
// pull mockSubStart_/mockSubAutoRenew_/mockSubFinalExpiry_ keys back out
},
(int _, int __, string err, object ___) => { });Gotchas worth knowing up front
- Attribute values are strings, not arbitrary JSON. There's no numeric or boolean type on the wire; Marketplace serializes numbers and booleans to strings before writing (entry.start.ToString(), "true") and parses them back out on read. Build that conversion in from the start, not after a value comes back the wrong type.
- Pass wipeExisting: false unless the intent is to erase everything. true clears every attribute the user has before writing the new ones, not just the keys being touched. Marketplace always passes false because it's incrementally adding keys; flipping that by accident silently wipes unrelated settings some other feature stored there.
- This is one shared bag per user, not namespaced per feature. Marketplace prefixes its keys (mockSubStart_, mockSubAutoRenew_) because Attributes has no built-in separation between what different systems might store there. Adopt a prefix convention early when more than one feature builds on Attributes.
- Attributes stop being the right tool once data has real structure. A handful of named settings fits fine here. The moment a feature needs nested objects, arrays, querying, or per-field ACLs, Entity storage (Article 4) is built for that instead of stretching Attributes to cover it.