游戏项目的Json文件合并打包处理

游戏项目的Json文件合并打包处理

前言

对于一个游戏项目来说,配置文件是一个必不可少的东西。策划配置Excel表格,程序通过工具将其转化成Json文件,再读取到项目中使用,这是十分常见的流程。

而随着项目的开发,业务功能的增多,相对应的配置文件数量也会越来越多,如何优化配置文件的读取流程就显得相当重要。

配置压缩

在我以前的工作中,常规的配置优化工作,其实就是对配置进行压缩处理。把多余的空格和换行删除,减小文件体积。
压缩前:

{
	"name": "json"
}

压缩后:

{"name":"json"}

更进一步的处理,则是提取配置中的Key值,然后在游戏内进行额外的解析工作。
例如:

[
	{
		"key1": "json1",
		"key2": "hello",
		"key3": 1
	}{
		"key1": "json2",
		"key2": "hi",
		"key3": 2
	}
]

对key值进行提取之后,配置格式也需要进行相应的修改:

{
	"key": ["key1","key2","key3"],
	"list":[
		{
			"0": "json1",
			"1": "hello",
			"2": 1
		},
		{
			"0": "json2",
			"1": "hi",
			"2": 2
		}
	]
}

然后再对其进行压缩,删除多余的空格和换行符。

合并配置

以上的两个处理,是我工作过程中使用的最多的,能够有效减小Json的体积大小,能一定程度上优化配置的读取。
但是还存在着另外一个问题,那就是IO读取和网络请求问题。
当项目的Json配置越来越多,业务功能关联性越来越强,每次打开一个功能,可能都需要加载很多张相关的Json配置。那么就会导致需要多次的IO读取,对于将Json配置文件存放在远程服务器上的项目,还需要进行多次网络请求和下载。
所以为了优化减少IO的读取和网络请求次数,可以将配置文件进行合并,合并成一个文件,在进游戏时,于某个时机进行加载和缓存,这样用到的时候就可以直接解析后使用,不用再额外请求。

工具脚本分享

在一份工作中,看到了公司使用了相关的工具进行配置的合并,但是很可惜,工具是被打包成exe执行文件了,没办法查看到源码。
所以通过项目内对配置的解析和使用,大概分析了一下合并的逻辑,在这里做一下分享和记录。
通过对项目内代码的分析,可以了解到合并后的文件,大致可以分为两个部分:

  • 第一部分记录了所有的Json文件的相关信息,信息包括了几个部分
    1. Json文件的总数量
    2. 每一个Json文件的文件名、文件名长度、文件内容对应的解析时的Begin值,文件内容数据的bytes长度
  • 第二部分存储了所有的Json文件具体内容

从上面的可以看出,这个合并后的文件,是使用二进制格式存储的。了解了这部分信息之后。脚本工具就可以使用js来实现了,具体代码如下:

const fs = require('fs');
const path = require('path');

/** Json文件存放目录 */
const fileDir = './resDir';
/** 输出MPQ压缩文件存放目录 */
const outDir = './mpqDir';

/** 整体所占的字节数 */
let totalBytes = 0;

/** 所有的Json文件名数组 */
const p = path.resolve(fileDir);
/** 读取目录下的所有Json文件 */
const files = fs.readdirSync(p);

/**
 * 文件信息Map: { [name: string]: { fileName: string, fileNameLen: number, fileData: string, fileDataLen: number, fileBegin: number } }
 * @param fileName json文件名
 * @param fileNameLen json文件名长度
 * @param fileData json文件数据字符串
 * @param fileDataLen json文件数据长度
 * @param fileBegin json文件数据开始字节位置
 */
const fileMap = {};

// 处理Json文件信息,存储到Map中
for (let i = 0; i < files.length; i++) {
    /** json文件名 */
    const fileName = files[i];
    /** 文件名的字符长度,例如 Cfg_Pet.json ,长度就是 12 了 */
    const fileNameLen = fileName.length;

    /** Json文件内容的总长度,一个字符就占一个字节,中文字符占两个字节,要用encoder转换一下才能拿到实际的字节数 */
    /** 将Json文件内容读取为string */
    let fileData = fs.readFileSync(fileDir + '/' + fileName, 'utf-8');
    let encoder = new TextEncoder();
    let bytes = encoder.encode(fileData);
    let fileDataLen = bytes.length;

    /** Json文件数据开始的字节位置,由于需要先将文件信息的内容先写入到Buffer中,所以这里先初始化为0,后面动态写入 */
    const fileBegin = 0;

    fileMap[fileName] = { fileName, fileNameLen, fileData, fileDataLen, fileBegin };
}

// 开始计算Buffer所需要的总字节数
// 最开始的两个字节,用于存储总文件数量
totalBytes += 2;// 2个字节 存总文件数量

// 遍历所有文件信息,计算每个文件信息所占的字节数
for (let key in fileMap) {
    const ele = fileMap[key];

    totalBytes += 2; // 2个字节 存文件名长度+2

    const nameLen = ele.fileNameLen;
    totalBytes += nameLen; // nameLen个字节 存文件名

    totalBytes += 4;// 4个字节 存文件的begin值,也就是Json文件数据开始的字节位置

    totalBytes += 4; // 4个字节 存文件的数据长度length值
}

const infoBytesLen = totalBytes;

// 开始计算每一个Json文件的具体数据内容所需要的字节数
let begin = totalBytes;// 第一个文件的数据开始位置
for (let key in fileMap) {
    const ele = fileMap[key];
    /** Json文件数据的总长度 */
    const dataLen = ele.fileDataLen;
    // 这里就开始填充Json的文件信息里的Begin
    ele.fileBegin = begin;
    // 下一个文件数据的开始位置
    begin += dataLen;
    // 对应增加总字节数
    totalBytes += dataLen; // 文件数据长度
}

// 至此Buffer所需要的总字节数已经计算完毕,每一个Json文件的信息也已经有了,开始写入二进制
// 创建一个Buffer
const buff = new ArrayBuffer(infoBytesLen);
// 创建一个DataView,用于操作Buffer
const dataview = new DataView(buff);
// 开始写入Buffer
let offset = 0;

// 写入总文件数量,占用两个字节
dataview.setInt16(offset, files.length);    // 2个字节 存总文件数量
offset += 2;

// 写入所有文件的基础信息,这里不包括Json文件数据
for (let key in fileMap) {
    const ele = fileMap[key];
    const name = ele.fileName;
    const nameLen = ele.fileNameLen;
    const dataLen = ele.fileDataLen;
    const begin = ele.fileBegin;

    // 写入Json文件名字长度,占两个字节
    dataview.setInt16(offset, nameLen);
    offset += 2;

    // 写入Json文件名,每一个字符占一个字节,所以总共占用nameLen个字节
    for (let i = 0; i < nameLen; i++) {
        dataview.setUint8(offset, name.charCodeAt(i));
        offset += 1;
    }

    // 写入Json文件数据开始的位置,占四个字节
    dataview.setUint32(offset, begin);
    offset += 4;

    // 写入Json文件数据长度,占四个字节
    dataview.setUint32(offset, dataLen);
    offset += 4;
}

// 将以上二进制数据写入到txt文件中
fs.writeFileSync(outDir + '/cfg_0.txt', dataview, { encoding: 'binary' });

// 开始写入每一个Json文件的实际数据,这里直接按顺序增量写入字符串数据,实际网络加载时,会自动将里面的字符串转换成对应的二进制数据
for (let key in fileMap) {
    const ele = fileMap[key];
    const data = ele.fileData;
    fs.appendFileSync(outDir + '/cfg_0.txt', data)
}
console.log('====== 写入成功 ======')

通过上面的Js脚本,基本上可以满足将多个Json文件合并成一个二进制文件的需求了。当然这个还可以继续进行优化,比如我上面所说的,提取每一个Json文件的key值,不过这里我没有处理,有需要的人可以自行进行拓展,也不是太难。

配置解析

合并完配置之后,通过在游戏内加载和解析,就可以使用相关的配置内容。
由于我使用的是cocos引擎,所以在这里我就用cocos引擎来简单说一下解析的过程。

/**
 * mpq文件Map,用于存储每一个Json文件的信息,不做具体配置内容的解析,按需解析,节省性能
 * @param name json文件名
 * @param begin json文件数据开始位置
 * @param length json文件数据长度
 * @param data 整个Buffer的DataView视图,用于解析读取数据时使用
 */
const mpqMap: { [jsonName: string]: { name: string, begin: number, length: number, data: ArrayBuffer } } = {};
// 二进制文件的远端存放地址
const url = 'cfg_0.txt';
cc.assetManager.loadRemote(url, { ext: '.bin' }, (err, text: cc.BufferAsset | ArrayBuffer) => {
    if (!err) {
        // 加载文件成功
        // 微信小游戏底层使用wx.request(在wx-download.js中做了处理)加载txt文件,出来的结果为ArrayBuffer
        // h5中出来的是Unit8Array
        const _plf = cc.sys.platform;
        let buffer: ArrayBuffer = null;
        if (_plf === cc.sys.VIVO_GAME || _plf === cc.sys.XIAOMI_GAME || _plf === cc.sys.WECHAT_GAME || _plf === cc.sys.OPPO_GAME) {
            buffer = text;
        } else {
            buffer = text['_buffer'];
        }
        cc.assetManager.releaseAsset(text);
        text = null;

        // 根据Buffer创建DataView视图
        const mpqData = new DataView(buffer);
        let offset = 0;
        // 读取总文件数量,占两个字节
        const fileLen: number = mpqData.getInt16(offset);
        offset += 2;

        // 开始解析全部文件信息
        for (let i = 0; i < fileLen; i++) {

            /** 配置信息结构体
             * @param name 文件名
             * @param begin 文件数据开始位置
             * @param length 文件数据长度
             * @param data 整个Buffer的DataView视图,用于解析读取数据时使用
             */
            const fileInfo: { data: ArrayBuffer, begin: Number, length: Number, name: string } = {};

            // Json文件名长度,占两个字节
            const nameLen: number = mpqData.getInt16(offset);
            offset += 2;

            // 获取Json文件名Buffer数据,解析Json文件名,占nameLen个字节
            const nameBuffer = new Uint8Array(buffer, offset, nameLen);
            const name: string = String.fromCharCode.apply(null, nameBuffer);
            offset += nameLen;

            // 获取Json文件数据开始位置,占4个字节
            const begin: number = mpqData.getUint32(offset);
            offset += 4;

            // 获取Json文件数据总长度,占4个字节
            const length: number = mpqData.getUint32(offset);
            offset += 4;

            fileInfo.name = name;
            fileInfo.begin = begin;
            fileInfo.length = length;
            fileInfo.data = buffer;
            mpqMap[name] = fileInfo;
        }
    }
})

const jsonMap: { [jsonName: string]: Object } = {};
// 解析配置
public parseMPQDataToJson(jsonName: string): void {
    const fileInfo: { name: string, begin: number, length: number, data: ArrayBuffer } = mpqMap[jsonName];
    if (!fileInfo) {
        return;
    }
    // 获取Buffer数据
    const buffer: ArrayBuffer = fileInfo.data;
    // 获取Json文件数据的Buffer
    const jsonDataBuffer = new Uint8Array(buffer, fileInfo.begin, fileInfo.length);
    // 将Buffer转化为字符串
    let jsonDataStr = String.fromCharCode.apply(null, jsonDataBuffer);
    // 对字符串进行转义,兼容处理中文字符,没有这一步中文会乱码
    jsonDataBuffer = decodeURIComponent(escape(jsonDataStr));
    // 将字符串转化为Json对象
    jsonMap[jsonName] = JSON.parse(jsonDataBuffer);
}

具体代码的作用,上面的注释都很清楚了,就不多说了。

结语

到这里Json文件的合并打包处理就结束了,它可以有效减少配置文件的读取次数和下载次数,但是同样存在缺陷,那就是它需要占用两份内存,一份是下载下来的二进制数据,一份则是使用的时候解析出来的Json数据,目前我还没想到有什么好办法可以解决这个缺陷,有好办法的朋友,也希望能够告诉我,让我学习一下,感谢。

要在Vue2项目中配置一个打包速度很快的Webpack文件,可以参考以下步骤: 1. 使用`vue-cli`创建一个Vue2项目: ``` vue create my-project ``` 2. 安装`webpack-merge`插件,用于合并Webpack配置: ``` npm install --save-dev webpack-merge ``` 3. 在项目根目录下创建一个`build`目录,并在其中创建一个名为`webpack.common.js`的文件,用于存放公共的Webpack配置: ``` const path = require('path'); const webpack = require('webpack'); const { VueLoaderPlugin } = require('vue-loader'); module.exports = { entry: { app: './src/main.js' }, output: { path: path.resolve(__dirname, '../dist'), filename: '[name].js' }, resolve: { extensions: ['.js', '.vue', '.json'], alias: { '@': path.resolve(__dirname, '../src') } }, module: { rules: [ { test: /\.vue$/, loader: 'vue-loader' }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: 'img/[name].[hash:7].[ext]' } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, loader: 'url-loader', options: { limit: 10000, name: 'fonts/[name].[hash:7].[ext]' } } ] }, plugins: [ new VueLoaderPlugin(), new webpack.optimize.ModuleConcatenationPlugin() ] }; ``` 4. 在`build`目录下创建一个名为`webpack.dev.js`的文件,用于存放开发环境的Webpack配置: ``` const merge = require('webpack-merge'); const common = require('./webpack.common.js'); module.exports = merge(common, { mode: 'development', devtool: 'cheap-module-eval-source-map', devServer: { contentBase: '../dist', hot: true }, module: { rules: [ { test: /\.css$/, use: [ 'style-loader', 'css-loader' ] }, { test: /\.scss$/, use: [ 'style-loader', 'css-loader', 'sass-loader' ] } ] }, plugins: [ new webpack.HotModuleReplacementPlugin() ] }); ``` 5. 在`build`目录下创建一个名为`webpack.prod.js`的文件,用于存放生产环境的Webpack配置: ``` const merge = require('webpack-merge'); const common = require('./webpack.common.js'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); module.exports = merge(common, { mode: 'production', devtool: 'source-map', optimization: { minimizer: [ new OptimizeCSSAssetsPlugin() ] }, module: { rules: [ { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader' ] }, { test: /\.scss$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader' ] } ] }, plugins: [ new CleanWebpackPlugin(), new MiniCssExtractPlugin({ filename: 'css/[name].[hash].css' }) ] }); ``` 6. 修改`package.json`文件,添加两个命令用于启动开发和生产环境的Webpack: ``` "scripts": { "serve": "webpack-dev-server --config build/webpack.dev.js", "build": "webpack --config build/webpack.prod.js" } ``` 7. 运行以下命令启动开发环境的Webpack: ``` npm run serve ``` 运行以下命令构建生产环境的Webpack: ``` npm run build ``` 这样,你就可以在Vue2项目中配置一个打包速度很快的Webpack文件了。其中,我们使用了`webpack-merge`插件来合并公共配置和环境特定的配置,使用了`clean-webpack-plugin`插件来清空打包目录,在生产环境中使用了`mini-css-extract-plugin`插件来提取CSS,使用了`optimize-css-assets-webpack-plugin`插件来优化CSS。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值