게임 저장 및 로드
Qt의 JSON 또는 CBOR 클래스를 사용하여 게임을 저장하고 로드하는 방법.
많은 게임이 저장 기능을 제공하므로 플레이어의 게임 진행 상황을 저장하고 나중에 로드할 수 있습니다. 게임을 저장하는 과정에는 일반적으로 각 게임 오브젝트의 멤버 변수를 파일로 직렬화하는 작업이 포함됩니다. 이를 위해 다양한 형식을 사용할 수 있는데, 그 중 하나가 JSON입니다. QJsonDocument 를 사용하면 문서를 CBOR 형식으로 직렬화할 수도 있는데, 이는 저장 파일을 읽기 쉽게 만들고 싶지 않거나(읽기 방법은 CBOR 데이터 파싱 및 표시하기 참조) 파일 크기를 줄여야 하는 경우에 유용합니다.
이 예시에서는 간단한 게임을 JSON 및 바이너리 형식으로 저장하고 로드하는 방법을 보여드리겠습니다.
캐릭터 클래스
Character 클래스는 게임 내 비플레이어 캐릭터(NPC)를 나타내며 플레이어의 이름, 레벨, 클래스 유형을 저장합니다.
정적 fromJson() 및 비정적 toJson() 함수를 제공하여 스스로 직렬화할 수 있습니다.
참고: 이 패턴(fromJson()/toJson())이 작동하는 이유는 QJsonObject가 소유하는 QJsonDocument 과 무관하게 구성될 수 있고 여기서 (탈)직렬화되는 데이터 유형이 값 유형이므로 복사할 수 있기 때문입니다. 문서형 객체를 전달해야 하는 XML 또는 QDataStream 와 같은 다른 형식으로 직렬화하거나 객체 ID가 중요한 경우(예: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
의 멤버 값을 할당합니다. QJsonObject::operator[] () 또는 QJsonObject::value()을 사용하여 JSON 객체 내의 값에 액세스할 수 있으며, 둘 다 const 함수이며 키가 유효하지 않은 경우 QJsonValue::Undefined 를 반환합니다. 특히 is...
함수(예: QJsonValue::isString(), QJsonValue::isDouble())는 QJsonValue::Undefined 에 대해 false
을 반환하므로 한 번의 조회로 올바른 유형뿐만 아니라 존재 여부도 확인할 수 있습니다.
값이 JSON 객체에 존재하지 않거나 잘못된 유형인 경우 해당 result
멤버에도 쓰지 않으므로 기본 생성자가 설정했을 수 있는 값을 보존할 수 있습니다. 즉, 기본값은 한 위치(기본 생성자)에 중앙에서 정의되며 직렬화 코드(DRY)에서 반복할 필요가 없습니다.
변수의 범위 지정과 검사를 분리하기 위해 C++17 if-with-이니셜라이저를 사용하는 것을 관찰하세요 v
. 이는 범위가 제한되어 있으므로 변수 이름을 짧게 유지할 수 있음을 의미합니다.
QJsonObject::contains()
을 사용하는 순진한 접근 방식과 비교해 보세요:
if (json.contains("name") && json["name"].isString()) result.mName = json["name"].toString();
는 가독성이 떨어질 뿐만 아니라 총 세 번의 조회가 필요하므로(컴파일러가 이를 하나로 최적화하지 않음 ) 세 배 더 느리고 "name"
를 세 번 반복합니다(DRY 원칙 위반).
QJsonObject Character::toJson() const { QJsonObject json; json["name"] = mName; json["level"] = mLevel; json["classType"] = mClassType; return json; }
toJson() 함수에서는 fromJson() 함수와 반대로, Character 객체의 값을 반환하는 새 JSON 객체에 할당합니다. 값에 액세스하는 것과 마찬가지로 QJsonObject::operator[]() 및 QJsonObject::insert()의 두 가지 방법으로 QJsonObject 에 값을 설정할 수 있습니다. 두 가지 방법 모두 지정된 키의 기존 값을 재정의합니다.
레벨 클래스
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 캐릭터 객체를 유지합니다. 또한 익숙한 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() 함수와 유사합니다.
게임 클래스
캐릭터와 레벨 클래스를 설정했으니 이제 게임 클래스로 넘어가겠습니다:
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()의 세 가지 함수를 노출합니다.
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에서 읽은 플레이어로 대체하는 것으로 시작합니다. 그런 다음 레벨 배열을 clear()하여 동일한 게임 오브젝트에서 loadGame()을 두 번 호출해도 이전 레벨이 남아 있지 않도록 합니다.
그런 다음 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으로 작성하는 것은 레벨을 작성하는 것과 비슷합니다.
bool Game::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()<< " using "<< (saveFormat != Json ? "CBOR": "JSON")<< "...\n"; return true; }
loadGame()에서 저장된 게임을 로드할 때 가장 먼저 하는 일은 저장된 형식에 따라 저장 파일을 여는 것입니다(JSON의 경우 "save.json"
, CBOR의 경우 "save.dat"
). 파일을 열 수 없는 경우 경고를 출력하고 false
을 반환합니다.
QJsonDocument::fromJson()와 QCborValue::fromCbor()는 모두 QByteArray 를 취하므로 저장 형식에 관계없이 저장 파일의 전체 내용을 하나로 읽을 수 있습니다.
QJsonDocument 를 구성한 후 게임 객체에 스스로 읽도록 지시한 다음 true
을 반환하여 성공을 나타냅니다.
bool Game::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 에 작성합니다. 지정한 형식으로 게임을 저장하기 위해 후속 QJsonDocument::toJson() 호출을 위한 QJsonDocument 또는 QCborValue::toCbor() 호출을 위한 QCborValue 으로 JSON 객체를 변환합니다.
모든 것을 하나로 묶기
이제 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로 직렬화할 수 있습니다. 실행 파일과 동일한 디렉터리에서 파일의 내용을 검사할 수 있지만(또는 예제를 다시 실행하여 "로드" 옵션도 지정해야 함), 바이너리 저장 파일에는 일부 가비지 문자가 포함될 수 있습니다(이는 정상입니다).
이것으로 예제를 마칩니다. 보시다시피 Qt의 JSON 클래스를 사용한 직렬화는 매우 간단하고 편리합니다. 예를 들어 QDataStream 보다 QJsonDocument 및 친구를 사용하면 사람이 읽을 수 있는 JSON 파일을 얻을 수 있을 뿐만 아니라 필요한 경우 코드를 다시 작성하지 않고도 바이너리 형식을 사용할 수 있는 옵션이 있다는 이점이 있습니다.
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.