系列教程:

  1. 地图编辑器开发(一)加载地图
  2. 地图编辑器开发(二)编辑地图
  3. 地图编辑器开发(三)测试阻挡
  4. 地图编辑器开发(四)导出阻挡信息
  5. 地图编辑器开发(五)导出地图资源

上一节篇尾提到,地图要切成小图便于加载,但地图编辑器是个网页版工具,要在网页上实现切图不是很方便,经过考虑之后,决定使用 nodejs 实现。

Electron

要做一个图形化的切图界面,加上要能使用 nodejs,最先想到的就是 Electron。Electron 是一个 nodejs 的扩展库,可以使用 JavaScript,HTML 和 CSS 构建跨平台的桌面应用程序。若之前写过网页,上手比较简单。

界面完全基于 HTML 和 CSS 实现,切图只需两个简单功能,选择图片和生成切图,生成包含地图块和缩小图片,小图用于提前显示和作小地图。

下面是界面的代码:

<!DOCTYPE html>
<html>
<head>
    <title>MapCutter</title>
    <meta charset="UTF-8">
    <link href="css/global.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <button id="btnLoad">打开文件...</button>
    <div id="divFilepath"></div>
    <img id="imgMap" width="100%"/>
    <button id="btnCut">切图</button>
</body>
<script src="render/index.js"></script>
</html>

android地图编辑器 地图编辑器代码_android地图编辑器


主要逻辑在 index.js 中实现,代码如下:

window.onload = function onload() {
	var btnLoad = document.querySelector("#btnLoad")
    btnLoad.onclick = function () {
        dialog.showOpenDialog({
            title: "选中地图图片文件...",
            defaultPath: '*.jpg',
            filters: [{
                name: "image",
                extensions: ["jpg", "png"],
            }],
        }).then(res => {
            let filepath = res.filePaths[0];
            if (!filepath) {
                return;
            }

            var divFilepath = document.querySelector("#divFilepath")
            divFilepath.innerHTML = filepath;

            var imgMap = document.querySelector("#imgMap")
            imgMap.setAttribute("src", filepath)

            g_filePath = filepath;
        }).catch(err => {
            console.error(err);
        });
    }

	var btnCut = document.querySelector("#btnCut")
    btnCut.onclick = function () {
        if (!g_filePath) {
            return;
        }
        let fileObj = path.parse(g_filePath)
        let outDir = path.join(fileObj.dir, fileObj.name)
        if (fs.existsSync(outDir)) {
            fs.rmdirSync(outDir, { recursive: true });
        }
        fs.mkdirSync(outDir)

        let srcImg = images(g_filePath);
        let size = srcImg.size();
        let wCnt = Math.ceil(size.width / g_sizeTitle)
        let hCnt = Math.ceil(size.height / g_sizeTitle)
        let dstImg, filename, outPath, yFrom, w, h;
        for (let x = 0; x < wCnt; x++) {
        for (let y = 0; y < hCnt; y++) {
            yFrom = size.height - (y + 1) * g_sizeTitle
            if (yFrom < 0) {
                h += yFrom
                yFrom = 0;
            } else{
                h = g_sizeTitle
            }
            w = Math.min(g_sizeTitle, size.width - x * g_sizeTitle)
            dstImg = images(srcImg, x * g_sizeTitle, yFrom, w, h)
                filename = "tile_" + x + "_" + y + ".jpg"
                outPath = path.join(outDir, filename);
                dstImg.save(outPath)
            }
        }

        // 小地图
        outPath = path.join(outDir, g_filenameSmall);
        srcImg.size(g_sizeSmall).save(outPath);

        dialog.showMessageBox({
            message: "完成切图",
            buttons: ["打开目录"],
            cancelId: 1,
        }).then((res) => {
            if (res.response === 0) {
                // 自动打开切图导出目录
                shell.showItemInFolder(outDir)
            }
        });
    }
}

切图功能用到了一个 nodejs 的依赖库 images,可以实现图片简单的分割和缩放,刚好满足需求。运行效果:

android地图编辑器 地图编辑器代码_切图_02

android地图编辑器 地图编辑器代码_地图编辑器_03


android地图编辑器 地图编辑器代码_开发工具_04


android地图编辑器 地图编辑器代码_切图_05


android地图编辑器 地图编辑器代码_开发工具_06

命令行

图形化界面所见即所得,易于使用,方便非专业人员使用。但若是需要处理的地图很多,需要大量重复的操作,比较繁琐。下面是一个命令行的工具,在配置文件中,配置好需要导出的地图路径,执行导出脚本,一次性就能导出全部地图了。

const fs = require("fs")
const path = require("path")
const images = require("images");
const exec = require('child_process').exec;

// 配置文件
const config = require("./resources/config");

const g_sizeTitle = 1024;
const g_sizeSmall = 400;
const g_filenameSmall = "small.jpg";

var cutImage = function (imgpath) {
    console.log("cut file: " + imgpath);

    let fileObj = path.parse(imgpath)
    let outDir = path.join(fileObj.dir, fileObj.name)
    if (fs.existsSync(outDir)) {
        fs.rmdirSync(outDir, { recursive: true });
    }
    fs.mkdirSync(outDir)

    let srcImg = images(imgpath);
    let size = srcImg.size();
    let wCnt = Math.ceil(size.width / g_sizeTitle)
    let hCnt = Math.ceil(size.height / g_sizeTitle)
    let dstImg, filename, outPath, yFrom, w, h;
    for (let x = 0; x < wCnt; x++) {
        for (let y = 0; y < hCnt; y++) {
            yFrom = size.height - (y + 1) * g_sizeTitle
            if (yFrom < 0) {
                h += yFrom
                yFrom = 0;
            } else{
                h = g_sizeTitle
            }
            w = Math.min(g_sizeTitle, size.width - x * g_sizeTitle)
            dstImg = images(srcImg, x * g_sizeTitle, yFrom, w, h)
            filename = "tile_" + x + "_" + y + ".jpg"
            outPath = path.join(outDir, filename);
            dstImg.save(outPath)
        }
    }

    // 小地图
    outPath = path.join(outDir, g_filenameSmall);
    srcImg.size(g_sizeSmall).save(outPath);

    return outDir;
}

var main = function () {
    let outDir;
    for (let i = 0; i < config.length; i++) {
        outDir = cutImage(config[i]);
    }
    let cmd = "start explorer \"" + outDir + "\\.." + "\""
    console.log(cmd);
    exec(cmd)
}

main()

其中配置文件 config,是一个简单的 json 文件,其中保存的是需要导出地图图片的数组,格式如下:

[
    "c:\\x\\MapEditor\\assets\\resources\\map\\1001.jpg",
    "c:\\x\\MapEditor\\assets\\resources\\map\\1002.jpg"
]

插件

图形化界面和脚本都是一个单独的工具,可以单独使用。既然地图编辑器使用 cocos creator 开发的,能不能把工具也集成进来呢?前面提到,地图编辑器运行时是一个网页,不好实现,只能想办法把工具集成到 cocos creator 的编辑器之中,刚好 cocos creator 有提供扩展编辑器的接口——插件,将上面命令行工具,经过简单地修改,就可以集成到 cocos creator 编辑器中。插件的主要代码如下:

'use strict';
// 地图资源目录
const MAP_ROOT = "db://assets/resources/map"
// 切图尺寸
const g_sizeTitle = 1024;
// 小地图尺寸
const g_sizeSmall = 400;
const g_filenameSmall = "small.jpg";

var cutImage = function (imgpath) {
    const fs = require("fs")
    const path = require("path")
    const images = require("images");

    // 重新创建导出目录
    let fileObj = path.parse(imgpath)
    let outDir = path.join(fileObj.dir, fileObj.name)
    if (fs.existsSync(outDir)) {
        // 删除旧的导出目录
        fs.readdirSync(outDir).forEach((file, index) => {
            const curPath = path.join(outDir, file);
            if (fs.lstatSync(curPath).isDirectory()) { // recurse
                deleteFolderRecursive(curPath);
            } else { // delete file
                fs.unlinkSync(curPath);
            }
        });
        fs.rmdirSync(outDir);
    }
    fs.mkdirSync(outDir)

    // 切图
    let srcImg = images(imgpath);
    let size = srcImg.size();
    let wCnt = Math.ceil(size.width / g_sizeTitle)
    let hCnt = Math.ceil(size.height / g_sizeTitle)
    let dstImg, filename, outPath, yFrom, w, h;
    for (let x = 0; x < wCnt; x++) {
        for (let y = 0; y < hCnt; y++) {
            yFrom = size.height - (y + 1) * g_sizeTitle
            if (yFrom < 0) {
                h += yFrom
                yFrom = 0;
            } else{
                h = g_sizeTitle
            }
            w = Math.min(g_sizeTitle, size.width - x * g_sizeTitle)
            dstImg = images(srcImg, x * g_sizeTitle, yFrom, w, h)
            filename = "tile_" + x + "_" + y + ".jpg"
            outPath = path.join(outDir, filename);
            dstImg.save(outPath)
        }
    }

    // 小地图
    outPath = path.join(outDir, g_filenameSmall);
    srcImg.size(g_sizeSmall).save(outPath);
}

module.exports = {
    messages: {
        cut: function () {
            Editor.assetdb.queryAssets(MAP_ROOT + "/*.*", 'texture', (err, assetInfos) => {
                if (err) {
                    Editor.console.error(err.message || err);
                    return
                }

                for (let i = 0; i < assetInfos.length; ++i) {
                    Editor.log("cute url: " + assetInfos[i].url)
                    // 切图
                    cutImage(assetInfos[i].path)
                }

                // 更新导入资源
                Editor.assetdb.refresh(MAP_ROOT);
            });
        }
    },
};

切图部分的代码跟命令行版本基本上没有什么变化,主要要修改的是地图输入的方式;在命令行中,通过配置文件输入需要切的图片路径,在插件中可以免除配置文件这个过程,直接遍历地图资源目录,获取图片路径作为输入。在完成切图之后,刷新导入资源,就可以在编辑器中看到,新切除的图片。

android地图编辑器 地图编辑器代码_开发工具_07


android地图编辑器 地图编辑器代码_切图_08


这样,就把切图功能作为插件集成到编辑器中,避免来去切换工具。

后记

回顾一下地图编辑器开发的整个流程,以下相关的全部博客:

  1. 加载地图
  2. 编辑地图
  • 地图滚动
  • 地图缩放
  • 画网格
  • 编辑格子信息
  1. 测试阻挡
  • A星寻路
  • 显示路径
  • 角色移动
  1. 导出
  • 导出地图信息
  • 地图数据压缩
  • 读取地图数据
  • 导出切图

后续会考虑使用这个地图编辑器,做一个MMORPG的demo。