# 3. Player Data Save and Load Part of [[Cloud Save and Unity Services]]. Prev: [[2. Authentication Bootstrap]]. Next: [[4. Write Locks Conflicts and Offline]]. All APIs in this part are on `CloudSaveService.Instance.Data.Player` (`IPlayerDataService`, Cloud Save **3.4.2**). Sign in first (part 2). Scripts were **not compiled** and were **not** play-tested on a live project. ## 3.1 Keys Official 3.4.2 remarks on `SaveAsync`: a key may contain **alphanumeric characters, dashes, and underscores**, and may be up to **255** characters. There is no useful client-side validation - a bad key surfaces as `CloudSaveValidationException` (or a more general `CloudSaveException`) from the service. Use boring keys that match local slots: `slot_0`, `slot_1`, `settings`. Do not put spaces or a file path in the key. ## 3.2 Save without a write lock (overwrite) ```csharp using System.Collections.Generic; using System.Threading.Tasks; using Unity.Services.CloudSave; public static class CloudSavePlayerData { public static async Task<IReadOnlyDictionary<string, string>> SaveJsonAsync(string key, string json) { // Overwrites any value currently stored under key. Returns updated write locks. return await CloudSaveService.Instance.Data.Player.SaveAsync( new Dictionary<string, object> { { key, json } }); } } ``` Official: `SaveAsync(IDictionary<string, object>)` uploads one or more pairs **without write-lock validation**, overwriting whatever is stored under those keys. The return value is `Task<Dictionary<string, string>>` - saved keys and their **updated write locks**. Keep that dictionary if the next write will be a locked write (part 4). You can pass several keys in one call. Prefer one key per slot file so a failed slot_1 write does not share a mental model with slot_0. ## 3.3 Load ```csharp using System.Collections.Generic; using System.Threading.Tasks; using Unity.Services.CloudSave; using Unity.Services.CloudSave.Models; public static class CloudSavePlayerData { public static async Task<string> LoadJsonAsync(string key) { var result = await CloudSaveService.Instance.Data.Player.LoadAsync( new HashSet<string> { key }); if (!result.TryGetValue(key, out Item item) || item?.Value == null) return null; return item.Value.GetAsString(); } } ``` Official: `LoadAsync(ISet<string>)` returns `Dictionary<string, Item>` for the keys that exist. A missing key is simply absent - there is no special "not found" item. `Item.Value` is `IDeserializable`. This series uses official **`GetAsString()`**, which is what High Scores Part 4 already calls, because we stored a JSON **string**. `GetAs<T>()` is also official on `IDeserializable` in 3.4.2; skip it here so you do not deserialize a different shape than `JsonUtility` already uses on disk. Useful official `Item` properties: `Key`, `Value`, `WriteLock`, `Created`, `Modified` (`DateTime?`). Also official: - `LoadAllAsync()` - every key for this player. Fine for a classroom slot or two; think before calling it on an account that stored a lot of keys. - `ListAllKeysAsync()` - returns `List<ItemKey>` (metadata). Confirm `ItemKey` fields in 3.4.2 IntelliSense if you build a browser UI; this series only needs Load of known slot keys. - `DeleteAsync(string key, …)` - one key; optional write-lock validation on the 3.4.2 overloads. Official: if the key does not exist, you do **not** get a dedicated "missing" signal. - `DeleteAllAsync()` - all keys, **without** write-lock validation. Treat this as dangerous; do not put it on a player-facing button in this course. `LoadAsync` / `SaveAsync` also have overloads that take `LoadOptions` / `SaveOptions` (AccessClass, and in some cases PlayerId). This series uses the default player-data path. If you need those options, open the 3.4.2 API page and confirm the types; do not invent AccessClass enum values from memory. ## 3.4 Exceptions you should catch Official throws (method-dependent): | Type | When | |---|---| | `CloudSaveException` | General unsuccessful request | | `CloudSaveValidationException` | Validation error (bad key, etc.) | | `CloudSaveRateLimitedException` | Rate limited | | `CloudSaveConflictException` | Write-lock mismatch (part 4, `SaveItem` / locked `DeleteAsync`) | Catch these around the **mirror** call. Do not let them fail the local save path. Rate limits are a good reason to retry later with backoff, not a good reason to tight-loop `SaveAsync` in `Update`. ## 3.5 Exercise After `UgsBootstrap.EnsureSignedInAsync`, save a tiny JSON string under `slot_demo`, load it back with `GetAsString()`, and log the returned write lock. Then load a key you never saved and confirm you get a missing entry, not a thrown "not found" you invented. ## Sources - `IPlayerDataService` 3.4.2: https://docs.unity3d.com/Packages/[email protected]/api/Unity.Services.CloudSave.Internal.IPlayerDataService.html - `Item` 3.4.2: https://docs.unity3d.com/Packages/[email protected]/api/Unity.Services.CloudSave.Models.Item.html - `IDeserializable` 3.4.2: https://docs.unity3d.com/Packages/[email protected]/api/Unity.Services.CloudSave.Internal.Http.IDeserializable.html