【从零开始的插件开发】使用 CC3.x + UI组件 + Vue3 开发一个插件

【从零开始的插件开发】使用 CC3.x + UI组件 + Vue3 开发一个插件

前言

近期发布了一个 Cocos Creator 2.4.x 的自动化插件,想移植到 3.x 版本,但 扩展编辑器 官方文档 的教程让我这种小白一言难尽,尽管我刚开发完一款 2.x 的插件,3.x 也让我折腾了很久。

首先得吐槽一下插件开发的官方文档,感觉不如 2.x 的文档,特别是 UI 组件 文档,感觉像是随便写的,虽然可以直接在 UI 组件面板 中看到代码,但示例代码也有很多坑。而 扩展改名 这篇文档更是重量级,真的会有人需要用到这篇文档吗?不过还是叠个甲,希望只是官方没有太多精力去维护插件开发的文档。

那么废话就到此为止,本篇文章算是个技术总结,毕竟我也算是从零开始开发的插件,踩了许多坑,虽然没啥技术力,但入门应该是够了。下面将一步一步教大家通过 CC3.x + UI组件 + Vue3 的形式开发一个简单的可视化插件。

为何选择 Vue3 模板开发?

Cocos Creator 3.x 支持创建 HTML 面板Vue2.x 面板Vue3.x 面板,对于有对应技术栈的开发者来说,选择对口的模板是最好的选择。但如果是一位平时只使用 Cocos Creator,不太接触其他框架的小白来说(比如我),不妨选择最新的技术栈,这样能通过学习插件开发的同时,了解下 Vue3 的基础概念(如果以后不做 Cocos 了,方便快速转行)。

当然,我也是一个 Vue3 入门的新手,对 Vue3 理解不够透彻,文章中会大量引用 Vue3 官方文档 的内容来进行解释,同时也引用了大量的 扩展编辑器 官方文档 来解释,本篇文章只能算是官方文档的补充说明。(并不是真的教官方写文档

创建扩展

首先我们通过 扩展模板与编译构建 文档中的方法来创建一个 Vue3.x 面板 扩展,我们将 扩展名 改为 extension-vue,方便后面讲解。

创建完成后,在项目工程的 extensions 文件夹中会生成这么一个目录结构:

目录结构

目录结构的详细解释,在 目录结构 中有记载,包括 package.json 的字段含义,也在同一篇文档中。

此时已经创建好了一个扩展模板,需要按照 README.zh-CN.md 中的方法来安装 npm 依赖

注意:使用 npm 需要安装 Nodejs 环境,如果你还没有安装,推荐 Windows 开发者安装 NVM for Windows 方便进行 Nodejs 的版本管理。(总有一天会需要用到切换版本的功能

安装 npm 依赖 需要打开终端来执行命令,如果你对终端的用法不了解,可以在 VSCode 中使用快捷键 Ctrl + Shift + ` 来打开,然后以此执行以下命令:


# 切换到扩展目录下

cd .\extensions\extension-vue\

# 安装 npm 依赖

npm install

然后我们回到 Cocos Creator 编辑器,在 扩展 -> 扩展管理器 -> 已安装扩展 中启用创建好的扩展。

提示:在开发插件的过程中如果遇到数据没刷新之类的问题,可以使用此方法来重新关闭开启插件,以重新加载插件。

启用插件_开启

接着就能通过 面板 -> extension-vue -> 默认面板 打开扩展模板面板,此时已经完成了扩展的创建。

使用 UI 组件布局

面板布局主要在 extension-vue/static/template/default/index.html 中进行修改,下面统一用 index.html 来代指。

如果需要添加 CSS 样式,可以在 extension-vue/static/style/default/index.css 中进行添加,下面统一用 index.css 来代指。

我们根据 UI 组件 文档的步骤,通过 开发者 -> UI 组件 来打开 UI 组件面板,这个面板里的参考代码很重要(也有很多坑),我们整个开发过程中都会用到,可以一直开着这个面板。

我们回到 index.html 中,我们先将 <my-counter></my-counter><h1 id="text"></h1> 注释掉,接下来要添加的所有 UI 组件标签都需要写在 <div id="app"></div> 标签内。

接下来我们尝试用 UI 组件拼出一个界面来。

使用 ui-prop 组件

ui-prop 组件能让可视化界面有一个整洁的排版,是很适合用来设置布局的组件。

我们先来看一下 UI 组件面板中的 layout -> ui-prop 组件示例:

先将第一个示例添加到 index.html 的 app 标签中,并且添加一个 <h1></h1> 标签来作为面板的标题。


<div>

    <div id="app">

        <!-- 所有 UI 组件写在 <div id="app"></div> 标签内 -->

        <h1>Extension Vue</h1>

        <ui-prop>

            <ui-label slot="label">Checkbox</ui-label>

            <ui-checkbox slot="content"></ui-checkbox>

        </ui-prop>

       

        <ui-prop>

            <ui-label slot="label">Name</ui-label>

            <ui-input slot="content"></ui-input>

        </ui-prop>

       

        <ui-prop>

            <ui-label slot="label">Color</ui-label>

            <ui-color slot="content"></ui-color>

        </ui-prop>

       

        <ui-prop>

            <ui-label slot="label">Select</ui-label>

            <ui-select slot="content">

                <option value="1">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sit, sint.</option>

                <option value="2">Sapiente ratione esse voluptatibus repudiandae illo numquam quas nulla id.</option>

            </ui-select>

        </ui-prop>

    </div>

</div>

然后我们通过 面板 -> extension-vue -> 默认面板 来查看面板效果:

这样看起来不太美观,标题可以居中显示,而且 ui-prop 之间得加点间距。

3.x 版本的 UI 组件没有 2.x 的 ui-box-container 组件来框柱 ui-prop,看起来也不太好看,来看看 2.x 的效果:

既然 3.x 没有,那我们就用 CSS 来实现一个,在 index.css 中添加以下样式:


/* index.css */

h1 {

    /* 文本居中 */

    text-align: center;

}

.box {

    /* 外边距 10px */

    margin: 10px;

    /* 内边距 5px */

    padding: 5px;

    /* 虚线边框 */

    border-style: dashed;

    /* 边框宽度 1px */

    border-width: 1px;

    /* 边框颜色为灰色 */

    border-color: gray;

}

ui-prop {

    /* 添加上边距 5px */

    margin-top: 5px;

}

然后我们将所有 ui-prop 组件添加到一对 <div></div> 标签中,并为 div 添加 css 中定义的 box 样式。


<!-- index.html -->

<div>

    <div id="app">

        <!-- 所有 UI 组件写在 <div id="app"></div> 标签内 -->

        <h1>Extension Vue</h1>

        <div class="box">

            <ui-prop>

                <ui-label slot="label">Checkbox</ui-label>

                <ui-checkbox slot="content"></ui-checkbox>

            </ui-prop>

           

            <ui-prop>

                <ui-label slot="label">Name</ui-label>

                <ui-input slot="content"></ui-input>

            </ui-prop>

           

            <ui-prop>

                <ui-label slot="label">Color</ui-label>

                <ui-color slot="content"></ui-color>

            </ui-prop>

           

            <ui-prop>

                <ui-label slot="label">Select</ui-label>

                <ui-select slot="content">

                    <option value="1">Lorem ipsum dolor sit amet consectetur, adipisicing elit. Sit, sint.</option>

                    <option value="2">Sapiente ratione esse voluptatibus repudiandae illo numquam quas nulla id.</option>

                </ui-select>

            </ui-prop>

        </div>

    </div>

</div>

这样,我们的面板就变得整洁了起来:

注意:编写好 html 后,需要关闭面板重新打开才能看到修改后的内容。

使用 ui-prop 时需要注意 slot 的用法,slot="label" 的组件会放到 ui-prop 左边的显示区域,slot="content" 则是显示在右边的显示区域,如果在 ui-prop 标签对中添加元素而不使用 slot 属性的话,就会被另起一行显示。

如果我们想在 Checkbox 的 ui-prop 中再添加一个 ui-checkbox 元素,且不添加 slot 属性的话,就会变成如下的效果:


<!-- index.html -->

...

<ui-prop>

    <ui-label slot="label">Checkbox</ui-label>

    <ui-checkbox slot="content"></ui-checkbox>

    <ui-checkbox></ui-checkbox>

</ui-prop>

...

所以正确的做法应该是:


<!-- index.html -->

...

<ui-prop>

    <ui-label slot="label">Checkbox</ui-label>

    <ui-checkbox slot="content"></ui-checkbox>

    <ui-checkbox slot="content"></ui-checkbox>

</ui-prop>

...

吐槽:UI 组件面板中的示例代码很好,但是详细解释太少了,要么就应该像 2.x 一样在官方文档中详细写出每个属性的用法。

至此,我们已经尝试使用 UI 组件、HTML 原生标签、CSS 样式来完成基本的面板布局,接下来就可以靠搭积木的方式打造自己的扩展面板了。

如果对 HTML 和 CSS 不熟悉,或者已经忘得差不多了,可以到 W3school 官网 找你想要的内容。

使用 Vue3 的特性

我们来做一个根据扩展名来统计文件数量的扩展。

编写逻辑代码

先打开 extension-vue/src/panels/default/index.ts,这是面板的入口文件。

在模板代码中,我们主要修改 ready 函数中的内容,ready 函数是面板启动后触发的钩子函数。

ready 函数体中,有如下的代码:


import { createApp } from 'vue';

// ...

module.exports = Editor.Panel.define({

    $: {

        app: '#app',

    },

    ready() {

        const app = createApp({});

        app.mount(this.$.app);

    }

})

这段代码的意思就是创建一个 Vue App 挂载到 #app 标签上,就是为什么我们在 index.html 中需要把布局写到 <div id="app"></div> 标签对中的原因。

在这篇演示中,我们的逻辑代码主要写在 createApp({}) 中。

首先我们定义两个数据,用于存放每个扩展名对应的文件数量和文件的总数量:


createApp({

    data() {

        return {

            /** 扩展名对应的文件数量 */

            fileCount: {},

            /** 文件总数量 */

            fileNum: 0

        }

    }

})

接着我们来写主要的逻辑,将写在 methods: {} 中,data 中定义的数据我们可以通过 this.xxx 取到。


import { readdirSync, readFileSync, statSync } from 'fs-extra';

import { extname, join } from 'path';

// ...

createApp({

    data() {

        return {

            /** 扩展名对应的文件数量 */

            fileCount: {},

            /** 文件总数量 */

            fileNum: 0

        }

    },

    methods: {

        checkAssets() {

            // 定义两个数据的临时变量

            let fileCount: {[key: string]: number} = {};

            let fileNum = 0;

            // 获取 assets 文件夹的路径

            let assetsPath = join(Editor.Project.path, "assets");

            let checkDir = (dirPath: string) => {

                // 获取文件夹下所有的文件名

                let files = readdirSync(dirPath);

                // 遍历文件名

                files.forEach((fileName) => {

                    // 当前文件的路径

                    let subPath = join(dirPath, fileName);

                    // 文件的信息

                    let stat = statSync(subPath);

                    if (stat.isDirectory()) {

                        // 如果文件是个目录,递归查找

                        checkDir(subPath);

                    }

                    else if (stat.isFile()) {

                        // 获取文件的扩展名

                        let extName = extname(fileName);

                        if (extName != "" && !fileCount[extName]) {

                            fileCount[extName] = 1;

                        }

                        else if (extName != "" && fileCount[extName]) {

                            fileCount[extName] += 1;

                        }

                        else {

                            if (fileCount["other"]) {

                                fileCount["other"] += 1;

                            }

                            else {

                                fileCount["other"] = 1;

                            }

                        }

                        fileNum++;

                    }

                })

            }

            checkDir(assetsPath);

            // 将临时变量赋值给 data 中定义的变量

            this.fileCount = fileCount;

            this.fileNum = fileNum;

        }

    }

})

代码中我们用到了 Editor.Project.path 接口,用来获取项目目录路径,可以查看 Editor API 说明 文档来查询需要的功能。

我们这段逻辑需要在面板打开时就执行,所以我们需要用到 生命周期钩子 中的 beforeMount 生命周期:


// ...

createApp({

    data() {

        return {

            /** 扩展名对应的文件数量 */

            fileCount: {},

            /** 文件总数量 */

            fileNum: 0

        }

    },

    methods: {

        checkAssets() {

            // ...

        }

    },

    // 挂载应用之前

    beforeMount() {

        this.checkAssets();

    }

})

这样,我们的逻辑代码就写好了。

重要:TS 代码需要通过 tsc 命令编译成 JS 代码才能在扩展中生效,可以在终端中执行命令 npm run build,来编译 TS 脚本,或者执行 npm run watch 来监听 TS 脚本的变化,自动编译。

使用 Vue3 来布局

我们要对 index.html 进行重新布局,先删除掉之前的 UI 组件示例代码,我们重新编写一套:


<!-- index.html -->

<div>

    <div id="app">

        <h1>Extension Vue</h1>

        <div class="box">

            <ui-prop v-for="(value, key) in fileCount">

                <ui-label slot="label">{{ key }}</ui-label>

                <ui-input

                    slot="content"

                    readonly

                    :value="value"

                >

                    <span slot="suffix">个文件</span>

                </ui-input>

            </ui-prop>

            <ui-prop>

                <ui-label slot="label">总计</ui-label>

                <ui-input

                    slot="content"

                    readonly

                    :value="fileNum"

                >

                    <span slot="suffix">个文件</span>

                </ui-input>

            </ui-prop>

        </div>

    </div>

</div>

这里我们用到了 列表渲染 中的 v-for 来显示出所有扩展名对应的文件数量。

:valuev-bind:value 的简写,详情可参考 Attribute 绑定,通过这个方法,我们将 data 中定义的两个值绑定到 ui-input 标签的 value 属性上。

这样,我们基本算是完成了这个插件的主要功能,让我们通过 面板 -> extension-vue -> 默认面板 打开面板看看效果吧:

看来 Cocos Creator 3.7.3 的 Hello World 模板项目的 Assets 文件夹下有 58 个文件。

来点交互

我们这个扩展有个问题,他只能在打开面板的时候统计文件数量,如果我们在不关闭面板的情况下增删 Assets 目录中的文件,面板上的数据就不会刷新,我们为面板加一个按钮来手动刷新数据。


<!-- index.html -->

<div>

    <div id="app">

        <h1>Extension Vue</h1>

        <div class="box">

            <!-- ... -->

        </div>

        <div style="text-align: center;">

            <ui-button type="primary" @confirm="checkAssets">

                <ui-icon value="refresh"></ui-icon>

                刷新

            </ui-button>

        </div>

    </div>

</div>

这里我们用到了 事件处理,使用 @confirm 绑定了 checkAssets 函数,UI 组件支持的事件我们可以在 UI 组件面板 中查看:

其中 Events 就是该组件支持的事件名,示例代码也给出了 Vue 的写法。

吐槽:Vue 示例代码中应该是用 methods: {},不要将 Vue 示例代码原封不动的 Copy 到脚本里,会变得不幸。

在这段代码中,我们将 ui-button 也装进了一个 div 中,我们可以直接在标签中添加 style 属性更改标签的样式,但如果 style 内容较多,建议还是写在 index.css 中。

我们关闭面板,重新打开看看效果:

仍然是 58 个文件。(废话

不要关闭面板,我们在编辑器的资源管理器中添加一个名为 script 的文件夹,并创建一个 TypeScript 脚本文件,叫 HelloWorld

资源管理器

这时,我们点击 extension-vue 面板中的 刷新 按钮。

我们发现多了一个 .ts 类型文件,但是总计比之前多了 3 个,另外两个文件分别是 script.metaHelloWorld.ts.meta 文件,这是编辑器自动生成的文件。

至此,你已经尝试了 HTMLCSSUI 组件Vue3 列表渲染Vue3 事件处理Vue3 生命周期Editor 接口调用 等内容,相信你对 Cocos 扩展已经有了初步的了解。如果仍有疑问,请积极回帖探讨,我是个插件开发的萌新,本篇文章只是个人的经验总结,如果能让你入门插件开发,那是我的荣幸。

插件开发时,请灵活运用相关的文档:

吐槽:官方文档里是有 多语言系统(i18n) 文档的,但是这篇文档的入口被隐藏在了 基础功能 页面中,侧边栏找不到这个 i18n 文档的入口,而且 基础功能 文档,在侧边栏是叫 扩展功能详解,真的迷惑……

彩蛋

在扩展根目录中放入一个 PNG 格式的图片资源,命名为 logo.png,就能在扩展管理器中显示 LOGO 啦。

总结

插件开发的意义在于实现自动化,可以解放部分重复的工作,降低重复工作导致的人工犯错几率,作为程序员,就应该使用程序来帮助自己提高效率(这样才能更好的摸鱼)。

官方在插件开发的文档方面,从 2.x 开始一直都不太完善,感觉都是徒有文档,入门还是得自己摸索,希望有朝一日能有更完善的插件开发文档。

本篇内容中编写好的插件已经开源到 GitHub 仓库,需要的话请移步仓库查看。

另外,如果本篇文章并不能让你入门插件开发,论坛中也有其他大佬写的开发指南,可以参考他们的帖子:

[muzzik 教程]:3.x 插件开发指南

【muzzik教程】:插件开发之道

手把手教你如何使用 rollup + vue单文件 工作流开发 3.x 插件

【插件开发】升级Vue2.x+Element-ui+组件式开发Creator2.x

3赞

别用 vue 了,直接上 creator + Nodejs + html 混合开发,爽飞,端午写个使用文档(插件模板在商店 :yum:

4赞

好,等大佬的文档 :grinning:

大佬,文章中的自动化插件实现了啥功能呀~
有商店链接吗?

文章里面实现的是一个统计 assets 文件夹下,不同扩展名文件数量的,在最后 总结 部分有贴 github 链接。

如果你是说文章中的 garbage-maker 插件的话,商店链接是这个:Cocos Store

了解,看名字我还以为是实现一些自动化的流程操作,最近有相关的需求,没有的话就得自己动手了 :rofl:

朋友,问个问题,怎么直接在里面加载css样式,我用npm装了个插件,然后css样式不对。我都是到对应的插件里找到css样式,拷贝到index.css文件里去的

我一开始想用 element plus 来着,组件是导入了,但是 css 没能导入,按照 muzzik 大佬的方法也没成功,你可以试试 muzzik 大佬的方法 [提问]:3.x插件怎么引用element-ui组件?

https://github.com/1226085293/cc-plugin-template

插件模板,可以自己参考,我一直在用这个模板