使用Grunt实现资源自动化同步

2019-09-11 17:33:28 浏览数 (2)

同步美术、策划资源是日常开发中极为频繁的事情,shawn借用Web前端的一些思想和工具,将Grunt自动化框架引入Cocos Creator项目,可以实现相对高效地将图片、动画、配置、音效等游戏资源导入到客户端工程中。

grunt介绍

在开始之前先用简单介绍一下Grunt是什么:

为什么选择使用Grunt做自动化工具,简单总结以下几点:

  1. 使用JavaScript语言,与Cocos Creator开发使用相同的语言,减少学习成本
  2. 插件丰富,6000 (本篇文章只介绍两个grunt-sync和grunt-shell)
  3. 使用json配置插件完成任务,没有太多逻辑条件,使用简单容易上手,当配置好一个模块后,其它模块可以依葫芦画瓢,策划人员也可以上手配置
  4. Grunt基于Nodejs,可以借用大量插件与npm模块实现各种复杂需求
  5. 跨平台

安装grunt与插件

首先,使用npm安装全局grunt-cli工具:

代码语言:javascript复制
>npm install grunt-cli -g

然后在项目根目录初始化npm的包管理文件package.json:

代码语言:javascript复制
>npm init

输入npm init后一路回车,然后在项目中安装grunt npm模块:

代码语言:javascript复制
>npm install grunt --save-dev

grunt只是一个自动化框架,我们这里还需要安装上面说的两个插件

代码语言:javascript复制
>npm install grunt-sync --save-dev   //文件同步插件
>npm install grunt-shell --save-dev  //shell插件

Grunt任务模块目录结构

安装好Grunt的命令行、插件后,在项目根目录创建Gruntfile.js文件,这是Grunt自动任务的入口文件。同时在根项目新建一个tools目录,用于存放各种与项目有关的工具或脚本,再添加一个grunt-task目录,用于存放具体的grunt任务配置脚本,请看下图:

上图中xxx-task.js就是各子模块的自动化任务。

Gruntfile

shawn在早期使用Grunt时,将所有任务都编写在Gruntfile.js文件,当模块越来越多,维护起来越来越困难,因此将不同模块的自动化任务独立开来,在Gruntfile.js进行统一加载和任务注册,下面看下Gruntfile文件的内容:

代码语言:javascript复制
//引入rd模块读取文件
let rd = require('rd');
//获./tools/grunt-task目录下取所有文件
let taskScripts = rd.readFileSync('./tools/grunt-task');

module.exports = (grunt) => {
    //我们这里使用了grunt-shel&grunt-sync插件
    //下面shell与sync对象分别用于收集两种任务配置
    let shell = {};
    let sync = {};

    //将grunt设置为全局变量
    global.grunt = grunt;

    //require所有任务模块,放入tasks数组
    let tasks = [];
    taskScripts.forEach((script) => {
        let task = require(script);
        if (task.init) {
            //让task对象自己填充sync和shell内容
            task.init(sync, shell);
            tasks.push(task);
        }
    });
    //配置sync、shell两大任务
    grunt.initConfig({
        sync,
        shell,
    });

    //注册grunt-shell插件,用于执行外部shell命令
    grunt.loadNpmTasks('grunt-shell');
    //注册grunt-sync插件,用于本地文件同步
    grunt.loadNpmTasks('grunt-sync');

    //注册自定义的grunt任务
    tasks.forEach(task => task.registerTask());
};

简单说明一下:

  1. 加载tools/grunt-task下所有任务脚本
  2. 为每个任务对象传入sync、shell两个任务集合对象,各任务模块在sync、shell对象中配置自己的任务内容。
  3. 使用了两个grunt插件:grunt-shell、grunt-sync
  4. 最后调用所有task.registerTask()将命令注册到grunt命令行

在命令控制台上执行grunt --help会看到我们所编写的自动化任务,下图是shawn曾经项目中创建的Grunt任务:

文件同步任务

我们现在去执行 grunt--help还是空的,还没有注册具体的任务,前面讲过,Grunt是使用插件 JSON配置的方式来创建任务,我们看一个美术资源为例:

资源仓库客户端Assets按上图所示建立对应关系,其中绿色箭头是文件同步操作,其中headIcon目录中的图片,在项目中是动态加载的,需要同步到assets/resources/game1/texture目录下,这些操作我们可以使用grunt-sycn来完成。

其中比较特别是atlas目录,里面是经过分类需要合并图集的图片,文件合并后放到客户端项目assets/game1/texture/atlas目录,这个操作shawn是借用TexturePacker命令行工具 Nodejs脚本来完成。

UI资源同步

梳理好了资源目录结构,现在我们将上流程编写成grunt同步任务,创建一个game1-task.js的文件,内容如下:

代码语言:javascript复制
let path = require('path');
//获取grunt参数,是否模拟执行,不真实复制文件
let pretend = grunt.option('pretend');
//获取资源根路径,资源路径是定义在单独的define.js文件
let { UI_ROOT_PATH,ANI_ROOT_PATH } = require('./define');
//定义模块名,也就是在assets下的目录名
const moduleName = 'game1';
//定义同步任务
const syncTask = {
    //同步UI任务:普通图片、背景图片
    'sync-ui': {
        files: [
            //同步UI图片
            {
                //设置过滤器,排除atlas、headIcon、svn、隐藏文件
                src: ['**', '!atlas/', '!headIcon/', '!**/.*', '!**/.svn', '!**/.svn/**'],  
                dest: `./assets/${moduleName}/texture/ui`,  //目标路径,将文件同步到此处
                cwd: path.join(UI_ROOT_PATH, moduleName, 'ui')  //美术UI资源目录
            },
            //同步headIcon
            {
                //排除 atlas 目录及下面的子目录、文件
                src: ['**', '!**/.*', '!**/.svn', '!**/.svn/**'], 
                //同步到resources/moduleName/texture目录下
                dest: `./assets/resources/${moduleName}/texture`, 
                cwd: path.join(UI_ROOT_PATH, moduleName, 'ui', 'headIcon')
            },
            //同步背景图片
            {
                src: ['**', '!**/.*', '!**/.svn', '!**/.svn/**'],
                dest: `./assets/${moduleName}/texture/bg`,
                cwd: path.join(UI_ROOT_PATH, moduleName, 'bg')
            }
        ],
        verbose: true, // 显示日志
        pretend: false, // 模拟输出
        updateAndDelete: true, // 删除dst冗余文件
        compareUsing: 'md5', // 可选'mtime/md5'
        ignoreInDest: ['**/*.meta', '**/*.pac', '**/.svn/**'],  // 不删除.meta文件
    },
}

上面代码中sync-ui就是一个同步任务,其中files数组中配置同步目录,每一个数组元素包含三个字段:

  1. src: 文件过滤器
  2. dest: 目标路径,同步到那里去,以当前Gruntfile文件为相对路径
  3. cwd: 源路径,从那里去复制文件,同样以Gruntfile文件为相对路径

然后是同步选项:

  1. verbose: 打印日志输出,这对我们检查路径是否正确非常有用,建议设置为true
  2. pretend: 同步模拟,当值为真时,配合verbose使用只会显示要同步的文件,不会真实写入或删除文件目标文件
  3. updateAndDelete:删除冗余文件,比如你动随意放入一个图片到客户端ui目录,当执行ui同步资源,这个文件并未在资源仓库中,它会被同步删除掉
  4. compareUsing:文件比较策略,可选项"md5"与“mtime”,建议使用md5保证正确性
  5. ignoreInDest:指定同步时不删除那些文件,这个选项非常有用,我们都知道Cocos Creator会为每个文件生成同名.meta文件,这里一定要注意,不能被同步掉了,除了meta文件外,还有自动图集、svn等文件。下面是执行grunt-shell命令的效果:
代码语言:javascript复制
⮀ grunt up-hall --pretend
Running "sync:hall-ui" (sync) task
Copying ../../../hall/ui/hall_btn_jlb.png -> assets/hall/texture/ui/hall_btn_jlb.png
Copying ../../../hall/ui/hall_btn_kefu.png -> assets/hall/texture/ui/hall_btn_kefu.png
Copying ../../../hall/ui/hall_btn_mail.png -> assets/hall/texture/ui/hall_btn_mail.png
Copying ../../../hall/ui/hall_btn_set.png -> assets/hall/texture/ui/hall_btn_set.png
Copying ../../../hall/ui/hall_btn_share.png -> assets/hall/texture/ui/hall_btn_share.png
Copying ../../../hall/ui/hall_Btn_shop.png -> assets/hall/texture/ui/hall_Btn_shop.png
Copying ../../../hall/ui/hall_btn_yxsj.png -> assets/hall/texture/ui/hall_btn_yxsj.png
Copying ../../../hall/ui/hall_img_dianchi.png -> assets/hall/texture/ui/hall_img_dianchi.png
Copying ../../../hall/ui/hall_img_dianliang.png -> assets/hall/texture/ui/hall_img_dianliang.png
Copying ../../../hall/ui/hall_img_gold.png -> assets/hall/texture/ui/hall_img_gold.png
Copying ../../../hall/ui/hall_img_head_00.png -> assets/hall/texture/ui/hall_img_head_00.png
Copying ../../../hall/ui/hall_img_head_01.png -> assets/hall/texture/ui/hall_img_head_01.png
Copying ../../../hall/ui/hall_img_laba.png -> assets/hall/texture/ui/hall_img_laba.png
Copying ../../../hall/ui/hall_img_red.png -> assets/hall/texture/ui/hall_img_red.png
Copying ../../../hall/ui/hall_img_xinhao.png -> assets/hall/texture/ui/hall_img_xinhao.png
Copying ../../../hall/ui/hall_mask_bottom.png -> assets/hall/texture/ui/hall_mask_bottom.png
Unlinking assets/resources/hall/texture/headimg/default.png because it was removed from src.
Unlinking assets/resources/hall/texture/ui/ttz_bg_toast.png because it was removed from src.
Removing dir assets/resources/hall/texture/ui because not longer in src.

上面可以看到以Copying开头的是文件复制信息,使用verbose参数,它显示了从那儿复制文件那儿,Unlinking是删除文件,同样显示了被删除的文件路径。

动画资源同步

上面讲了UI资源的同步, 对于动画资源我们处理方式有些不同,因此需要单独创建一个同步任务:

代码语言:javascript复制
const syncTask = {
    'sync-ui': { ... }
    'sync-ani': {
         files: [
            //同步UI
            {
                src: ['**', '**/!.DS_Store', '!**/.svn', '!**/.svn/**', '!**/.gitignore'], //过滤器
                dest: `./assets/{moduleName}/animations`,
                cwd: path.join(ANI_ROOT_PATH, moduleName, 'animations')
            },
          ],
          verbose: true, // 显示日志
          pretend: pretend || false, // 模拟输出
          updateAndDelete: true, // 删除dst冗余文件
          compareUsing: 'md5', // 可选'mtime/md5'
          ignoreInDest: ['**/.svn/**', '.DS_Store', '**/.gitignore'],  // 不删除.svn下文件
    },    
    }
}

动画同步与UI同步最大的差别在于,ignoreInDes同步选项不能忽略meta文件。在shawn的项目中,动画是由美术人员在独立的Cocos Creator工程中编辑的,美术人员可以在动画工程中使用Cocos Creator动画编辑器或Spine、DragonBones等动画资源,使用Prefab进行整合,客户端主要依赖美术提供的动画prefab文件以及动画名字,动画同步任务需要将所有动画资源全部同步到客户端项目中,其中包括所有的meta文件。

这里可能会有一个小小的风险,就是动画工程中的meta文件与客户端界面中的meta文件发生UUID冲突,这种冲突的可能性是完全存在的,但在shawn一年半十多个子模块的动画项目中暂时还未遇到过,冲突的概率非常低。

图集合并同步

在UI目录中有一个类特殊的图片,需要做成图集提高游戏渲染性能,在一个游戏项目初期由于UI风格不稳定或使用临时图片,让美术同学经常去合并图集是一个效率较低的事情。因此shawn将需要合并图集的文件放入atlas的子目录中,由程序调用TexturePacker的命令行工具,以atlas子目录为单位生成图集,直接存入客户端模块atlas目录。

图集合并并完全是动态的,shawn编写了一个Node脚本,用于遍历atlas下的子目录文件,生成图集文件,然后再使用grunt-shell插件进行整合,看下面代码:

代码语言:javascript复制
//TexturePacker图集合并工具
let tpimg = require('../tpimage');
//shell任务
const shellTask = {
    //TexturePacker合并图片
    'tp-img': {
        command() {
            //工作路径,资源仓库atlas
            let cwd = path.join(UI_ROOT_PATH, `${moduleName}/ui/atlas`);
            //目标路径,图集文件保存到这里
            let dst = `./assets/${moduleName}/texture/ui/atlas`;
            //使用编写的Node脚本生成TexturePacker命令数组
            let commands = tpimg.tpdirs({ cwd, dst });

            //模拟输出命令
            if (pretend) {
                console.log(`cwd:${cwd}, dst:${dst}`);
                console.log(commands.join('n'));
                return '';
            }
            //返回命令字符串,由grunt-shell插件执行
            return commands.join('&&');
        }
    },
}

下面是任务执行时的效果:

两次执行,每一次执行时生成了icon.png、icon.plist,马上再次执行,提示未发生改变没有再重新生成图集,这比我们手动打图或使用Cocos Creator的自动图集效率要高。

资源仓库更新

上面介绍了美术UI、动画、图集等资源的同步,但一个完整的模块资源同步,还需要涉及到对资源仓库的更新,具体操作就是用git或svn将资源仓库更新到最新状态,下面看使用grunt-shell命令更新资源仓库:

代码语言:javascript复制
//svn更动画资源
'svn-ui': {
    command: [
        `svn up ${UI_ROOT_PATH}/${moduleName}/ui`,
        `svn up ${UI_ROOT_PATH}/${moduleName}/bg`
    ].json('&&')    
},

//svn更新动画资源
'svn-ani': {
    //如果使用nosvn参数,返回空字符串,跳过svn更新
    command: nosvn ? '' : `svn up ${ANI_ROOT_PATH}/${moduleName}/animation`,
}

有时会遇到svn暂时不可用,或有同学未安装svn命令行工具,会导致任务失败,可以shawn这里设置了一个nosvn的参数,用于控制是否跳过svn操作,直接返回了一个空字符串。

任务整合

一个子模块完整的资源同步任务大概需要经历下面几个步骤:

我们前面都建立的单个任务,使用grunt.registerTask可以将任意单个任务进行自由组合,看下图:

grunt.registerTask的参数包括:任务名、任务说明、子任务列表,子任务列表是一系列的任务或插件任务字符串的组合,上图中up-hall-svn任务,是由三个shell插件任务组成。

此时我们在项目根目录中,执行: >grunt up-hall即可享受到所有美术图片、动画、音效、配置等等资源的同步到我们的客户端hall模块。

我们经常会遇到这样一个场景:

美术同学:“xxx程序我增加了大厅商店道具张图片,你更新一下呢,我想看看效果”。 程序同学:“这几张图需要与策划配置文件配合才能生效,yyy策划你更新下商店配置”。 策划同学:“今天早上一来我就已经更好了,你直接取吧!”。 动画同学:“商店里几个动画特效我也更新了,你也放进去一下吧”。

程序同学打开终端,键入:grunt up-hall,屏幕一阵疯狂输闪... 1、2、3、4、5 .... 五秒过后,激活Cocos Creator窗口编译资源,观察控制台,一切ok!

程序同学:“你们使用我的IP:7456自己去看吧!” 美术、策划一脸惊讶地看着你,效率这么高...

曾经十几分钟都搞不定的事情,现在几秒就解决了,每天做10几遍都不会觉得累!也不需要做10遍,将grunt任务与Cocos Creator插件结合,嵌入到Cocos Creator界面菜单上面,让程序员多休息一会儿吧!

0 人点赞