Guardar y cargar una partida
Cómo guardar y cargar un juego utilizando las clases JSON o CBOR de Qt.
Muchos juegos proporcionan la funcionalidad de guardar, de modo que el progreso del jugador a través del juego puede ser guardado y cargado en un momento posterior. El proceso de guardar un juego generalmente implica serializar las variables miembro de cada objeto del juego a un archivo. Se pueden utilizar muchos formatos para este propósito, uno de los cuales es JSON. Con QJsonDocument, también tienes la posibilidad de serializar un documento en un formato CBOR, que es genial si no quieres que el archivo de guardado sea fácil de leer (pero mira Parsing and displaying CBOR data para saber cómo se puede leer), o si necesitas mantener el tamaño del archivo bajo.
En este ejemplo, demostraremos cómo guardar y cargar un juego simple desde y hacia los formatos JSON y binario.
La Clase Personaje
La clase Personaje representa un personaje no jugador (PNJ) en nuestro juego, y almacena el nombre del jugador, el nivel y el tipo de clase.
Proporciona funciones estáticas fromJson() y no estáticas toJson() para serializarse.
Nota: Este patrón (fromJson()/toJson()) funciona porque los QJsonObjects se pueden construir independientemente de un QJsonDocument propietario, y porque los tipos de datos que se (de)serializan aquí son tipos de valor, por lo que se pueden copiar. Cuando se serializa a otro formato - por ejemplo XML o QDataStream, que requieren pasar un objeto tipo documento - o cuando la identidad del objeto es importante (QObject subclases, por ejemplo), otros patrones pueden ser más adecuados. Véase el ejemplo de dombookmarks para XML, y la implementación de QListWidgetItem::read() y QListWidgetItem::write() para la serialización idiomática de QDataStream. Las funciones print() de este ejemplo son buenos ejemplos de serialización QTextStream, aunque, por supuesto, carecen de la parte de deserialización.
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; };
De particular interés para nosotros son las implementaciones de las funciones fromJson() y 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; }
En la función fromJson(), construimos un objeto local result Character y asignamos a los miembros de result los valores del argumento QJsonObject. Puede utilizar QJsonObject::operator[]() o QJsonObject::value() para acceder a los valores dentro del objeto JSON; ambas son funciones const y devuelven QJsonValue::Undefined si la clave no es válida. En particular, las funciones is... (por ejemplo QJsonValue::isString(), QJsonValue::isDouble()) devuelven false para QJsonValue::Undefined, por lo que podemos comprobar la existencia así como el tipo correcto en una sola búsqueda.
Si un valor no existe en el objeto JSON, o tiene un tipo incorrecto, tampoco escribimos en el miembro result correspondiente, preservando así cualquier valor que el constructor por defecto pueda haber establecido. Esto significa que los valores por defecto se definen de forma centralizada en una ubicación (el constructor por defecto) y no es necesario repetirlos en el código de serialización(DRY).
Observa el uso del if-with-initializer de C++17 para separar el ámbito y la comprobación de la variable v. Esto significa que podemos mantener el nombre de la variable corto, porque su alcance es limitado.
Compárese con el enfoque ingenuo que utiliza QJsonObject::contains():
if (json.contains("name") && json["name"].isString())
result.mName = json["name"].toString();que, además de ser menos legible, requiere un total de tres búsquedas (no, el compilador no las optimizará en una), por lo que es tres veces más lento y repite "name" tres veces (violando el principio DRY).
QJsonObject Character::toJson() const { QJsonObject json; json["name"] = mName; json["level"] = mLevel; json["classType"] = mClassType; return json; }
En la función toJson(), hacemos lo contrario de la función fromJson(); asignamos valores del objeto Character a un nuevo objeto JSON que luego devolvemos. Al igual que con el acceso a valores, existen dos formas de establecer valores en QJsonObject: QJsonObject::operator[]() y QJsonObject::insert(). Ambas anularán cualquier valor existente en la clave dada.
La clase 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; };
Queremos que cada uno de los niveles de nuestro juego tenga varios NPCs, por lo que mantenemos un QList de objetos Personaje. También proporcionamos las conocidas funciones fromJson() y 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; }
Los contenedores se pueden escribir y leer desde JSON utilizando QJsonArray. En nuestro caso, construimos un QJsonArray a partir del valor asociado a la clave "npcs". A continuación, para cada elemento QJsonValue de la matriz, llamamos a toObject() para obtener el objeto JSON del carácter. Character::fromJson() puede entonces convertir ese QJSonObject en un objeto Personaje para añadirlo a nuestro array NPC.
Nota: Los contenedores asociados pueden ser escritos almacenando la clave en cada objeto valor (si no lo está ya). Con este enfoque, el contenedor se almacena como un array normal de objetos, pero el índice de cada elemento se utiliza como la clave para construir el contenedor cuando se lee de nuevo.
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; }
De nuevo, la función toJson() es similar a la función fromJson(), excepto que invertida.
La clase Game
Habiendo establecido las clases Personaje y Nivel, podemos pasar a la clase Juego:
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; };
En primer lugar, definimos el enum SaveFormat. Esto nos permitirá especificar el formato en el que el juego debe ser guardado: Json o Binary.
A continuación, proporcionamos los accesores para el jugador y los niveles. A continuación, exponemos tres funciones: newGame(), saveGame() y loadGame().
Las funciones read() y toJson() son utilizadas por saveGame() y loadGame().
Nota: A pesar de que Game es una clase de valor, asumimos que el autor quiere que el juego tenga identidad, como la tendría su ventana principal. Por lo tanto, no usamos una función estática fromJson(), que crearía un nuevo objeto, sino una función read() que podemos llamar sobre objetos existentes. Hay una correspondencia 1:1 entre read() y fromJson(), en el sentido de que una puede implementarse en términos de la otra:
void read(const QJsonObject &json) { *this = fromJson(json); } static Game fromObject(const QJsonObject &json) { Game g; g.read(json); return g; }
Sólo usamos lo que es más conveniente para quienes llaman a las funciones.
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); }
Para configurar un nuevo juego, creamos el jugador y rellenamos los niveles y sus NPCs.
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 función read() comienza reemplazando el jugador por el que se ha leído de JSON. Luego limpiamos() el array de niveles para que al llamar loadGame() sobre el mismo objeto Game dos veces no queden niveles viejos por ahí.
A continuación, rellenamos el array de niveles leyendo cada nivel de 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; }
Escribir el juego en JSON es similar a escribir un nivel.
bool Game::loadGame(Game::SaveFormat saveFormat) { QFile loadFile(saveFormat == Json ? "save.json"_L1 : "save.dat"_L1); if (!loadFile.open(QIODevice::Sólo lectura)) { 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; }
Al cargar una partida guardada en loadGame(), lo primero que hacemos es abrir el archivo de guardado en función del formato en el que se guardó; "save.json" para JSON, y "save.dat" para CBOR. Imprimimos una advertencia y devolvemos false si no se pudo abrir el archivo.
Como QJsonDocument::fromJson() y QCborValue::fromCbor() toman ambos un QByteArray, podemos leer todo el contenido del archivo guardado en uno, independientemente del formato de guardado.
Después de construir el QJsonDocument, instruimos al objeto Game para que se lea a sí mismo y luego devolvemos true para indicar el éxito.
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; }
Como es lógico, saveGame() se parece mucho a loadGame(). Determinamos la extensión del archivo basándonos en el formato, imprimimos una advertencia y devolvemos false si falla la apertura del archivo. A continuación, escribimos el objeto Game en un QJsonObject. Para guardar el juego en el formato especificado, convertimos el objeto JSON en un QJsonDocument para una llamada posterior a QJsonDocument::toJson(), o en un QCborValue para QCborValue::toCbor().
Atando cabos
Ahora estamos listos para entrar en 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...
Dado que sólo estamos interesados en demostrar la serialización de un juego con JSON, nuestro juego no es realmente jugable. Por lo tanto, sólo necesitamos QCoreApplication y no tenemos bucle de eventos. Al inicio de la aplicación analizamos los argumentos de la línea de comandos para decidir cómo iniciar el juego. Para el primer argumento están disponibles las opciones "new" (por defecto) y "load". Cuando se especifica "new" se generará un nuevo juego, y cuando se especifica "load" se cargará un juego previamente guardado. Para el segundo argumento "json" (por defecto) y "binary" están disponibles como opciones. Este argumento decidirá en qué archivo se guarda y/o desde cuál se carga. Entonces seguimos adelante y asumimos que el jugador se lo ha pasado muy bien y ha progresado mucho, alterando el estado interno de nuestros objetos Personaje, Nivel y Juego.
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; }
Cuando el jugador ha terminado, guardamos su juego. Para propósitos de demostración, podemos serializar a JSON o CBOR. Puedes examinar el contenido de los archivos en el mismo directorio que el ejecutable (o volver a ejecutar el ejemplo, asegurándote de especificar también la opción "load"), aunque el archivo binario de guardado contendrá algunos caracteres basura (lo cual es normal).
Con esto concluye nuestro ejemplo. Como puedes ver, la serialización con las clases JSON de Qt es muy simple y conveniente. Las ventajas de usar QJsonDocument y amigos sobre QDataStream, por ejemplo, es que no sólo obtienes archivos JSON legibles por humanos, sino que también tienes la opción de usar un formato binario si es necesario, sin reescribir ningún código.
Ver también JSON Support in Qt, CBOR Support in Qt, y 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.