Tutorial: A Deep Dive into Cocos Creator 3.0’s Extension System
Extension System
Writing extensions has been supported since the first version of Cocos Creator. But at that time, extensions didn’t have a lot of capabilities.
After 1.x and 2.x iterations, the extension system has gradually opened up a lot of functionalities, such as build extensions, scene extensions.
In 2019, we carried out a massive refactoring for Cocos Creator, and one of the top priorities was the editor’s extension capabilities. It was a difficult decision, but a step that had to be taken.
In this change, it was clarified that all functionalities are divided into extensions, which means that an extension is a functionality. The functionality calls are also simplified to communication between extensions, and hide the superfluous internal details, providing a unified functionality extension mechanism.
Structure of Extensions
Each extension is a complete functional module, which contains:
- Main logic
- One or more panels
- Registration of other functional module configurations that need to be used
They are all registered through package.json
. Let’s take a look at the format of package.json
.
{
"name": "test-extension",
"package_version": 2,
"main": ". /dist/main.js",
"panels": {},
"contributions": {}
}
name: the name of the extension, we recommend following the npm naming convention.
package_version: the system version number used, you need to fill in the fixed number 2.
main: the logical entry of the extension.
panels: the panels that need to be registered.
contributions: other functionalities used.
For more details, please refer to documentation.
Simplified Communication Mechanism
When extensions need to call each other, we provide a set of easier messaging mechanism compared to 2.x.
There are only two types of message operations between extensions, one is a one-to-one operation:
// Sending and not caring about the returned data
Editor.Message.send(extensionName, messageName, ...args);
// wait for the data to be returned after sending and pass it through promise
const result = await Editor.Message.request(extensionName, messageName, ...args);
The other one is one-to-many operations:
// send a notification to all extensions
Editor.Message.broadcast('scene:change-node', dump);
In the new messaging mechanism, we only need to care about what method in which functionality needs to be used. For example, the Inspector will go to the Scene and query the currently selected node for information.
// Send a query-node request to scene and wait for the return
const info = await Editor.Message.request('scene', 'query-node', uuid);
However, the simplification in use brings complications in the registration mechanism. We also register messages on an extension basis, and when the extension receives a message, it autonomously chooses whether it needs to be forwarded to the main portal for processing, or to a panel for processing:
{
"name": "test-extension",
"package_version": 2,
"main": ". /dist/main.js",
"panels": {
"default": {
"main": ". /dist/panels/default/index.js"
}
},
"contributions": {
"message": {
"to-main": {
"methods": ["record"]
},
"to-panel": {
"methods": ["default.record"]
}
}
}
}
When the extension receives a to-main message, it forwards it to the record
method on the methods defined in ./dist/main.js
.
When the extension receives a to-panel message, it is forwarded to the record
method on the methods defined in ./dist/panels/default/index.js
. The trigger methods are an array, and can of course be sent to multiple locations at once.
The messages that are already open in the editor can be checked via Developer → Message List.
If we want to open our messages for others to use, we can add a public
property with a value of true
when registering the message
, and it will be automatically displayed in this panel.
Unified Registration Method
We have just written the message
data in contributions
in package.json
. This means that the current extension uses the message
functionality and provides data to the message
functionality according to the message functionality’s agreed format.
Other functionalities are registered through a similar mechanism, for example, we add a top menu button:
{
"name": "test-extension",
"package_version": 2,
"main": ". /dist/main.js",
"panels": {},
"contributions": {
"message": {
"print-test-log": {
"methods": ["printTestLog"]
}
},
"menu": [
{
"path": "i18n:menu.node",
"label": "test",
"message": "print-test-log"
}
]
}
}
Once registered, a test button will be added to the node
in the top menu. When the button is pressed, a print-test-log
message will be sent, which will eventually trigger the methods.printTestLog
in /dist/main.js
.
Isn’t that a bit convoluted? Let’s understand why we need to go through such a big detour.
In the new extension system, all operations interact via message
, triggering a message
when a menu is pressed and another message
when a shortcut key is pressed.
This unifies the interaction within the whole editor, as long as we know the message content and data format, then we can call any method in any function, of course, provided that the function opens the message
interface that allows everyone to operate.
The message is equivalent to the API of each functional module, so all operations call the message directly, and the whole extension mechanism is based on contributions
and message
mechanisms.
FAQ
Q: How does the panel interact with the extension’s main portal in your own extension?
A: It is forwarded by message to itself and then to the main portal, which communicates through the message mechanism.
Q: I require the same JS script in different files, but the data inside is not shared.
A: The different portals in the extension may run in different processes and they cannot read data directly from each other, because these processes are isolated from each other. If you need to interact with data, you need to use the message mechanism.
Q: There are some electron interfaces that suddenly don’t work in a certain version.
A: We do not recommend using the electron interface directly. If there is a functionality that the editor does not encapsulate and you definitely need to use it, you can give feedback in the forum.
Because the electron interface may be deprecated or even deleted with the version upgrade, which will probably lead to the extension does not work properly.
Q: I want to write an interface like the Inspector panel, but the editor does not give more documentation about it.
A: As mentioned before, the interaction in the editor is through the message mechanism, so we can use Developer → Message Debugging Tool to capture the message of an operation in the editor and imitate sending it to achieve similar functionalities.
For example, in the figure below, after opening the debugging tool, click Start and modify the properties in the Inspector panel once, you can see that many operations are captured, among which the data modification operation is set-property.
Updating Extensions
You must be wondering, will the extensions we wrote in 2.x still work properly in 3.0?
In fact, we only need to modify it a little, and it will work.
-
Modify
package.json
Change
messages
tomethods
in the extension’s main portal and panel portal file, add acontributions
attribute topackage.json
, then forward the message.Next, modify the panel definition data to a new format and write it to the
panels
object data inpackage.json
. -
Replace Original Editor Interfaces
Search for the Editor string in the extension, compare the interface and replace it with the 3.x interface.
We can export the editor interface definition file in the 3.x editor by selecting the Export .d.ts option in the top menu. In general, all 2.x interfaces can be found in 3.x.
The biggest change is the replacement of all
callback
withpromise
events is no longer present in the message handler, so there is no need to call theevent.reply
method. -
Tidy Up the Layout
Because of some default style changes, some of the interfaces may be displayed incorrectly. We have compiled a list of examples of default UI components in the top menu Developer –> UI Components. Some of the CSS may need to be adjusted.
-
Use the New Registration Method
There are some big changes in this section, such as the hooks for building extensions. The usage in 3.x is completely different from 2.x, so a new method of registration is needed.