ゲームの保存と読み込み

QtのJSONやCBORクラスを使ってゲームをセーブしたりロードしたりする方法です。

多くのゲームにはセーブ機能があり、プレイヤーのゲームの進行状況を保存し、後でロードすることができます。ゲームを保存するプロセスでは、一般的に各ゲームオブジェクトのメンバ変数をファイルにシリアライズします。この目的のために多くのフォーマットを使用することができ、その1つはJSONです。QJsonDocument では、CBORフォーマットでドキュメントをシリアライズすることもできます。これは、セーブファイルを読みやすくしたくない場合(ただし、CBORデータを読み込む方法についてはCBORデータの解析と表示を参照してください)や、ファイルサイズを抑える必要がある場合に便利です。

この例では、簡単なゲームをJSON形式とバイナリ形式でセーブしたりロードしたりする方法を示します。

キャラクタークラス

Character クラスは、ゲーム内のノンプレイヤー キャラクター (NPC) を表し、プレイヤーの名前、レベル、およびクラス タイプを保存します。

静的な fromJson() 関数と非静的な toJson() 関数を提供し、自身をシリアライズします。

注: このパターン(fromJson()/toJson())が機能するのは、QJsonObject が所有するQJsonDocument とは無関係に構築できるからであり、ここで(デ)シリアライズされるデータ型は値型であるため、コピーすることができます。他のフォーマット、例えばXMLやQDataStream 、ドキュメントのようなオブジェクトを渡す必要がある場合、またはオブジェクトの識別が重要な場合(例えばQObject サブクラス)、他のパターンがより適しているかもしれません。XMLについてはdombookmarksの例を、イディオム的なQDataStream 直列化についてはQListWidgetItem::read() とQListWidgetItem::write() の実装を参照してください。この例のprint() 関数は、QTextStream のシリアライズの良い例です。

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

特に興味深いのは、fromJson() と 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;
}

fromJson()関数では、ローカルのresult Characterオブジェクトを構築し、result'のメンバの値をQJsonObject 引数から代入します。JSONオブジェクト内の値にアクセスするには、QJsonObject::operator[] ()またはQJsonObject::value ()のいずれかを使用できます。どちらもconst関数で、キーが無効な場合はQJsonValue::Undefined 。特に、is... 関数(たとえば、QJsonValue::isString()、QJsonValue::isDouble())は、QJsonValue::Undefined に対してfalse を返すので、1回の検索で、正しい型だけでなく、存在もチェックできます。

値がJSONオブジェクトに存在しない場合、または型が正しくない場合、対応するresult メンバにも書き込まれないため、デフォルトコンストラクタが設定した値は保持されます。これは、デフォルト値が1つの場所(デフォルト コンストラクタ)で一元的に定義され、シリアライズ コードで繰り返される必要がないことを意味します(DRY)。

C++17 の if-with-initializerを使用して、変数のスコープとチェックを分離していることに注目してください(v )。これは、変数のスコープが限定されているため、変数名を短く保つことができることを意味します。

QJsonObject::contains() を使ったナイーブなアプローチと比較してみてください:

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

これは可読性が低いだけでなく、合計3回のルックアップを必要とする(コンパイラーはこれらを1回に最適化しない)ため、3倍遅くなり、"name" を3回繰り返す(DRY原則に違反する)。

QJsonObject Character::toJson() const
{
    QJsonObject json;
    json["name"] = mName;
    json["level"] = mLevel;
    json["classType"] = mClassType;
    return json;
}

toJson()関数では、fromJson()関数の逆を行います。Characterオブジェクトの値を新しいJSONオブジェクトに代入して返します。値へのアクセスと同様に、QJsonObject に値を設定するには、QJsonObject::operator[]() とQJsonObject::insert() の2つの方法があります。どちらも、指定されたキーの既存の値を上書きします。

レベル・クラス

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

ゲーム内のレベルには、それぞれ複数のNPCを配置したいので、CharacterオブジェクトのQList 。また、おなじみの fromJson() と 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;
}

コンテナは、QJsonArray を使用して、JSON に書き込んだり、JSON から読み込んだりできます。この例では、キー"npcs" に関連付けられた値からQJsonArray を構築します。次に、配列の各要素QJsonValue に対して toObject() を呼び出し、Character の JSON オブジェクトを取得します。そして、Character::fromJson() は、その QJSonObject を Character オブジェクトに変換して、NPC 配列に追加します。

注: 連想コンテナは、各値オブジェクトにキーを格納することで記述できます(まだ格納されていない場合)。この方法では、コンテナはオブジェクトの通常の配列として格納されますが、各要素のインデックスが、コンテナを読み込む際に、コンテナを構築するためのキーとして使用されます。

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

繰り返しますが、toJson() 関数は、逆を除けば fromJson() 関数と似ています。

ゲーム・クラス

キャラクター・クラスとレベル・クラスを確立したら、次はゲーム・クラスです:

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

まず最初に、SaveFormat enumを定義します。これにより、ゲームの保存形式を指定できます。Json またはBinary

次に、プレイヤーとレベルのアクセサを提供します。次に、newGame()、saveGame()、loadGame() の 3 つの関数を公開します。

read() および toJson() 関数は、 saveGame() および loadGame() で使用されます。

注: Game は値クラスですが、メイン・ウィンドウが持つようなアイデンティティをゲームに持たせることを想定しています。そのため、新しいオブジェクトを作成する静的なfromJson()関数は使用せず、既存のオブジェクトに対して呼び出すことができるread()関数を使用します。read()とfromJson()の間には1対1の対応関係があり、一方をもう一方の関数で実装することができます:

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

私たちは、関数の呼び出し側にとってより便利な方を使うだけです。

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

新しいゲームをセットアップするために、プレーヤーを作成し、レベルとそのNPCを入力します。

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

read()関数は、プレーヤーをJSONから読み込んだものに置き換えることから始めます。次に、同じ Game オブジェクトで loadGame() を 2 回呼び出すと、古いレベルがうろつくことがないように、レベル配列を clear() します。

次に、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;
}

JSONへのゲームの書き込みは、レベルの書き込みに似ています。

boolGame::loadGame(Game::SaveFormat saveFormat) { 次のようにします。    QFileloadFile(saveFormat==Json? "save.json"_L1 :"save.dat"_L1);if(!loadFile.open(QIODevice::ReadOnly)){の場合        qWarning("Couldn't open save file.");
       return false; }.  QByteArraysaveData=loadFile.readAll();    QJsonDocumentloadDoc(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")<< "...♪";return true; }

loadGame()でセーブされたゲームをロードするとき、最初に行うことは、どのフォーマットでセーブされたかに基づいてセーブファイルを開くことです。JSONの場合は"save.json" 、CBORの場合は"save.dat" 。ファイルを開けなかった場合は警告を表示し、false

QJsonDocument::fromJson() とQCborValue::fromCbor() はどちらもQByteArray を取るので、保存形式に関係なく、保存ファイルの内容全体を読み込むことができる。

QJsonDocument を構築した後、Game オブジェクトに自分自身を読み込むように指示し、成功を示すためにtrue を返す。

boolGame::saveGame(Game::SaveFormat saveFormat)const{ 次のようになります。    QFilesaveFile(saveFormat==Json? "save.json"_L1 :"save.dat"_L1);if(!saveFile.open(QIODevice::WriteOnly)){の場合        qWarning("Couldn't open save file.");
       return false; }.  QJsonObjectgameObject=toJson(); saveFile.write(saveFormat==Json?QJsonDocument(gameObject).toJson() : QCborValue::fromJsonValue(gameObject).toCbor());return true; }

驚くことではないが、saveGame()はloadGame()とよく似ている。フォーマットに基づいてファイルの拡張子を判断し、警告を表示して、ファイルのオープンに失敗したらfalse を返す。QJsonObject指定されたフォーマットでゲームを保存するために、JSON オブジェクトをQJsonDocument に変換してQJsonDocument::toJson() を呼び出すか、QCborValue に変換してQCborValue::toCbor() を呼び出します。

すべてを結びつける

これで、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...

JSONを使ったゲームのシリアライゼーションをデモンストレーションすることにしか興味がないので、このゲームは実際にはプレイできない。したがって、必要なのはQCoreApplication 、イベント ループはありません。アプリケーションの起動時に、コマンドライン引数を解析して、ゲームの開始方法を決定する。最初の引数には、"new"(デフォルト)と "load "というオプションがあります。new "を指定すると新しいゲームが生成され、"load "を指定すると以前に保存したゲームがロードされます。第二引数には "json"(デフォルト)と "binary "がオプションとして利用可能です。この引数によって、どのファイルに保存するか、あるいはどのファイルからロードするかを決定する。続いて、プレイヤーがとても楽しんで、たくさん進んだと仮定して、キャラクター、レベル、ゲームオブジェクトの内部状態を変更します。

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

プレイヤーが終了したら、ゲームをセーブします。デモンストレーションのために、JSONかCBORにシリアライズします。実行ファイルと同じディレクトリにあるファイルの中身を調べることができます(あるいは、"load "オプションを指定してサンプルを再実行することもできます)。

これでこの例は終わりです。お分かりのように、QtのJSONクラスを使ったシリアライゼーションはとてもシンプルで便利です。例えば、QDataStream よりもQJsonDocument とその仲間を使う利点は、人間が読める JSON ファイルが得られるだけでなく、必要であればコードを書き換えることなくバイナリ形式を使用するオプションがあることです。

プロジェクト例 @ code.qt.io

Qt における JSON サポートQt における CBOR サポートデータ入出力も参照してください

© 2025 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.