(三)C++游戏开发-本地存储-JSON文件读写

简介

这章将讲述JSON文件的读写,使用的解析库是RapidJSON

这个库的评价和性能还是蛮好的,官方网站也有很详细的教程和文档,这里只记录一些比较基础的

第一个例子,对本专栏第一篇文章中讲到的英雄联盟数据进行简单的读写操作。

第二个例子,进行稍微复杂的读取操作,从JSON文件中读取俄罗斯方块的方块数据。

相关知识

首先非常清晰的明白个概念
JSON节点只有Object和Array两种类型。Object是一个键值对,key=>value, key必须是字符串,value可以是布尔值、整型等各种类型。

RapidJSON中的节点只有rapidjson::Value类型,泛指Object和Array,如果你换成JAVA,JS等其它语言,大多数分开的JSONObject或者JSONArray,这一点差异可能会带来一定的理解障碍,很多人觉得这点很奇怪,我觉得正好相反。

环境

下载

RapidJSON官方网站

安装

因为RapidJSON只有头文件,用的时候用到include头文件

在Visual Studio里面项目属性如下图配置
在这里插入图片描述

准备数据

原文件非常复杂的,简单起见,这里只提取出部分数据,包含几种基本的数据类型

文件:./json/Riven.json

{
  "id": "Riven",
  "key": "92",
  "name": "放逐之刃",
  "title": "锐雯",
  "skins": [ 1, 2, 3, 4, 5 ],
  "stats": {
    "hp": 560,
    "mp": 0,
    "movespeed": 340,
    "armor": 33,
    "spellblock": 32.1,
    "attackrange": 125,
    "crit": 0,
    "attackdamage": 64,
    "attackspeed": 0.625
  }
}

我的电脑(中文Windows 10)上面创建.txt时,默认编码是GB2312,需要另存为UTF-8
在这里插入图片描述

基本步骤

  1. 从文件中读取数据
  2. 使用RapidJSON解析库进行解析,
    1. 将字符串转换成rapidjson::Document
    2. rapidjson::Document查询变量

读(ASCLL)

用到的头文件
这里我们直接使用上一章从文件读取字符串的函数

#include "BaseFileIO.h"
//Read
#include "rapidjson/document.h"
//Write
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#include "rapidjson/prettywriter.h"

为了方便解析,在读取文件之后我们直接交给Rapid进行解析,然后返回rapidjson::Document以便后面获取变量。

rapidjson::Document GetDocument(const char* fileName) {
    rapidjson::Document doc;
    char* ptr = GetCharsV(fileName);
    doc.Parse(ptr);
    delete ptr;
    return doc;
}

测试用例

void ReadJSON() {
    //1.从文件中读取数据,将字符串转换成`rapidjson::Document`
    const char* fileName = "./json/Riven.json";
    rapidjson::Document doc = GetDocument(fileName);
    if (!doc.IsObject()) {
        cout << "Faild to valid JSON";
        return;
    }

    //2.从`rapidjson::Document`获取变量
    //拷贝到变量中使用
    string id = doc["id"].GetString();
    cout << "id:" << id << " characters size:" << id.size() << " id[0]:" << id[0] << endl;
    cout << "key:" << doc["key"].GetString() << endl;
    string name = doc["name"].GetString();
    cout << "name:" << name << " characters size:" << name.size() << " key[0]:" << name[0] << endl;
    cout << "title:" << doc["title"].GetString() << endl;

    //遍历数组
    cout << "skins:";
    for (auto& kv : doc["skins"].GetArray()) {
        cout << kv.GetInt() << ",";
    }
    cout << endl;

    //深层变量
    rapidjson::Value& vStats = doc["stats"];
    rapidjson::Value& vHp = vStats["hp"];
    rapidjson::Value& vSpellblock = vStats["spellblock"];

    cout << "hp:" << vHp.GetInt() << endl;
    cout << "spellblock:" << vSpellblock.GetDouble() << endl;
}

输出

MBaseIO::GetCharsV:
gcount:308      size:325                strlen:308      vec.size(): 326
{
  "id": "Riven",
  "key": "92",
  "name": "鏀鹃€愪箣鍒?,
  "title": "閿愰洴",
  "skins": [ 1, 2, 3, 4, 5 ],
  "stats": {
    "hp": 560,
    "mp": 0,
    "movespeed": 340,
    "armor": 33,
    "spellblock": 32.1,
    "attackrange": 125,
    "crit": 0,
    "attackdamage": 64,
    "attackspeed": 0.625
  }
}
id:Riven characters size:5 id[0]:R
key:92
title:閿愰洴箣鍒?characters size:12 key[0]:?
skins:1,2,3,4,5,
hp:560
spellblock:32.1

数值和ASCLL正常获取,中文显然又开始乱码了,要解决这个问题,不是简单的将GetChars换成GetWChars就行了。rapidjson默认的Document和Value是使用char作为解析单元的。

读(UTF-8)

这个RapidJSON有个“诡异”的地方,首先来看一下它Document和Value的定义

//! GenericValue with UTF8 encoding
typedef GenericValue<UTF8<> > Value;
typedef GenericDocument<UTF8<> > Document;
///
// UTF8
//! UTF-8 encoding.
/*! http://en.wikipedia.org/wiki/UTF-8
    http://tools.ietf.org/html/rfc3629
    \tparam CharType Code unit for storing 8-bit UTF-8 data. Default is char.
    \note implements Encoding concept
*/
template<typename CharType = char>
struct UTF8;

Document和Value默认解码方式为UTF8,而UTF8又是使用char来存放。

我们都知道一个UTF-8码点的宽度是1到4个字节,char很显然不能满足,而wchar_t能够满足这个条件

为什么RaidJSON不用wchar_t而是char呢?

RapidJSON:Encoding
Character Type
As shown in the declaration, each encoding has a CharType template parameter. Actually, it may be a little bit confusing, but each CharType stores a code unit, not a character (code point). As mentioned in previous section, a code point may be encoded to 1–4 code units for UTF-8.

大概意思
每一个编码都有一个Character Type模板参数,实际上Character Type存储的是一个编码单位,而不是一个字符,在UTF8中一个字符可能会编码成1到4个char。

也就是说在rapidjson中,char 是UTF-8的编码单位,把char用作存储单位无可厚非,因为对数据的整体结构没有影响,解析库根据逗号,引号,分号这些来“断句”

关键问题是在查询变量的时候,GetString只能获得一个char类型的编码单位,而我们想要取出来的是完整的字符,而不是三分之一的字符,所以这个地方,如果任然使用rapidjson默认的UTF8的Character Type就会导致这种情况的发生。

我的解决方法就是,任然使用UTF-8进行解码,但是将存储单位换成wchar_t,最终将GetDocument升级成GetWDocument

typedef rapidjson::GenericDocument<rapidjson::UTF8<wchar_t> > WDocument;
typedef rapidjson::GenericValue<rapidjson::UTF8<wchar_t> > WValue;
WDocument GetWDocument(const char* fileName) {
    WDocument doc;
    wchar_t* ptr = GetWCharsV(fileName);
    doc.Parse(ptr);
    delete ptr;
    return doc;
}

新的测试用例

不一定全部都要用wcout,忘记改了

void ReadWJSON() {
    const char* fileName = "./json/Riven.json";
    WDocument doc = GetWDocument(fileName);

    if (!doc.IsObject()) {
        cout << "Faild to valid JSON";
        return;
    }

    wstring id = doc[L"id"].GetString();
    wcout << L"id:" << id << "\tid.size()" << id.size() << "\tid[0]:" << id[0] << L"\n";
    wcout << L"key:" << doc[L"key"].GetString() << L"\n";

    wstring name = doc[L"name"].GetString();
    wcout << L"name:" << name << L"\tname.size():" << name.size() << L"\tname[0]:" << name[0] << L"\n";
    wcout << L"title:" << doc[L"title"].GetString() << L"\n";

    cout << "skins:";
    for (auto& kv : doc[L"skins"].GetArray()) {
        cout << kv.GetInt() << ",";
    }
    cout << endl;

    WValue& vStats = doc[L"stats"];
    WValue& vHp = vStats[L"hp"];
    WValue& vSpellblock = vStats[L"spellblock"];

    cout << "hp:" << vHp.GetInt() << endl;
    cout << "spellblock:" << vSpellblock.GetDouble() << endl;

    //更改之后保存
    const char* newName = "./json/Riven-Update.json";
    vHp.SetInt(100000);
    SaveWRapidJson(doc, newName);
}

输出

MBaseIO::GetWCharsV:
gcount:296      size:325                wcslen: 296     vec.size(): 326
{
  "id": "Riven",
  "key": "92",
  "name": "放逐之刃",
  "title": "锐雯",
  "skins": [ 1, 2, 3, 4, 5 ],
  "stats": {
    "hp": 560,
    "mp": 0,
    "movespeed": 340,
    "armor": 33,
    "spellblock": 32.1,
    "attackrange": 125,
    "crit": 0,
    "attackdamage": 64,
    "attackspeed": 0.625
  }
}
id:Riven        id.size()5      id[0]:R
key:92
name:放逐之刃   name.size():4   name[0]:放
title:锐雯
skins:1,2,3,4,5,
hp:560
spellblock:32.1

MBaseIO::OverWriteUTF8:
wcslen(content):393

写(ASCLL)

这个过程也是比较简单的,就是把Document先转换成char然后在调用上一章里面保存char的函数去进行保存

bool SaveRapidJson(rapidjson::Document& doc, const char* fileName) {
    //1.Document=>char*
    rapidjson::StringBuffer buffer;
    rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
    doc.Accept(writer);
    const char* output = buffer.GetString();
    //2.char*=>file
    OverWriteFile(fileName, output);
    return true;
}

StringBuffer类似于vector

在rapidjson中,PrettyWriter和Writer都可以用来保存数据

Writer是一个handle,accept将自己交给writer处理,writer将doc的字符串输入给buffer
最后将buffer中的字符串保存到文件,完成JSON的写出

Writer输出的是没有缩进和换行的字符串,适合于程序内部和网络传输,具有同样功能的PrettyWriter可以输出缩进和换行,但看起来有些冗长

测试用例

//从零构建一个JSON DOM并写入文件
void WriteJSON() {
    //1.初始化Document
    rapidjson::Document doc;
    doc.SetObject();

    //2.添加内容
    doc.AddMember("key1", "string", doc.GetAllocator());
    //Object
    rapidjson::Value key2(rapidjson::kObjectType);
    key2.AddMember("key2.1", 123, doc.GetAllocator());
    key2.AddMember("key2.2", true, doc.GetAllocator());
    doc.AddMember("key2", key2, doc.GetAllocator());
    doc.AddMember("key3", 1.23, doc.GetAllocator());
    //Array
    rapidjson::Value key4(rapidjson::kArrayType);
    for (int i = 1; i < 9; i++) {
        key4.PushBack(i, doc.GetAllocator());
    }
    doc.AddMember("key4", key4, doc.GetAllocator());

    //3.保存到文件
    SaveRapidJson(doc, "./json/WriteJSON.json");
}

Document和Value使用默认构造函数构造,类型都是Null,需要SetXXX()或者直接赋值设置类型

输出

{
    "key1": "string",
    "key2": {
        "key2.1": 123,
        "key2.2": true
    },
    "key3": 1.23,
    "key4": [
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8
    ]
}

写(UTF-8)

这个地方和之前读的时候的思路是一样的,既然把Document都换成了以wchar_t为编码单位的WDocument,那么要想对其进行保存,必然要将所有涉及到的类都进行响应修改

typedef rapidjson::GenericStringBuffer<rapidjson::UTF8<wchar_t> > WStringBuffer;
typedef rapidjson::PrettyWriter<WStringBuffer, rapidjson::UTF8<wchar_t>, rapidjson::UTF8<wchar_t>> WPrettyWriter;
bool SaveWRapidJson(WDocument& doc, const char* fileName) {
    WStringBuffer buffer;
    WPrettyWriter writer(buffer);
    doc.Accept(writer);
    const wchar_t* output = buffer.GetString();
    OverWriteUTF8(fileName, output);
    return true;
}

测试用例

在上面读取UTF-8文件的测试用例中你可能已经发现了,最下面有一段代码

//更改HP后之后保存
const char* newName = "./json/Riven-Update.json";
vHp.SetInt(100000);
SaveWRapidJson(doc, newName);

输出

这地方用RapidJson自带的格式化输出方法还是有些诡异,数组全部都给变成单列的了,我暂时没有去找怎么改这个。

{
    "id": "Riven",
    "key": "92",
    "name": "放逐之刃",
    "title": "锐雯",
    "skins": [
        1,
        2,
        3,
        4,
        5
    ],
    "stats": {
        "hp": 100000,
        "mp": 0,
        "movespeed": 340,
        "armor": 33,
        "spellblock": 32.1,
        "attackrange": 125,
        "crit": 0,
        "attackdamage": 64,
        "attackspeed": 0.625
    }
}

RapidJson还有很多东西,这里就不一一列举了,查看官方文档是最佳选择。

综合案例

俄罗斯方块大家都玩过,甚至也自己制作过,网上有很多的代码,包括我自己也写过,大多数都是将方块数据在代码中硬编码,这是可行的,但是在现代游戏中,很多游戏数据都是存储在外存,当游戏需要时动态进行加载和卸载。

下面我们就单纯的从JSON文件中,加载俄罗斯方块的方块数据。

准备数据

{
	"Blocks": [
		[
			[
			  0,0,0,0,
			  0,1,0,0,
			  0,1,0,0,
			  0,1,1,0
			],
			[
			  0,0,0,0,
			  0,0,0,0,
			  0,0,1,0,
			  1,1,1,0
			],
			[
			  0,0,0,0,
			  0,1,1,0,
			  0,0,1,0,
			  0,0,1,0
			],
			[
			  0,0,0,0,
			  0,0,0,0,
			  1,1,1,0,
			  1,0,0,0
			]
		],
		[
			[
			  0,0,0,0,
			  0,0,1,0,
			  0,1,1,0,
			  0,0,1,0
			],
			[
			  0,0,0,0,
			  0,0,0,0,
			  1,1,1,0,
			  0,1,0,0
			],
			[
			  0,0,0,0,
			  0,1,0,0,
			  0,1,1,0,
			  0,1,0,0
			],
			[
			  0,0,0,0,
			  0,0,0,0,
			  0,1,0,0,
			  1,1,1,0
			]
		],
		[
			[
			  0,0,0,0,
			  0,0,0,0,
			  0,1,1,0,
			  1,1,0,0
			],
			[
			  0,0,0,0,
			  0,1,0,0,
			  0,1,1,0,
			  0,0,1,0
			]
		],
		[
			[
			  0,0,0,0,
			  0,0,0,0,
			  0,1,1,0,
			  0,1,1,0
			]
		],
		[
			[
			  0,1,0,0,
			  0,1,0,0,
			  0,1,0,0,
			  0,1,0,0
			],
			[
			  0,0,0,0,
			  0,0,0,0,
			  0,0,0,0,
			  1,1,1,1
			]
		]
	]
}

请注意,在Blocks内部,第一层是形状的数量,第二层是方块的元素,0表示无,1表示有。

方块是4*4的矩阵,JSON文件中使用了一维数组表示二维数组,在程序中是一个三维的数组,存储若干个二维矩阵。为什么不分形状进行存储,这一点以后有时间会说。

过程基本上都在注释里面了,解析的过程可能有点复杂

//4*4方块矩阵
const int BLOCK_LENGTH = 4;

void ReadBlocks() {
    const char* fileName = "./json/Blocks.json";
    rapidjson::Document doc = GetDocument(fileName);
    if (!doc.IsObject()) {
        cout << "Faild to valid JSON";
        return;
    }
    //获取节点
    rapidjson::Value& vBlocks = doc["Blocks"];

    //形状数量
    const int shapeNum = vBlocks.Size();

    //形状的方块数量
    int* blockNum = new int[shapeNum];

    //形状范围
    int* sumNum = new int[shapeNum];

    //初始化二维数组
    for (int i = 0; i < shapeNum; i++) {
        blockNum[i] = 0;
        sumNum[i] = 0;
    }
    //方块数量
    int count = 0;
    for (int i = 0; i < shapeNum; i++) {
        //每个形状对应的角度数量
        blockNum[i] = vBlocks[i].Size();
        sumNum[i] += 0;
        count += blockNum[i];
    }
    //初始化三维数组[13][BLOCK_LENGTH][BLOCK_LENGTH]
    int*** blocks = new int** [count];
    for (int i = 0; i < count; i++) {
        blocks[i] = new int* [BLOCK_LENGTH];
        for (int j = 0; j < BLOCK_LENGTH; j++) {
            blocks[i][j] = new int[BLOCK_LENGTH];
            for (int k = 0; k < BLOCK_LENGTH; k++) {
                blocks[i][j][k] = 0;
            }
        }
    }
    //将文件中的数组读取到三维数组
    int n = 0;
    for (int i = 0; i < shapeNum; i++) {
        for (int j = 0; j < blockNum[i]; j++) {
            rapidjson::Value matrix = vBlocks[i][j].GetArray();
            for (int k = 0; k < static_cast<int>(matrix.Size()); k++) {
                int row = static_cast<int>(floor(k / BLOCK_LENGTH));
                int col = k % BLOCK_LENGTH;
                blocks[n][row][col] = matrix[k].GetInt();
            }
            n++;
        }
    }
    //查看现在三维数组中的数据
    for (int i = 0; i < count; i++) {
        for (int j = 0; j < BLOCK_LENGTH; j++) {
            for (int k = 0; k < BLOCK_LENGTH; k++) {
                cout << blocks[i][j][k] << " ";
            }
            cout << std::endl;
        }
        cout << std::endl;
    }
    //释放
    for (int i = 0; i < count; i++) {
        for (int j = 0; j < BLOCK_LENGTH; j++) {
            delete[] blocks[i][j];
        }
        delete[] blocks[i];
    }
    delete[] blocks;
    delete[] blockNum;
    delete[] sumNum;
}

输出

0 0 0 0
0 1 0 0
0 1 0 0
0 1 1 0

0 0 0 0
0 0 0 0
0 0 1 0
1 1 1 0

0 0 0 0
0 1 1 0
0 0 1 0
0 0 1 0

0 0 0 0
0 0 0 0
1 1 1 0
1 0 0 0

0 0 0 0
0 0 1 0
0 1 1 0
0 0 1 0

0 0 0 0
0 0 0 0
1 1 1 0
0 1 0 0

0 0 0 0
0 1 0 0
0 1 1 0
0 1 0 0

0 0 0 0
0 0 0 0
0 1 0 0
1 1 1 0

0 0 0 0
0 0 0 0
0 1 1 0
1 1 0 0

0 0 0 0
0 1 0 0
0 1 1 0
0 0 1 0

0 0 0 0
0 0 0 0
0 1 1 0
0 1 1 0

0 1 0 0
0 1 0 0
0 1 0 0
0 1 0 0

0 0 0 0
0 0 0 0
0 0 0 0
1 1 1 1

OK,会使用JSON读写文件之后,基本上已经能搞定很多数据存储问题了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锋哥游戏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值