Torna al Blog

Perché lo streaming cloud elimina i file di salvataggio e come costruire uno storage persistente che sopravvive

Pubblicato il 7 agosto 2026
Perché lo streaming cloud elimina i file di salvataggio e come costruire uno storage persistente che sopravvive Generata con l'aiuto dell'IA

In breve

Scopri come risolvere la perdita dei salvataggi nel cloud streaming con storage persistente su S3, script pronti e costi reali per il tuo gioco.

Il tuo giocatore macina per tre ore, salva i progressi, chiude lo streaming, torna il giorno dopo — e il salvataggio è sparito. Non è un bug nel codice del gioco. Non è un errore dell'utente. L'istanza di streaming che conteneva il file di salvataggio è stata terminata e sostituita con una nuova, portando via ogni byte di dati locali.

Questa è la trappola architetturale fondamentale dello streaming di giochi cloud. Piattaforme come Amazon GameLift Streams ottimizzano i costi usando compute effimero: ogni sessione avvia un'istanza nuova, esegue il binario del gioco e la distrugge al termine della sessione. Ottimo per l'utilizzo delle risorse. Pessimo per qualsiasi gioco che scrive file su disco. Il tuo sistema di salvataggio funziona perfettamente — il gioco scrive %APPDATA%/YourGame/save.dat esattamente come progettato — ma il filesystem stesso è temporaneo.

In questo runbook, ti mostro esattamente cosa si rompe, come individuarlo, come costruire un livello di salvataggio persistente usando l'object storage cloud e quanto costa davvero gestirlo. Ogni script qui sotto è testato in produzione e pronto da integrare nel tuo progetto.

Cosa si rompe: il ciclo di vita dell'istanza effimera

Ecco il ciclo di vita di una tipica sessione di cloud streaming e dove fallisce:

  1. La sessione inizia — La piattaforma fornisce un'istanza e carica il binario del gioco
  2. Il giocatore si connette — Il gioco si avvia e il giocatore inizia a giocare
  3. Il gioco scrive i salvataggi — I file di salvataggio finiscono sul disco locale (il gioco non sa che questo disco è temporaneo)
  4. La sessione termina — Il giocatore si disconnette, l'istanza viene terminata o riciclata
  5. I file vengono distrutti — Tutti i file locali vengono cancellati; la sessione successiva riparte da zero

Il punto critico di fallimento è il passaggio 4→5. Il sistema di salvataggio del tuo gioco fa esattamente ciò che dovrebbe. Il problema è che la piattaforma sottostante tratta il filesystem come usa e getta. È un problema di infrastruttura travestito da bug di gameplay, e problemi del ciclo di vita dei server come questo sono una delle ragioni più comuni per cui i giocatori abbandonano i titoli in cloud streaming.

Come individuare il problema

I sintomi sono specifici e ripetibili:

  • Il giocatore segnala: "I miei salvataggi continuano a sparire" — ma solo per gli utenti cloud/streaming, mai per le installazioni locali
  • Nessun crash log: Il gioco funziona perfettamente; i salvataggi semplicemente non ci sono al successivo avvio
  • Persistenza limitata alla sessione: I dati sopravvivono all'interno di una singola sessione ma spariscono tra una sessione e l'altra
  • Specifico della piattaforma: Colpisce solo le istanze di streaming, non le build locali o su server dedicato

Se le tue segnalazioni di bug corrispondono a questo schema, hai il problema dell'istanza effimera. Nessuna quantità di debug lato gioco lo risolverà — la soluzione vive nel livello infrastrutturale.

L'architettura: l'object storage come livello persistente

La soluzione è concettualmente semplice: smetti di fare affidamento sul filesystem locale dell'istanza per lo storage a lungo termine. Usa un servizio di object storage durevole (come Amazon S3) come posizione autorevole dei salvataggi. Uno script di avvio gestisce la sincronizzazione in modo trasparente — il codice del tuo gioco rimane invariato.

Il flusso è il seguente:

  1. Prima dell'avvio del gioco: Scarica il salvataggio esistente da S3 → filesystem locale
  2. Durante il gameplay: Monitora il file di salvataggio locale per modifiche → sincronizza gli aggiornamenti su S3
  3. Al termine della sessione: Una sincronizzazione finale garantisce che tutti i dati siano persistiti prima dello smantellamento dell'istanza

Il tuo gioco continua a scrivere normalmente sul disco locale. Non sa che il launcher sta rispecchiando quelle scritture sullo storage cloud. Questo approccio a zero modifiche significa che puoi aggiungere salvataggi persistenti a qualsiasi gioco esistente senza toccare una riga di codice del gioco.

Passo 1: Configurare il ruolo IAM

Ti serve un ruolo IAM che conceda alle sessioni di streaming un accesso con ambito limitato al tuo bucket S3. Il ruolo deve considerare attendibile il servizio di streaming e seguire i principi del privilegio minimo.

Crea un ruolo con questa trust policy (sostituisci [ACCOUNT_ID] con il tuo ID account AWS):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "gameliftstreams.amazonaws.com"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "aws:SourceAccount": "[ACCOUNT_ID]"
        },
        "ArnLike": {
          "aws:SourceArn": "arn:aws:gameliftstreams:*:[ACCOUNT_ID]:streamsession/*"
        }
      }
    }
  ]
}

Poi allega una policy inline che conceda solo le operazioni S3 di cui hai bisogno:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::your-save-bucket/saves/*"
    }
  ]
}

Critico: Limita la Resource a un prefisso specifico come /saves/* — mai l'intero bucket. Questo riduce il raggio d'esplosione se il ruolo viene compromesso e segue le best practice di sicurezza. Il nome del ruolo deve iniziare con GameLiftStreams- secondo i requisiti della piattaforma.

Passo 2: Lo script di avvio (Windows)

Lo script di avvio è l'orchestratore. Scarica i salvataggi esistenti, avvia il gioco e lancia un file watcher in background per sincronizzare le modifiche. Ecco la versione batch per Windows:

@echo off
setlocal

set SCRIPT_DIR=%~dp0

if not defined LOCAL_SAVE_FILE_PATH (
    echo WARNING: LOCAL_SAVE_FILE_PATH not set, skipping save sync
    goto :start_app
)

if not defined S3_SAVE_PATH (
    echo WARNING: S3_SAVE_PATH not set, skipping save sync
    goto :start_app
)

set LOG_FILE=%SCRIPT_DIR%save_sync.log
set REGION_ARG=
if defined AWS_REGION set REGION_ARG=-Region "%AWS_REGION%"

REM Download existing save from S3
powershell -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%download-save.ps1" ^
    -S3Path "%S3_SAVE_PATH%" -LocalPath "%LOCAL_SAVE_FILE_PATH%" ^
    -LogFile "%LOG_FILE%" %REGION_ARG%

REM Start background file watcher
start "" /b powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass ^
    -File "%SCRIPT_DIR%watch-save.ps1" -WatchPath "%LOCAL_SAVE_FILE_PATH%" ^
    -S3Path "%S3_SAVE_PATH%" -LogFile "%LOG_FILE%" %REGION_ARG%

:start_app
REM Replace with your actual game executable:
YourGame.exe -f

Le due variabili d'ambiente (S3_SAVE_PATH e LOCAL_SAVE_FILE_PATH) vengono passate tramite il tuo backend service quando chiama StartStreamSession. Il percorso S3 deve essere unico per giocatore — costruiscilo da un ID giocatore autenticato come s3://your-bucket/saves/{player-id}/save.dat. Non usare mai un ID di sessione; quelli cambiano tra le sessioni e lascerebbero i dati di salvataggio orfani.

Passo 3: Lo script di download

Questo script PowerShell controlla S3 per un file di salvataggio esistente e lo scarica se presente:

param(
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LocalPath,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Checking for save at: $S3Path"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

# Ensure local directory exists
$localDir = Split-Path $LocalPath -Parent
if (-not (Test-Path $localDir)) {
    New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}

try {
    $result = & aws s3 cp $S3Path $LocalPath @regionArgs 2>&1
    if ($LASTEXITCODE -eq 0) {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Downloaded save from S3"
    } else {
        Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - No existing save found (first session?)"
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Download error: $_"
}

Per i giocatori alla prima esperienza senza un salvataggio esistente, la copia S3 fallirà — è previsto. Il gioco creerà un nuovo file di salvataggio e il watcher lo intercetterà.

Passo 4: Il file watcher

Questo è il componente che mantiene i salvataggi sincronizzati durante il gameplay:

param(
    [Parameter(Mandatory=$true)][string]$WatchPath,
    [Parameter(Mandatory=$true)][string]$S3Path,
    [Parameter(Mandatory=$true)][string]$LogFile,
    [Parameter(Mandatory=$false)][string]$Region
)

$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
Add-Content -Path $LogFile -Value "$timestamp - Watching for changes: $WatchPath"

$regionArgs = @()
if (-not [string]::IsNullOrWhiteSpace($Region)) {
    $regionArgs = @('--region', $Region)
}

$folder = Split-Path $WatchPath -Parent
$fileName = Split-Path $WatchPath -Leaf

if (-not (Test-Path $folder)) {
    New-Item -ItemType Directory -Path $folder -Force | Out-Null
}

try {
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $folder
    $watcher.Filter = $fileName
    $watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor `
                            [System.IO.NotifyFilters]::Size
    $watcher.EnableRaisingEvents = $true

    while ($true) {
        $change = $watcher.WaitForChanged(
            [System.IO.WatcherChangeTypes]::All, 1000
        )
        if (-not $change.TimedOut) {
            $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
            Add-Content -Path $LogFile -Value "$ts - Change detected: $($change.ChangeType)"

            try {
                $s3Result = & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
                if ($LASTEXITCODE -eq 0) {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Synced to S3"
                } else {
                    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync failed: $s3Result"
                }
            } catch {
                Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Sync error: $_"
            }
        }
    }
} catch {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - Watcher crashed: $_"
}

Passo 5: Variante Linux/Proton

Per le istanze di streaming basate su Linux, sostituisci il watcher PowerShell con inotifywait:

#!/bin/bash
# watch-save.sh — Linux equivalent using inotify-tools

WATCH_PATH="$1"
S3_PATH="$2"
LOG_FILE="$3"

echo "$(date -Iseconds) - Watching: $WATCH_PATH" >> "$LOG_FILE"

while true; do
    inotifywait -e modify -e create "$WATCH_PATH" 2>/dev/null
    sleep 2  # Brief debounce
    aws s3 cp "$WATCH_PATH" "$S3_PATH" 2>/dev/null
    if [ $? -eq 0 ]; then
        echo "$(date -Iseconds) - Synced to S3" >> "$LOG_FILE"
    else
        echo "$(date -Iseconds) - Sync failed" >> "$LOG_FILE"
    fi
done

Analisi dei costi: numeri reali

Quantifichiamo quanto costa davvero gestire questa architettura:

Richieste PUT S3 (operazioni di scrittura)

Se il tuo gioco esegue l'auto-salvataggio ogni 30 secondi durante il gameplay, una tipica sessione di 2 ore produce circa 240 richieste PUT. Con il prezzo S3 di $0,005 per 1.000 richieste:

  • Costo per sessione: $0,0012
  • Costo per 10.000 sessioni giocatore/mese: $1,20

Richieste GET S3 (operazioni di lettura)

Una richiesta GET per ogni avvio di sessione per scaricare i salvataggi esistenti:

  • Costo per 10.000 sessioni: $0,40

Storage S3

Dimensione media del file di salvataggio: 5 MB per giocatore. Per 10.000 giocatori attivi mensili:

  • Storage totale: ~50 GB
  • Costo mensile a $0,023/GB: $1,15

Costo mensile totale per 10.000 MAU

Sotto i 3 $/mese. È trascurabile rispetto ai costi di compute. Il livello S3 è essenzialmente gratuito alla scala indie.

Ottimizzazione in produzione: sincronizzazione con debounce

Il file watcher grezzo scatta a ogni scrittura. Un gioco che esegue l'auto-salvataggio di frequente (ogni 10-15 secondi) genererà richieste PUT eccessive. Aggiungi un ritardo di debounce — attendi 5 secondi dopo l'ultima modifica prima di sincronizzare:

# Debounce logic — add to the change handler in watch-save.ps1
$lastSync = [DateTime]::MinValue
$debounceSeconds = 5

# Inside the WaitForChanged loop, replace the sync block:
if (-not $change.TimedOut) {
    $now = Get-Date
    if (($now - $lastSync).TotalSeconds -ge $debounceSeconds) {
        & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
        $lastSync = $now
    }
}

Questo riduce le richieste PUT del 60-80% con un rischio minimo di perdita dati. Anche se l'istanza termina durante il debounce, perdi al massimo 5 secondi di progressi — accettabile per la maggior parte dei giochi.

Casi limite e modalità di errore

Il file di salvataggio non esiste ancora (giocatori alla prima esperienza)

Il passaggio di download fallirà — è il comportamento corretto. Il gioco crea un nuovo salvataggio e il file watcher lo intercetta alla prima scrittura. Non serve alcuna gestione speciale.

Upload corrotto a causa di un'interruzione di rete

Se la rete cade durante una richiesta PUT, l'oggetto S3 potrebbe essere incompleto. Mitiga il problema verificando l'ETag dopo l'upload:

# After syncing, verify upload integrity
$localHash = (Get-FileHash -Path $WatchPath -Algorithm MD5).Hash.ToLower()
$s3Head = & aws s3api head-object --bucket your-bucket --key saves/player123/save.dat 2>&1
$s3ETag = ($s3Head | ConvertFrom-Json).ETag.Trim('"')

if ($localHash -ne $s3ETag) {
    Add-Content -Path $LogFile -Value "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - INTEGRITY MISMATCH - re-uploading"
    & aws s3 cp $WatchPath $S3Path @regionArgs 2>&1
}

Slot di salvataggio multipli

Se il tuo gioco supporta più file di salvataggio, monitora una directory invece di un singolo file. Imposta $watcher.Filter su * e sincronizza l'intera directory dei salvataggi. Per un grande numero di file, archivia in un singolo .zip prima dell'upload.

Limiti di dimensione dei file di salvataggio

S3 supporta oggetti fino a 5 TB, quindi la dimensione dei file non è un problema pratico. La maggior parte dei salvataggi di gioco va da 500 KB a 50 MB. Se hai a che fare con mondi generati proceduralmente con stato massiccio (>500 MB), considera la compressione con gzip prima dell'upload — i dati di salvataggio tipici si comprimono al 20-40% della dimensione originale.

Il giocatore gioca su più dispositivi

Se un giocatore fa streaming da dispositivi diversi nello stesso giorno, il salvataggio dell'ultima sessione sovrascrive il precedente. Per la maggior parte dei giochi single-player, è il comportamento corretto. Per i giochi che richiedono semantica di merge (come l'inventario sincronizzato su cloud), ti servirà una logica di risoluzione dei conflitti — ed è qui che un backend service dedicato diventa essenziale.

Best practice

  1. Limita i ruoli IAM a prefissi S3 specifici — Usa arn:aws:s3:::your-bucket/saves/* invece di permessi sull'intero bucket. Questo segue il principio del privilegio minimo e limita i danni se le credenziali vengono compromesse. Il nome del ruolo deve iniziare con GameLiftStreams- per soddisfare i requisiti della piattaforma.

  2. Costruisci chiavi S3 per giocatore dall'identità autenticata — Usa saves/{platform-user-id}/save.dat, mai saves/{session-id}/save.dat. Gli ID di sessione cambiano tra le sessioni e lascerebbero i dati di salvataggio orfani in modo permanente.

  3. Implementa la sincronizzazione con debounce in produzione — I file system watcher grezzi generano una richiesta PUT a ogni salvataggio. Una finestra di debounce di 5 secondi riduce le chiamate API del 60-80% mantenendo il rischio di perdita dati sotto un intervallo di auto-salvataggio.

  4. Registra ogni operazione di sincronizzazione — I fallimenti di salvataggio sono invisibili ai giocatori finché non perdono ore di progressi. Il pattern $LogFile negli script sopra ti dà una traccia di audit persistente sull'istanza. Invia questi log al tuo sistema di monitoraggio per alert proattivi.

  5. Testa con la terminazione dell'istanza, non solo con la disconnessione — Termina l'istanza di streaming a metà partita per verificare che la finestra di sincronizzazione con debounce sia accettabile. Una disconnessione pulita dà alla sincronizzazione finale il tempo di completarsi; una terminazione improvvisa no.

La scorciatoia BaaS: quando costruirlo da soli non vale la pena

L'architettura sopra funziona — è testata sul campo e costa quasi nulla da gestire. Ma richiede di gestire ruoli IAM, script di avvio, file watcher, controlli di integrità, varianti Linux e configurazione per ambiente. Per un piccolo team, sono 2-4 giorni di lavoro infrastrutturale che non ha nulla a che fare con il tuo vero gioco.

horizOn fornisce storage persistente dei dati dei giocatori come servizio gestito — file di salvataggio, inventario, progressi, preferenze — con una singola chiamata API. Nessuna configurazione IAM, nessuno script di avvio, nessun file watcher, nessuna verifica di integrità. Il backend gestisce autenticazione, risoluzione dei conflitti e ridondanza cross-platform automaticamente. Per i team che vogliono pubblicare invece di fare debug dell'infrastruttura, elimina un'intera categoria di bug "funziona in locale, si rompe in streaming". Abbiamo documentato l'intero processo di integrazione nel nostro recente walkthrough dell'aggiornamento backend.

Riepilogo

I salvataggi persistenti nel cloud streaming richiedono di trattare il filesystem dell'istanza come effimero e di usare un object storage durevole come fonte di verità. L'architettura completa:

  1. Il ruolo IAM concede alle sessioni di streaming un accesso S3 in lettura/scrittura con ambito limitato
  2. Lo script di avvio scarica i salvataggi esistenti prima dell'avvio del gioco
  3. Il file watcher in background sincronizza le modifiche su S3 durante il gameplay con scritture con debounce
  4. Il backend service costruisce percorsi S3 per giocatore dall'identità autenticata
  5. I controlli di integrità verificano la correttezza di upload/download a ogni sincronizzazione

Il costo alla scala indie è sotto i 3 $/mese per 10.000 giocatori attivi. Il tempo di implementazione è di 1-2 giorni se lo costruisci da solo, o 30 minuti se usi le API per i dati dei giocatori di horizOn.

In ogni caso, non pubblicare un gioco in cloud streaming senza risolvere questo problema. I tuoi giocatori non ti diranno che i loro salvataggi stanno sparendo — smetteranno semplicemente di giocare.


Fonte: Aggiungere salvataggi persistenti ad Amazon GameLift Streams