前一节把测试功能加上了,地图信息编辑好,测试通过之后,需要导出到游戏中。这节来看数据的导出。主要包括以下几个功能点:
在导出地图信息之后,把数据格式定义好,必备数据有两个:阻挡遮罩信息和格子尺寸,其他的数据可以按需添加。
getExportData() {
return {
size: [this.oriW, this.oriH],
cell: [this.CELL_W, this.CELL_H],
block: this.blockInfoList,
};
},
size 保存的整个地图的尺寸,block时阻挡和遮罩信息,是一个二维数组
在工具栏添加一个导出按钮,点击之后导出数据,如图:
导出的格式我这里用的 json,用通用数据格式方便数据的交换。
cocos creator 打包之后是跨平台的,不同的平台数据导出方式有很大差异;因为平时都是用浏览器开发调试,因此只做了在浏览器里面的导出。基本思路是模拟点击链接下载文件,流程:
saveForWebBrowser(json, filename) {
if (!cc.sys.isBrowser) {
return;
}
// 创建a标签
let downloadLink = document.createElement("a");
downloadLink.download = filename;
downloadLink.innerHTML = "Download File";
downloadLink.style.display = "none";
// 设置下载内容
let JsonString = JSON.stringify(json);
let textFileAsBlob = new Blob([JsonString]);
downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
downloadLink.onclick = () => {
// 移除a标签
document.body.removeChild(downloadLink);
};
document.body.appendChild(downloadLink);
// 模拟点击a标签
downloadLink.click();
}
点击导出按钮,会弹出另存为的弹窗,其中默认文件名字是程序中设置的 filename。
导出之后可以看到,数据30k+,挺大的,其中主要是格子的状态的二维数组,按图上的尺寸,总共有:7920/80 * 7160 / 40 = 17721
个数组元素,需要进行压缩。
数据的压缩我试过好几种方式,都介绍一下。总的来看有两种思路:第一种是将数据转化成字符串,然后在对字符串尽兴压缩;第二种是压缩二维数组的元素。
在进行字符串压缩之前,先把二维数组转换成字符串。将二维数组转成一维数组,然后将每个元素都变成字符串。
compressString(matrix) {
// 将状态转换成一行制字符串
let info = "";
for (let i = 0; i < matrix.length; i++) {
info += matrix[i].join("");
}
return info;
},
压缩出来的数据大致是这样的结构:
然后就是要对这个字符串进行压缩。
这里面有很多重复的0,1,2,很容易想到的一种压缩方式是合并重复的字符,比如 16个连续的 “0000000000000000”,合并为 “[0,16]”。
compressSame(info) {
// 合并连续相同字符串,比如 "00000000" => "[0,8]"
let same = "";
let cur = info[0];
let cnt = 1;
for (let i = 1; i < info.length; i++) {
if (info[i] === cur) {
cnt += 1;
} else {
same += this.getCompressSameItem(cur, cnt);
cur = info[i];
cnt = 1;
}
}
same += this.getCompressSameItem(cur, cnt);
return same;
},
这种方式适合阻挡和遮罩都很少时,比如什么都不编辑,可以得到最大程度的压缩:
这种情况过于理想,统计了正式地图中字符出现的比例,大概是: “0”:“1”:“2” = 2:2:1,因此并不是很适合。
字符串中有很多的01,可以联想到二进制,如果只有01的话,可以用尽可能大的进制表示,这样一个字符就能表示更多的位数。但是,问题是其中出现了2,要怎么解决?可能用两位二进制表示一个状态,比如:00,01,10 分别表示 0, 1,2;这样字符串长度变成了原来的两倍,比如原来的字符是 120,变成二进制之后则是 011000。
compressBase2(matrix) {
// 将状态转换成一行二进制字符串
let info = "";
let st;
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
st = matrix[i][j];
if (st === 1) {
st = "01";
} else if (st === 2) {
st = "10";
} else {
st = "00";
}
info += st;
}
}
return info;
},
要将二进制转成64进制显示,需要先定义好64进制显示的字符串,我这里定义的是:0-9,a-z,A-Z,+,/,然后就是将每6位(64是2的6次方)二进制,转换成1位64进制。具体转换代码如下:
const BASE64 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/";
compressBase64(info) {
// 将二进制字符串转换成64进制字符串
let bin, idx, base64 = "";
for (let i = 0; i < info.length; i += 6) {
bin = info.substr(i, 6);
idx = parseInt(bin, 2);
if (isNaN(idx) || idx >= BASE64.length) {
console.error("error input binary");
return;
}
base64 += BASE64[idx];
}
return base64;
},
这样转换下来:block * 2 /6 = block/3,可以将字符串压缩到原来的 1/3 长度。
类似上面将字符串转换成二进制字符串,把整数0,1,2转成一个整数的2位 ,比如1,2,0,其二进制为01,10和00,合并成一个数即为 011000,就是整数24。要想用整数的每2位表示一个状态码,需要了解 JavaScript 中的位操作和数字的表示。
JavaScript Number 遵守 IEEE 754 规范。一个数占 64 bit = 1bit 符号位 + 11bit 指数 + 52bit 尾数,因此位运算最大可利用 52bit。但是所有按位运算都以 32 位二进制数执行。在执行位运算之前,JavaScript 将数字转换为 32 位有符号整数。执行按位操作后,结果将转换回 64 位 JavaScript 数。
那么状态码为 0, 1, 2,可用 2bit 表示,一个 32bit 整数可表示 16 个状态码。可以将数组元素个数压缩到原来的 1/16,听起来就很诱人。
compressNumber(matrix) {
// 初始化新数组
let matrix_new = [];
let len = Math.floor(matrix.length * matrix[0].length / VALID_BIT);
for (let idx_new = 0; idx_new < len; idx_new++) {
matrix_new[idx_new] = 0;
}
// 用一个 32bit 整数表示 16 个状态码
let idx, idx_new, bit;
for (let x = 0; x < matrix.length; x++) {
for (let y = 0; y < matrix[x].length; y++) {
idx = x * matrix[x].length + y;
idx_new = Math.floor(idx / VALID_BIT);
bit = idx % VALID_BIT * 2;
matrix_new[idx_new] = matrix_new[idx_new] | matrix[x][y] << bit;
}
}
return matrix_new;
},
压缩之后的内容如下:
压缩前后对比:
我尝试了以上几种压缩方式,仅供参考,具体使用哪种,可以酌情考虑。
每次重新打开都需要全部重新编辑,不能显示上次编辑的内容,这明现是不合理的,因此需要从之前导出的内容中恢复数据。因为我这里的是最后一种压缩方式,只实现了一种恢复方式。
// 从压缩内容中获取该坐标的原始值
getOriVal(info, x, y) {
// info 是文件中json的数据,xy 为格子坐标
let cnt_h = Math.ceil(info.size[1]/info.cell[1]);
let idx = x * cnt_h + y;
let idx_new = Math.floor(idx / VALID_BIT);
let st_new = info.block[idx_new];
let bit = idx % VALID_BIT * 2;
let st = st_new >> bit & 3
return st;
},
地图尺寸一般是屏幕尺寸的几本甚至几十倍上百倍,若一次性加载耗费时间很长,游戏在进入场景之前会卡很久,后续还需要加一个切图功能,将地图切成小尺寸的很多块,然后逐块加载。网页比较难实现切图,可能会考虑其他工具。做好之后,放在下一篇中分享出来。