← Files UnityARCHIVED FILE
skills/build-live-game/references/player-account.md
29.2 KB · Oct 4, 2026 · 12:33 UTC
# Player Account Reference
## Table of Contents
- [Architecture Overview](#architecture-overview)
- [Access Classes](#access-classes)
- [Assembly References](#assembly-references)
- [Package Setup](#package-setup)
- [Sign-In: Anonymous](#sign-in-anonymous)
- [Sign-In: Unity (Browser)](#sign-in-unity-browser)
- [PlayerAccountService API](#playeraccountservice-api)
- [Sign-In: Username/Password](#sign-in-usernamepassword)
- [Username/Password Constraints](#usernamepassword-constraints)
- [Player Identity](#player-identity)
- [Cloud Save Operations](#cloud-save-operations)
- [Namespace Aliases](#namespace-aliases)
- [Save Default Data](#save-default-data-private-to-player)
- [Save Public Data](#save-public-data-visible-to-other-players)
- [Load Default Data](#load-default-data)
- [Load Public Data](#load-public-data-own)
- [Load Public Data (Other Player)](#load-public-data-another-player)
- [Load All Keys](#load-all-keys)
- [Server-Authoritative Writes via Cloud Code (Optional)](#server-authoritative-writes-via-cloud-code-optional)
- [Reading Protected Data](#reading-protected-data-client-side)
- [Writing Protected/Custom Data](#writing-protectedcustom-data-server-side)
- [Full Client Implementation](#full-client-implementation)
- [PlayerAccountTester](#playeraccounttester)
- [Assembly Definition](#assembly-definition)
- [Error Handling](#error-handling)
- [AuthenticationException](#authenticationexception)
- [PlayerAccountsException](#playeraccountsexception)
- [CloudSaveException](#cloudsaveexception)
- [Validation](#validation)
- [Asset Store Building Block](#asset-store-building-block)
---
## Architecture Overview
| Concern | Service | API |
|---|---|---|
| Sign in (anonymous) | Authentication | `SignInAnonymouslyAsync()` |
| Sign in (Unity browser) | PlayerAccounts then Authentication | `StartSignInAsync()` then `SignInWithUnityAsync(AccessToken)` |
| Sign in (username/password) | Authentication | `SignUpWithUsernamePasswordAsync` / `SignInWithUsernamePasswordAsync` |
| Player identity | Authentication | `PlayerId`, `PlayerName`, `UpdatePlayerNameAsync` |
| Per-player data | Cloud Save | `CloudSaveService.Instance.Data.Player` |
```
UnityServices.InitializeAsync()
|
v
AuthenticationService.Instance
|
+--------+-----------+
| | |
v v v
Anon Unity/PA Password
| (browser) |
| | |
| PlayerAccountService.Instance
| .StartSignInAsync()
| |
| SignedIn event fires
| |
| SignInWithUnityAsync(AccessToken)
| | |
+--------+-----------+
|
v
PlayerId / PlayerName
|
v
CloudSaveService.Instance.Data.Player
| | |
v v v
Default Public Protected
(owner (anyone (owner read,
read+write) read, server-only
owner write)
write)
```
---
## Access Classes
Cloud Save player data supports three access classes. The access class used when saving determines which access class must be used when loading.
| Access Class | Read | Write | Use Case |
|---|---|---|---|
| Default | Owner only | Owner only | Private settings, preferences, game progress |
| Public | Any player | Owner only | Display names, public profiles, shared stats |
| Protected | Owner only | Server only (Cloud Code) | Anti-cheat data, server-awarded state |
---
## Assembly References
Your `.asmdef` must reference all four assemblies. `Unity.Services.Authentication.PlayerAccounts` is a **separate assembly** from `Unity.Services.Authentication`, even though both ship inside the `com.unity.services.authentication` package.
| Assembly Name | Package | Purpose |
|---|---|---|
| `Unity.Services.Core` | `com.unity.services.core` | `UnityServices.InitializeAsync()` |
| `Unity.Services.Authentication` | `com.unity.services.authentication` | `AuthenticationService.Instance`, sign-in methods, player identity |
| `Unity.Services.Authentication.PlayerAccounts` | `com.unity.services.authentication` (separate assembly in same package) | `PlayerAccountService.Instance`, browser-based Unity sign-in |
| `Unity.Services.CloudSave` | `com.unity.services.cloudsave` | `CloudSaveService.Instance.Data.Player`, save/load operations |
---
## Package Setup
Ensure the following packages are listed in `Packages/manifest.json`:
```json
{
"dependencies": {
"com.unity.services.authentication": "3.6.1",
"com.unity.services.cloudsave": "3.4.0",
"com.unity.services.core": "1.16.0"
}
}
```
**Optional** — add `com.unity.services.cloudcode` if using Protected player data or Custom (shared) data:
```json
"com.unity.services.cloudcode": "2.10.3"
```
---
## Sign-In: Anonymous
```csharp
using Unity.Services.Core;
using Unity.Services.Authentication;
using UnityEngine;
async Task SignInAnonymouslyAsync()
{
await UnityServices.InitializeAsync();
if (!AuthenticationService.Instance.IsSignedIn)
{
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
Debug.Log($"Signed in as {AuthenticationService.Instance.PlayerId}");
}
```
`SignInAnonymouslyAsync` handles both new and returning players. If a session token is cached from a previous session, it signs in silently with the cached token. If no token exists, it creates a new anonymous account. Always check `IsSignedIn` before calling to avoid double sign-in errors.
---
## Sign-In: Unity (Browser)
Uses `PlayerAccountService` to open a system browser for Unity account sign-in. After the user signs in via the browser, the `SignedIn` event fires and provides an `AccessToken` that is passed to `AuthenticationService.Instance.SignInWithUnityAsync`.
**Important:** The `Task` returned by `StartSignInAsync` completes when the browser **opens**, NOT when sign-in finishes. You must subscribe to the `SignedIn` event to know when authentication is complete.
```csharp
using Unity.Services.Core;
using Unity.Services.Authentication;
using Unity.Services.Authentication.PlayerAccounts;
using UnityEngine;
void Start()
{
// Wire the event BEFORE calling StartSignInAsync
PlayerAccountService.Instance.SignedIn += OnPlayerAccountSignedIn;
}
async Task StartSignInWithUnityAsync()
{
await UnityServices.InitializeAsync();
if (PlayerAccountService.Instance.IsSignedIn)
{
// Already signed in to Player Accounts -- go straight to Authentication
OnPlayerAccountSignedIn();
return;
}
// Opens the system browser. Task completes when browser opens, NOT when sign-in finishes.
await PlayerAccountService.Instance.StartSignInAsync();
}
async void OnPlayerAccountSignedIn()
{
try
{
await AuthenticationService.Instance.SignInWithUnityAsync(
PlayerAccountService.Instance.AccessToken);
Debug.Log($"Signed in as {AuthenticationService.Instance.PlayerId}");
}
catch (AuthenticationException ex)
{
Debug.LogError($"Unity sign-in failed: {ex.Message}");
}
catch (RequestFailedException ex)
{
Debug.LogError($"Request failed: {ex.Message}");
}
}
```
Namespace: `Unity.Services.Authentication.PlayerAccounts`. Access via `PlayerAccountService.Instance` (`IPlayerAccountService`).
If using any authentication other than anonymous, it requires the Unity identity provider to be enabled in **Project Settings > Services > Authentication > Identity Providers**.
### PlayerAccountService API
| Member | Type | Description |
|---|---|---|
| `StartSignInAsync(bool isSigningUp = false)` | `Task` | Opens browser for sign-in (or sign-up if `true`). Task completes when browser opens. |
| `SignOut()` | `void` | Signs out of Player Accounts and revokes the access token. Synchronous. |
| `RefreshTokenAsync()` | `Task` | Refreshes the current access token using the refresh token. |
| `AccessToken` | `string` | Access token obtained during sign-in. Pass this to `SignInWithUnityAsync`. |
| `IdToken` | `string` | ID token obtained during sign-in. |
| `IdTokenClaims` | `IdToken` | Parsed claims from the ID token (email, subject, etc.). |
| `IsSignedIn` | `bool` | Whether the player is signed in to Player Accounts. |
| `AccountPortalUrl` | `string` | URL to the Unity Player Account portal. |
| `SignedIn` | `event Action` | Fires when browser sign-in completes successfully. |
| `SignedOut` | `event Action` | Fires when the player signs out. |
| `SignInFailed` | `event Action<RequestFailedException>` | Fires when sign-in fails. |
---
## Sign-In: Username/Password
Two separate methods: `SignUpWithUsernamePasswordAsync` for first-time registration, `SignInWithUsernamePasswordAsync` for subsequent sign-ins.
```csharp
using Unity.Services.Core;
using Unity.Services.Authentication;
using UnityEngine;
async Task SignUpWithPasswordAsync(string username, string password)
{
await UnityServices.InitializeAsync();
try
{
await AuthenticationService.Instance.SignUpWithUsernamePasswordAsync(username, password);
Debug.Log($"Signed up and signed in as {AuthenticationService.Instance.PlayerId}");
}
catch (AuthenticationException ex) when (ex.ErrorCode == AuthenticationErrorCodes.AccountAlreadyLinked)
{
// Error code 10003: username already exists. Sign in instead.
Debug.LogWarning("Username already exists, signing in instead.");
await AuthenticationService.Instance.SignInWithUsernamePasswordAsync(username, password);
}
catch (AuthenticationException ex)
{
Debug.LogError($"Sign-up failed: {ex.Message} (code: {ex.ErrorCode})");
}
}
async Task SignInWithPasswordAsync(string username, string password)
{
await UnityServices.InitializeAsync();
try
{
await AuthenticationService.Instance.SignInWithUsernamePasswordAsync(username, password);
Debug.Log($"Signed in as {AuthenticationService.Instance.PlayerId}");
}
catch (AuthenticationException ex)
{
Debug.LogError($"Sign-in failed: {ex.Message} (code: {ex.ErrorCode})");
}
}
```
Requires the username/password identity provider in **Project Settings > Services > Authentication > Identity Providers**.
### Username/Password Constraints
| Field | Constraint |
|---|---|
| Username length | 3-20 characters |
| Username characters | `A-Z`, `a-z`, `0-9`, `.`, `-`, `@`, `_` |
| Username case | Case insensitive (stored lowercase) |
| Password length | 8-30 characters |
| Password requirements | At least 1 lowercase + 1 uppercase + 1 number + 1 symbol |
---
## Player Identity
After any successful sign-in, the following properties and methods are available on `AuthenticationService.Instance`:
| Member | Type | Description |
|---|---|---|
| `PlayerId` | `string` | Unique player identifier, available immediately after sign-in |
| `PlayerName` | `string` | Player display name (may be null until fetched) |
| `IsSignedIn` | `bool` | True if a token exists in memory |
| `IsAnonymous` | `bool` | True if signed in anonymously |
| `SignedIn` | `event Action` | Fires on successful sign-in |
| `SignedOut` | `event Action` | Fires on sign-out |
| `Expired` | `event Action` | Fires when access token expires; SDK auto-attempts refresh |
```csharp
// Get player name. autoGenerate: true creates a random name if none exists.
string name = await AuthenticationService.Instance.GetPlayerNameAsync(autoGenerate: true);
// Update player name.
string updatedName = await AuthenticationService.Instance.UpdatePlayerNameAsync("NewDisplayName");
// Sign out. Synchronous (void), NOT async.
AuthenticationService.Instance.SignOut();
// Optionally clear cached credentials:
// AuthenticationService.Instance.SignOut(clearCredentials: true);
```
**Warning:** `SignOut` is synchronous (`void`). There is no `SignOutAsync` method.
---
## Cloud Save Operations
### Namespace Aliases
Use namespace aliases to avoid ambiguity between root-level and `Models.Data.Player` option classes:
```csharp
using Unity.Services.CloudSave;
using Unity.Services.CloudSave.Models;
using Unity.Services.CloudSave.Models.Data.Player;
using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
using PlayerLoadAllOptions = Unity.Services.CloudSave.Models.Data.Player.LoadAllOptions;
using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
```
> **Namespace collision:** `SaveOptions` and `LoadOptions` exist in both `Unity.Services.CloudSave`
> and `Unity.Services.CloudSave.Models.Data.Player`. Always use the `using` aliases to avoid
> ambiguity errors.
### Save Default Data (Private to Player)
```csharp
var data = new Dictionary<string, object>
{
{ "settings_volume", 0.8f },
{ "settings_difficulty", "hard" },
{ "last_login", DateTime.UtcNow.ToString("o") }
};
await CloudSaveService.Instance.Data.Player.SaveAsync(data);
```
### Save Public Data (Visible to Other Players)
```csharp
var publicData = new Dictionary<string, object>
{
{ "display_name", "Hero123" },
{ "avatar_id", 42 }
};
var publicSaveOptions = new PlayerSaveOptions(new PublicWriteAccessClassOptions());
await CloudSaveService.Instance.Data.Player.SaveAsync(publicData, publicSaveOptions);
```
### Load Default Data
```csharp
var keys = new HashSet<string> { "settings_volume", "settings_difficulty" };
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(keys);
if (result.TryGetValue("settings_volume", out var volumeItem))
{
float volume = volumeItem.Value.GetAs<float>();
}
```
For string values saved via `Dictionary<string, object>`, `GetAsString()` returns the plain string. For complex objects, `GetAsString()` returns the JSON; use `item.Value.GetAs<T>()` to deserialize.
### Load Public Data (Own)
```csharp
var publicLoadOptions = new PlayerLoadOptions(new PublicReadAccessClassOptions());
var publicResult = await CloudSaveService.Instance.Data.Player.LoadAsync(
new HashSet<string> { "display_name", "avatar_id" }, publicLoadOptions);
```
### Load Public Data (Another Player)
```csharp
var otherPlayerOptions = new PlayerLoadOptions(new PublicReadAccessClassOptions(otherPlayerId));
var otherResult = await CloudSaveService.Instance.Data.Player.LoadAsync(
new HashSet<string> { "display_name", "avatar_id" }, otherPlayerOptions);
```
### Load All Keys
```csharp
using Unity.Services.CloudSave.Models.Data.Player;
using PlayerLoadAllOptions = Unity.Services.CloudSave.Models.Data.Player.LoadAllOptions;
var allItems = await CloudSaveService.Instance.Data.Player.LoadAllAsync(
new PlayerLoadAllOptions(new DefaultReadAccessClassOptions()));
foreach (var kv in allItems)
Debug.Log($"{kv.Key}: {kv.Value.Value.GetAsString()}");
```
**Access class matching rule:** Data saved with `PublicWriteAccessClassOptions` must be loaded with `PublicReadAccessClassOptions`. Data saved with no options (Default) must be loaded with `DefaultReadAccessClassOptions` or no options. Mismatched access classes return empty results without error.
---
## Server-Authoritative Writes via Cloud Code (Optional)
The Default and Public access classes above are client-writable — suitable for preferences, display names, and non-sensitive data. Two scenarios require routing writes through a Cloud Code module instead:
- **Non-player shared data** (guild info, level configs, global state) — no single player owns this data, so it must be written via Cloud Code to the **Custom** bucket using `IGameApiClient`.
- **Tamper-proof player data** — if the game design requires anti-cheat protection for specific player state (e.g. XP, currency, unlocked items), write to the **Protected** bucket via Cloud Code. This is a game-design decision, not a requirement.
### Reading Protected Data (Client-Side)
```csharp
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
new HashSet<string> { "player_level", "currency" },
new PlayerLoadOptions(new ProtectedReadAccessClassOptions()));
if (result.TryGetValue("player_level", out var item))
{
int level = item.Value.GetAs<int>();
}
```
> **Common mistake:** Using default `LoadOptions` instead of `ProtectedReadAccessClassOptions`
> returns no data, because the keys were written to the Protected bucket by Cloud Code.
### Writing Protected/Custom Data (Server-Side)
Writes to Protected and Custom buckets must go through a Cloud Code module. The module uses `IGameApiClient` with the service token to call Cloud Save server-side APIs.
To create the module, follow [cloud-code.md — Module Creation](cloud-code.md#module-creation). All scaffolding files are required:
- [ ] `.sln` (generate fresh GUIDs)
- [ ] `.csproj` (net9.0, CloudCode.Apis + CloudCode.Core)
- [ ] `ModuleSetup.cs` (registers `GameApiClient`)
- [ ] `Properties/PublishProfiles/FolderProfile.pubxml` — **without this file, deployment fails with "Failed to retrieve main project"**
- [ ] `Assets/CloudCode/<ModuleName>.ccmr` (points to the `.sln`)
---
## Full Client Implementation
A complete `MonoBehaviour` covering all three sign-in methods, player identity, and Cloud Save operations.
```csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Unity.Services.Authentication;
using Unity.Services.Authentication.PlayerAccounts;
using Unity.Services.CloudSave;
using Unity.Services.CloudSave.Models.Data.Player;
using Unity.Services.Core;
using UnityEngine;
using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
using PlayerLoadAllOptions = Unity.Services.CloudSave.Models.Data.Player.LoadAllOptions;
using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
public class PlayerAccountManager : MonoBehaviour
{
public string PlayerId => AuthenticationService.Instance.PlayerId;
public string PlayerName => AuthenticationService.Instance.PlayerName;
public bool IsSignedIn => AuthenticationService.Instance.IsSignedIn;
public event Action SignedIn;
async void Start()
{
await UnityServices.InitializeAsync();
AuthenticationService.Instance.SignedIn += () =>
{
Debug.Log($"Signed in: {AuthenticationService.Instance.PlayerId}");
SignedIn?.Invoke();
};
// Wire PlayerAccountService event BEFORE any StartSignInAsync call
PlayerAccountService.Instance.SignedIn += OnPlayerAccountSignedIn;
}
// --- Sign-In Methods ---
public async Task SignInAnonymouslyAsync()
{
if (AuthenticationService.Instance.IsSignedIn) return;
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
/// <summary>
/// Starts the browser-based Unity sign-in flow.
/// The returned Task completes when the browser opens, NOT when sign-in finishes.
/// Subscribe to SignedIn to know when authentication is complete.
/// </summary>
public async Task StartSignInWithUnity()
{
if (PlayerAccountService.Instance.IsSignedIn)
{
OnPlayerAccountSignedIn();
return;
}
await PlayerAccountService.Instance.StartSignInAsync();
}
async void OnPlayerAccountSignedIn()
{
try
{
await AuthenticationService.Instance.SignInWithUnityAsync(
PlayerAccountService.Instance.AccessToken);
}
catch (AuthenticationException ex)
{
Debug.LogError($"Unity sign-in failed: {ex.Message}");
}
catch (RequestFailedException ex)
{
Debug.LogError($"Request failed: {ex.Message}");
}
}
public async Task SignUpWithPasswordAsync(string username, string password)
{
try
{
await AuthenticationService.Instance.SignUpWithUsernamePasswordAsync(username, password);
}
catch (AuthenticationException ex) when (ex.ErrorCode == AuthenticationErrorCodes.AccountAlreadyLinked)
{
// Error code 10003: username already exists
Debug.LogWarning("Username already exists. Use SignInWithPasswordAsync instead.");
throw;
}
}
public async Task SignInWithPasswordAsync(string username, string password)
{
await AuthenticationService.Instance.SignInWithUsernamePasswordAsync(username, password);
}
public void SignOut()
{
AuthenticationService.Instance.SignOut();
// Optionally also sign out of Player Accounts:
// PlayerAccountService.Instance.SignOut();
}
// --- Player Identity ---
public async Task<string> GetPlayerNameAsync()
{
return await AuthenticationService.Instance.GetPlayerNameAsync(autoGenerate: true);
}
public async Task<string> UpdatePlayerNameAsync(string newName)
{
return await AuthenticationService.Instance.UpdatePlayerNameAsync(newName);
}
// --- Cloud Save: Default (Private) ---
public async Task SaveDefaultAsync(string key, object value)
{
await CloudSaveService.Instance.Data.Player.SaveAsync(
new Dictionary<string, object> { { key, value } });
}
public async Task<string> LoadDefaultAsync(string key)
{
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
new HashSet<string> { key });
return result.TryGetValue(key, out var item) ? item.Value.GetAsString() : null;
}
// --- Cloud Save: Public ---
public async Task SavePublicAsync(string key, object value)
{
var options = new PlayerSaveOptions(new PublicWriteAccessClassOptions());
await CloudSaveService.Instance.Data.Player.SaveAsync(
new Dictionary<string, object> { { key, value } }, options);
}
public async Task<string> LoadPublicAsync(string key)
{
var options = new PlayerLoadOptions(new PublicReadAccessClassOptions());
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
new HashSet<string> { key }, options);
return result.TryGetValue(key, out var item) ? item.Value.GetAsString() : null;
}
// --- Cloud Save: All Keys ---
public async Task<List<string>> LoadAllDefaultKeysAsync()
{
var result = await CloudSaveService.Instance.Data.Player.LoadAllAsync(
new PlayerLoadAllOptions(new DefaultReadAccessClassOptions()));
return new List<string>(result.Keys);
}
}
```
---
## PlayerAccountTester
Attach alongside `PlayerAccountManager` on the same GameObject. Signs in anonymously, logs identity, then exercises a Cloud Save round-trip and lists all Default keys.
```csharp
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
/// <summary>
/// Attach alongside PlayerAccountManager to run a quick in-Editor smoke test.
/// On start: signs in anonymously and logs player identity.
/// After sign-in: saves a probe value to the Default bucket, loads it back,
/// and logs all Default keys visible for this player.
/// </summary>
public class PlayerAccountTester : MonoBehaviour
{
[SerializeField] PlayerAccountManager m_Manager;
async void Start()
{
if (m_Manager == null)
m_Manager = GetComponent<PlayerAccountManager>();
m_Manager.SignedIn += OnSignedIn;
await m_Manager.SignInAnonymouslyAsync();
}
async void OnSignedIn()
{
LogIdentity("Signed in");
await Task.Delay(2000);
// Fetch the server-side player name — may be null after anonymous sign-in.
await m_Manager.GetPlayerNameAsync();
LogIdentity("After name fetch");
// Round-trip a probe value through the Default Cloud Save bucket.
const string key = "tester_probe";
const string expected = "ok";
await m_Manager.SaveDefaultAsync(key, expected);
var loaded = await m_Manager.LoadDefaultAsync(key);
var match = loaded == expected ? "OK" : $"MISMATCH (got '{loaded}')";
Debug.Log($"[PlayerAccountTester] Save→Load round-trip: {match}");
// List all keys in the Default bucket for this player.
var keys = await m_Manager.LoadAllDefaultKeysAsync();
LogKeys(keys);
}
void LogIdentity(string label)
{
var sb = new StringBuilder();
sb.AppendLine($"[PlayerAccountTester] {label}:");
sb.AppendLine($" PlayerId : {m_Manager.PlayerId ?? "<null>"}");
sb.AppendLine($" PlayerName : {(string.IsNullOrEmpty(m_Manager.PlayerName) ? "<none>" : m_Manager.PlayerName)}");
sb.AppendLine($" IsSignedIn : {m_Manager.IsSignedIn}");
Debug.Log(sb.ToString());
}
void LogKeys(List<string> keys)
{
var sb = new StringBuilder();
sb.AppendLine($"[PlayerAccountTester] Default bucket keys ({keys.Count}):");
foreach (var k in keys)
sb.AppendLine($" {k}");
Debug.Log(sb.ToString());
}
}
```
---
## Assembly Definition
Your `.asmdef` must reference both `Unity.Services.Authentication` and `Unity.Services.Authentication.PlayerAccounts`. These are **separate assemblies** within the same package (`com.unity.services.authentication`). Missing the PlayerAccounts reference causes `PlayerAccountService` to be unresolvable. `Unity.Services.CloudCode` is only needed if using Protected or Custom data writes.
```json
{
"name": "MyGame.PlayerAccount",
"rootNamespace": "",
"references": [
"Unity.Services.Core",
"Unity.Services.Authentication",
"Unity.Services.Authentication.PlayerAccounts",
"Unity.Services.CloudSave",
"Unity.Services.CloudCode"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
```
---
## Error Handling
### AuthenticationException
Thrown by all `AuthenticationService` methods. Extends `RequestFailedException`.
```csharp
try
{
await AuthenticationService.Instance.SignInAnonymouslyAsync();
}
catch (AuthenticationException ex)
{
// ex.ErrorCode contains a numeric error code from AuthenticationErrorCodes
Debug.LogError($"Auth failed: {ex.Message} (code: {ex.ErrorCode})");
}
catch (RequestFailedException ex)
{
// Network or other service errors
Debug.LogError($"Request failed: {ex.Message}");
}
```
Key error codes:
| Constant | Value | Meaning |
|---|---|---|
| `AccountAlreadyLinked` | 10003 | Username already exists (on sign-up) or external ID already linked |
| `InvalidParameters` | 10002 | Bad input parameters |
| `ClientInvalidUserState` | 10000 | Operation not valid in current state |
### PlayerAccountsException
Thrown by `PlayerAccountService` methods. Also extends `RequestFailedException`.
```csharp
try
{
await PlayerAccountService.Instance.StartSignInAsync();
}
catch (PlayerAccountsException ex)
{
Debug.LogError($"Player Accounts error: {ex.Message} (code: {ex.ErrorCode})");
}
```
Key error codes (from `PlayerAccountsErrorCodes`):
| Constant | Value | Meaning |
|---|---|---|
| `InvalidState` | 10101 | Player is already signed in |
| `MissingClientId` | 10102 | Client ID not configured in Unity Dashboard |
### CloudSaveException
Thrown by Cloud Save operations.
```csharp
try
{
await CloudSaveService.Instance.Data.Player.SaveAsync(data);
}
catch (CloudSaveValidationException ex)
{
foreach (var detail in ex.Details)
Debug.LogError($"Validation: {detail.Field} {string.Join(", ", detail.Messages)}");
}
catch (CloudSaveRateLimitedException ex)
{
Debug.LogError($"Rate limited. Retry after {ex.RetryAfter}s");
}
catch (CloudSaveException ex)
{
Debug.LogError($"Cloud Save error: {ex.Message} (reason: {ex.Reason})");
}
```
---
## Validation
After implementing a player account feature, verify:
1. **Compile check:** The project compiles without errors. All namespaces (`Unity.Services.Core`, `Unity.Services.Authentication`, `Unity.Services.Authentication.PlayerAccounts`, `Unity.Services.CloudSave`) resolve correctly.
2. **Initialization order:** `UnityServices.InitializeAsync()` is called before any sign-in method. Service singletons (`AuthenticationService.Instance`, `PlayerAccountService.Instance`, `CloudSaveService.Instance`) are accessed only after initialization.
3. **Assembly references:** The `.asmdef` references **both** `Unity.Services.Authentication` and `Unity.Services.Authentication.PlayerAccounts`. These are separate assemblies in the same package; omitting the PlayerAccounts reference makes `PlayerAccountService` unresolvable.
4. **SignedIn event wiring:** `PlayerAccountService.Instance.SignedIn` is subscribed **before** calling `StartSignInAsync()`. The `StartSignInAsync` Task completes when the browser opens, not when sign-in finishes.
5. **Access class matching:** Data saved with `PublicWriteAccessClassOptions` is loaded with `PublicReadAccessClassOptions`. Data saved with no options (Default) is loaded with `DefaultReadAccessClassOptions` or no options. Mismatched access classes return empty results without error.
6. **SignOut is synchronous:** `AuthenticationService.Instance.SignOut()` and `PlayerAccountService.Instance.SignOut()` are both `void` methods. Do not `await` them.
---
## Asset Store Building Block
- **Player Account Building Block:** Complete reference implementation with sign-in UI (anonymous, Unity browser, username/password), Cloud Save data management, and a Cloud Code module with generated client bindings. [Asset Store](https://assetstore.unity.com/packages/essentials/tutorial-projects/unity-building-block-player-account-341928)
SHA-256: 17c13b9252e48a82b9fabaaa394ac76f0a189186fa4b369e7c69cacdfd812ef2