Speichern und Laden eines Spiels

Wie man ein Spiel mit Qt's JSON oder CBOR Klassen speichert und lädt.

Viele Spiele bieten eine Speicherfunktion, so dass der Fortschritt des Spielers im Spiel gespeichert und zu einem späteren Zeitpunkt geladen werden kann. Der Prozess des Speicherns eines Spiels beinhaltet im Allgemeinen die Serialisierung der Mitgliedsvariablen jedes Spielobjekts in eine Datei. Zu diesem Zweck können viele Formate verwendet werden, eines davon ist JSON. Mit QJsonDocument haben Sie auch die Möglichkeit, ein Dokument in einem CBOR-Format zu serialisieren, was sich anbietet, wenn Sie nicht wollen, dass die Speicherdatei einfach zu lesen ist (siehe aber Parsen und Anzeigen von CBOR-Daten, wie sie gelesen werden können ), oder wenn Sie die Dateigröße gering halten müssen.

In diesem Beispiel wird gezeigt, wie man ein einfaches Spiel in JSON- und Binärformaten speichert und lädt.

Die Zeichenklasse

Die Klasse Character stellt einen Nicht-Spieler-Charakter (NPC) in unserem Spiel dar und speichert den Namen, die Stufe und den Klassentyp des Spielers.

Sie bietet statische fromJson()- und nicht-statische toJson()-Funktionen, um sich selbst zu serialisieren.

Hinweis: Dieses Muster (fromJson()/toJson()) funktioniert, weil QJsonObjects unabhängig von einem eigenen QJsonDocument konstruiert werden können und weil die Datentypen, die hier (de)serialisiert werden, Wertetypen sind, also kopiert werden können. Bei der Serialisierung in ein anderes Format - z.B. XML oder QDataStream, die die Übergabe eines dokumentähnlichen Objekts erfordern - oder wenn die Objektidentität wichtig ist (z.B.QObject Unterklassen), können andere Muster besser geeignet sein. Siehe das dombookmarks Beispiel für XML, und die Implementierung von QListWidgetItem::read() und QListWidgetItem::write() für die idiomatische QDataStream Serialisierung. Die print() Funktionen in diesem Beispiel sind gute Beispiele für QTextStream Serialisierung, 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 fromJson()-Funktion konstruieren wir ein lokales result Character-Objekt und weisen result's Mitgliedern Werte aus dem QJsonObject Argument 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 die Funktionen is... (z. B. QJsonValue::isString(), QJsonValue::isDouble()) geben false für QJsonValue::Undefined zurück, so dass wir in einem einzigen Lookup sowohl die Existenz als auch den richtigen 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 Mitglied von result, so dass alle Werte, die der Standardkonstruktor möglicherweise gesetzt hat, erhalten bleiben. Dies bedeutet, dass die Standardwerte zentral an einer Stelle (dem Standardkonstruktor) definiert werden und im Serialisierungscode nicht wiederholt werden müssen(DRY).

Beachten Sie die Verwendung von C++17 if-with-initializer zur Trennung von Scoping und Überprüfung der Variable v. Dies bedeutet, dass wir den Variablennamen kurz halten können, da sein Geltungsbereich begrenzt ist.

Vergleichen Sie dies mit dem naiven Ansatz, der QJsonObject::contains() verwendet:

if (json.contains("name") && json["name"].isString())
    result.mName = json["name"].toString();

Dieser ist nicht nur weniger lesbar, sondern erfordert auch insgesamt drei Suchvorgänge (nein, der Compiler optimiert diese nicht auf einen), 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() machen wir das Gegenteil der Funktion fromJson(); wir weisen Werte aus dem Character-Objekt einem neuen JSON-Objekt zu, das wir dann zurückgeben. Wie beim Zugriff auf Werte gibt es zwei Möglichkeiten, Werte auf einer QJsonObject zu setzen: QJsonObject::operator[]() und QJsonObject::insert(). Beide überschreiben jeden vorhandenen Wert für den 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 wollen, dass die Levels in unserem Spiel jeweils mehrere NPCs haben, also halten wir eine QList von Character-Objekten vor. Wir stellen auch die bekannten fromJson()- und toJson()-Funktionen zur Verfügung.

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 konstruieren wir ein QJsonArray aus dem Wert, der mit dem Schlüssel "npcs" verbunden ist. Dann rufen wir für jedes QJsonValue Element in dem Array die Funktion toObject() auf, um das JSON-Objekt des Zeichens zu erhalten. Character::fromJson() kann dann dieses QJSonObject in ein Character-Objekt umwandeln, das an unser NPC-Array angehängt wird.

Hinweis: Assoziierte Container können geschrieben werden, indem der Schlüssel in jedem Value-Objekt gespeichert wird (wenn er es nicht schon ist). Bei diesem Ansatz wird der Container als normales Array von Objekten gespeichert, aber der Index jedes Elements wird als Schlüssel verwendet, um den Container zu konstruieren, wenn er wieder eingelesen wird.

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 ist die Funktion toJson() ähnlich wie die Funktion fromJson(), nur umgekehrt.

Die Klasse Game

Nachdem wir die Charakter- und die Level-Klasse erstellt haben, können wir uns der Game-Klasse zuwenden:

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;
};

Als erstes definieren wir das SaveFormat enum. Damit können wir das Format festlegen, in dem das Spiel gespeichert werden soll: Json oder Binary.

Als nächstes stellen wir Accessors für den Spieler und die Levels bereit. Anschließend stellen wir drei Funktionen bereit: 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 hat, ähnlich wie Ihr Hauptfenster. Wir verwenden daher keine statische fromJson()-Funktion, die ein neues Objekt erzeugen würde, sondern eine read()-Funktion, die wir bei vorhandenen Objekten aufrufen können. Es gibt eine 1:1-Entsprechung zwischen read() und fromJson(), so dass die eine Funktion in Form der anderen implementiert werden kann:

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 bequemer 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 bestücken die Level und ihre NSCs.

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 read()-Funktion beginnt mit dem Ersetzen des Spielers durch den aus JSON gelesenen. Anschließend wird das Level-Array mit clear() gelöscht, damit beim zweiten Aufruf von loadGame() für dasselbe Game-Objekt keine alten Level übrig bleiben.

Dann füllen wir das Level-Array auf, indem wir jedes Level aus einem QJsonArray lesen.

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 Schreiben des Spiels in JSON ist ähnlich wie das Schreiben 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)<< "Loaded save for "<< loadDoc["player"]["name"].toString()<< " using "<< (saveFormat != Json ? "CBOR": "JSON")<< "...\n"; return true; }

Wenn wir ein gespeichertes Spiel in loadGame() laden, öffnen wir als erstes die Speicherdatei, je nachdem, in welchem Format sie gespeichert wurde; "save.json" für JSON und "save.dat" für CBOR. Wir geben eine Warnung aus und geben false zurück, wenn die Datei nicht geöffnet werden konnte.

Da QJsonDocument::fromJson() und QCborValue::fromCbor() beide ein QByteArray annehmen, können wir den gesamten Inhalt der Speicherdatei in eine Datei lesen, unabhängig vom Speicherformat.

Nach der Erstellung von QJsonDocument weisen wir das Game-Objekt an, sich selbst zu lesen, und geben dann 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() sehr ähnlich wie loadGame() aussieht. Wir bestimmen die Dateierweiterung anhand des Formats, geben eine Warnung aus und geben false zurück, wenn das Öffnen der Datei fehlschlägt. Anschließend schreiben wir das Game-Objekt in ein QJsonObject. Um das Spiel in dem angegebenen Format zu speichern, wandeln wir das JSON-Objekt entweder in ein QJsonDocument für einen nachfolgenden QJsonDocument::toJson()-Aufruf oder in ein QCborValue für QCborValue::toCbor().

Alles zusammenbinden

Wir sind nun bereit, 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 nur daran interessiert sind, die Serialisierung eines Spiels mit JSON zu demonstrieren, ist unser Spiel nicht wirklich spielbar. Daher benötigen wir nur 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 erzeugt, und wenn "load" angegeben wird, wird ein zuvor gespeichertes Spiel geladen. Für das zweite Argument stehen "json" (Standard) und "binary" als Optionen zur Verfügung. Dieses Argument entscheidet, in welcher Datei gespeichert und/oder aus welcher Datei geladen wird. Wir gehen dann davon aus, dass der Spieler viel Spaß hatte und viele Fortschritte gemacht hat, indem er den internen Zustand unserer Charakter-, Level- und Spielobjekte verändert hat.

    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 die Daten entweder in JSON oder CBOR serialisieren. Sie können den Inhalt der Dateien im gleichen Verzeichnis wie die ausführbare Datei untersuchen (oder das Beispiel erneut ausführen und dabei sicherstellen, dass Sie auch die Option "load" angeben), obwohl die binäre Speicherdatei einige Müllzeichen 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 Freunden gegenüber QDataStream besteht darin, dass Sie nicht nur von Menschen lesbare JSON-Dateien erhalten, sondern auch die Möglichkeit haben, ein binäres Format zu verwenden, wenn dies erforderlich ist, ohne dass Sie irgendeinen Code neu schreiben müssen.

Beispielprojekt @ code.qt.io

Siehe auch JSON-Unterstützung in Qt, CBOR-Unterstützung in Qt und Dateneingabe/Ausgabe.

© 2025 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.