Sauvegarder et charger une partie
Comment sauvegarder et charger une partie à l’aide des classes JSON ou CBOR de Qt.
De nombreux jeux proposent une fonctionnalité de sauvegarde, afin que la progression du joueur puisse être enregistrée et chargée ultérieurement. Le processus de sauvegarde d'une partie consiste généralement à sérialiser les variables membres de chaque objet du jeu dans un fichier. De nombreux formats peuvent être utilisés à cette fin, dont le JSON. Avec QJsonDocument, vous avez également la possibilité de sérialiser un document au format CBOR, ce qui est idéal si vous ne souhaitez pas que le fichier de sauvegarde soit facilement lisible (mais consultez la section « Analyse et affichage des données CBOR » pour savoir comment le lire), ou si vous devez limiter la taille du fichier.
Dans cet exemple, nous allons montrer comment enregistrer et charger un jeu simple aux formats JSON et binaire.
La classe Character
La classe Character représente un personnage non joueur (PNJ) dans notre jeu et stocke le nom, le niveau et la classe du personnage.
Elle fournit une fonction statique `fromJson()` et une fonction non statique `toJson()` pour se sérialiser.
Remarque : ce modèle (fromJson()/toJson()) fonctionne car les objets QJsonObject peuvent être construits indépendamment d’un QJsonDocument, et parce que les types de données (dé)sérialisés ici sont des types de valeur, qui peuvent donc être copiés. Lors de la sérialisation vers un autre format — par exemple XML ou QDataStream, qui nécessitent le passage d’un objet de type document — ou lorsque l’identité de l’objet est importante (les sous-classes deQObject, par exemple), d’autres modèles peuvent s’avérer plus adaptés. Consultez l’exemple `dombookmarks ` pour le XML, ainsi que l’implémentation de ` QListWidgetItem::read()` et ` QListWidgetItem::write()` pour une sérialisation idiomatique de ` QDataStream `. Les fonctions ` print() ` de cet exemple constituent de bons exemples de sérialisation de ` QTextStream `, même si, bien sûr, elles ne prennent pas en charge 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() présentent un intérêt particulier pour nous :
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 créons un objet local de type Character result et attribuons aux membres de result les valeurs provenant de l’argument QJsonObject. Vous pouvez utiliser soit QJsonObject::operator[]() soit QJsonObject::value() pour accéder aux valeurs contenues dans l’objet JSON ; ces deux fonctions sont des fonctions const et renvoient QJsonValue::Undefined si la clé n’est pas valide. En particulier, les fonctions is... (par exemple QJsonValue::isString() et QJsonValue::isDouble()) renvoient false pour QJsonValue::Undefined, ce qui nous permet de vérifier à la fois l’existence et le type correct en une seule consultation.
Si une valeur n’existe pas dans l’objet JSON ou si son type est incorrect, nous n’écrivons pas non plus dans le membre result correspondant, préservant ainsi 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 à 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).
Notez l’utilisation de la construction « if-with-initializer » de C++17 pour séparer la portée et la vérification de la variable ` v`. Cela nous permet de conserver un nom de 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 au total trois recherches (non, le compilateur ne les optimisera pas en une seule), ce qui la rend trois fois plus lente et répète trois fois l’ "name" (en violation du principe DRY).
QJsonObject Character::toJson() const
{
QJsonObject json;
json["name"] = mName;
json["level"] = mLevel;
json["classType"] = mClassType;
return json;
}Dans la fonction toJson(), nous effectuons l’opération inverse de celle de la fonction fromJson() : nous attribuons 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 d’attribuer des valeurs à un objet QJsonObject: QJsonObject::operator[]() et QJsonObject::insert(). Les deux méthodes écraseront toute valeur existante associée à 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 souhaitons que chaque niveau de notre jeu comporte plusieurs PNJ ; nous conservons donc un ` QList ` d’objets `Character`. Nous fournissons également les fonctions familières `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 au format 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 récupérer l’objet JSON du Character. La méthode Character::fromJson() peut ensuite convertir ce QJSonObject en un objet Character afin de l'ajouter à notre tableau de PNJ.
Remarque : les conteneurs associés peuvent être créés en stockant la clé dans chaque objet de valeur (si ce n’est pas déjà le cas). Avec cette approche, le conteneur est stocké sous la forme d’un tableau classique d’objets, mais l’index de chaque élément est utilisé comme clé pour reconstruire 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;
}Là encore, la fonction toJson() est similaire à la fonction fromJson(), mais en sens inverse.
La classe Game
Maintenant que nous avons défini 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’énumération « 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 qu'Game soit une classe de valeur, nous partons du principe que l'auteur souhaite que le jeu dispose d'une identité, à l'instar de votre fenêtre principale. Nous n’utilisons donc pas une 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 biunivoque entre read() et fromJson(), dans la mesure où l’une peut être implémentée en termes 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 de ces 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 configurer une nouvelle partie, nous créons le joueur et remplissons les niveaux ainsi que 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 lu à partir du fichier JSON. Nous appelons ensuite la fonction `clear()` sur le tableau des niveaux afin que le fait d’appeler deux fois la fonction `loadGame()` sur le même objet `Game` n’entraîne pas la persistance d’anciens niveaux.
Nous remplissons ensuite le tableau des niveaux en lisant chaque niveau à partir d'un ` 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 au format JSON s'effectue de la même manière que 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) << « Fichier de sauvegarde chargé pour » << loadDoc["player"]["name"].toString()
<< « à l'aide de » << (saveFormat != Json ? « CBOR »: « JSON ») << « ...\n »;
return true;
}Lors du chargement d’une partie sauvegardée dans la fonction `loadGame()`, la première chose que nous faisons est d’ouvrir le fichier de sauvegarde en fonction du format dans lequel il a été enregistré : ` "save.json" ` pour JSON, et ` "save.dat" ` pour CBOR. Nous affichons un message d’avertissement et renvoyons ` false ` si le fichier n’a pas pu être ouvert.
Comme les fonctions QJsonDocument::fromJson() et QCborValue::fromCbor() acceptent toutes deux un objet QByteArray, nous pouvons lire l’intégralité du contenu du fichier de sauvegarde dans un seul objet, quel que soit le format de sauvegarde.
Après avoir créé l’ QJsonDocument, nous demandons à l’objet Game de se lire lui-même, puis nous renvoyons true pour indiquer que l’opération a réussi.
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, la fonction `saveGame()` ressemble beaucoup à `loadGame()`. Nous déterminons l'extension du fichier en fonction du format, affichons un avertissement et renvoyons ` false ` si l'ouverture du fichier échoue. Nous écrivons ensuite l’objet Game dans un fichier QJsonObject. Pour enregistrer la partie dans le format spécifié, nous convertissons l’objet JSON soit en QJsonDocument pour un appel ultérieur à QJsonDocument::toJson(), soit en QCborValue pour QCborValue::toCbor().
Rassembler le tout
Nous sommes désormais prêts à entrer dans la fonction 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...Comme notre seul objectif est de montrer comment sérialiser un jeu avec JSON, notre jeu n'est pas réellement jouable. Nous n'avons donc besoin que de la fonction ` QCoreApplication ` et n'avons pas de boucle d'événements. Au démarrage de l'application, nous analysons les arguments de ligne de commande pour déterminer comment lancer le jeu. Pour le premier argument, les options « new » (par défaut) et « load » sont disponibles. Lorsque « new » est spécifié, une nouvelle partie est générée ; lorsque « load » est spécifié, une partie précédemment enregistrée est chargée. Pour le deuxième argument, les options « json » (par défaut) et « binary » sont disponibles. Cet argument détermine le fichier dans lequel la partie sera enregistrée et/ou à partir duquel elle sera chargée. Nous partons ensuite du principe que le joueur s'est bien amusé et a bien progressé, ce qui modifie 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 enregistrons sa partie. À des fins de démonstration, nous pouvons sérialiser au format JSON ou CBOR. Vous pouvez examiner le contenu des fichiers dans le même répertoire que l’exécutable (ou relancer l’exemple, en veillant à spécifier également l’option « load »), bien que le fichier de sauvegarde binaire contienne quelques caractères indésirables (ce qui est normal).
Voilà qui conclut notre exemple. Comme vous pouvez le constater, la sérialisation à l’aide des classes JSON de Qt est très simple et pratique. L’avantage d’utiliser QJsonDocument et ses dérivés plutôt que QDataStream, par exemple, est que vous obtenez non seulement des fichiers JSON lisibles par l’utilisateur, mais que vous avez également la possibilité d’utiliser un format binaire si nécessaire, sans avoir à réécrire le moindre code.
Voir également la prise en charge de JSON dans Qt, la prise en charge de CBOR dans Qt et l’entrée-sortie de données.
© 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.