警告

本节包含自动从C++翻译到Python的片段,可能包含错误。

保存和加载游戏#

如何使用Qt的JSON或CBOR类保存和加载游戏。

许多游戏提供保存功能,以便玩家可以在以后的时间保存和加载他们在游戏中的进度。保存游戏的过程通常涉及将每个游戏对象的成员变量序列化到文件中。可以为此目的使用多种格式,其中之一是JSON。使用QJsonDocument,您还可以将文档序列化为CBOR格式,这在您不想让保存文件容易被阅读时(但请参阅解析和显示CBOR数据了解如何读取),或者在您需要减少文件大小时非常有用。

在本例中,我们将演示如何将一个简单的游戏保存到JSON和二进制格式,并从这些格式加载。

角色类#

角色类代表我们游戏中的非玩家角色(NPC),并存储玩家的名字、等级和职业类型。

它提供了从Json()和toJson()静态和非静态函数来自动序列化自身的功能。

注意

此模式(fromJson()/toJson())之所以可行,是因为QJsonObjects可以独立于拥有QJsonDocument的对象构造,并且因为在此处进行序列化和反序列化的数据类型是值类型,因此可以被复制。当将数据序列化到另一个格式时——例如XML或QDataStream,它们需要传递类似文档的对象——或者当对象的身份很重要时(例如QObject的子类),可能更适合其他模式。请参阅XML的dombookmarks示例和QDataStream序列化的idiomatic实现。此示例中的print()函数是QTextStream序列化的良好例子,尽管当然它们缺少解序列化部分。

class Character():

    Q_GADGET
# public
    ClassType = { Warrior, Mage, Archer }
    Q_ENUM(ClassType)
    Character()
    Character(QString name, int level, ClassType classType)
    name = QString()
    def setName(name):
    level = int()
    def setLevel(level):
    classType = ClassType()
    def setClassType(classType):
    fromJson = Character(QJsonObject json)
    toJson = QJsonObject()
    def print(s, 0):
# private
    mName = QString()
    mLevel = 0
    mClassType = Warrior()

我们特别感兴趣的是fromJson()和toJson()函数的实现

def fromJson(self, QJsonObject json):

    result = Character()
    if QJsonValue v = json["name"]; v.isString():
        result.mName = v.toString()
    if QJsonValue v = json["level"]; v.isDouble():
        result.mLevel = v.toInt()
    if QJsonValue v = json["classType"]; v.isDouble():
        result.mClassType = ClassType(v.toInt())
    return result

在fromJson()函数中,我们构造一个本地的Character对象result,并从传入的QJsonObject参数中分配result成员的值。您可以使用operator[]()或value()来访问JSON对象中的值;这两个函数都是const函数,如果键无效则返回Undefined。

如果JSON对象中不存在某个值,或者值的类型不正确,我们同样不会写入对应的result成员,这有助于保持任何默认构造器可能设置的值。这意味着默认值在一个位置(默认构造器)集中定义,无需在序列化代码中重复(遵循DRY原则)。

注意使用C++17 if-with-initializer的使用来分离变量v的作用域和检查。这可以使变量名更短,因为它的作用域有限。

与使用QJsonObject::contains()的简单方法相比

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

这种方法不仅可读性较差,而且需要进行总共三个查找(编译器不会将它们优化成一次),因此速度慢三倍,并且重复了"名字"三次(违反DRY原则)。

def toJson(self):

    json = QJsonObject()
    json["name"] = mName
    json["level"] = mLevel
    json["classType"] = mClassType
    return json

在toJson()函数中,我们执行fromJson()函数的反向;将Character对象的值分配给一个新的JSON对象,然后返回该对象。与访问值一样,有为QJsonObject设置值的两种方法:operator[]()和insert()。两个函数都将覆盖给定键的任何现有值。

Level类#

class Level():

# public
    Level() = default
    Level = explicit(QString name)
    name = QString()
npcs = QList()
    def setNpcs(npcs):
    fromJson = Level(QJsonObject json)
    toJson = QJsonObject()
    def print(s, 0):
# private
    mName = QString()
mNpcs = QList()

我们希望游戏中的每个级别都拥有几个NPC,因此我们保留一个Character对象的QList。我们还提供了熟悉的fromJson()和toJson()函数。

def fromJson(self, QJsonObject json):

    result = Level()
    if QJsonValue v = json["name"]; v.isString():
        result.mName = v.toString()
    if QJsonValue v = json["npcs"]; v.isArray():
        npcs = v.toArray()
        result.mNpcs.reserve(npcs.size())
        for npc in npcs:
            result.mNpcs.append(Character.fromJson(npc.toObject()))

    return result

可以使用 QJsonArray 将容器写入和读取到 JSON 中。在我们的情况中,我们从与键 "npcs" 关联的值构建一个新的 QJsonArray。然后,对于数组中的每个 QJsonValue 元素,我们调用 toObject() 来获取角色的 JSON 对象。然后使用 Character::fromJson() 将 QJSonObject 转换为 Character 对象,并附加到我们的 NPC 数组中。

注意

通过将键存储在每个值对象中(如果尚未有)来编写的 关联容器。采用这种方法,容器存储为对象的正常数组,但每个元素的索引用于在读取时构造容器。

def toJson(self):

    json = QJsonObject()
    json["name"] = mName
    npcArray = QJsonArray()
    for npc in mNpcs:
        npcArray.append(npc.toJson())
    json["npcs"] = npcArray
    return json

同样,toJson() 函数与fromJson() 函数类似,但顺序相反。

游戏类

在建立了角色和层级类之后,我们可以继续构建游戏类

class Game():

# public
    SaveFormat = { Json, Binary }
    player = Character()
levels = QList()
    def newGame():
    loadGame = bool(SaveFormat saveFormat)
    saveGame = bool(SaveFormat saveFormat)
    def read(json):
    toJson = QJsonObject()
    def print(s, 0):
# private
    mPlayer = Character()
mLevels = QList()

首先,我们定义了 SaveFormat 枚举。这将使我们能够指定游戏应以何种格式保存:JsonBinary

接下来,我们提供了对玩家和层级的访问器。然后,我们暴露了三个函数:newGame()、saveGame() 和 loadGame()。

read() 和 toJson() 函数由 saveGame() 和 loadGame() 使用。

注意

尽管 Game 是一个值类,但我们假设作者希望游戏具有标识性,就像你的主窗口一样。因此,我们不使用会创建新对象的静态 fromJson() 函数,而使用可以在现有对象上调用的 read() 函数。read() 和 fromJson() 之间有一对一的关系,即一个可以由另一个实现。

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

我们只使用对函数的调用者更方便的方式。

def newGame(self):

    mPlayer = Character()
    mPlayer.setName("Hero")
    mPlayer.setClassType(Character.Archer)
    mPlayer.setLevel(QRandomGenerator.global().bounded(15, 21))
    mLevels.clear()
    mLevels.reserve(2)
    village = Level("Village")
villageNpcs = QList()
    villageNpcs.reserve(2)
    villageNpcs.append(Character("Barry the Blacksmith",
                                 QRandomGenerator.global().bounded(8, 11), Character.Warrior))
    villageNpcs.append(Character("Terry the Trader",
                                 QRandomGenerator.global().bounded(6, 8), Character.Warrior))
    village.setNpcs(villageNpcs)
    mLevels.append(village)
    dungeon = Level("Dungeon")
dungeonNpcs = QList()
    dungeonNpcs.reserve(3)
    dungeonNpcs.append(Character("Eric the Evil",
                                 QRandomGenerator.global().bounded(18, 26), Character.Mage))
    dungeonNpcs.append(Character("Eric's Left Minion",
                                 QRandomGenerator.global().bounded(5, 7), Character.Warrior))
    dungeonNpcs.append(Character("Eric's Right Minion",
                                 QRandomGenerator.global().bounded(4, 9), Character.Warrior))
    dungeon.setNpcs(dungeonNpcs)
    mLevels.append(dungeon)

要设置新游戏,我们创建玩家并填充层级和它们的 NPC。

def read(self, json):

    if QJsonValue v = json["player"]; v.isObject():
        mPlayer = Character.fromJson(v.toObject())
    if QJsonValue v = json["levels"]; v.isArray():
        levels = v.toArray()
        mLevels.clear()
        mLevels.reserve(levels.size())
        for level in levels:
            mLevels.append(Level.fromJson(level.toObject()))

read() 函数首先将玩家替换为从 JSON 读取的玩家。然后,我们清空 level 数组,这样在同一个 Game 对象上两次调用 loadGame() 不会使旧层级留下来。

然后,我们通过从 QJsonArray 中读取每个层级来填充层级数组。

def toJson(self):

    json = QJsonObject()
    json["player"] = mPlayer.toJson()
    levels = QJsonArray()
    for level in mLevels:
        levels.append(level.toJson())
    json["levels"] = levels
    return json

将游戏写入 JSON 与写入层级相似。

def loadGame(self, Game.SaveFormat saveFormat):

    loadFile = QFile(saveFormat == Json if "save.json" else "save.dat")
    if not loadFile.open(QIODevice.ReadOnly):
        qWarning("Couldn't open save file.")
        return False

    saveData = loadFile.readAll()
    QJsonDocument loadDoc(saveFormat == Json
                          ? QJsonDocument.fromJson(saveData)
    super().__init__(QCborValue.fromCbor(saveData).toMap().toJsonObject()))
    read(loadDoc.object())
    QTextStream(stdout) << "Loaded save for " << loadDoc["player"]["name"].toString()
                        << " using " << (saveFormat != Json if "CBOR" else "JSON") << "...\n"
    return True

在 loadGame() 中加载保存的游戏时,我们首先根据保存的格式打开保存文件;对于 JSON,为 "save.json",对于 CBOR,为 "save.dat"。如果无法打开文件,我们将打印警告并返回 false

由于 fromJson()fromCbor() 都接受一个 QByteArray ,因此我们可以将保存文件的全部内容读取到一个中,无论保存格式如何。

构建完 QJsonDocument 后,我们指示游戏对象读取自身,然后返回 true 来表示成功。

def saveGame(self, Game.SaveFormat saveFormat):

    saveFile = QFile(saveFormat == Json if "save.json" else "save.dat")
    if not saveFile.open(QIODevice.WriteOnly):
        qWarning("Couldn't open save file.")
        return False

    gameObject = toJson()
    saveFile.write(saveFormat == Json ? QJsonDocument(gameObject).toJson()
    super().__init__(gameObject).toCbor())
    return True

不出所料,saveGame() 的样子非常像 loadGame()。我们根据格式确定文件扩展名,如果文件打开失败,则打印警告并返回 false。然后我们编写游戏对象到 QJsonObject 中。为了以指定的格式保存游戏,我们将 JSON 对象转换为要么是以便后续 toJson() 调用一个 QJsonDocument ,要么是用于 toCbor() 的一个 QCborValue

整合一切#

我们现在可以进入 main() 函数

if __name__ == "__main__":

    app = QCoreApplication(argc, argv)
    args = QCoreApplication.arguments()
    bool newGame
            = args.size() <= 1 or QString.compare(args[1], "load", Qt.CaseInsensitive) != 0
    bool json
            = args.size() <= 2 or QString.compare(args[2], "binary", Qt.CaseInsensitive) != 0
    game = Game()
    if newGame:
        game.newGame()
    elif not game.loadGame(json if Game.Json else Game.Binary):
        return 1
    # Game is played; changes are made...

由于我们只对使用 JSON 进行游戏序列化进行演示有兴趣,因此我们的游戏实际上不可玩。因此,我们只需要 QCoreApplication 并且没有事件循环。在应用程序启动时,我们解析命令行参数以决定如何开始游戏。对于第一个参数,有“new”(默认)和“load”可用的选项。当指定“new”时,将生成一个新游戏,而当指定“load”时,将加载先前保存的游戏。对于第二个参数,“json”(默认)和“binary”可用的选项。该参数将决定要将哪个文件保存或从中读取。然后我们继续前进,假设玩家玩得很开心,并取得了很大的进展,改变我们的角色、等级和游戏对象的内部状态。

s = QTextStream(stdout)
s << "Game ended in the following state:\n"
game.print(s)
if not game.saveGame(json if Game.Json else Game.Binary):
    return 1
return 0

当玩家完成游戏后,我们保存他们的游戏。为了演示目的,我们可以将游戏序列化到 JSON 或 CBOR。您可以检查与可执行文件同一目录中的文件内容(或重新运行示例,同时确保也指定了“load”选项),尽管二进制保存文件将包含一些垃圾字符(这是正常的)。

这让我们的例子告一段落。如您所见,使用 Qt 的 JSON 类进行序列化非常简单方便。例如,使用 QJsonDocument 和相关类相较于 QDataStream 的优点在于,您不仅得到人类可读的 JSON 文件,还可以根据需要使用二进制格式,无需 重新编写任何代码。

另请参阅

Qt 中的 JSON 支持 Qt 中的 CBOR 支持 数据 输入 输出

示例项目 @ code.qt.io