地图编辑器开发(四)

前一节把测试功能加上了,地图信息编辑好,测试通过之后,需要导出到游戏中。这节来看数据的导出。主要包括以下几个功能点:

  • 导出地图信息
  • 地图数据压缩
  • 读取地图数据

导出地图信息

数据内容

在导出地图信息之后,把数据格式定义好,必备数据有两个:阻挡遮罩信息和格子尺寸,其他的数据可以按需添加。

getExportData() {
    return {
        size: [this.oriW, this.oriH],
        cell: [this.CELL_W, this.CELL_H],
        block: this.blockInfoList,
    };
},

size 保存的整个地图的尺寸,block时阻挡和遮罩信息,是一个二维数组

导出

在工具栏添加一个导出按钮,点击之后导出数据,如图:
toolbar
导出的格式我这里用的 json,用通用数据格式方便数据的交换。
cocos creator 打包之后是跨平台的,不同的平台数据导出方式有很大差异;因为平时都是用浏览器开发调试,因此只做了在浏览器里面的导出。基本思路是模拟点击链接下载文件,流程:

  1. 动态创建一个隐藏的链接
  2. 自动调用a标签点击方法
  3. 设置下载数据
  4. 点击之后移除A标签
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。
地图编辑器开发(四)_第1张图片
地图编辑器开发(四)_第2张图片
地图编辑器开发(四)_第3张图片
导出之后可以看到,数据30k+,挺大的,其中主要是格子的状态的二维数组,按图上的尺寸,总共有:7920/80 * 7160 / 40 = 17721 个数组元素,需要进行压缩。

地图数据压缩

数据的压缩我试过好几种方式,都介绍一下。总的来看有两种思路:第一种是将数据转化成字符串,然后在对字符串尽兴压缩;第二种是压缩二维数组的元素。

压缩字符串

转成成字符串

在进行字符串压缩之前,先把二维数组转换成字符串。将二维数组转成一维数组,然后将每个元素都变成字符串。

compressString(matrix) {
     // 将状态转换成一行制字符串
     let info = "";
     for (let i = 0; i < matrix.length; i++) {
         info += matrix[i].join("");
     }
     return info;
 },

压缩出来的数据大致是这样的结构:
地图编辑器开发(四)_第4张图片
然后就是要对这个字符串进行压缩。

合并连续相同字符

这里面有很多重复的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;
},

这种方式适合阻挡和遮罩都很少时,比如什么都不编辑,可以得到最大程度的压缩:
str_comb
这种情况过于理想,统计了正式地图中字符出现的比例,大概是: “0”:“1”:“2” = 2:2:1,因此并不是很适合。

64进制压缩

转换成二进制

字符串中有很多的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进制显示,需要先定义好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 长度。

base64

压缩数组元素

类似上面将字符串转换成二进制字符串,把整数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;
},

压缩之后的内容如下:
bit32
压缩前后对比:

地图编辑器开发(四)_第5张图片
我尝试了以上几种压缩方式,仅供参考,具体使用哪种,可以酌情考虑。

读取地图数据

每次重新打开都需要全部重新编辑,不能显示上次编辑的内容,这明现是不合理的,因此需要从之前导出的内容中恢复数据。因为我这里的是最后一种压缩方式,只实现了一种恢复方式。

// 从压缩内容中获取该坐标的原始值
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;
},

地图尺寸一般是屏幕尺寸的几本甚至几十倍上百倍,若一次性加载耗费时间很长,游戏在进入场景之前会卡很久,后续还需要加一个切图功能,将地图切成小尺寸的很多块,然后逐块加载。网页比较难实现切图,可能会考虑其他工具。做好之后,放在下一篇中分享出来。

你可能感兴趣的:(游戏开发,开发工具)