Skip to main contentSkip to contact
Ocean View Games
Ocean View
Games
Blog header banner

Cross-Platform Save Systems: Cloud Sync Done Right

David Edgecombe

By David Edgecombe

·11 min read

What this post covers

A technical guide to implementing cross-platform cloud save systems in Unity, covering conflict resolution, offline-first architecture, and platform APIs.

Few systems in game development are as universally needed - and as consistently underestimated - as save data management. When your game lives on a single platform and a single device, saving is straightforward. The moment you add a second platform or a second device, you inherit an entirely new class of engineering problems: conflict resolution, offline play, schema evolution, and platform-specific API compliance.

We have built save systems for games across the complexity spectrum. Our work on Domi Online, a persistent MMORPG with a server-authoritative backend, demanded a fundamentally different architecture than a mobile puzzle game. David's time working on RuneScape Mobile at Jagex also reinforced the wider lesson behind this article: long-lived games make persistence and backwards compatibility architectural concerns, not implementation details.

This post is a technical guide for Unity developers building cross-platform cloud save systems. We will cover architecture decisions, platform-specific APIs, conflict resolution strategies, offline-first design, and schema migration for live games.


Why Cloud Save Is Harder Than It Looks

At first glance, cloud save seems simple: serialise the game state, upload it to a server, download it on another device. In practice, every step introduces complexity:

  • What happens when the player is offline? Mobile players lose connectivity constantly. Your game must remain fully playable.
  • What happens when two devices have divergent saves? The player progresses on their phone, then opens the game on their tablet before the phone syncs. Which save wins?
  • What happens when you update the game? The save schema changes, but the player's cloud data is still in the old format.
  • What happens when the platform API fails? Google Play Games Services, Game Center, and Steam Cloud each have their own failure modes, rate limits, and quirks.

A robust cloud save system must handle all of these cases gracefully, without data loss and without confusing the player.


Architecture: The Three Layers

We recommend structuring your cloud save system as three distinct layers. This separation makes each layer independently testable and replaceable.

graph TD
    A[Game Runtime] <-->|Read/Write| B[Local Cache]
    B <-->|Sync| C[Sync Manager]
    C <-->|Upload/Download| D[Cloud Backend]
    C -->|Conflict?| E[Resolution Strategy]
    E --> B

Layer 1: The Save Data Model

Your save data should be a plain C# class (or set of classes) with no dependencies on Unity types, MonoBehaviour, or any platform SDK. This makes it serialisable, testable, and portable.

[System.Serializable]
public class PlayerSaveData
{
    public int schemaVersion;
    public long lastModifiedUtc;
    public string deviceId;
    public PlayerProgress progress;
    public InventoryData inventory;
    public SettingsData settings;
}

Key principles:

  • Include a schema version - you will need this for migration (covered below).
  • Include a timestamp - lastModifiedUtc is essential for conflict resolution. Use UTC epoch milliseconds to avoid timezone issues.
  • Include a device identifier - useful for debugging and for conflict resolution UI.
  • Separate volatile and stable data - settings change rarely; progress changes constantly. Splitting them reduces sync frequency and payload size.

Layer 2: The Local Persistence Layer

This layer handles reading from and writing to the device's local storage. It operates independently of any cloud service and is the foundation of your offline-first design.

Responsibilities:

  1. Serialisation - convert the save model to bytes. We recommend JSON for debug-friendliness during development and binary (MessagePack or a custom binary format) for production. JSON is human-readable but larger; binary is compact but opaque.

  2. Encryption - if your save data contains anything the player could benefit from tampering with (currency, progression, unlocks), encrypt it locally. AES-256 with a device-derived key is a reasonable baseline. For competitive games, this is non-negotiable.

  3. Atomic writes - never write directly to the save file. Write to a temporary file, then rename it. This prevents corruption if the app is killed mid-write. On mobile, this is more common than you might expect.

  4. Backup copies - maintain the previous save as a rollback option. If the current save is corrupted, the player loses one session instead of everything.

Layer 3: The Cloud Sync Layer

This layer manages communication with the cloud backend. It should be completely decoupled from the local persistence layer via an interface, allowing you to swap cloud providers without touching your save logic.

public interface ICloudSaveProvider
{
    Task<CloudSaveResult> UploadAsync(byte[] data, SaveMetadata metadata);
    Task<CloudSaveDownload> DownloadAsync();
    Task<bool> DeleteAsync();
    bool IsAuthenticated { get; }
}

This interface can then have concrete implementations for each platform: GameCenterSaveProvider, GooglePlaySaveProvider, SteamCloudSaveProvider, and a PlayFabSaveProvider or custom backend provider for your own infrastructure.


Platform-Specific APIs

Each platform provides its own cloud save API with distinct characteristics. Here is what you need to know about each.

Platform Comparison

Platform APIs, quotas, and behaviours change over time. Treat this as an architectural comparison and verify the current platform documentation before implementation.

Platform Comparison
Apple Game Center Google Play Games Steam Cloud
API GKSavedGame (GameKit) Saved Games API (Play Games SDK v2) ISteamRemoteStorage (Steamworks)
Storage limit No hard limit, Apple recommends under a few MB 3 MB per save slot (multiple slots) Configurable per app (typically 100 MB-1 GB)
Conflict handling Detects conflicts, provides array of conflicting saves. You must resolve explicitly Conflict callback with local and server versions. You implement resolution Last-write-wins by default. Use RemoteStorageFileConflict_t for custom handling
Quirks Tied to Apple ID. No Game Center sign-in means no cloud save Requires Google Play Games sign-in, which some players decline. Need a fallback File-based, not structured data. Syncs based on configured file paths in Steamworks
Offline Local saves work independently, sync on reconnect Caches locally, syncs when connectivity restores Steam Deck and offline mode use local cache, sync on reconnection

Custom Backend (PlayFab, Firebase, or Self-Hosted)

For cross-platform games that need a single source of truth regardless of storefront, a custom backend is often the right choice:

Custom Backend (PlayFab, Firebase, or Self-Hosted)
Backend Description Best for
PlayFab Microsoft's BaaS with Player Data APIs and automatic timestamp-based conflict resolution Games already using PlayFab for multiplayer or analytics
Firebase / Firestore Google's offering with strong offline support and automatic sync Mobile games needing reliable cross-device sync
Self-hosted Maximum control but maximum maintenance burden Games with server-authoritative state (like Domi Online, where the server is the canonical source of truth)

Key Takeaway: For single-platform games, use the native platform API. For cross-platform games, use a backend service (PlayFab, Firebase, or custom) as the canonical source and treat platform APIs as optional sync accelerators.


Conflict Resolution Strategies

Save conflicts are inevitable in any cloud sync system. The question is not whether they will happen, but how you handle them when they do.

Strategy 1: Last Write Wins (LWW)

The simplest approach: compare timestamps and keep the newest save.

Pros: Easy to implement, easy to reason about.

Cons: The "newest" save is not always the "best" save. A player who progresses significantly on Device A, then opens Device B briefly (which uploads an older save with a newer timestamp), will lose their progress.

When to use: Casual games where save data is small and progress loss is minor.

Strategy 2: Merge by Field

Compare individual fields and take the "best" value from each version. For example:

  • highScore: take the maximum
  • totalCoinsEarned: take the maximum
  • levelsCompleted: take the union of completed levels
  • settings: take the most recently modified

Pros: Preserves the most progress. Players rarely notice conflicts.

Cons: More complex to implement. Requires defining merge rules for every field. Some fields cannot be merged meaningfully.

When to use: Mobile games with incremental progress (puzzle games, idle games, educational titles).

Strategy 3: Player Choice

Present the conflict to the player with clear information: "Device A: Level 47, 3 hours ago. Device B: Level 42, 1 hour ago. Which save would you like to keep?"

Pros: The player always gets what they expect. No silent data loss.

Cons: Interrupts the play experience. Confusing for non-technical players. Requires UI work.

When to use: Games where progress is significant and reversible loss would be frustrating (RPGs, strategy games, lengthy campaigns).

Strategy 4: Server-Authoritative (No Conflicts)

The server is the canonical source of truth. The client sends actions (not state), and the server applies them. There is no concept of "conflicting saves" because the server's state is always correct.

Pros: Eliminates conflicts entirely. Prevents cheating.

Cons: Requires a persistent server connection (or an action queue for offline play). Significantly more complex and expensive to build.

When to use: Competitive multiplayer games, games with real-money economies, MMOs. This is the architecture we use for Domi Online, where the server-authoritative backend built with FishNet on AWS is the single source of truth for all player state.


Offline-First Architecture

Mobile players will lose connectivity. Your game must continue to work. This is not an edge case - it is the normal operating condition for mobile games.

An offline-first save system follows these principles:

  1. The local save is always the primary data source at runtime. The game reads from and writes to local storage. Cloud sync happens asynchronously in the background.

  2. Never block gameplay on a network call. If the cloud upload fails, queue it for retry. If the cloud download fails, use the local save. The player should never see a loading spinner waiting for a cloud operation.

  3. Sync on key events, not on a timer. Good sync triggers include:

    • App launch (download latest cloud save)
    • App backgrounding / suspension (upload current save)
    • After significant progress milestones (level completion, boss defeat)
    • When network connectivity is restored after a loss
  4. Handle the "cold start" case. When a player installs the game on a new device, the local save does not exist. The game must attempt a cloud download before showing the main menu. This is the one case where a brief loading state is acceptable.

  5. Implement retry with exponential backoff. Cloud APIs fail. Rate limits kick in. Network conditions fluctuate. A retry strategy with exponential backoff (1s, 2s, 4s, 8s, capped at 60s) prevents hammering the server while ensuring eventual consistency.


Schema Migration for Live Games

Your save data format will change. You will add new features, remove deprecated fields, restructure data for performance, or fix bugs in how data was stored. When this happens, players will have cloud saves in the old format that your new code must be able to read.

The Version Field Approach

This is why the schemaVersion field in your save data model is critical. Every time you change the save structure, increment the version and write a migration function.

public static PlayerSaveData Migrate(PlayerSaveData data)
{
    if (data.schemaVersion < 2)
    {
        // v1 -> v2: Added inventory system
        data.inventory ??= new InventoryData();
    }
    if (data.schemaVersion < 3)
    {
        // v2 -> v3: Renamed 'coins' to 'softCurrency'
        data.progress.softCurrency = data.progress.coins;
    }
    data.schemaVersion = CURRENT_VERSION;
    return data;
}

Migration Rules

  1. Migrations must be forward-only. Never delete a migration function. A player who has not opened your game since version 1 must be able to migrate through v1 -> v2 -> v3 -> ... -> current in a single chain.

  2. Migrations must be idempotent. Running the same migration twice on the same data must produce the same result.

  3. Test migrations with real data. Capture save files from each version and include them in your test suite. Automated migration tests catch regressions that manual testing will miss.

  4. Handle missing fields with defaults. When deserialising an old save, new fields will be null or zero. Your migration code must provide sensible defaults.

  5. Consider backward compatibility. If your game allows older clients to connect (common in soft-launch), the server must handle both old and new save formats simultaneously.

Key Takeaway: Ship a schema version field from day one, even if you do not plan to change the format. Adding it retroactively to saves that do not have it is the single most painful migration you will ever write.


Security Considerations

Save data security depends on your game's context:

  • Single-player casual games - basic encryption prevents casual tampering but is not worth significant investment. Players who hack their own save data in a non-competitive game are not causing harm.

  • Competitive games with leaderboards - encrypt local saves, validate on upload, and consider server-side verification of key metrics (scores, completion times).

  • Games with real-money economies - server-authoritative state is mandatory. Local save data should be treated as a cache, not a source of truth. This is the approach we took with Domi Online, where the high-stakes economy demanded that all player state be validated server-side.

Common Attack Vectors

  1. Local save file editing - players modify JSON or binary saves on rooted/jailbroken devices. Encryption and checksum validation mitigate this.

  2. Time manipulation - players change their device clock to exploit timestamp-based logic (cooldowns, daily rewards). Use server time for anything with economic impact.

  3. Replay attacks - players restore a backup save after spending premium currency to "refund" the purchase. Server-side receipt validation and sequential save versioning prevent this.


Implementation Checklist

Before shipping your cloud save system, verify that it handles these scenarios:

  • Player has no internet connection at launch
  • Cloud download fails mid-transfer
  • Cloud upload fails mid-transfer
  • Player opens game on two devices simultaneously
  • Player's device clock is set incorrectly
  • Player updates the app and the save schema has changed
  • Player uninstalls and reinstalls the app
  • Player declines platform sign-in (Game Center, Google Play Games)
  • Cloud storage quota is exceeded
  • Player's cloud save is corrupted or empty
  • Player switches platform accounts

Each of these is a real scenario we have encountered in production games. Testing them before launch is significantly cheaper than debugging them after.


Choosing the Right Approach for Your Game

The architecture you need depends on your game's characteristics:

Choosing the Right Approach for Your Game
Game Type Recommended Approach
Single-player mobile (casual) Local save + platform API (Game Center / Google Play) with LWW
Single-player mobile (mid-core) Local save + platform API with merge-by-field
Cross-platform single-player Local save + backend service (PlayFab/Firebase) with player choice on conflict
Cooperative multiplayer Backend service with server-authoritative state for shared data, local for settings
Competitive multiplayer / MMO Fully server-authoritative, local cache for offline resilience

Start with the simplest approach that meets your requirements. You can always upgrade from LWW to merge-by-field, or from platform APIs to a custom backend, if your game's needs evolve.

Share