Contents
How To Save Player Progress In Unity
Design a Unity player-progress save model that can sync to cloud storage without blocking gameplay.
Unity save systems work best when gameplay code does not know where saves are stored. Keep save creation, serialization, local-first SDK writes, and cloud sync behind a small service.
This tutorial uses the public Unity facade shape: PersistlyGameSaves.ConfigureAsync, SaveDataAsync, LoadDataAsync, and ForceSyncDataAsync for one-save games. Named slots are still available when your game needs manual saves or multiple slots.
Save Model
Start with a serializable data object. Avoid storing scene objects, transforms from temporary enemies, or runtime-only references.
[Serializable]
public sealed class PlayerSaveData
{
public int schemaVersion = 1;
public string updatedAt;
public string checkpointId;
public int level;
public int xp;
public int coins;
public List<InventoryItemSave> inventory = new();
}
[Serializable]
public sealed class InventoryItemSave
{
public string itemId;
public int quantity;
}What Belongs In Progress Saves
| Data | Include? | Notes |
|---|---|---|
| Player level and XP | Yes | Core progression. |
| Inventory item IDs | Yes | Store stable IDs, not prefab names if they can change. |
| Current checkpoint | Yes | Prefer checkpoint IDs over raw world position. |
| Active particle effects | No | Rebuild from gameplay data. |
| Temporary combat targets | No | Usually stale after reload. |
Save Service Shape
Centralize save operations so UI, gameplay systems, and cloud sync do not duplicate persistence logic.
using System;
using System.Linq;
using System.Threading.Tasks;
using Persistly.Unity;
using UnityEngine;
public sealed class SaveService
{
public bool SyncPending { get; private set; }
public async Task ConfigureAsync(string runtimeKey, string playerRef)
{
await PersistlyGameSaves.ConfigureAsync(new PersistlyGameSavesSettings(runtimeKey)
{
PlayerRef = playerRef,
Store = new FilePersistlyGameSavesStore(Application.persistentDataPath),
});
}
public async Task SaveAsync(PlayerState playerState)
{
var save = BuildSave(playerState);
var saved = await PersistlyGameSaves.Shared.SaveDataAsync(save);
if (saved.Status == PersistlySlotStatus.LocalSaved)
{
SyncPending = true;
}
var sync = await PersistlyGameSaves.Shared.ForceSyncDataAsync();
if (sync.Status == PersistlySlotStatus.Synced)
{
SyncPending = false;
}
if (sync.Status == PersistlySlotStatus.Conflict)
{
ShowConflictRecovery();
}
}
public async Task<PlayerSaveData?> LoadAsync()
{
var loaded = await PersistlyGameSaves.Shared.LoadDataAsync<PlayerSaveData>();
return loaded.Data;
}
private static PlayerSaveData BuildSave(PlayerState data)
{
return new PlayerSaveData
{
updatedAt = DateTimeOffset.UtcNow.ToString("O"),
checkpointId = data.CheckpointId,
level = data.Level,
xp = data.Xp,
coins = data.Coins,
inventory = data.Inventory.Select(item => new InventoryItemSave
{
itemId = item.Id,
quantity = item.Quantity
}).ToList()
};
}
}playerRef should be a non-secret reference from your own game or account system. Persistly does not expose public account lookup or recovery by playerRef.
Player-facing rule: if
SaveDataAsyncwrites locally, the game can continue. Cloud sync failure should become pending data, not lost progress.
Save Timing
Use explicit save points for high-value changes and periodic autosave for longer sessions.
Recommended Events
- After checkpoint activation
- After mission completion
- After purchase or reward grant
- Before returning to title screen
- During controlled autosave intervals
Unity-Specific Notes
Use OnApplicationPause and OnApplicationQuit as backup triggers, not as the only save path. Mobile platforms may suspend quickly, and asynchronous work may not complete during shutdown.
private async void OnApplicationPause(bool paused)
{
if (!paused)
{
return;
}
await saveService.SaveAsync(playerState);
await PersistlyGameSaves.Shared.ForceSyncDataAsync();
}Multiple Slots Or Manual Saves
The default data helpers are intentionally small. When your game has a slot-select screen or manual save slots, switch to named slots:
await PersistlyGameSaves.Shared.SaveSlotAsync("warrior", warriorState);
await PersistlyGameSaves.Shared.SaveSlotAsync("mage", mageState);
var warrior = await PersistlyGameSaves.Shared.LoadSlotAsync<PlayerSaveData>("warrior");Use stable slot keys that describe player intent, not database IDs.
Loading Strategy
Load local data first, then refresh from Persistly when your game needs a cloud check.
- Read local save through
LoadDataAsync. - Apply it if valid.
- Optionally call
RefreshDataAsyncorForceSyncDataAsyncfrom a safe lifecycle moment. - Apply refreshed data only after schema validation.
- If Persistly reports a conflict, show conflict recovery instead of overwriting silently.
Production Checklist
- Validate every save before applying it.
- Keep one migration path per
schemaVersion. - Use stable IDs for inventory, quests, and checkpoints.
- Show "sync pending" when cloud writes fail.
- Test saves with airplane mode, app suspend, and forced quit.