Qt之JSON保存與讀取

簡述

許多遊戲提供保存功能,使得玩家在遊戲中的進度可以被保存,並在以後再玩的時候進行加載。保存遊戲的過程通常涉及將每個遊戲對象的成員變量序列化爲文件。要實現這個功能,可以採取許多格式,其中之一就是 JSON - 使用 QJsonDocument。如果不希望保存的文件可讀,或者不需要保持文件大小,還能夠以二進制格式序列化文檔,這就厲害了~O(∩_∩)O~。

下面,將演示如何以 JSON 和二進制格式來保存和加載一個簡單的遊戲。

Character 類

Character 類表示遊戲中的非玩家角色(NPC),並存儲玩家的姓名、級別和類類型。

提供了 read() 和 write() 函數來序列化成員變量。

class Character
  {
  public:
      enum ClassType {
          Warrior, Mage, Archer
      };

      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);

      void read(const QJsonObject &json);
      void write(QJsonObject &json) const;
  private:
      QString mName;
      int mLevel;
      ClassType mClassType;
  };
  • 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

我們感興趣的是讀和寫函數的實現:

void Character::read(const QJsonObject &json)
  {
      mName = json["name"].toString();
      mLevel = json["level"].toDouble();
      mClassType = ClassType(qRound(json["classType"].toDouble()));
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在 read() 函數中,由 QJsonObject 參數分配 Character 的成員變量,可以使用 QJsonObject::operator 或者 QJsonObject::value() 來訪問 JSON 對象中的值,它們均是 const 函數。如果指定的 key 無效,則返回 QJsonValue::Undefined。

注意:在嘗試讀取值之前,應該使用 QJsonObject::contains() 檢測 key 是否有效,這裏假設是有效的,所以沒有檢測。

void Character::write(QJsonObject &json) const
  {
      json["name"] = mName;
      json["level"] = mLevel;
      json["classType"] = mClassType;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在 write() 函數中,處理與 read() 相反。將 Character 的值分配給 QJsonObject 對象。與訪問值一樣,也有兩種方式來設置 QJsonObject 的值:QJsonObject::operator 和 QJsonObject::insert(),它們都會覆蓋指定 key 對應的值。

Level 類

接下來是 Level 類,表示遊戲中的級別。

class Level
  {
  public:
      Level();

      const QList<Character> &npcs() const;
      void setNpcs(const QList<Character> &npcs);

      void read(const QJsonObject &json);
      void write(QJsonObject &json) const;
  private:
      QList<Character> mNpcs;
  };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

遊戲中有很多級別,每個級別都有幾個 NPC,所以需要一個 QList 保存 Character 對象。其次,還提供了熟悉的 read() 和 write() 函數。

void Level::read(const QJsonObject &json)
  {
      mNpcs.clear();
      QJsonArray npcArray = json["npcs"].toArray();
      for (int npcIndex = 0; npcIndex < npcArray.size(); ++npcIndex) {
          QJsonObject npcObject = npcArray[npcIndex].toObject();
          Character npc;
          npc.read(npcObject);
          mNpcs.append(npc);
      }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

容器可以使用 QJsonArray 寫入和讀取 JSON。示例中,用關鍵字“npcs”及其相關聯的值構造了一個 QJsonArray。然後,對於數組中的每個 QJsonValue 元素,調用 toObject() 來獲取 Character 的 JSON 對象。最後,Character 對象可以讀取其 JSON 並附加到 NPC 列表中。

void Level::write(QJsonObject &json) const
  {
      QJsonArray npcArray;
      foreach (const Character npc, mNpcs) {
          QJsonObject npcObject;
          npc.write(npcObject);
          npcArray.append(npcObject);
      }
      json["npcs"] = npcArray;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

write() 函數類似於 read(),除了功能相反以外。

Game 類

建立了 Character 和 Level 類後,繼續看 Game 類:

class Game
  {
  public:
      Game();

      // 遊戲保存的格式 Json、Binary(二進制)
      enum SaveFormat {
          Json, Binary
      };

      const Character &player() const;
      const QList<Level> &levels() const;

      void newGame();
      bool loadGame(SaveFormat saveFormat);
      bool saveGame(SaveFormat saveFormat) const;

      void read(const QJsonObject &json);
      void write(QJsonObject &json) const;
  private:
      Character mPlayer;
      QList<Level> mLevels;
  };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

首先,定義了 SaveFormat 枚舉。這將允許指定遊戲應該保存的格式:Json 或 Binary。

接下來,我們爲玩家和級別提供了訪問器。然後暴漏了三個函數:newGame()、saveGame() 和 loadGame()。

read() 和 write() 函數由 saveGame() 和 loadGame() 使用。

void Game::newGame() {
      mPlayer = Character();
      mPlayer.setName(QStringLiteral("Hero"));
      mPlayer.setClassType(Character::Archer);
      mPlayer.setLevel(15);

      mLevels.clear();

      Level village;
      QList<Character> villageNpcs;
      villageNpcs.append(Character(QStringLiteral("Barry the Blacksmith"), 10, Character::Warrior));
      villageNpcs.append(Character(QStringLiteral("Terry the Trader"), 10, Character::Warrior));
      village.setNpcs(villageNpcs);
      mLevels.append(village);

      Level dungeon;
      QList<Character> dungeonNpcs;
      dungeonNpcs.append(Character(QStringLiteral("Eric the Evil"), 20, Character::Mage));
      dungeonNpcs.append(Character(QStringLiteral("Eric's Sidekick #1"), 5, Character::Warrior));
      dungeonNpcs.append(Character(QStringLiteral("Eric's Sidekick #2"), 5, Character::Warrior));
      dungeon.setNpcs(dungeonNpcs);
      mLevels.append(dungeon);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

要設置一個新遊戲,需要創建玩家並填充級別和對應的 NPC。

void Game::read(const QJsonObject &json)
  {
      mPlayer.read(json["player"].toObject());

      mLevels.clear();
      QJsonArray levelArray = json["levels"].toArray();
      for (int levelIndex = 0; levelIndex < levelArray.size(); ++levelIndex) {
          QJsonObject levelObject = levelArray[levelIndex].toObject();
          Level level;
          level.read(levelObject);
          mLevels.append(level);
      }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

read() 函數中首先要做的是告訴玩家讀取自身的數據。然後清除級別列表,以便多次調用 loadGame() 不會導致舊級別的存在。

然後通過從 QJsonArray 讀取每個 Level 來填充級別列表。

void Game::write(QJsonObject &json) const
  {
      QJsonObject playerObject;
      mPlayer.write(playerObject);
      json["player"] = playerObject;

      QJsonArray levelArray;
      foreach (const Level level, mLevels) {
          QJsonObject levelObject;
          level.write(levelObject);
          levelArray.append(levelObject);
      }
      json["levels"] = levelArray;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

將遊戲寫入 JSON 類似於如何寫 Level。

bool Game::loadGame(Game::SaveFormat saveFormat)
  {
      QFile loadFile(saveFormat == Json
          ? QStringLiteral("save.json")
          : QStringLiteral("save.dat"));

      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::fromBinaryData(saveData));

      read(loadDoc.object());

      return true;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

當在 loadGame() 中加載保存的遊戲時,做的第一件事是根據保存文件的格式打開保存文件,save.json 用於 JSON,save.dat 用於二進制。如果無法打開文件,打印一個警告,返回 false。

由於 QJsonDocument 的 fromJson() 和 fromBinaryData() 函數都使用 QByteArray,因此無論保存格式如何,都可以用其中的一個來轉換保存文件的整個內容。

在構造 QJsonDocument 之後,讓 Game 對象讀取自身數據,然後返回 true 以示成功。

bool Game::saveGame(Game::SaveFormat saveFormat) const
  {
      QFile saveFile(saveFormat == Json
          ? QStringLiteral("save.json")
          : QStringLiteral("save.dat"));

      if (!saveFile.open(QIODevice::WriteOnly)) {
          qWarning("Couldn't open save file.");
          return false;
      }

      QJsonObject gameObject;
      write(gameObject);
      QJsonDocument saveDoc(gameObject);
      saveFile.write(saveFormat == Json
          ? saveDoc.toJson()
          : saveDoc.toBinaryData());

      return true;
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

毫不奇怪,saveGame() 看起來非常像 loadGame()。基於格式確定文件擴展名,如果文件打開失敗,打印警告,返回 false。然後將 Game 對象寫入一個 QJsonDocument,並調用 QJsonDocument::toJson() 或 QJsonDocument::toBinaryData() 來保存遊戲,具體取決於指定的格式。

使用

現在準備進入 main() 函數:

int main(int argc, char *argv[])
  {
      // 因爲只想展示使用 JSON 遊戲的序列化,實際上游戲是不可玩的。因此,只需要 QCoreApplication,沒有事件循環。
      QCoreApplication app(argc, argv);

      Game game;
      game.newGame();
      // 遊戲開始,加載數據...

      // 假設玩家度過了快樂的時光,取得了偉大的成就,改變 Character、Level 和 Game 對象的內部狀態。
      if (!game.saveGame(Game::Json))
          return 1;

      if (!game.saveGame(Game::Binary))
          return 1;

      Game fromJsonGame;
      if (!fromJsonGame.loadGame(Game::Json))
          return 1;

      Game fromBinaryGame;
      if (!fromBinaryGame.loadGame(Game::Binary))
          return 1;

      return 0;
  }
  • 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

當玩家結束遊戲後,保存遊戲數據。爲了演示,序列化爲 JSON 和二進制。可以在與可執行文件相同的目錄中檢查文件的內容,但二進制保存文件將包含一些無用字符(這是正常的)。

爲了顯示可以再次加載保存的文件,爲每種格式調用 loadGame(),失敗時返回 1。假設一切順利,返回 0 表示成功。

如你所見,使用 Qt 的 JSON 類進行序列化非常簡單和方便。使用 QJsonDocument 比 QDataStream 的優點在於,不僅可以得到易讀的 JSON 文件,如果需要,也可以選擇使用二進制格式,而不需重寫任何代碼。

save.json 文件(JSON )如下所示:

{
    "levels": [
        {
            "npcs": [
                {
                    "classType": 0,
                    "level": 10,
                    "name": "Barry the Blacksmith"
                },
                {
                    "classType": 0,
                    "level": 10,
                    "name": "Terry the Trader"
                }
            ]
        },
        {
            "npcs": [
                {
                    "classType": 1,
                    "level": 20,
                    "name": "Eric the Evil"
                },
                {
                    "classType": 0,
                    "level": 5,
                    "name": "Eric's Sidekick #1"
                },
                {
                    "classType": 0,
                    "level": 5,
                    "name": "Eric's Sidekick #2"
                }
            ]
        }
    ],
    "player": {
        "classType": 2,
        "level": 15,
        "name": "Hero"
    }
}
  • 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

save.dat 文件(二進制)如下所示:

qbjs   ?     ?     levels<     4  ?     ?     npcs  ?     ?  `      T   ?      classType         Z   level ?   name   Barry the Blacksmith     $   0   \      P   ?  	 classType         Z   level ?   name   Terry the Trader     $   0   ?  ?     <     8     npcs          P      D   :        classType ?   level ?   name  
 Eric the Evil       (   \      P   ?     classType         ?   level ?   name   Eric's Sidekick #1   $   0   \      P   ?  	 classType         ?   level ?   name   Eric's Sidekick #2   $   0   ?  ?       ?    L   playerH      <   Z      classType ?   level ?   name   Hero        (      T  
  • 1
  • 2

更多參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章