Ein Spiel speichern und laden
So speichern und laden Sie ein Spiel mithilfe der JSON- oder CBOR-Klassen von Qt.
Viele Spiele bieten eine Speicherfunktion, damit der Spielfortschritt des Spielers gespeichert und zu einem späteren Zeitpunkt wieder geladen werden kann. Das Speichern eines Spiels umfasst im Allgemeinen die Serialisierung der Member-Variablen jedes Spielobjekts in eine Datei. Zu diesem Zweck können viele Formate verwendet werden, darunter auch JSON. Mit „ QJsonDocument “ haben Sie zudem die Möglichkeit, ein Dokument im CBOR-Format zu serialisieren. Dies ist besonders dann von Vorteil, wenn die Speicherdatei nicht leicht lesbar sein soll (siehe jedoch „Parsen und Anzeigen von CBOR-Daten“ für Informationen zum Auslesen ) oder wenn Sie die Dateigröße gering halten müssen.
In diesem Beispiel zeigen wir, wie man ein einfaches Spiel im JSON- und im Binärformat speichert und wieder lädt.
Die Charakterklasse
Die Klasse „Character“ repräsentiert einen Nicht-Spieler-Charakter (NPC) in unserem Spiel und speichert den Namen, das Level und die Charakterklasse des Spielers.
Sie stellt die statische Funktion `fromJson()` und die nicht-statische Funktion `toJson()` zur Verfügung, um sich selbst zu serialisieren.
Hinweis: Dieses Muster (fromJson()/toJson()) funktioniert, da QJsonObjects unabhängig von einem besitzenden „ QJsonDocument “ erstellt werden können und da es sich bei den hier (de-)serialisierten Datentypen um Werttypen handelt, die kopiert werden können. Bei der Serialisierung in ein anderes Format – zum Beispiel XML oder QDataStream, die die Übergabe eines dokumentähnlichen Objekts erfordern – oder wenn die Objektidentität wichtig ist (zum Beispiel bei Unterklassen vonQObject ), sind möglicherweise andere Muster besser geeignet. Siehe das Beispiel „dombookmarks“ für XML und die Implementierung von ` QListWidgetItem::read()` und ` QListWidgetItem::write()` für die idiomatische Serialisierung von ` QDataStream `. Die Funktionen ` print() ` in diesem Beispiel sind gute Beispiele für die Serialisierung von ` QTextStream `, auch wenn ihnen natürlich die Deserialisierungsseite fehlt.
class Character
{
Q_GADGET
public:
enum ClassType { Warrior, Mage, Archer };
Q_ENUM(ClassType)
Character();
Character(const QString &name, int level, ClassType classType);
QString name() const;
void setName(const QString &name);
int level() const;
void setLevel(int level);
ClassType classType() const;
void setClassType(ClassType classType);
static Character fromJson(const QJsonObject &json);
QJsonObject toJson() const;
void print(QTextStream &s, int indentation = 0) const;
private:
QString mName;
int mLevel = 0;
ClassType mClassType = Warrior;
};Von besonderem Interesse für uns sind die Implementierungen der Funktionen `fromJson()` und `toJson()`:
Character Character::fromJson(const QJsonObject &json)
{
Character result;
if (const QJsonValue v = json["name"]; v.isString())
result.mName = v.toString();
if (const QJsonValue v = json["level"]; v.isDouble())
result.mLevel = v.toInt();
if (const QJsonValue v = json["classType"]; v.isDouble())
result.mClassType = ClassType(v.toInt());
return result;
}In der Funktion `fromJson()` erstellen wir ein lokales ` result `-Objekt vom Typ „Character“ und weisen den Elementen von ` result` Werte aus dem Argument ` QJsonObject ` zu. Sie können entweder ` QJsonObject::operator[]()` oder ` QJsonObject::value()` verwenden, um auf Werte innerhalb des JSON-Objekts zuzugreifen; beide sind `const`-Funktionen und geben ` QJsonValue::Undefined ` zurück, wenn der Schlüssel ungültig ist. Insbesondere geben die ` is... `-Funktionen (z. B. ` QJsonValue::isString()`, ` QJsonValue::isDouble()`) ` false ` für ` QJsonValue::Undefined` zurück, sodass wir in einem einzigen Abruf sowohl das Vorhandensein als auch den korrekten Typ überprüfen können.
Wenn ein Wert im JSON-Objekt nicht vorhanden ist oder den falschen Typ hat, schreiben wir auch nicht in das entsprechende „ result “-Element, wodurch alle Werte erhalten bleiben, die der Standardkonstruktor möglicherweise gesetzt hat. Das bedeutet, dass Standardwerte zentral an einer Stelle (dem Standardkonstruktor) definiert werden und im Serialisierungscode nicht wiederholt werden müssen (DRY).
Beachten Sie die Verwendung des C++17-Konstrukts „if-with-initializer“, um den Gültigkeitsbereich und die Überprüfung der Variablen ` v` voneinander zu trennen. Dadurch können wir den Variablennamen kurz halten, da sein Gültigkeitsbereich begrenzt ist.
Vergleichen Sie dies mit dem naiven Ansatz unter Verwendung von ` QJsonObject::contains()`:
if (json.contains("name") && json["name"].isString())
result.mName = json["name"].toString();Dieser ist nicht nur weniger lesbar, sondern erfordert auch insgesamt drei Nachschläge (nein, der Compiler optimiert diese nicht zu einem einzigen), ist also dreimal langsamer und wiederholt „ "name" “ dreimal (was gegen das DRY-Prinzip verstößt).
QJsonObject Character::toJson() const
{
QJsonObject json;
json["name"] = mName;
json["level"] = mLevel;
json["classType"] = mClassType;
return json;
}In der Funktion `toJson()` führen wir das Gegenteil der Funktion `fromJson()` durch: Wir weisen Werte aus dem `Character`-Objekt einem neuen JSON-Objekt zu, das wir anschließend zurückgeben. Wie beim Abrufen von Werten gibt es zwei Möglichkeiten, Werte einem ` QJsonObject` zuzuweisen: ` QJsonObject::operator[]()` und ` QJsonObject::insert()`. Beide überschreiben jeden bereits vorhandenen Wert unter dem angegebenen Schlüssel.
Die Klasse „Level“
class Level
{
public:
Level() = default;
explicit Level(const QString &name);
QString name() const;
QList<Character> npcs() const;
void setNpcs(const QList<Character> &npcs);
static Level fromJson(const QJsonObject &json);
QJsonObject toJson() const;
void print(QTextStream &s, int indentation = 0) const;
private:
QString mName;
QList<Character> mNpcs;
};Wir möchten, dass die Levels in unserem Spiel jeweils mehrere NPCs enthalten, daher verwalten wir ein ` QList ` aus `Character`-Objekten. Außerdem stellen wir die bekannten Funktionen `fromJson()` und `toJson()` bereit.
Level Level::fromJson(const QJsonObject &json)
{
Level result;
if (const QJsonValue v = json["name"]; v.isString())
result.mName = v.toString();
if (const QJsonValue v = json["npcs"]; v.isArray()) {
const QJsonArray npcs = v.toArray();
result.mNpcs.reserve(npcs.size());
for (const QJsonValue &npc : npcs)
result.mNpcs.append(Character::fromJson(npc.toObject()));
}
return result;
}Mit ` QJsonArray` können Container in JSON geschrieben und aus JSON gelesen werden. In unserem Fall erstellen wir ein ` QJsonArray ` aus dem Wert, der dem Schlüssel ` "npcs"` zugeordnet ist. Anschließend rufen wir für jedes ` QJsonValue `-Element im Array die Funktion `toObject()` auf, um das JSON-Objekt des Charakters abzurufen. Character::fromJson() kann dieses QJSonObject dann in ein Character-Objekt umwandeln, das unserem NPC-Array angehängt wird.
Hinweis: Assoziative Container können erstellt werden, indem der Schlüssel in jedem Wert-Objekt gespeichert wird (sofern dies nicht bereits der Fall ist). Bei diesem Ansatz wird der Container als reguläres Array von Objekten gespeichert, aber der Index jedes Elements wird beim Zurücklesen als Schlüssel zum Aufbau des Containers verwendet.
QJsonObject Level::toJson() const
{
QJsonObject json;
json["name"] = mName;
QJsonArray npcArray;
for (const Character &npc : mNpcs)
npcArray.append(npc.toJson());
json["npcs"] = npcArray;
return json;
}Auch hier gilt: Die Funktion `toJson()` ähnelt der Funktion `fromJson()`, nur in umgekehrter Reihenfolge.
Die Klasse „Game“
Nachdem wir die Klassen „Character“ und „Level“ definiert haben, können wir nun zur Klasse „Game“ übergehen:
class Game
{
public:
enum SaveFormat { Json, Binary };
Character player() const;
QList<Level> levels() const;
void newGame();
bool loadGame(SaveFormat saveFormat);
bool saveGame(SaveFormat saveFormat) const;
void read(const QJsonObject &json);
QJsonObject toJson() const;
void print(QTextStream &s, int indentation = 0) const;
private:
Character mPlayer;
QList<Level> mLevels;
};Zunächst definieren wir die Enumeration „ SaveFormat “. Damit können wir das Format festlegen, in dem das Spiel gespeichert werden soll: „ Json “ oder „ Binary “.
Als Nächstes stellen wir Zugriffsmethoden für den Spieler und die Levels bereit. Anschließend definieren wir drei Funktionen: newGame(), saveGame() und loadGame().
Die Funktionen `read()` und `toJson()` werden von `saveGame()` und `loadGame()` verwendet.
Hinweis: Obwohl ` Game ` eine Wertklasse ist, gehen wir davon aus, dass der Autor möchte, dass ein Spiel eine Identität besitzt, ähnlich wie es bei Ihrem Hauptfenster der Fall wäre. Wir verwenden daher keine statische fromJson()-Funktion, die ein neues Objekt erstellen würde, sondern eine read()-Funktion, die wir auf bestehende Objekte anwenden können. Es besteht eine 1:1-Entsprechung zwischen read() und fromJson(), da sich die eine Funktion anhand der anderen implementieren lässt:
void read(const QJsonObject &json) { *this = fromJson(json); }
static Game fromObject(const QJsonObject &json) { Game g; g.read(json); return g; }Wir verwenden einfach das, was für die Aufrufer der Funktionen praktischer ist.
void Game::newGame()
{
mPlayer = Character();
mPlayer.setName("Hero"_L1);
mPlayer.setClassType(Character::Archer);
mPlayer.setLevel(QRandomGenerator::global()->bounded(15, 21));
mLevels.clear();
mLevels.reserve(2);
Level village("Village"_L1);
QList<Character> villageNpcs;
villageNpcs.reserve(2);
villageNpcs.append(Character("Barry the Blacksmith"_L1,
QRandomGenerator::global()->bounded(8, 11), Character::Warrior));
villageNpcs.append(Character("Terry the Trader"_L1,
QRandomGenerator::global()->bounded(6, 8), Character::Warrior));
village.setNpcs(villageNpcs);
mLevels.append(village);
Level dungeon("Dungeon"_L1);
QList<Character> dungeonNpcs;
dungeonNpcs.reserve(3);
dungeonNpcs.append(Character("Eric the Evil"_L1,
QRandomGenerator::global()->bounded(18, 26), Character::Mage));
dungeonNpcs.append(Character("Eric's Left Minion"_L1,
QRandomGenerator::global()->bounded(5, 7), Character::Warrior));
dungeonNpcs.append(Character("Eric's Right Minion"_L1,
QRandomGenerator::global()->bounded(4, 9), Character::Warrior));
dungeon.setNpcs(dungeonNpcs);
mLevels.append(dungeon);
}Um ein neues Spiel einzurichten, erstellen wir den Spieler und füllen die Levels sowie deren NPCs mit Daten.
void Game::read(const QJsonObject &json)
{
if (const QJsonValue v = json["player"]; v.isObject())
mPlayer = Character::fromJson(v.toObject());
if (const QJsonValue v = json["levels"]; v.isArray()) {
const QJsonArray levels = v.toArray();
mLevels.clear();
mLevels.reserve(levels.size());
for (const QJsonValue &level : levels)
mLevels.append(Level::fromJson(level.toObject()));
}
}Die Funktion `read()` ersetzt zunächst den Spieler durch den aus JSON gelesenen. Anschließend leeren wir das Level-Array mit `clear()`, damit beim zweimaligen Aufruf von `loadGame()` für dasselbe `Game`-Objekt keine alten Levels zurückbleiben.
Anschließend füllen wir das Level-Array, indem wir jedes Level aus einem ` QJsonArray` einlesen.
QJsonObject Game::toJson() const
{
QJsonObject json;
json["player"] = mPlayer.toJson();
QJsonArray levels;
for (const Level &level : mLevels)
levels.append(level.toJson());
json["levels"] = levels;
return json;
}Das Speichern des Spiels in JSON erfolgt ähnlich wie das Speichern eines Levels.
bool Game::loadGame(Game::SaveFormat saveFormat)
{
QFile loadFile(saveFormat== Json ? "save.json"_L1 : "save.dat"_L1);
if (!loadFile.open(QIODevice::ReadOnly)) {
qWarning("Couldn't open save file.");
return false;
}
QByteArray saveData = loadFile.readAll();
QJsonDocument loadDoc(saveFormat== Json
? QJsonDocument::fromJson(saveData)
: QJsonDocument(QCborValue::fromCbor(saveData).toMap().toJsonObject()));
read(loadDoc.object());
QTextStream(stdout) << „Speicherstand für “ << loadDoc["player"]["name"].toString()
<< „ geladen mit “ << (saveFormat != Json ? „CBOR“: „JSON“) << „...\n“;
return true;
}Beim Laden eines Spielstandes in `loadGame()` öffnen wir zunächst die Speicherdatei entsprechend dem Format, in dem sie gespeichert wurde: ` "save.json" ` für JSON und ` "save.dat" ` für CBOR. Wir geben eine Warnung aus und geben ` false ` zurück, falls die Datei nicht geöffnet werden konnte.
Da sowohl QJsonDocument::fromJson() als auch QCborValue::fromCbor() einen QByteArray als Parameter erwarten, können wir den gesamten Inhalt der Speicherdatei in eine einzige Variable einlesen, unabhängig vom Speicherformat.
Nachdem wir das ` QJsonDocument` erstellt haben, weisen wir das `Game`-Objekt an, sich selbst einzulesen, und geben anschließend ` true ` zurück, um den Erfolg anzuzeigen.
bool Game::saveGame(Game::SaveFormat saveFormat) const
{
QFile saveFile(saveFormat== Json ? "save.json"_L1 : "save.dat"_L1);
if (!saveFile.open(QIODevice::WriteOnly)) {
qWarning("Couldn't open save file.");
return false;
}
QJsonObject gameObject = toJson();
saveFile.write(saveFormat== Json ? QJsonDocument(gameObject).toJson()
: QCborValue::fromJsonValue(gameObject).toCbor());
return true;
}Es überrascht nicht, dass saveGame() der Funktion loadGame() sehr ähnlich ist. Wir ermitteln die Dateiendung anhand des Formats, geben eine Warnung aus und geben „ false “ zurück, falls das Öffnen der Datei fehlschlägt. Anschließend schreiben wir das Game-Objekt in eine „ QJsonObject “. Um das Spiel im angegebenen Format zu speichern, konvertieren wir das JSON-Objekt entweder in ein „ QJsonDocument “ für einen nachfolgenden Aufruf von „ QJsonDocument::toJson()“ oder in ein „ QCborValue “ für „ QCborValue::toCbor()“.
Alles zusammenführen
Nun sind wir bereit, die Funktion `main()` aufzurufen:
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
const QStringList args = QCoreApplication::arguments();
const bool newGame
= args.size() <= 1 || QString::compare(args[1], "load"_L1, Qt::CaseInsensitive) != 0;
const bool json
= args.size() <= 2 || QString::compare(args[2], "binary"_L1, Qt::CaseInsensitive) != 0;
Game game;
if (newGame)
game.newGame();
else if (!game.loadGame(json ? Game::Json : Game::Binary))
return 1;
// Game is played; changes are made...Da wir lediglich die Serialisierung eines Spiels mit JSON demonstrieren möchten, ist unser Spiel eigentlich nicht spielbar. Daher benötigen wir lediglich „ QCoreApplication “ und haben keine Ereignisschleife. Beim Start der Anwendung analysieren wir die Befehlszeilenargumente, um zu entscheiden, wie das Spiel gestartet werden soll. Für das erste Argument stehen die Optionen „new“ (Standard) und „load“ zur Verfügung. Wenn „new“ angegeben wird, wird ein neues Spiel generiert, und wenn „load“ angegeben wird, wird ein zuvor gespeichertes Spiel geladen. Für das zweite Argument stehen die Optionen „json“ (Standard) und „binary“ zur Verfügung. Dieses Argument bestimmt, in welche Datei gespeichert und/oder aus welcher Datei geladen wird. Wir gehen dann davon aus, dass der Spieler viel Spaß hatte und große Fortschritte gemacht hat, und passen den internen Zustand unserer Objekte „Character“, „Level“ und „Game“ entsprechend an.
QTextStream s(stdout);
s << "Game ended in the following state:\n";
game.print(s);
if (!game.saveGame(json ? Game::Json : Game::Binary))
return 1;
return 0;
}Wenn der Spieler fertig ist, speichern wir sein Spiel. Zu Demonstrationszwecken können wir entweder in JSON oder in CBOR serialisieren. Sie können den Inhalt der Dateien im selben Verzeichnis wie die ausführbare Datei überprüfen (oder das Beispiel erneut ausführen und dabei darauf achten, auch die Option „load“ anzugeben), wobei die binäre Speicherdatei einige unerwünschte Zeichen enthalten wird (was normal ist).
Damit ist unser Beispiel abgeschlossen. Wie Sie sehen können, ist die Serialisierung mit den JSON-Klassen von Qt sehr einfach und bequem. Der Vorteil der Verwendung von ` QJsonDocument ` und ähnlichen Funktionen gegenüber beispielsweise ` QDataStream` besteht darin, dass Sie nicht nur für Menschen lesbare JSON-Dateien erhalten, sondern bei Bedarf auch die Möglichkeit haben, ein binäres Format zu verwenden, ohne Code umschreiben zu müssen.
Siehe auch JSON-Unterstützung in Qt, CBOR-Unterstützung in Qt und Dateneingabe/-ausgabe.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.