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

【本文参与征文活动】

#玄学的插件开发–前言

很多小伙伴可能都或多或少的接触过插件,也自己尝试过写写插件,但是可能会遇到很多问题:api不熟悉,不会写html/css代码,require报错,不清楚插件运行机制导致无从下手,以及插件内各种玄学问题…,导致入门就劝退大部分的人,最初我也被这些问题困扰过,那么, 我就在下面教大家如何入玄门,成为玄学插件开发的其中一员

#玄学的插件开发–作用

插件有什么用?我学会了能带来什么?这是很多人想问的,这里我可以明确的告诉大家,插件开发的价值和意义,比你想象的大得多。

例如一个自动生成脚本插件,它可以将预备好的内容写入新的脚本,可以免去大家每次的手动操作; 资源压缩插件,可以在项目编译结束后自动压缩项目内的图片,资源等; ui节点绑定插件,可以免去每次要用节点都得去添加属性在去拖节点的烦恼;

当然如果做出了自己满意的插件,也可以提交到插件商店,为自己增加一笔额外的收入又能帮助大家,何乐而不为呢?
总之———插件开发的作用就是能让我们更好的偷鸡,避免一些重复且枯燥的工作内容,省下来的时间可以做其他东西,提升工作效率

#玄学的插件开发–入门

好了,既然都知道了插件的重要性,那么我们该如何入门插件开发呢?
演示cocos版本:2.3.4

## 新建插件:

  1. 首先我们在引擎的顶部菜单栏依次点击:扩展->创建扩展插件->项目专用插件


    然后在弹出的窗口内输入插件名确认即可.

  2. 这时候我们应该会看到控制台的打印

    这就表示我们新创建的插件已经被creator成功加载了,回到顶部菜单->扩展,应该就能看见我们刚刚创建的插件了

  3. 插件创建完成了,那插件的代码在哪儿呢? 别急,打开你项目根目录下的packages文件夹,瞧瞧,它不就躺在那儿吗?接下来我们去剖析一下新建插件的代码,方便大家了解。

## 剖析代码(package.json)

  1. 首先我们打开新建插件目录下的package.json文件
{
  "name": "hello-world",
  "version": "0.0.1",
  "description": "The package template for getting started.",
  "author": "Cocos Creator",
  "main": "main.js",
  "main-menu": {
    "i18n:MAIN_MENU.package.title/hello-world/open": {
      "message": "hello-world:open"
    },
    "i18n:MAIN_MENU.package.title/hello-world/hello": {
      "message": "hello-world:say-hello"
    }
  },
  "panel": {
    "main": "panel/index.js",
    "type": "dockable",
    "title": "hello-world",
    "width": 400,
    "height": 300
  }
}

内容如上(根据引擎版本可能会有不同差异,当前2.3.4)

"name": "hello-world": 编辑器识别的插件名, 传递事件、加载/卸载插件会用到, 如下图

"version": "0.0.1":插件版本号,用于插件商店展示以及版本更新

"description": "The package template for getting started.":插件说明

"author": "Cocos Creator":插件作者

"main": "main.js"启动脚本(重要)

插件菜单数据,插件在顶部菜单栏的展示信息(重要)

"main-menu": {
    "i18n:MAIN_MENU.package.title/hello-world/open": {
      "message": "hello-world:open"
    },
    "i18n:MAIN_MENU.package.title/hello-world/hello": {
      "message": "hello-world:say-hello"
    }
}

hello-world/openhello-world/hello 和菜单栏对应

“message”: “hello-world:open”: "hello-world:open"中的hello-world为插件名,对应上面的name字段,这里 表示了该菜单的点击会发送一次message,接收对象为hello-world插件main.js中监听的open事件

插件面板数据(重要)

"panel": {
    "main": "panel/index.js",
    "type": "dockable",
    "title": "hello-world",
    "width": 400,
    "height": 300
  }

“main”: “panel/index.js”:插件面板的入口脚本
“type”: “dockable”: 面板类型
“title”: “hello-world”:面板标题
“width”: 400:面板初始宽
“height”: 300:面板初始高

到此 package.json 文件内容已经解析完毕,可参考官方文档https://docs.cocos.com/creator/2.3/manual/zh/extension/extends-main-menu.html

## 剖析代码(main.js)

'use strict';

module.exports = {
  load () {
    **加载时执行**
  },

  unload () {
    **卸载时执行**
  },

  **这里为main.js注册的IPC消息**
  messages: {
    **这里对应了package.json中的hello-world/open**
    'open' () {
      这里表示打开 hello-world 插件的面板
      Editor.Panel.open('hello-world');
    },
    **这里对应了package.json中的hello-world/hello**
    'say-hello' () {
      // 打印
      Editor.log('Hello World!');
      **这里为发送 hello-world:hello 事件到 hello-world 插件面板**
      Editor.Ipc.sendToPanel('hello-world', 'hello-world:hello');
    },
    'clicked' () {
      // 打印
      Editor.log('Button clicked!');
    }
  },
};

## 剖析代码(index.js)

// panel/index.js, this filename needs to match the one registered in package.json
Editor.Panel.extend({
  // 面板的css样式
  style: `
    :host { margin: 5px; }
    h2 { color: #f90; }
  `,

  // 面板的html代码,用于展示
  template: `
    <h2>hello-world</h2>
    <hr />
    <div>State: <span id="label">--</span></div>
    <hr />
    <ui-button id="btn">Send To Main</ui-button>
  `,

  // 视图绑定数据
  $: {
    btn: '#btn', // 对应 id="btn"的标签
    label: '#label',  // 对应 id="label"的标签
  },

  // method executed when template and styles are successfully loaded and initialized
  ready () {
    // 添加按钮回调
    this.$btn.addEventListener('confirm', () => {
      // 发送到 hello-world 插件的 main.js 脚本中监听的 clicked 事件
      Editor.Ipc.sendToMain('hello-world:clicked');
    });
  },

  // 面板注册的IPC消息,只在面板打开情况下有效
  messages: {
    'hello-world:hello' (event) {
      // 更改绑定组件的label文本
      this.$label.innerText = 'Hello!';
    }
  }
});

好了,看完上面的代码剖析,让我们来实验看看,毕竟实验得真知:
初始状态的面板

点击顶部菜单蓝中的hello后

看到了吗?点击hello后 面板的State文本改变为了Hello!并且控制台打印了Hello World!

这是因为hello菜单对应的消息是 “message”: “hello-world:say-hello” ,而在 main.js 中注册了 say-hello 的回调事件, 回调事件中的 Editor.log('Hello World!') 语句触发了打印;
Editor.Ipc.sendToPanel('hello-world', 'hello-world:hello') 语句则向hello-world插件的面板 发送了事件hello-world:hello, 而我们又在 index.js 中注册了 ‘hello-world:hello’ 事件,其中改变了面板的 label。

好了,看完新手教学,接下来就该进入正题了

#玄学的插件开发–给自己的插件加个“开门方式”(快捷键)

  • 我们只需要在菜单数据的下面加个"accelerator": "CmdOrCtrl+Shift+s"字段,即可定义快捷键启动,但要 注意不要和其他快捷键冲突

当然,加了快捷键后如果不想让这个菜单展示出来,加上图中的visible字段即可

#玄学的插件开发–分离css和html

在插件开发过程中,必然会写html和css代码,这会导致我们的 index.js 中的代码越来越臃肿,那么如何解决呢?很简单,方法是使用文件划分,把 **index.js 中的 css 和 html 代码放回他们本该在的 .css 文件和 .html 文件中, 文件划分完了,那么如何使用呢?这里需要用到编辑器自带的 fs模块 , 用于文件读写操作, 例:

const fs = require('fire-fs');

module panel {
	// css style for panel
	export const style = fs.readFileSync(Editor.url(`packages://hello-world/panel/index.css`));
	// html template for panel
	export const template = fs.readFileSync(Editor.url(`packages://hello-world/panel/index.html`));

fs.readFileSync: 同步读取文件
Editor.url: 将插件路径转为全路径
hello-world/panel/index.css: 我们分离的css文件路径
hello-world/panel/index.html: 我们分离的html文件路径

分离后可单独用浏览器运行调试,就算有不懂的也可以问问web前端或者自己查资料学习

#玄学的插件开发–想要炫酷的控件又不想学习html和css? 满足你

掌握 UI-Kit,一切控件还不是小case

  1. 可以先在编辑器顶部菜单的开发者->UI Kit Preview / UI Kit Preview Extra中预览效果后再去翻阅文档抄袭(参考)代码


文档:https://docs.cocos.com/creator/2.3/manual/zh/extension/using-ui-kit.html?h=%E6%8E%8C%E6%8F%A1%20ui%20kit

#玄学的插件开发–如何正确的require?

想当初被这个问题也困扰过,老是找不到文件。这里给大家避坑!

  1. index.js(也就是package.json中定义的面板入口脚本,可自定义)
    在index.js中,相对路径require是无效的!!,必须得用 全路径的方式require
    可参照上方使用 require(Editor.url(packages://...)); 的方式加载
    也可使用 require(${Editor.Project.path}/packages/...)); 的方式加载

  2. 非index.js脚本(main.js或者其他用户脚本)
    可以使用相对路径加载,也可以使用绝对路径加载

Editor.Project.path: 当前项目根目录

#玄学的插件开发–使用TypeScript编写插件

为何要用TS?明白人都懂,光是代码提示就已经很香了,可以避免JS中变量名重复,使用不存在变量的问题,让你插件开发效率更高,更何况还有智能代码修复。一个字:香

参照我之前写的 https://forum.cocos.org/t/muzzik-ts/98678 ,这篇帖子中我已经说过使用ts写插件的方法,这里不过多赘述,只给大家讲讲坑点:

  • 1.import 和 export 无效
  • 2.导出必须要使用 module.exports = XXX; 的方式,否则require会找不到
  • 3.-
module b {}
let a = b;
module.exports = a;

这样是不行的,必须直接使用 module.exports = b; 这样的方式

  • 4.正确导入一个ts脚本的方式是require后再使用///<reference path="xxx.ts"/>在main.ts中引用这个ts文件,这样你才能拿到这个ts脚本中的类型提示。

  • 5.如何解决TS重定义错误, 使用//@ts-ignore直接略过错误

  • 6.改为Ts后需要将packages.json中的入口脚本路径和面板入口脚本路径改为编译后的js脚本路径


#玄学的插件开发–场景“jio”本

在插件开发过程中我们不可避免的要获取部分场景的数据,或者要用场景内的API,这时候场景脚本的作用就来了:
场景脚本添加和编写参考:https://docs.cocos.com/creator/2.3/manual/zh/extension/scene-script.html?h=%E5%9C%BA%E6%99%AF%E8%84%9A%E6%9C%AC

**那么我们可以用场景脚本做什么?**老实人告诉你:

  • 1.可以使用cc.全家桶,什么骚操作都可以用它来完成:比如用获取的预制体uuid转换为路径后使用cc.loader.loadRes加载,可以获取预制体的信息、可以用cc.director.getScene获取当前场景信息

  • 2.场景脚本内可以使用require, 也可以使用cc.require

  • 3.cc.require: 根据脚本名加载assets中的同名脚本,注意脚本中使用 export 导出的方式和 module.exports不一样,注意cc.require的接收数据

  • 4.可以使用 Editor.Scene.callSceneScript(插件名, ‘事件名’);的方式来给场景脚本发送事件,传递的参数尽量使用boolean,number,string,其余类型测试发现数据不对。


#玄学的插件开发–全局变量

有时候在插件中要使用全局变量在多个脚本/插件中传递数据,这时候怎么办呢?
这里给大家看看我的方式:

//@ts-ignore
let muzzik_a: any = Editor;
muzzik_a = muzzik_a.__muzzik || (muzzik_a.__muzzik = {});
muzzik_a = muzzik_a.xxx || (muzzik_a.xxx = new global);
let global_o: global = muzzik_a;

可以直接使用Editor来存储全局变量,xxx为插件名,global是该插件的全局变量数据类型,可自定义获取方式。

#玄学的插件开发–公共代码库(重点!!)

看到这里很多小伙伴都疑惑了,插件还有公共代码库?如果 多个插件require同一个路径的脚本那么这个插件上架时还要手动修改路径和公共脚本文件位置,而分开放置根本不能做到一套修改,全部生效的效果!那我们如何创建并使用公共代码库呢?下面给大家讲解:

  • 1.首先在 项目根目录/packages目录下创建一个公共代码脚本存放的文件加,例如我的为_module
  • 2.在代码中require
    同样,我们也是先使用///<reference path="../_module/log_base.ts"/>这样的方式引入公共代码。
    然后在脚本中根据位置require公共脚本,这里最好使用相对路径,比绝对路径方便。
    //@ts-ignore const log_base = require("../../_module/log_base");
    js生成说明,如果引用了外部的ts脚本,那么tsc -p 或者 tsc -w生成的 js 路径是会变的,如下图:

例如我的生成目录是js, 如果没有引用外部脚本,那么core和panel和main.js应该生成在js目录下面, 但我们引用了外部ts脚本,那么生成目录就会成如图所示,所以需要注意下require路径是否正确

  • 3.如果公共脚本依赖另一个公共脚本,需要引用它,这样在插件的main.ts中引用log_base再编译就会自动生成instance.js了,就 不需要引用log_base后再去引用instance,而只需要引用一次 log_base

  • 4.上面的步骤完成后再 使用tsc -p 或者 tsc -w编译自己的插件 就可以自动生成公共脚本的依赖js脚本啦。


#玄学的插件开发–利用公共代码库自定义打印类

在插件开发中,我们有时候经常需要使用Editor.log打印信息,那么如果不在前面加上插件名会难以区分是哪个插件的打印信息,这时候有自定义打印类就很有用了

  • 1.在公共代码文件夹里新建一个 log_base.ts 文件

  • 2.在当前插件目录中新建一个 log.ts 文件

  • 3.这样在别的脚本require后就可直接使用log的单例打印信息了,且会自动带上你定义的插件名


#玄学的插件开发–老妈子语录

  • 1.不知道这个对象有什么接口?可以使用
    Editor.log(JSON.stringify(Object.getOwnPropertyNames(对象)));
    进行打印
  • 2.require公共脚本文件出错?记得检查有没有使用///<reference path= 引入脚本
  • 3.tsconfig.json可以按照自己需求配置

#玄学的插件开发–落幕

插件api查询:https://forum.cocos.org/t/creator-api/92605
css深入学习:https://www.runoob.com/css/css-tutorial.html
html深入学习:https://www.runoob.com/html/html-tutorial.html

32赞

厉害了厉害了

3D插件能跟着这个学么

3d的插件系统和2d的不太一样,需要根据3d的插件更改部分内容,后面3.0出来插件系统会统一,到时候就可以2/3d通用了

mark6666

赶紧收藏一波,另外给无私奉献的大佬打call 。 666

尝试引入vue 第三方ui框架(自带ui控件很多功能都没有),然后发现vue用的1.0.8,我改成2.x之后引入elementui发现不生效,不知道是不是因为shadomDOM的缘故。
有没有大佬可以指点一下?

///<reference path="" /> 这部分感觉有点乱, 看了好几遍才看懂. 直接上传个插件代码应该会清晰一点.

参照教程, 试了一下:
custom-component.zip (9.6 KB)

终端进入 custom-component 目录, 运行 tsc, 就可以生成js.

1赞

:+1: :+1: :+1:

很详细,先收藏,大佬厉害。

大佬,多个插件的执行顺序怎么控制?目前发现自己写copy sdkbox_config.json文件的脚本先自行(监听 build-finished ),然后被sdkbox 插件覆盖了

如果是自己的插件可以用消息通知,别人的插件就不行了,除非你能改源码,或者可以看看sdkbox执行前后的区别,用来对比后再做修改

厉害,mark

好帖,mark

可以可以!

66666666666666,mark!!!

mark666

厉害,好厉害

mark!

mark!