Sur cette page

Sauvegarde et chargement d'un jeu

Comment sauvegarder et charger un jeu en utilisant les classes JSON ou CBOR de Qt.

De nombreux jeux proposent une fonctionnalité de sauvegarde, de sorte que la progression du joueur dans le jeu peut être sauvegardée et chargée ultérieurement. Le processus de sauvegarde d'un jeu implique généralement la sérialisation des variables membres de chaque objet du jeu dans un fichier. De nombreux formats peuvent être utilisés à cette fin, dont JSON. Avec QJsonDocument, vous avez également la possibilité de sérialiser un document au format CBOR, ce qui est très utile si vous ne voulez pas que le fichier de sauvegarde soit facile à lire (mais voir Parsing and displaying CBOR data pour savoir comment il peut être lu), ou si vous avez besoin de réduire la taille du fichier.

Dans cet exemple, nous allons montrer comment sauvegarder et charger un jeu simple depuis et vers les formats JSON et binaire.

La classe de caractères

La classe Character représente un personnage non joueur (PNJ) dans notre jeu, et stocke le nom, le niveau et le type de classe du joueur.

Elle fournit des fonctions statiques fromJson() et non statiques toJson() pour se sérialiser.

Remarque : ce schéma (fromJson()/toJson()) fonctionne parce que les QJsonbjects peuvent être construits indépendamment d'un QJsonDocument propriétaire, et parce que les types de données (dé)sérialisés ici sont des types de valeurs, et peuvent donc être copiés. Lors de la sérialisation dans un autre format - par exemple XML ou QDataStream, qui nécessite la transmission d'un objet de type document - ou lorsque l'identité de l'objet est importante (QObject sous-classes, par exemple), d'autres modèles peuvent être plus appropriés. Voir l'exemple dombookmarks pour XML, et l'implémentation de QListWidgetItem::read() et QListWidgetItem::write() pour la sérialisation idiomatique QDataStream. Les fonctions print() de cet exemple sont de bons exemples de sérialisation QTextStream, même s'il leur manque bien sûr la désérialisation.

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

Les implémentations des fonctions fromJson() et toJson() nous intéressent particulièrement :

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

Dans la fonction fromJson(), nous construisons un objet local result Character et attribuons aux membres de result les valeurs de l'argument QJsonObject. Vous pouvez utiliser QJsonObject::operator[]() ou QJsonObject::value() pour accéder aux valeurs de l'objet JSON ; les deux sont des fonctions constantes et renvoient QJsonValue::Undefined si la clé n'est pas valide. En particulier, les fonctions is... (par exemple QJsonValue::isString(), QJsonValue::isDouble()) renvoient false pour QJsonValue::Undefined, ce qui nous permet de vérifier l'existence et le type correct en une seule consultation.

Si une valeur n'existe pas dans l'objet JSON ou si elle n'a pas le bon type, nous n'écrivons pas non plus dans le membre result correspondant, ce qui préserve toutes les valeurs que le constructeur par défaut a pu définir. Cela signifie que les valeurs par défaut sont définies de manière centralisée en un seul endroit (le constructeur par défaut) et n'ont pas besoin d'être répétées dans le code de sérialisation(DRY).

Observez l'utilisation de C++17 if-with-initializer pour séparer le scoping et la vérification de la variable v. Cela signifie que nous pouvons garder le nom de la variable court, car sa portée est limitée.

Comparez cela à l'approche naïve utilisant QJsonObject::contains():

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

qui, en plus d'être moins lisible, nécessite un total de trois recherches (non, le compilateur ne les optimisera pas en une seule), est donc trois fois plus lente et répète "name" trois fois (ce qui viole le principe DRY).

QJsonObject Character::toJson() const
{
    QJsonObject json;
    json["name"] = mName;
    json["level"] = mLevel;
    json["classType"] = mClassType;
    return json;
}

Dans la fonction toJson(), nous faisons l'inverse de la fonction fromJson() ; nous assignons les valeurs de l'objet Character à un nouvel objet JSON que nous renvoyons ensuite. Comme pour l'accès aux valeurs, il existe deux façons de définir des valeurs sur un site QJsonObject: QJsonObject::operator[]() et QJsonObject::insert(). Ces deux méthodes remplacent toute valeur existante pour la clé donnée.

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

Nous voulons que les niveaux de notre jeu aient chacun plusieurs PNJ, nous gardons donc un QList d'objets Character. Nous fournissons également les fonctions fromJson() et toJson().

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

Les conteneurs peuvent être écrits et lus à partir de JSON à l'aide de QJsonArray. Dans notre cas, nous construisons un QJsonArray à partir de la valeur associée à la clé "npcs". Ensuite, pour chaque élément QJsonValue du tableau, nous appelons toObject() pour obtenir l'objet JSON du caractère. Character::fromJson() peut alors transformer ce QJSonObject en un objet Personnage à ajouter à notre tableau de PNJ.

Note : Les conteneurs associés peuvent être écrits en stockant la clé dans chaque objet valeur (si ce n'est pas déjà le cas). Avec cette approche, le conteneur est stocké comme un tableau normal d'objets, mais l'index de chaque élément est utilisé comme clé pour construire le conteneur lors de sa relecture.

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

Encore une fois, la fonction toJson() est similaire à la fonction fromJson(), sauf qu'elle est inversée.

La classe Game

Après avoir établi les classes Character et Level, nous pouvons passer à la classe Game :

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

Tout d'abord, nous définissons l'enum SaveFormat. Cela nous permettra de spécifier le format dans lequel le jeu doit être sauvegardé : Json ou Binary.

Ensuite, nous fournissons des accesseurs pour le joueur et les niveaux. Nous exposons ensuite trois fonctions : newGame(), saveGame() et loadGame().

Les fonctions read() et toJson() sont utilisées par saveGame() et loadGame().

Remarque : bien que Game soit une classe de valeur, nous supposons que l'auteur souhaite qu'un jeu ait une identité, tout comme votre fenêtre principale. Nous n'utilisons donc pas de fonction statique fromJson(), qui créerait un nouvel objet, mais une fonction read() que nous pouvons appeler sur des objets existants. Il existe une correspondance 1:1 entre read() et fromJson(), dans la mesure où l'une peut être implémentée en fonction de l'autre :

void read(const QJsonObject &json) { *this = fromJson(json); }
static Game fromObject(const QJsonObject &json) { Game g; g.read(json); return g; }

Nous utilisons simplement ce qui est le plus pratique pour les appelants des fonctions.

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

Pour mettre en place un nouveau jeu, nous créons le joueur et remplissons les niveaux et leurs PNJ.

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

La fonction read() commence par remplacer le joueur par celui qui a été lu à partir de JSON. Nous effaçons ensuite() le tableau des niveaux afin que l'appel à loadGame() sur le même objet Game deux fois ne se traduise pas par d'anciens niveaux qui traînent.

Nous remplissons ensuite le tableau de niveaux en lisant chaque niveau à partir d'un fichier QJsonArray.

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

L'écriture du jeu en JSON est similaire à l'écriture d'un niveau.

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

Lors du chargement d'une partie sauvegardée dans loadGame(), la première chose que nous faisons est d'ouvrir le fichier de sauvegarde en fonction du format dans lequel il a été sauvegardé ; "save.json" pour JSON, et "save.dat" pour CBOR. Nous affichons un avertissement et renvoyons false si le fichier n'a pas pu être ouvert.

Puisque QJsonDocument::fromJson() et QCborValue::fromCbor() prennent tous deux un QByteArray, nous pouvons lire tout le contenu du fichier de sauvegarde en un seul, quel que soit le format de sauvegarde.

Après avoir construit QJsonDocument, nous demandons à l'objet Game de se lire lui-même et renvoyons true pour indiquer le succès.

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

Sans surprise, saveGame() ressemble beaucoup à loadGame(). Nous déterminons l'extension du fichier en fonction du format, nous affichons un avertissement et nous renvoyons false si l'ouverture du fichier échoue. Nous écrivons ensuite l'objet Game dans un fichier QJsonObject. Pour sauvegarder le jeu dans le format spécifié, nous convertissons l'objet JSON en un fichier QJsonDocument pour un appel ultérieur à QJsonDocument::toJson(), ou en un fichier QCborValue pour QCborValue::toCbor().

Lier le tout

Nous sommes maintenant prêts à entrer dans main() :

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

Puisque nous sommes seulement intéressés par la démonstration de la sérialisation d'un jeu avec JSON, notre jeu n'est pas réellement jouable. Par conséquent, nous n'avons besoin que de QCoreApplication et n'avons pas de boucle événementielle. Au démarrage de l'application, nous analysons les arguments de la ligne de commande pour décider comment démarrer le jeu. Pour le premier argument, les options "new" (par défaut) et "load" sont disponibles. Lorsque "new" est spécifié, un nouveau jeu sera généré, et lorsque "load" est spécifié, un jeu précédemment sauvegardé sera chargé. Pour le deuxième argument, les options "json" (par défaut) et "binary" sont disponibles. Cet argument déterminera le fichier dans lequel le jeu sera sauvegardé et/ou chargé. Nous partons ensuite du principe que le joueur s'est bien amusé et a fait beaucoup de progrès, en modifiant l'état interne de nos objets Character, Level et Game.

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

Lorsque le joueur a terminé, nous sauvegardons son jeu. À des fins de démonstration, nous pouvons sérialiser en JSON ou CBOR. Vous pouvez examiner le contenu des fichiers dans le même répertoire que l'exécutable (ou réexécuter l'exemple en veillant à spécifier également l'option "load"), bien que le fichier de sauvegarde binaire contienne quelques caractères parasites (ce qui est normal).

Voilà qui conclut notre exemple. Comme vous pouvez le voir, la sérialisation avec les classes JSON de Qt est très simple et pratique. L'avantage d'utiliser QJsonDocument et ses amis plutôt que QDataStream, par exemple, est que vous obtenez non seulement des fichiers JSON lisibles par l'homme, mais que vous avez également la possibilité d'utiliser un format binaire si nécessaire, sans réécrire de code.

Exemple de projet @ code.qt.io

Voir aussi Support JSON dans Qt, Support CBOR dans Qt, et Data Input Output.

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