Warum Cloud-Streaming Spielstände löscht und wie du persistenten Speicher aufbaust, der überlebt
Kurz und knapp
Erfahre, warum Cloud-Streaming Spielstände löscht und wie du mit S3 persistenten Speicher für Game-Saves aufbaust, der Instanz-Terminierung übersteht.
Dein Spieler grindet drei Stunden, speichert seinen Fortschritt, schließt den Stream, kommt am nächsten Tag zurück – und sein Spielstand ist weg. Kein Bug in deinem Game-Code. Kein Benutzerfehler. Die Streaming-Instanz, die seinen Spielstand enthielt, wurde beendet und durch eine frische ersetzt – und hat dabei jedes Byte lokaler Daten mitgenommen.
Das ist die grundlegende architektonische Falle des Cloud-Game-Streamings. Plattformen wie Amazon GameLift Streams optimieren auf Kosteneffizienz, indem sie ephemeres Compute nutzen: Jede Session startet eine frische Instanz, führt dein Game-Binary aus und reißt sie beim Session-Ende wieder ab. Super für die Ressourcennutzung. Furchtbar für jedes Spiel, das Dateien auf die Platte schreibt. Dein Save-System funktioniert einwandfrei – das Spiel schreibt %APPDATA%/YourGame/save.dat exakt wie designed – aber das Dateisystem selbst ist temporär.
In diesem Runbook zeige ich dir genau, was kaputtgeht, wie du es erkennst, wie du mit Cloud-Object-Storage eine persistente Save-Schicht aufbaust und was das im Betrieb tatsächlich kostet. Jedes Skript unten ist produktionserprobt und bereit, direkt in dein Projekt übernommen zu werden.
Was kaputtgeht: Der Lebenszyklus der ephemeren Instanz
So sieht der Lebenszyklus einer typischen Cloud-Streaming-Session aus – und wo er scheitert:
- Session startet – Die Plattform stellt eine Instanz bereit und lädt dein Game-Binary hoch
- Player verbindet sich – Das Spiel startet, der Player beginnt zu spielen
- Spiel schreibt Saves – Save-Dateien landen auf der lokalen Platte (das Spiel ahnt nicht, dass diese Platte temporär ist)
- Session endet – Der Player trennt die Verbindung, die Instanz wird beendet oder recycelt
- Dateien werden vernichtet – Alle lokalen Dateien werden gelöscht; die nächste Session startet bei null
Der kritische Fehlerpunkt ist Schritt 4→5. Dein Save-System macht genau das, was es soll. Das Problem ist, dass die Plattform darunter das Dateisystem als Wegwerfware behandelt. Das ist ein Infrastrukturproblem im Gewand eines Gameplay-Bugs, und Server-Lifecycle-Probleme wie diese sind einer der häufigsten Gründe, warum Player Cloud-Streaming-Titel aufgeben.
So erkennst du das Problem
Die Symptome sind spezifisch und wiederholbar:
- Player meldet: „Mein Spielstand verschwindet ständig“ – aber nur bei Cloud-/Streaming-Nutzern, nie bei lokalen Installationen
- Keine Crash-Logs: Das Spiel läuft einwandfrei; die Saves sind nur beim nächsten Start nicht da
- Session-gebundene Persistenz: Daten überleben innerhalb einer Session, verschwinden aber zwischen Sessions
- Plattformspezifisch: Betrifft nur Streaming-Instanzen, nicht lokale oder Dedicated-Server-Builds
Wenn deine Bug-Reports diesem Muster entsprechen, hast du das Ephemeral-Instance-Problem. Egal wie viel du auf der Game-Seite debuggst – die Lösung liegt in der Infrastruktur-Schicht.
Die Architektur: Object Storage als persistente Schicht
Die Lösung ist konzeptionell einfach: Verlass dich für die langfristige Speicherung nicht mehr auf das lokale Dateisystem der Instanz. Nutze einen dauerhaften Object-Storage-Dienst (wie Amazon S3) als maßgeblichen Speicherort für Saves. Ein Launcher-Skript übernimmt die Synchronisierung transparent – dein Game-Code bleibt unverändert.
Der Ablauf sieht so aus:
- Vor dem Spielstart: Vorhandenen Save von S3 herunterladen → lokales Dateisystem
- Während des Gameplays: Lokale Save-Datei auf Änderungen überwachen → Updates zu S3 synchronisieren
- Beim Session-Ende: Ein letzter Sync stellt sicher, dass alle Daten persistiert sind, bevor die Instanz abgerissen wird
Dein Spiel schreibt weiterhin normal auf die lokale Platte. Es ahnt nicht, dass der Launcher diese Schreibvorgänge in den Cloud-Storage spiegelt. Dieser Zero-Modification-Ansatz bedeutet: Du kannst persistente Saves in jedes bestehende Spiel nachrüsten, ohne eine Zeile Game-Code anzufassen.
Schritt 1: IAM-Rolle konfigurieren
Du brauchst eine IAM-Rolle, die Streaming-Sessions abgegrenzten Zugriff auf deinen S3-Bucket gewährt. Die Rolle muss dem Streaming-Dienst vertrauen und dem Least-Privilege-Prinzip folgen.
Erstelle eine Rolle mit dieser Trust Policy (ersetze [ACCOUNT_ID] durch deine AWS-Account-ID):
{
"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/*"
}
}
}
]
}
Hänge dann eine Inline-Policy an, die nur die S3-Operationen gewährt, die du brauchst:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::your-save-bucket/saves/*"
}
]
}
Kritisch: Scope die Resource auf ein bestimmtes Präfix wie /saves/* – niemals auf den gesamten Bucket. Das begrenzt den Blast Radius, falls die Rolle kompromittiert wird, und folgt den Security-Best-Practices. Der Rollenname muss laut Plattform-Anforderungen mit GameLiftStreams- beginnen.
Schritt 2: Das Launcher-Skript (Windows)
Das Launcher-Skript ist der Orchestrator. Es lädt vorhandene Saves herunter, startet das Spiel und führt einen Hintergrund-File-Watcher aus, um Änderungen zu synchronisieren. Hier ist die Windows-Batch-Version:
@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
Die beiden Umgebungsvariablen (S3_SAVE_PATH und LOCAL_SAVE_FILE_PATH) werden über deinen Backend-Service übergeben, wenn er StartStreamSession aufruft. Der S3-Pfad muss pro Player eindeutig sein – konstruiere ihn aus einer authentifizierten Player-ID wie s3://your-bucket/saves/{player-id}/save.dat. Verwende niemals eine Session-ID; die ändert sich zwischen Sessions und würde Save-Daten verwaist zurücklassen.
Schritt 3: Das Download-Skript
Dieses PowerShell-Skript prüft S3 auf eine vorhandene Save-Datei und lädt sie herunter, falls sie existiert:
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: $_"
}
Bei Erstspielern ohne vorhandenen Save schlägt der S3-Copy fehl – das ist erwartet. Das Spiel erstellt eine neue Save-Datei, und der Watcher nimmt sie auf.
Schritt 4: Der File Watcher
Das ist die Komponente, die Saves während des Gameplays synchron hält:
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: $_"
}
Schritt 5: Linux/Proton-Variante
Für Linux-basierte Streaming-Instanzen ersetzt du den PowerShell-Watcher durch 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
Kostenaufschlüsselung: Echte Zahlen
Lass uns beziffern, was diese Architektur im Betrieb tatsächlich kostet:
S3-PUT-Requests (Schreiboperationen)
Wenn dein Spiel während des Gameplays alle 30 Sekunden automatisch speichert, erzeugt eine typische 2-Stunden-Session ungefähr 240 PUT-Requests. Bei einem S3-Preis von $0,005 pro 1.000 Requests:
- Kosten pro Session: $0,0012
- Kosten pro 10.000 Player-Sessions/Monat: $1,20
S3-GET-Requests (Leseoperationen)
Ein GET-Request pro Session-Start, um vorhandene Saves herunterzuladen:
- Kosten pro 10.000 Sessions: $0,40
S3-Speicher
Durchschnittliche Save-Datei: 5 MB pro Player. Bei 10.000 monatlich aktiven Playern:
- Gesamtspeicher: ~50 GB
- Monatliche Kosten bei $0,023/GB: $1,15
Gesamtkosten pro Monat für 10.000 MAU
Unter $3/Monat. Das ist vernachlässigbar im Vergleich zu den Compute-Kosten. Die S3-Schicht ist im Indie-Maßstab praktisch kostenlos.
Produktionsoptimierung: Debounced Syncing
Der rohe File Watcher feuert bei jedem Schreibvorgang. Ein Spiel, das häufig automatisch speichert (alle 10-15 Sekunden), erzeugt übermäßig viele PUT-Requests. Füge eine Debounce-Verzögerung hinzu – warte 5 Sekunden nach der letzten Änderung, bevor du synchronisierst:
# 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
}
}
Das reduziert die PUT-Requests um 60-80 % bei minimalem Datenverlustrisiko. Selbst wenn die Instanz mitten im Debounce beendet wird, verlierst du höchstens 5 Sekunden Fortschritt – akzeptabel für die meisten Spiele.
Edge Cases und Fehlermodi
Save-Datei existiert noch nicht (Erstspieler)
Der Download-Schritt schlägt fehl – das ist korrektes Verhalten. Das Spiel erstellt einen neuen Save, und der File Watcher nimmt ihn beim ersten Schreibvorgang auf. Keine spezielle Behandlung nötig.
Korrupter Upload durch Netzwerkunterbrechung
Wenn das Netzwerk während eines PUT-Requests ausfällt, könnte das S3-Objekt unvollständig sein. Entschärfe das, indem du den ETag nach dem Upload verifizierst:
# 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
}
Mehrere Save-Slots
Wenn dein Spiel mehrere Save-Dateien unterstützt, überwache ein Verzeichnis statt einer einzelnen Datei. Setze $watcher.Filter auf * und synchronisiere das gesamte Save-Verzeichnis. Bei vielen Dateien archiviere sie vor dem Upload in eine einzelne .zip-Datei.
Größenlimits für Save-Dateien
S3 unterstützt Objekte bis zu 5 TB, daher ist die Dateigröße praktisch kein Thema. Die meisten Game-Saves liegen zwischen 500 KB und 50 MB. Wenn du mit prozedural generierten Welten mit massivem State (>500 MB) arbeitest, erwäge, vor dem Upload mit gzip zu komprimieren – typische Save-Daten komprimieren auf 20-40 % der Originalgröße.
Player spielt auf mehreren Geräten
Wenn ein Player am selben Tag von verschiedenen Geräten streamt, überschreibt der Save der letzten Session den vorherigen. Für die meisten Singleplayer-Spiele ist das korrektes Verhalten. Für Spiele, die Merge-Semantik brauchen (wie Cloud-synchronisierte Inventare), benötigst du Konfliktlösungslogik – und genau hier wird ein dedizierter Backend-Service unverzichtbar.
Best Practices
IAM-Rollen auf bestimmte S3-Präfixe scopen – Verwende
arn:aws:s3:::your-bucket/saves/*statt Bucket-weiter Berechtigungen. Das folgt dem Least-Privilege-Prinzip und begrenzt den Schaden, falls Credentials leaken. Der Rollenname muss mitGameLiftStreams-beginnen, um die Plattform-Anforderungen zu erfüllen.Pro-Player-S3-Keys aus authentifizierter Identität konstruieren – Verwende
saves/{platform-user-id}/save.dat, niemalssaves/{session-id}/save.dat. Session-IDs ändern sich zwischen Sessions und würden Save-Daten dauerhaft verwaist zurücklassen.Debounced Syncing in Produktion implementieren – Rohe Dateisystem-Watcher erzeugen bei jedem Save einen PUT-Request. Ein 5-Sekunden-Debounce-Fenster reduziert die API-Calls um 60-80 % und hält das Datenverlustrisiko unter einem Auto-Save-Intervall.
Jede Sync-Operation loggen – Speicherfehler bleiben für Player unsichtbar, bis sie Stunden an Fortschritt verlieren. Das
$LogFile-Muster in den Skripten oben gibt dir einen persistenten Audit-Trail auf der Instanz. Schicke diese Logs an dein Monitoring-System für proaktives Alerting.Mit Instanz-Terminierung testen, nicht nur mit Disconnect – Töte die Streaming-Instanz mitten im Spiel, um zu verifizieren, dass dein Debounce-Sync-Fenster akzeptabel ist. Ein sauberer Disconnect gibt dem finalen Sync Zeit, abzuschließen; eine abrupte Terminierung nicht.
Der BaaS-Shortcut: Wenn sich der Selbstbau nicht lohnt
Die Architektur oben funktioniert – sie ist kampferprobt und kostet fast nichts im Betrieb. Aber sie verlangt, dass du IAM-Rollen, Launcher-Skripte, File Watcher, Integritätschecks, Linux-Varianten und Umgebungskonfiguration verwaltest. Für ein kleines Team sind das 2-4 Tage Infrastrukturarbeit, die nichts mit deinem eigentlichen Spiel zu tun hat.
horizOn bietet persistenten Player-Data-Speicher als Managed Service – Save-Dateien, Inventar, Fortschritt, Präferenzen – mit einem einzigen API-Call. Keine IAM-Konfiguration, keine Launcher-Skripte, keine File Watcher, keine Integritätsprüfung. Das Backend übernimmt Authentifizierung, Konfliktlösung und Cross-Platform-Redundanz automatisch. Für Teams, die lieber shipen statt Infrastruktur zu debuggen, eliminiert das eine ganze Kategorie von „funktioniert lokal, bricht im Streaming“-Bugs. Wir haben den vollständigen Integrationsprozess in unserem aktuellen Backend-Update-Walkthrough dokumentiert.
Zusammenfassung
Persistente Game-Saves im Cloud-Streaming erfordern, das Instanz-Dateisystem als ephemer zu behandeln und dauerhaften Object Storage als Source of Truth zu nutzen. Die vollständige Architektur:
- IAM-Rolle gewährt Streaming-Sessions abgegrenzten S3-Lese-/Schreibzugriff
- Launcher-Skript lädt vorhandene Saves vor dem Spielstart herunter
- Hintergrund-File-Watcher synchronisiert Änderungen während des Gameplays mit Debounce-Writes zu S3
- Backend-Service konstruiert Pro-Player-S3-Pfade aus der authentifizierten Player-Identität
- Integritätschecks verifizieren die Korrektheit von Upload/Download bei jedem Sync
Die Kosten im Indie-Maßstab liegen unter $3/Monat für 10.000 aktive Player. Die Implementierungszeit beträgt 1-2 Tage, wenn du es selbst baust, oder 30 Minuten, wenn du die horizOn-Player-Data-APIs nutzt.
Wie auch immer: Shippe kein Cloud-Streaming-Spiel, ohne dieses Problem zu lösen. Deine Player werden dir nicht sagen, dass ihre Saves verschwinden – sie hören einfach auf zu spielen.
Quelle: Persistente Game-Saves zu Amazon GameLift Streams hinzufügen