このページでは

ゲームのセーブとロード

QtのJSONまたはCBORクラスを使用してゲームを保存・読み込む方法。

多くのゲームにはセーブ機能が搭載されており、プレイヤーの進行状況を保存し、後で読み込むことができます。ゲームのセーブ処理では、一般的に各ゲームオブジェクトのメンバ変数をファイルにシリアライズします。この目的にはさまざまな形式が使用できますが、その一つがJSONです。QJsonDocument を使用すると、ドキュメントをCBOR形式でシリアライズすることも可能です。これは、セーブファイルを簡単に読み取られたくない場合(ただし、読み取り方法については「CBORデータの解析と表示」を参照)、あるいはファイルサイズを小さく抑える必要がある場合に最適です。

この例では、単純なゲームをJSON形式およびバイナリ形式で保存・読み込む方法について説明します。

Character クラス

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 文字オブジェクトを生成し、引数QJsonObject からresult のメンバに値を割り当てています。 JSONオブジェクト内の値にアクセスするには、QJsonObject::operator[]() またはQJsonObject::value() のいずれかを使用できます。どちらも const関数であり、キーが無効な場合はQJsonValue::Undefined を返します。特に、is... 関数(例:QJsonValue::isString()、QJsonValue::isDouble())は、QJsonValue::Undefined に対してfalse を返すため、1回の検索で存在の有無と正しい型の両方を確認できます。

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

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

これに対し、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 に値を設定するには 2 つの方法があります。QJsonObject::operator[]() とQJsonObject::insert() です。どちらも、指定されたキーの既存の値を上書きします。

Levelクラス

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を配置したいので、QList にCharacterオブジェクトを保持します。また、おなじみの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 書き込みや読み取りが可能です。今回のケースでは、キー「"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() 関数と似ていますが、処理の流れが逆になっています。

Game クラス

CharacterクラスとLevelクラスが定義できたので、次はGameクラスに進みましょう:

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 列挙型を定義します。これにより、ゲームの保存形式(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 から読み込んだプレイヤーに置き換えることから始めます。その後、levels 配列を clear() でクリアします。これにより、同じ Game オブジェクトに対して loadGame() を 2 回呼び出しても、古いレベルが残ってしまうことを防ぎます。

次に、QJsonArray から各Levelを読み込んで、level配列に設定します。

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)
{
    QFile loadFile(saveFormat==Json? "save.json"_L1 :"save.dat"_L1);

    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(QCborValue::fromCbor(saveData).toMap().toJsonObject()));

    read(loadDoc.object());

    QTextStream(stdout)<< "「"  <<loadDoc["player"]["name"].toString()
                        << "  のセーブデータを " <<(saveFormat!=Json? "CBOR":"JSON")<< "...\n"  を使用して読み込みました";
   return true;
}

loadGame() でセーブデータを読み込む際、まず最初に、セーブされた形式に基づいてセーブファイルを開きます。JSONの場合は `"save.json" `、CBORの場合は `"save.dat" ` です。ファイルを開くことができなかった場合は、警告を出力し、false を返します。

QJsonDocument::fromJson() とQCborValue::fromCbor() はどちらもQByteArray を引数として受け取るため、保存形式に関係なく、セーブファイルの内容全体を1つの に読み込むことができます。

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

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

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

    QJsonObject gameObject=toJson();
    saveFile.write(saveFormat==Json?QJsonDocument(gameObject).toJson()
                                      : QCborValue::fromJsonValue(gameObject).toCbor());

    return true;
}

当然のことながら、saveGame() は loadGame() と非常によく似ています。フォーマットに基づいてファイル拡張子を決定し、警告を出力し、ファイルのオープンに失敗した場合は `false ` を返します。 次に、GameオブジェクトをQJsonObject に書き込みます。指定された形式でゲームを保存するために、JSONオブジェクトを、後続のQJsonDocument::toJson()呼び出し用にQJsonDocument に変換するか、QCborValue::toCbor()用にQCborValue に変換します。

すべてをまとめると

これで、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」が指定された場合は以前にセーブされたゲームが読み込まれます。 2番目の引数には、「json」(デフォルト)と「binary」のオプションが利用可能です。この引数によって、どのファイルにセーブするか、あるいはどのファイルから読み込むかが決定されます。その後、プレイヤーがゲームを存分に楽しみ、大きく進捗したものと仮定し、Character、Level、Game オブジェクトの内部状態を変更していきます。

    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 サポート、およびデータの入出力。

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