savegame ex.: revamp the way we (de)serialize JSON
JSON, unlike, say, QDataStream, allows building up objects independent of some central object, and combining them into a QJsonDocument later. This suggests returning QJsonObjects from a toJson() const method instead of having the caller supply a QJsonObject. Doing it this way enables transparent move semantics to kick in, too. For deserialization, use a fromJson() named constructor for value-like classes (where identity doesn't matter, only equality). Keep using read(), too, and add a note to explain when to use which form. Also, avoid the triple lookup from if (json.contains("key") && json["key"].isSoughtType()) mFoo = json["key"].toSoughtType(); by using C++17 if-with-initializer and showing the trick with Undefined never being of isSoughtType(): if (const QJsonValue v = json["key"]; v.isSoughtType()) mFoo = v.toSoughtType(); Adjust the discussion to match the new code, up the copyright years and rename some qdoc snippet markers from nondescript [0]/[1] to [toJson]/[fromJson]. Task-number: QTBUG-108857 Change-Id: Icaa14acc7464fef00a59534679d710252e921383 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io> (cherry picked from commit 5a3ac484dbcc64c8ee7a57854fcdde6b4b067aaa) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
This commit is contained in:
parent
6382fdca60
commit
7eafdf4c81
@ -48,28 +48,34 @@ void Character::setClassType(Character::ClassType classType)
|
|||||||
mClassType = classType;
|
mClassType = classType;
|
||||||
}
|
}
|
||||||
|
|
||||||
//! [0]
|
//! [fromJson]
|
||||||
void Character::read(const QJsonObject &json)
|
Character Character::fromJson(const QJsonObject &json)
|
||||||
{
|
{
|
||||||
if (json.contains("name") && json["name"].isString())
|
Character result;
|
||||||
mName = json["name"].toString();
|
|
||||||
|
|
||||||
if (json.contains("level") && json["level"].isDouble())
|
if (const QJsonValue v = json["name"]; v.isString())
|
||||||
mLevel = json["level"].toInt();
|
result.mName = v.toString();
|
||||||
|
|
||||||
if (json.contains("classType") && json["classType"].isDouble())
|
if (const QJsonValue v = json["level"]; v.isDouble())
|
||||||
mClassType = ClassType(json["classType"].toInt());
|
result.mLevel = v.toInt();
|
||||||
|
|
||||||
|
if (const QJsonValue v = json["classType"]; v.isDouble())
|
||||||
|
result.mClassType = ClassType(v.toInt());
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
//! [0]
|
//! [fromJson]
|
||||||
|
|
||||||
//! [1]
|
//! [toJson]
|
||||||
void Character::write(QJsonObject &json) const
|
QJsonObject Character::toJson() const
|
||||||
{
|
{
|
||||||
|
QJsonObject json;
|
||||||
json["name"] = mName;
|
json["name"] = mName;
|
||||||
json["level"] = mLevel;
|
json["level"] = mLevel;
|
||||||
json["classType"] = mClassType;
|
json["classType"] = mClassType;
|
||||||
|
return json;
|
||||||
}
|
}
|
||||||
//! [1]
|
//! [toJson]
|
||||||
|
|
||||||
void Character::print(int indentation) const
|
void Character::print(int indentation) const
|
||||||
{
|
{
|
||||||
|
@ -31,8 +31,8 @@ public:
|
|||||||
ClassType classType() const;
|
ClassType classType() const;
|
||||||
void setClassType(ClassType classType);
|
void setClassType(ClassType classType);
|
||||||
|
|
||||||
void read(const QJsonObject &json);
|
static Character fromJson(const QJsonObject &json);
|
||||||
void write(QJsonObject &json) const;
|
QJsonObject toJson() const;
|
||||||
|
|
||||||
void print(int indentation = 0) const;
|
void print(int indentation = 0) const;
|
||||||
private:
|
private:
|
||||||
|
@ -24,45 +24,81 @@
|
|||||||
The Character class represents a non-player character (NPC) in our game, and
|
The Character class represents a non-player character (NPC) in our game, and
|
||||||
stores the player's name, level, and class type.
|
stores the player's name, level, and class type.
|
||||||
|
|
||||||
It provides read() and write() functions to serialise its member variables.
|
It provides static fromJson() and non-static toJson() functions to
|
||||||
|
serialise itself.
|
||||||
|
|
||||||
|
\note This pattern (fromJson()/toJson()) works because QJsonObjects can be
|
||||||
|
constructed independent of an owning QJsonDocument, and because the data
|
||||||
|
types being (de)serialized here are value types, so can be copied. When
|
||||||
|
serializing to another format — for example XML or QDataStream, which require passing
|
||||||
|
a document-like object — or when the object identity is important (QObject
|
||||||
|
subclasses, for example), other patterns may be more suitable. See the
|
||||||
|
\l{xml/dombookmarks} and \l{xml/streambookmarks} examples for XML, and the
|
||||||
|
implementation of \l QListWidgetItem::read() and \l QListWidgetItem::write()
|
||||||
|
for idiomatic QDataStream serialization.
|
||||||
|
|
||||||
\snippet serialization/savegame/character.h 0
|
\snippet serialization/savegame/character.h 0
|
||||||
|
|
||||||
Of particular interest to us are the read and write function
|
Of particular interest to us are the fromJson() and toJson() function
|
||||||
implementations:
|
implementations:
|
||||||
|
|
||||||
\snippet serialization/savegame/character.cpp 0
|
\snippet serialization/savegame/character.cpp fromJson
|
||||||
|
|
||||||
In the read() function, we assign Character's members values from the
|
In the fromJson() function, we construct a local \c result Character object
|
||||||
QJsonObject argument. You can use either \l QJsonObject::operator[]() or
|
and assign \c{result}'s members values from the QJsonObject argument. You
|
||||||
QJsonObject::value() to access values within the JSON object; both are
|
can use either \l QJsonObject::operator[]() or QJsonObject::value() to
|
||||||
const functions and return QJsonValue::Undefined if the key is invalid. We
|
access values within the JSON object; both are const functions and return
|
||||||
check if the keys are valid before attempting to read them with
|
QJsonValue::Undefined if the key is invalid. In particular, the \c{is...}
|
||||||
QJsonObject::contains().
|
functions (for example \l QJsonValue::isString(), \l
|
||||||
|
QJsonValue::isDouble()) return \c false for QJsonValue::Undefined, so we
|
||||||
|
can check for existence as well as the correct type in a single lookup.
|
||||||
|
|
||||||
\snippet serialization/savegame/character.cpp 1
|
If a value does not exist in the JSON object, or has the wrong type, we
|
||||||
|
don't write to the corresponding \c result member, either, thereby
|
||||||
|
preserving any values the default constructor may have set. This means
|
||||||
|
default values are centrally defined in one location (the default
|
||||||
|
constructor) and need not be repeated in serialisation code
|
||||||
|
(\l{https://en.wikipedia.org/wiki/Don%27t_repeat_yourself}{DRY}).
|
||||||
|
|
||||||
In the write() function, we do the reverse of the read() function; assign
|
Observe the use of
|
||||||
values from the Character object to the JSON object. As with accessing
|
\l{https://en.cppreference.com/w/cpp/language/if#If_statements_with_initializer}
|
||||||
values, there are two ways to set values on a QJsonObject:
|
{C++17 if-with-initializer} to separate scoping and checking of the variable \c v.
|
||||||
\l QJsonObject::operator[]() and QJsonObject::insert(). Both will override
|
This means we can keep the variable name short, because its scope is limited.
|
||||||
any existing value at the given key.
|
|
||||||
|
|
||||||
Next up is the Level class:
|
Compare that to the naïve approach using \c QJsonObject::contains():
|
||||||
|
|
||||||
|
\badcode
|
||||||
|
if (json.contains("name") && json["name"].isString())
|
||||||
|
result.mName = json["name"].toString();
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
which, beside being less readable, requires a total of three lookups (no,
|
||||||
|
the compiler will \e not optimize these into one), so is three times
|
||||||
|
slower and repeats \c{"name"} three times (violating the DRY principle).
|
||||||
|
|
||||||
|
\snippet serialization/savegame/character.cpp toJson
|
||||||
|
|
||||||
|
In the toJson() function, we do the reverse of the fromJson() function;
|
||||||
|
assign values from the Character object to a new JSON object we then
|
||||||
|
return. As with accessing values, there are two ways to set values on a
|
||||||
|
QJsonObject: \l QJsonObject::operator[]() and \l QJsonObject::insert().
|
||||||
|
Both will override any existing value at the given key.
|
||||||
|
|
||||||
|
\section1 The Level Class
|
||||||
|
|
||||||
\snippet serialization/savegame/level.h 0
|
\snippet serialization/savegame/level.h 0
|
||||||
|
|
||||||
We want to have several levels in our game, each with several NPCs, so we
|
We want the levels in our game to each each have several NPCs, so we keep a QList
|
||||||
keep a QList of Character objects. We also provide the familiar read() and
|
of Character objects. We also provide the familiar fromJson() and toJson()
|
||||||
write() functions.
|
functions.
|
||||||
|
|
||||||
\snippet serialization/savegame/level.cpp 0
|
\snippet serialization/savegame/level.cpp fromJson
|
||||||
|
|
||||||
Containers can be written and read to and from JSON using QJsonArray. In our
|
Containers can be written to and read from JSON using QJsonArray. In our
|
||||||
case, we construct a QJsonArray from the value associated with the key
|
case, we construct a QJsonArray from the value associated with the key
|
||||||
\c "npcs". Then, for each QJsonValue element in the array, we call
|
\c "npcs". Then, for each QJsonValue element in the array, we call
|
||||||
toObject() to get the Character's JSON object. The Character object can then
|
toObject() to get the Character's JSON object. Character::fromJson() can
|
||||||
read their JSON and be appended to our NPC array.
|
then turn that QJSonObject into a Character object to append to our NPC array.
|
||||||
|
|
||||||
\note \l{Container Classes}{Associate containers} can be written by storing
|
\note \l{Container Classes}{Associate containers} can be written by storing
|
||||||
the key in each value object (if it's not already). With this approach, the
|
the key in each value object (if it's not already). With this approach, the
|
||||||
@ -70,11 +106,13 @@
|
|||||||
element is used as the key to construct the container when reading it back
|
element is used as the key to construct the container when reading it back
|
||||||
in.
|
in.
|
||||||
|
|
||||||
\snippet serialization/savegame/level.cpp 1
|
\snippet serialization/savegame/level.cpp toJson
|
||||||
|
|
||||||
Again, the write() function is similar to the read() function, except
|
Again, the toJson() function is similar to the fromJson() function, except
|
||||||
reversed.
|
reversed.
|
||||||
|
|
||||||
|
\section1 The Game Class
|
||||||
|
|
||||||
Having established the Character and Level classes, we can move on to
|
Having established the Character and Level classes, we can move on to
|
||||||
the Game class:
|
the Game class:
|
||||||
|
|
||||||
@ -86,26 +124,43 @@
|
|||||||
Next, we provide accessors for the player and levels. We then expose three
|
Next, we provide accessors for the player and levels. We then expose three
|
||||||
functions: newGame(), saveGame() and loadGame().
|
functions: newGame(), saveGame() and loadGame().
|
||||||
|
|
||||||
The read() and write() functions are used by saveGame() and loadGame().
|
The read() and toJson() functions are used by saveGame() and loadGame().
|
||||||
|
|
||||||
\snippet serialization/savegame/game.cpp 0
|
\div{class="admonition note"}\b{Note:}
|
||||||
|
Despite \c Game being a value class, we assume that the author wants a game to have
|
||||||
|
identity, much like your main window would have. We therefore don't use a
|
||||||
|
static fromJson() function, which would create a new object, but a read()
|
||||||
|
function we can call on existing objects. There's a 1:1 correspondence
|
||||||
|
between read() and fromJson(), in that one can be implemented in terms of
|
||||||
|
the other:
|
||||||
|
|
||||||
|
\code
|
||||||
|
void read(const QJsonObject &json) { *this = fromJson(json); }
|
||||||
|
static Game fromObject(const QJsonObject &json) { Game g; g.read(json); return g; }
|
||||||
|
\endcode
|
||||||
|
|
||||||
|
We just use what's more convenient for callers of the functions.
|
||||||
|
\enddiv
|
||||||
|
|
||||||
|
\snippet serialization/savegame/game.cpp newGame
|
||||||
|
|
||||||
To setup a new game, we create the player and populate the levels and their
|
To setup a new game, we create the player and populate the levels and their
|
||||||
NPCs.
|
NPCs.
|
||||||
|
|
||||||
\snippet serialization/savegame/game.cpp 1
|
\snippet serialization/savegame/game.cpp read
|
||||||
|
|
||||||
The first thing we do in the read() function is tell the player to read
|
The read() function starts by replacing the player with the
|
||||||
itself. We then clear the level array so that calling loadGame() on the
|
one read from JSON. We then clear() the level array so that calling
|
||||||
same Game object twice doesn't result in old levels hanging around.
|
loadGame() on the same Game object twice doesn't result in old levels
|
||||||
|
hanging around.
|
||||||
|
|
||||||
We then populate the level array by reading each Level from a QJsonArray.
|
We then populate the level array by reading each Level from a QJsonArray.
|
||||||
|
|
||||||
\snippet serialization/savegame/game.cpp 2
|
\snippet serialization/savegame/game.cpp toJson
|
||||||
|
|
||||||
We write the game to JSON similarly to how we write Level.
|
Writing the game to JSON is similar to writing a level.
|
||||||
|
|
||||||
\snippet serialization/savegame/game.cpp 3
|
\snippet serialization/savegame/game.cpp loadGame
|
||||||
|
|
||||||
When loading a saved game in loadGame(), the first thing we do is open the
|
When loading a saved game in loadGame(), the first thing we do is open the
|
||||||
save file based on which format it was saved to; \c "save.json" for JSON,
|
save file based on which format it was saved to; \c "save.json" for JSON,
|
||||||
@ -119,14 +174,16 @@
|
|||||||
After constructing the QJsonDocument, we instruct the Game object to read
|
After constructing the QJsonDocument, we instruct the Game object to read
|
||||||
itself and then return \c true to indicate success.
|
itself and then return \c true to indicate success.
|
||||||
|
|
||||||
\snippet serialization/savegame/game.cpp 4
|
\snippet serialization/savegame/game.cpp saveGame
|
||||||
|
|
||||||
Not surprisingly, saveGame() looks very much like loadGame(). We determine
|
Not surprisingly, saveGame() looks very much like loadGame(). We determine
|
||||||
the file extension based on the format, print a warning and return \c false
|
the file extension based on the format, print a warning and return \c false
|
||||||
if the opening of the file fails. We then write the Game object to a
|
if the opening of the file fails. We then write the Game object to a
|
||||||
QJsonDocument, and call either QJsonDocument::toJson() or to
|
QJsonObject. To save the game in the format that was specified, we
|
||||||
QJsonDocument::toBinaryData() to save the game, depending on which format
|
convert the JSON object into either a QJsonDocument for a subsequent
|
||||||
was specified.
|
QJsonDocument::toJson() call, or a QCborValue for QCborValue::toCbor().
|
||||||
|
|
||||||
|
\section1 Tying It All Together
|
||||||
|
|
||||||
We are now ready to enter main():
|
We are now ready to enter main():
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ QList<Level> Game::levels() const
|
|||||||
return mLevels;
|
return mLevels;
|
||||||
}
|
}
|
||||||
|
|
||||||
//! [0]
|
//! [newGame]
|
||||||
void Game::newGame()
|
void Game::newGame()
|
||||||
{
|
{
|
||||||
mPlayer = Character();
|
mPlayer = Character();
|
||||||
@ -59,9 +59,9 @@ void Game::newGame()
|
|||||||
dungeon.setNpcs(dungeonNpcs);
|
dungeon.setNpcs(dungeonNpcs);
|
||||||
mLevels.append(dungeon);
|
mLevels.append(dungeon);
|
||||||
}
|
}
|
||||||
//! [0]
|
//! [newGame]
|
||||||
|
|
||||||
//! [3]
|
//! [loadGame]
|
||||||
bool Game::loadGame(Game::SaveFormat saveFormat)
|
bool Game::loadGame(Game::SaveFormat saveFormat)
|
||||||
{
|
{
|
||||||
QFile loadFile(saveFormat == Json
|
QFile loadFile(saveFormat == Json
|
||||||
@ -87,9 +87,9 @@ bool Game::loadGame(Game::SaveFormat saveFormat)
|
|||||||
<< (saveFormat != Json ? "CBOR" : "JSON") << "...\n";
|
<< (saveFormat != Json ? "CBOR" : "JSON") << "...\n";
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
//! [3]
|
//! [loadGame]
|
||||||
|
|
||||||
//! [4]
|
//! [saveGame]
|
||||||
bool Game::saveGame(Game::SaveFormat saveFormat) const
|
bool Game::saveGame(Game::SaveFormat saveFormat) const
|
||||||
{
|
{
|
||||||
QFile saveFile(saveFormat == Json
|
QFile saveFile(saveFormat == Json
|
||||||
@ -101,52 +101,44 @@ bool Game::saveGame(Game::SaveFormat saveFormat) const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject gameObject;
|
QJsonObject gameObject = toJson();
|
||||||
write(gameObject);
|
|
||||||
saveFile.write(saveFormat == Json
|
saveFile.write(saveFormat == Json
|
||||||
? QJsonDocument(gameObject).toJson()
|
? QJsonDocument(gameObject).toJson()
|
||||||
: QCborValue::fromJsonValue(gameObject).toCbor());
|
: QCborValue::fromJsonValue(gameObject).toCbor());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
//! [4]
|
//! [saveGame]
|
||||||
|
|
||||||
//! [1]
|
//! [read]
|
||||||
void Game::read(const QJsonObject &json)
|
void Game::read(const QJsonObject &json)
|
||||||
{
|
{
|
||||||
if (json.contains("player") && json["player"].isObject())
|
if (const QJsonValue v = json["player"]; v.isObject())
|
||||||
mPlayer.read(json["player"].toObject());
|
mPlayer = Character::fromJson(v.toObject());
|
||||||
|
|
||||||
if (json.contains("levels") && json["levels"].isArray()) {
|
if (const QJsonValue v = json["levels"]; v.isArray()) {
|
||||||
QJsonArray levelArray = json["levels"].toArray();
|
const QJsonArray levels = v.toArray();
|
||||||
mLevels.clear();
|
mLevels.clear();
|
||||||
mLevels.reserve(levelArray.size());
|
mLevels.reserve(levels.size());
|
||||||
for (const QJsonValue &v : levelArray) {
|
for (const QJsonValue &level : levels)
|
||||||
QJsonObject levelObject = v.toObject();
|
mLevels.append(Level::fromJson(level.toObject()));
|
||||||
Level level;
|
|
||||||
level.read(levelObject);
|
|
||||||
mLevels.append(level);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//! [1]
|
//! [read]
|
||||||
|
|
||||||
//! [2]
|
//! [toJson]
|
||||||
void Game::write(QJsonObject &json) const
|
QJsonObject Game::toJson() const
|
||||||
{
|
{
|
||||||
QJsonObject playerObject;
|
QJsonObject json;
|
||||||
mPlayer.write(playerObject);
|
json["player"] = mPlayer.toJson();
|
||||||
json["player"] = playerObject;
|
|
||||||
|
|
||||||
QJsonArray levelArray;
|
QJsonArray levels;
|
||||||
for (const Level &level : mLevels) {
|
for (const Level &level : mLevels)
|
||||||
QJsonObject levelObject;
|
levels.append(level.toJson());
|
||||||
level.write(levelObject);
|
json["levels"] = levels;
|
||||||
levelArray.append(levelObject);
|
return json;
|
||||||
}
|
|
||||||
json["levels"] = levelArray;
|
|
||||||
}
|
}
|
||||||
//! [2]
|
//! [toJson]
|
||||||
|
|
||||||
void Game::print(int indentation) const
|
void Game::print(int indentation) const
|
||||||
{
|
{
|
||||||
|
@ -26,7 +26,7 @@ public:
|
|||||||
bool saveGame(SaveFormat saveFormat) const;
|
bool saveGame(SaveFormat saveFormat) const;
|
||||||
|
|
||||||
void read(const QJsonObject &json);
|
void read(const QJsonObject &json);
|
||||||
void write(QJsonObject &json) const;
|
QJsonObject toJson() const;
|
||||||
|
|
||||||
void print(int indentation = 0) const;
|
void print(int indentation = 0) const;
|
||||||
private:
|
private:
|
||||||
|
@ -25,39 +25,37 @@ void Level::setNpcs(const QList<Character> &npcs)
|
|||||||
mNpcs = npcs;
|
mNpcs = npcs;
|
||||||
}
|
}
|
||||||
|
|
||||||
//! [0]
|
//! [fromJson]
|
||||||
void Level::read(const QJsonObject &json)
|
Level Level::fromJson(const QJsonObject &json)
|
||||||
{
|
{
|
||||||
if (json.contains("name") && json["name"].isString())
|
Level result;
|
||||||
mName = json["name"].toString();
|
|
||||||
|
|
||||||
if (json.contains("npcs") && json["npcs"].isArray()) {
|
if (const QJsonValue v = json["name"]; v.isString())
|
||||||
QJsonArray npcArray = json["npcs"].toArray();
|
result.mName = v.toString();
|
||||||
mNpcs.clear();
|
|
||||||
mNpcs.reserve(npcArray.size());
|
if (const QJsonValue v = json["npcs"]; v.isArray()) {
|
||||||
for (const QJsonValue &v : npcArray) {
|
const QJsonArray npcs = v.toArray();
|
||||||
QJsonObject npcObject = v.toObject();
|
result.mNpcs.reserve(npcs.size());
|
||||||
Character npc;
|
for (const QJsonValue &npc : npcs)
|
||||||
npc.read(npcObject);
|
result.mNpcs.append(Character::fromJson(npc.toObject()));
|
||||||
mNpcs.append(npc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
//! [0]
|
|
||||||
|
|
||||||
//! [1]
|
return result;
|
||||||
void Level::write(QJsonObject &json) const
|
}
|
||||||
|
//! [fromJson]
|
||||||
|
|
||||||
|
//! [toJson]
|
||||||
|
QJsonObject Level::toJson() const
|
||||||
{
|
{
|
||||||
|
QJsonObject json;
|
||||||
json["name"] = mName;
|
json["name"] = mName;
|
||||||
QJsonArray npcArray;
|
QJsonArray npcArray;
|
||||||
for (const Character &npc : mNpcs) {
|
for (const Character &npc : mNpcs)
|
||||||
QJsonObject npcObject;
|
npcArray.append(npc.toJson());
|
||||||
npc.write(npcObject);
|
|
||||||
npcArray.append(npcObject);
|
|
||||||
}
|
|
||||||
json["npcs"] = npcArray;
|
json["npcs"] = npcArray;
|
||||||
|
return json;
|
||||||
}
|
}
|
||||||
//! [1]
|
//! [toJson]
|
||||||
|
|
||||||
void Level::print(int indentation) const
|
void Level::print(int indentation) const
|
||||||
{
|
{
|
||||||
|
@ -21,8 +21,8 @@ public:
|
|||||||
QList<Character> npcs() const;
|
QList<Character> npcs() const;
|
||||||
void setNpcs(const QList<Character> &npcs);
|
void setNpcs(const QList<Character> &npcs);
|
||||||
|
|
||||||
void read(const QJsonObject &json);
|
static Level fromJson(const QJsonObject &json);
|
||||||
void write(QJsonObject &json) const;
|
QJsonObject toJson() const;
|
||||||
|
|
||||||
void print(int indentation = 0) const;
|
void print(int indentation = 0) const;
|
||||||
private:
|
private:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user