Ein kleines Update geht raus, ein paar neue Level, ein umbenanntes Feld im Code. Am nächsten Morgen stehen die ersten Rezensionen im Store: Der Fortschritt ist weg. Eine andere Spielerin schreibt, dass ihr Tablet den Spielstand vom Handy überschrieben hat, und plötzlich ist sie wieder in Welt zwei statt in Welt fünf. Der Code dahinter ist oft nur ein paar Zeilen lang und hat im Editor nie Probleme gemacht.
Speichern wirkt in Unity zunächst trivial. Ein Aufruf von PlayerPrefs.SetInt, fertig. In der Praxis scheitern Speichersysteme aber fast immer an denselben drei Stellen: Das Spiel wird mitten im Schreiben beendet, das Datenformat ändert sich mit einem Update, oder jemand spielt auf mehreren Geräten. Dieser Beitrag beantwortet die Frage, wie Sie einen Unity Spielstand speichern, ohne in diese Fallen zu laufen. Sie erfahren, wofür PlayerPrefs gedacht ist, wie Sie Spielstände sauber als JSON-Datei ablegen, wie Versionierung funktioniert und was ein Cloud Save leisten muss, wenn mehrere Geräte im Spiel sind.
Drei Ebenen, drei Werkzeuge
Bevor es um Code geht, hilft eine klare Aufteilung. Die meisten Spiele speichern drei Arten von Daten, und jede passt zu einem anderen Werkzeug:
- Einstellungen wie Lautstärke, Grafikstufe, Sprache oder „Tutorial gesehen“. Das sind wenige kleine Werte. Dafür ist PlayerPrefs gemacht.
- Der eigentliche Spielstand mit Level, Inventar, Quests und Freischaltungen. Diese Daten sind strukturiert und wachsen mit dem Spiel. Sie gehören in eine eigene Datei, zum Beispiel als JSON.
- Fortschritt, der einen Gerätewechsel überleben soll. Sobald Spieler neu installieren, das Handy wechseln oder auf zwei Geräten spielen, brauchen Sie eine Kopie auf einem Server, also einen Cloud Save.
Die drei schließen sich nicht aus. Die meisten Spiele nutzen am Ende mindestens zwei davon, etwa PlayerPrefs für Einstellungen und eine JSON-Datei für den Spielstand, die zusätzlich in die Cloud gespiegelt wird.
PlayerPrefs: gut für Einstellungen, nicht für Spielstände
Was PlayerPrefs kann
PlayerPrefs speichert laut Unity-Dokumentation genau drei Datentypen: int, float und string. Die Werte liegen in einem lokalen Speicher der jeweiligen Plattform, und zwar ohne Verschlüsselung. Unity rät ausdrücklich davon ab, sensible Daten in PlayerPrefs abzulegen.
Für Einstellungen ist das genau richtig. Ein Schlüssel, ein Wert, fertig. Problematisch wird es, wenn ein ganzer Spielstand in PlayerPrefs wandert, etwa als Dutzende einzelne Schlüssel oder als ein riesiger String. Dann fehlt jede Struktur, Sie können den Stand schlecht sichern oder übertragen, und auf manchen Plattformen stoßen Sie an Grenzen.
Wo die Daten landen
Wo PlayerPrefs die Daten ablegt, hängt von der Plattform ab. Die Unity-Dokumentation nennt diese Orte, wobei CompanyName und ProductName aus Ihren Player Settings stammen:
- Windows: in der Registry unter
HKEY_CURRENT_USER\Software\CompanyName\ProductName - macOS: als plist-Datei unter
~/Library/Preferences/, benannt nach dem Bundle Identifier - Linux: unter
~/.config/unity3d/CompanyName/ProductName - Android: als SharedPreferences-Datei unter
/data/data/pkg-name/shared_prefs/pkg-name.v2.playerprefs.xml - iOS: über die Systemschnittstelle NSUserDefaults
- Web: in der IndexedDB des Browsers, und zwar mit höchstens 1 MB
- Universal Windows Platform: als Datei
playerprefs.datim Ordner LocalState des App-Pakets
Gerade die 1 MB im Web zeigen, warum PlayerPrefs für Spielstände eine schlechte Grundlage ist. Ein Hinweis für die Fehlersuche: Im Play Mode des Editors nutzt Unity eigene Orte. Unter Windows liegen die Werte dann unter HKEY_CURRENT_USER\Software\Unity\UnityEditor\CompanyName\ProductName, und die Schlüsselnamen sind dort mit einem Hash versehen. Wer im falschen Registry-Zweig sucht, findet nichts.
Wann wirklich geschrieben wird
Ein Detail, das viele übersehen: Laut Dokumentation schreibt Unity PlayerPrefs automatisch beim Beenden der Anwendung in OnApplicationQuit auf die Platte, auf der Universal Windows Platform beim Suspend. Sie können mit PlayerPrefs.Save() selbst speichern. Unity weist aber darauf hin, dass das kurze Ruckler verursachen kann und deshalb nicht mitten im Gameplay passieren sollte.
Aus unserer Praxis: Auf Smartphones beendet das Betriebssystem Apps im Hintergrund oft ohne Vorwarnung. Verlassen Sie sich dort nicht auf das Beenden der App. Speichern Sie stattdessen an ruhigen Stellen wie dem Levelende oder dem Pausenmenü und zusätzlich in OnApplicationPause(true). Unity ruft diese Methode auf, wenn die Anwendung pausiert, auf Mobilgeräten in der Regel beim Wechsel in den Hintergrund.
Der Spielstand als JSON-Datei
Der richtige Ordner: persistentDataPath
Für den eigentlichen Spielstand ist eine eigene Datei die bessere Wahl. Unity stellt dafür Application.persistentDataPath bereit. Laut Dokumentation ist das der Ordner für Daten, die zwischen zwei Starts erhalten bleiben sollen. Auf iOS und Android überstehen die Dateien dort auch App-Updates. Einige Beispiele aus der Dokumentation:
- Windows:
%userprofile%\AppData\LocalLow\CompanyName\ProductName - macOS:
~/Library/Application Support/mit Firmen- und Produktnamen - Android: der App-Ordner, den das System über
getExternalFilesDirliefert - iOS: der Ordner Documents im Container der App
- Web: ein Bereich in der IndexedDB des Browsers
Bauen Sie den Pfad immer vollständig mit Path.Combine(Application.persistentDataPath, "save.json"). Unity warnt, dass manche APIs relative Pfade sonst auf den Projektordner beziehen.
JsonUtility und seine Grenzen
Unity bringt mit JsonUtility einen eigenen JSON-Serializer mit. Er ist laut Dokumentation deutlich schneller als verbreitete .NET-Bibliotheken, erzeugt wenig Garbage und darf auch in einem Hintergrundthread laufen. Dafür gelten dieselben Regeln wie im Inspector:
- Serialisiert werden nur Felder, also öffentliche Felder oder Felder mit
[SerializeField]. Properties landen nicht im JSON. - Eigene Klassen und Structs brauchen das Attribut
[Serializable]. - Dictionaries werden nicht unterstützt. Wer ein Inventar als Dictionary hält, bekommt beim Speichern schlicht nichts davon in die Datei.
- Arrays und primitive Typen können Sie nicht direkt übergeben, sondern nur als Feld in einer Wrapper-Klasse.
Zwei weitere Eigenschaften sind für Spielstände Gold wert. Felder im JSON, die es in Ihrer Klasse nicht gibt, ignoriert Unity. Fehlt umgekehrt ein Feld im JSON, behält es den Wert, den Ihre Klasse beim Erzeugen hat. Darauf baut später die Versionierung auf. Brauchen Sie Dictionaries oder Vererbung, empfiehlt die Dokumentation eine allgemeine .NET-JSON-Bibliothek als Ergänzung. Unity bietet dafür ein eigenes Paket für Newtonsoft Json an. Für die meisten Spielstände reicht aber eine Liste aus kleinen Einträgen statt eines Dictionaries völlig aus.
Code-Beispiel: Speichern ohne Datenverlust
Der häufigste Fehler beim Speichern ist ein einfaches File.WriteAllText direkt auf die Spielstanddatei. Bricht das Spiel genau in diesem Moment ab, weil der Akku leer ist oder das System die App beendet, bleibt eine halb geschriebene Datei zurück. Beim nächsten Start ist der Spielstand unlesbar.
Ein bewährtes Muster dagegen: erst in eine temporäre Datei schreiben, dann die alte Datei ersetzen und dabei eine Sicherung behalten. Beim Laden greift das Spiel auf die Sicherung zurück, wenn die Hauptdatei kaputt ist.
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
[Serializable]
public class InventoryEntry
{
public string itemId;
public int amount;
}
[Serializable]
public class SaveData
{
public int version;
public int level = 1;
public int coins;
public List<InventoryEntry> inventory = new List<InventoryEntry>();
}
public static class SaveSystem
{
public const int CurrentVersion = 2;
private static string SavePath => Path.Combine(Application.persistentDataPath, "save.json");
private static string TempPath => SavePath + ".tmp";
private static string BackupPath => SavePath + ".bak";
public static void Save(SaveData data)
{
data.version = CurrentVersion;
File.WriteAllText(TempPath, JsonUtility.ToJson(data));
if (File.Exists(SavePath))
{
// Ersetzt die alte Datei und behält sie als Sicherung
File.Replace(TempPath, SavePath, BackupPath);
}
else
{
File.Move(TempPath, SavePath);
}
}
public static SaveData Load()
{
return TryRead(SavePath) ?? TryRead(BackupPath) ?? new SaveData { version = CurrentVersion };
}
private static SaveData TryRead(string path)
{
if (!File.Exists(path)) return null;
try
{
return SaveMigrations.FromJson(File.ReadAllText(path));
}
catch (Exception e)
{
Debug.LogWarning($"Spielstand {path} ist nicht lesbar: {e.Message}");
return null;
}
}
}
Bricht das Spiel jetzt beim Schreiben ab, trifft es höchstens die temporäre Datei. Der letzte gute Stand bleibt erhalten. Wichtig ist das Inventar als List<InventoryEntry> statt als Dictionary, damit JsonUtility es auch wirklich speichert. Das Muster ist keine Vorgabe von Unity, sondern gängige Praxis. Testen Sie es auf allen Zielplattformen, denn gerade im Web und auf Konsolen gelten für Dateizugriffe eigene Regeln.
Versionierung: Spielstände, die Updates überleben
Ihr Spiel entwickelt sich weiter, und damit auch der Spielstand. Felder kommen dazu, andere werden umbenannt oder anders aufgebaut. Ohne Plan passiert dann genau das aus der Einleitung: Weil JsonUtility unbekannte Felder still ignoriert, ist ein umbenanntes Feld nach dem Update einfach leer.
Die Lösung ist eine Versionsnummer in jeder Datei und eine kleine Migration beim Laden. Die Unity-Dokumentation beschreibt dafür selbst einen passenden Weg: zuerst nur die gemeinsamen Felder lesen und danach entscheiden, in welchen Typ der Rest gehört. Angenommen, in Version 1 hieß das Feld für Münzen noch gold und heißt jetzt coins:
using System;
using UnityEngine;
public static class SaveMigrations
{
[Serializable]
private class SaveHeader
{
public int version;
}
[Serializable]
private class SaveDataV1
{
public int level;
public int gold;
}
public static SaveData FromJson(string json)
{
SaveHeader header = JsonUtility.FromJson<SaveHeader>(json);
if (header.version >= 2)
{
return JsonUtility.FromJson<SaveData>(json);
}
// Version 1 oder Dateien aus der Zeit vor dem Versionsfeld
SaveDataV1 old = JsonUtility.FromJson<SaveDataV1>(json);
return new SaveData
{
version = 2,
level = old.level,
coins = old.gold
};
}
}
Ein Detail ist hier wichtig: Fehlt das Feld version in einer alten Datei, bleibt es in SaveHeader bei 0. Gäbe die Klasse dem Feld einen anderen Startwert, etwa die aktuelle Version, würden alte Dateien nie migriert. Deshalb bekommt die Versionsnummer im Header keinen eigenen Standardwert.
Drei Regeln aus unserer Praxis:
- Versionsfeld ab dem ersten Release. Nachträglich lässt es sich nur mühsam einführen.
- Nie ein Feld umbenennen, ohne zu migrieren. Lieber ein neues Feld anlegen und das alte in der Migration übertragen.
- Alte Spielstände aufheben. Legen Sie aus jeder veröffentlichten Version eine Testdatei ab und laden Sie alle in einem automatischen Test. So merken Sie vor dem Release, wenn eine Migration bricht.
Schummeln und Verschlüsselung realistisch sehen
Alles, was auf dem Gerät liegt, lässt sich auslesen und ändern. Das gilt für PlayerPrefs genauso wie für JSON-Dateien. Verschlüsselung macht das schwerer, aber nicht unmöglich, denn der Schlüssel steckt am Ende im Spiel selbst. Sie hält also vor allem Gelegenheitsbastler ab, die mit einem Texteditor ihre Münzen verzehnfachen wollen.
Für Einstellungen und einfache Werte reicht oft ein Wrapper um PlayerPrefs. Wir haben dafür bei ProjectMakers das Open-Source-Paket PmPrefs veröffentlicht. Es verschlüsselt Werte mit AES, speichert beliebige serialisierbare Objekte und bringt ein Editorfenster zum Ansehen und Bearbeiten mit. Wichtig, wie im README beschrieben: Der eingebaute Standardschlüssel ist öffentlich, setzen Sie also einen eigenen.
Alles, was wirklich zählt, gehört nicht auf das Gerät. Ranglisten, Käufe oder Belohnungen, die andere Spieler betreffen, prüft und speichert ein Server.
Cloud Save und mehrere Geräte
Wann sich ein Cloud Save lohnt
Lokale Dateien reichen, solange ein Spieler auf genau einem Gerät spielt. Sobald er neu installiert, das Handy wechselt oder abends am PC und unterwegs auf dem Smartphone weiterspielt, braucht der Spielstand einen Platz auf einem Server. Erscheint Ihr Spiel nur auf einer Plattform, können plattformeigene Dienste wie Steam Cloud oder iCloud eine Option sein. Für Spiele auf mehreren Plattformen brauchen Sie einen Speicher, der an ein Spielerkonto gebunden ist und nicht an eine Plattform.
Das eigentliche Problem: Konflikte
Das Hochladen selbst ist selten das Problem. Schwierig wird es, wenn zwei Geräte unterschiedliche Stände haben. Ein typischer Fall: Das Tablet lag eine Woche offline in der Schublade. Inzwischen hat die Spielerin auf dem Handy drei Welten geschafft. Dann startet sie das Tablet, es geht online und lädt seinen alten Stand hoch. Gilt die Regel „der letzte Schreiber gewinnt“, ist der Fortschritt vom Handy weg.
Die Lösung im Prinzip: Jeder Spielstand auf dem Server trägt eine Revision. Ein Gerät darf nur speichern, wenn es die aktuelle Revision kennt. Hat sich der Stand inzwischen geändert, lehnt der Server das Speichern ab, und das Spiel muss entscheiden. Mögliche Strategien sind der Stand mit dem größeren Fortschritt, der neuere Stand oder eine Nachfrage beim Spieler mit beiden Ständen zur Auswahl. Das Grundproblem kennen Sie vielleicht aus der Backend-Entwicklung, wo doppelte oder verspätete Anfragen ebenfalls Daten überschreiben können. Mehr dazu in unserem Beitrag zu idempotentem API-Design.
Selbst bauen oder fertig nutzen
Wer einen Cloud Save selbst baut, braucht mehr als einen Upload-Endpunkt. Dazu gehören eine Anmeldung, die auch anonym funktioniert, ein Speicher pro Spieler, Revisionen für Konflikte, eine Behandlung für Verbindungsabbrüche und ein Weg, den lokalen Stand mit dem Serverstand abzugleichen. Das ist machbar, kostet aber Zeit, die vielen kleinen Teams für das eigentliche Spiel fehlt.
Eine fertige Alternative ist unser Game-Backend horizOn. Es bietet in den SDKs für Unity, Godot und Unreal einen Cloud Save, der an das Spielerkonto gebunden ist. Seit Oktober 2026 arbeitet er mit Revisionen und verhindert damit, dass ein älterer Stand bei einem Gerätewechsel einen neueren überschreibt. Wie mit einem Konflikt umgegangen wird, entscheidet weiterhin Ihr Spiel. Wer den Server lieber selbst betreibt, kann den Open-Source Simple Server nutzen, der Cloud Saves bis 300 KB pro Spieler speichert. Revisionen gibt es dort allerdings nicht, sie sind dem gehosteten horizOn vorbehalten.
Checkliste: Speichersystem in fünf Schritten
- Daten richtig aufteilen. Einstellungen in PlayerPrefs, den Spielstand als JSON-Datei in
Application.persistentDataPath. - Sicher schreiben. Erst in eine temporäre Datei, dann ersetzen und eine Sicherung behalten.
- Versionieren ab dem ersten Release. Versionsfeld in jeder Datei, Migration beim Laden und Testdateien aus jeder veröffentlichten Version.
- Zur richtigen Zeit speichern. An ruhigen Stellen und in
OnApplicationPause(true), nicht mitten im Gameplay. - Mehrere Geräte ernst nehmen. Cloud Save mit Revisionen statt „der letzte Schreiber gewinnt“, plus eine klare Regel für Konflikte.
Fazit
PlayerPrefs ist kein schlechtes Werkzeug, es ist nur für etwas anderes gedacht. Für Einstellungen ist es ideal, für Spielstände fehlen Struktur, Sicherheit und auf manchen Plattformen schlicht der Platz. Der Spielstand gehört in eine eigene JSON-Datei, die sicher geschrieben wird, eine Versionsnummer trägt und Updates übersteht. Sobald mehrere Geräte im Spiel sind, entscheidet nicht der Upload über die Qualität eines Cloud Saves, sondern der Umgang mit Konflikten.
Ein praktischer nächster Schritt: Öffnen Sie Ihr aktuelles Speichersystem und prüfen Sie drei Dinge. Gibt es ein Versionsfeld? Wird über eine temporäre Datei geschrieben? Und was passiert, wenn die Datei beim Laden kaputt ist? Wenn Ihr Spiel auf Mobilgeräten läuft, lohnt sich außerdem ein Blick in unseren Tipp zur Mobile-Optimierung für Android und iOS, denn Speichern zur falschen Zeit kostet dort schnell spürbare Frames. Und wenn Sie Unterstützung beim Speichersystem oder beim Backend für Ihr Spiel brauchen, sind wir mit unseren Leistungen zur Game-Entwicklung für Sie da.