Contents
TutorialUnity

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.

c#
[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

DataInclude?Notes
Player level and XPYesCore progression.
Inventory item IDsYesStore stable IDs, not prefab names if they can change.
Current checkpointYesPrefer checkpoint IDs over raw world position.
Active particle effectsNoRebuild from gameplay data.
Temporary combat targetsNoUsually stale after reload.

Save Service Shape

Centralize save operations so UI, gameplay systems, and cloud sync do not duplicate persistence logic.

c#
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 SaveDataAsync writes 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.

  • 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.

c#
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:

c#
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.

  1. Read local save through LoadDataAsync.
  2. Apply it if valid.
  3. Optionally call RefreshDataAsync or ForceSyncDataAsync from a safe lifecycle moment.
  4. Apply refreshed data only after schema validation.
  5. 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.