Tutorial: A Deep Dive into Cocos Creator 3.0's Extension System

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:

  1. Main logic
  2. One or more panels
  3. 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.

  1. Modify package.json

    Change messages to methods in the extension’s main portal and panel portal file, add a contributions attribute to package.json, then forward the message.

    Next, modify the panel definition data to a new format and write it to the panels object data in package.json.

  2. 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 with promise events is no longer present in the message handler, so there is no need to call the event.reply method.

  3. 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.

  4. 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.

3 Likes