# 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