summaryrefslogtreecommitdiffstats
path: root/examples/corelib/serialization/savegame/doc/src/savegame.qdoc
blob: 37a461d7bd5d0ac78d1f8d3b9eb68659bf0d7ef0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only

/*!
    \example serialization/savegame
    \examplecategory {Input/Output}
    \title JSON Save Game Example

    \brief The JSON Save Game example demonstrates how to save and load a
    small game using QJsonDocument, QJsonObject and QJsonArray.

    Many games provide save functionality, so that the player's progress through
    the game can be saved and loaded at a later time. The process of saving a
    game generally involves serializing each game object's member variables to a
    file. Many formats can be used for this purpose, one of which is JSON.  With
    QJsonDocument, you also have the ability to serialize a document in a \l
    {RFC 7049} {CBOR} format, which is great if you don't want the save file to
    be easy to read (but see \l {Parsing and displaying CBOR data} for how it \e
    can be read), or if you need to keep the file size down.

    In this example, we'll demonstrate how to save and load a simple game to
    and from JSON and binary formats.

    \section1 The Character Class

    The Character class represents a non-player character (NPC) in our game, and
    stores the player's name, level, and class type.

    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{dombookmarks} example for XML, and the implementation of
    \l QListWidgetItem::read() and \l QListWidgetItem::write()
    for idiomatic QDataStream serialization. The \c{print()} functions in this example
    are good examples of QTextStream serialization, even though they, of course, lack
    the deserialization side.

    \snippet serialization/savegame/character.h 0

    Of particular interest to us are the fromJson() and toJson() function
    implementations:

    \snippet serialization/savegame/character.cpp fromJson

    In the fromJson() function, we construct a local \c result Character object
    and assign \c{result}'s members values from the QJsonObject argument. You
    can use either \l QJsonObject::operator[]() or QJsonObject::value() to
    access values within the JSON object; both are const functions and return
    QJsonValue::Undefined if the key is invalid. In particular, the \c{is...}
    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.

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

    Observe the use of
    \l{https://en.cppreference.com/w/cpp/language/if#If_statements_with_initializer}
    {C++17 if-with-initializer} to separate scoping and checking of the variable \c v.
    This means we can keep the variable name short, because its scope is limited.

    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

    We want the levels in our game to each each have several NPCs, so we keep a QList
    of Character objects. We also provide the familiar fromJson() and toJson()
    functions.

    \snippet serialization/savegame/level.cpp fromJson

    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
    \c "npcs". Then, for each QJsonValue element in the array, we call
    toObject() to get the Character's JSON object. Character::fromJson() can
    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
    the key in each value object (if it's not already). With this approach, the
    container is stored as a regular array of objects, but the index of each
    element is used as the key to construct the container when reading it back
    in.

    \snippet serialization/savegame/level.cpp toJson

    Again, the toJson() function is similar to the fromJson() function, except
    reversed.

    \section1 The Game Class

    Having established the Character and Level classes, we can move on to
    the Game class:

    \snippet serialization/savegame/game.h 0

    First of all, we define the \c SaveFormat enum. This will allow us to
    specify the format in which the game should be saved: \c Json or \c Binary.

    Next, we provide accessors for the player and levels. We then expose three
    functions: newGame(), saveGame() and loadGame().

    The read() and toJson() functions are used by saveGame() and loadGame().

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

    \snippet serialization/savegame/game.cpp read

    The read() function starts by replacing the player with the
    one read from JSON. We then clear() the level array so that calling
    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.

    \snippet serialization/savegame/game.cpp toJson

    Writing the game to JSON is similar to writing a level.

    \snippet serialization/savegame/game.cpp loadGame

    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,
    and \c "save.dat" for CBOR. We print a warning and return \c false if the
    file couldn't be opened.

    Since \l QJsonDocument::fromJson() and \l QCborValue::fromCbor() both take
    a QByteArray, we can read the entire contents of the save file into one,
    regardless of the save format.

    After constructing the QJsonDocument, we instruct the Game object to read
    itself and then return \c true to indicate success.

    \snippet serialization/savegame/game.cpp saveGame

    Not surprisingly, saveGame() looks very much like loadGame(). We determine
    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
    QJsonObject. To save the game in the format that was specified, we
    convert the JSON object into either a QJsonDocument for a subsequent
    QJsonDocument::toJson() call, or a QCborValue for QCborValue::toCbor().

    \section1 Tying It All Together

    We are now ready to enter main():

    \snippet serialization/savegame/main.cpp 0

    Since we're only interested in demonstrating \e serialization of a game with
    JSON, our game is not actually playable. Therefore, we only need
    QCoreApplication and have no event loop. On application start-up we parse
    the command-line arguments to decide how to start the game. For the first
    argument the options "new" (default) and "load" are available. When "new"
    is specified a new game will be generated, and when "load" is specified a
    previously saved game will be loaded in. For the second argument
    "json" (default) and "binary" are available as options. This argument will
    decide which file is saved to and/or loaded from. We then move ahead and
    assume that the player had a great time and made lots of progress, altering
    the internal state of our Character, Level and Game objects.

    \snippet serialization/savegame/main.cpp 1

    When the player has finished, we save their game. For demonstration
    purposes, we can serialize to either JSON or CBOR. You can examine the
    contents of the files in the same directory as the executable (or re-run
    the example, making sure to also specify the "load" option), although the
    binary save file will contain some garbage characters (which is normal).

    That concludes our example. As you can see, serialization with Qt's JSON
    classes is very simple and convenient. The advantages of using QJsonDocument
    and friends over QDataStream, for example, is that you not only get
    human-readable JSON files, but you also have the option to use a binary
    format if it's required, \e without rewriting any code.

    \sa {JSON Support in Qt}, {CBOR Support in Qt}, {Data Input Output}
*/