Cocos Creator 3.2: Custom Build Template

Auf der Suche nach einem Framework für die Umsetzung der PC-Version des Brettspieles Penta Game mit Web-Technologien bin ich vor kurzem auf das chinesische Cocos Creator gestoßen. Die aktuelle Version ist 3.2.0 und deutsche Quellen zum Thema sind leider noch äußerst rar. Daher nun an dieser Stelle ein paar höchst persönliche Eindrücke:

Wenn man den Beschreibungen auf der Homepage folgt, ist das Cocos Framework mit einem großen Funktionsumfang gesegnet, welcher im Detail auf Englisch hier nachgesehen werden kann:

Aber davon sind lediglich folgende Eigenschaften für mich ausschlaggebend.

  • WYSIWYG Scene Editor bietet eine sehr übersichtliche und intuitiv zu erfassende, komponenten- basierte Entwicklung und Asset Management. Das offiziell nur Windows Installer vorliegen, war für mich kein Problem.
  • Als Build Ziel: Web-Desktop bekommt man ohne viel Nachdenken eine Web-Anwendung und darüber hinaus (von mir bislang ungetestet) weitere Optionen in Richtung Native.
  • Die Programmierung erfolgt aufbauend auf Standard Web-Technologien vorzugsweise in Type Script oder aber auch Java Script. Dabei bildet NodeJS wohl das Basis Framework für den Creator und alle Meta-Informationen und Konfigurationen werden im JSON-Format gespeichert.
  • Zudem liefert der Cocos Creator eine problemlose Anbindung an Visual Studio Code als Code Editor.
  • Und mit der MIT Lizenz, wird uns eine überaus Entwickler- freundliche Weiter-Verwertung zugestanden.

Nicht ganz so gut gefallen hat mir:

  • Für die Nutzung des Cocos Creator Dash Boards wird ein Online Account in China benötigt, welchen zu administrieren aber trotz voreingestellter Chinesischer Sprache Dank DeepL Online Übersetzer problemlos möglich war. Es musste ja nur die Sprache auf Englisch gestellt werden. :-)
  • Wohl den recht kurzen Entwicklungs- Zyklen geschuldet ist, dass im Regelfall online zu findende Programmier- Beispiele früherer Versionen etwas aufwändiger auf 3.2.x zu übertragen sind.

Alles in Allem aber habe ich als Neueinsteiger in der Web-Anwendungs und Spiele-Entwicklung sehr von den Vorarbeiten der Cocos-Engine und Cocos-Framework Entwickler profitieren können. Allen voran waren folgende Punkte sehr hilfreich:

  • Organisation der (GUI) Knoten Elemente als Baum.
  • WYSWYG und Drag/Drop Eigenschaften Editor für Knoten und Komponenten.
  • Event-basierte Interaktionen zwischen den verschiedenen Knoten Typen.
  • Relative Positionierung von Kinder Knoten in Bezug zum Eltern Element.
  • Vorgegebene Game-Loop.
  • Intuitive Life Cycle Callback Funktionen.
  • Leicht erweiterbarer Build-Prozess durch Benutzer- definierte Build Scripte.

Soviel also nun zu den Vorzügen des Cocos Creator. Von einer näheren Nutzung des erweiterbaren Build-Prozesses möchte ich Euch aber jetzt anfangen zu berichten.

Custom Build Template

Als Anwendungsfall dient mir der Wunsch nach einem Label innerhalb der Web-Anwendung, welches aus dem JSON-Inhalt der Asset-Datei app.json die 2 Schlüssel-Werte „buildID“ und „versionID“ ausliest, um seinen Inhalt Text daraus zu bilden. Hier ein aktuelles Beispiel für die

app.json:
 1
 2
 3
 4
{
    "buildID": 45,
    "versionID": "0.1.0"
}

Der gewünschte Label-Text wäre dann: „v.0.1.0.045“. Und das dazu passende Komponenten Type Script könnte so aussehen:

lblVersionInfo.ts:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { _decorator, Component, Label, JsonAsset } from 'cc';
const { ccclass, property } = _decorator;

export class AppInfo{
    buildID: number = 0;
    versionID: string = "0.0.0";
}

@ccclass('LblVersionInfo')
export class LblVersionInfo extends Component {
    @property(JsonAsset)
    appJSON: JsonAsset|null = null;

    start () {
        const lblComp: Label|null = this.node.getComponent(Label);
        if (lblComp){
            const appInfo: AppInfo = ( this.appJSON && this.appJSON.json 
                                     ? <AppInfo>this.appJSON.json 
                                     : new AppInfo() );
            lblComp.string = "v." + appInfo.versionID.toString() + "." + 
                             appInfo.buildID.toString().padStart(3, "0");
        } else 
            console.log( "LblVersionInfo.start: invalid label component:", 
                         lblComp);
    }
}

Es soll an der Stelle nicht weiter kommentiert werden. – Abgesehen vielleicht davon, dass durch die Definition von:

11
12
    @property(JsonAsset)
    appJSON: JsonAsset|null = null;

.. der Komponente 'LblVersionInfo' per Drag & Drop direkt im Cocos Creator das app.json JSON-Asset zugeordnet werden kann. So macht komponieren Spaß :-)

Aber was nützt es, wenn die buildID nicht automatisch bei jedem Build inkrementiert wird?

Wenig. Also muss ein eigenes Build-Script her, welches genau dies für uns übernimmt. Und das ist entsprechend folgender Quelle auch gar nicht so schwierig.

https://docs.cocos.com/creator/3.1/manual/en/editor/publish/custom-build-plugin.html

  1. Über [Project / Generate Builder Extension] erstellt man eine neue Build Erweiterung
  2. Führt npm install aus dem extension Verzeichnis heraus aus
  3. Und aktiviert über den Extension Manager die neue Erweiterung. (Bei mir: [lokal])

Leider bleiben die Versuche [refresh] und [deaktivieren/aktivieren] der Extension erfolglos, weshalb für jede Änderung die IDE geschlossen werden muss. Zurück zum Dash Board und Projekt erneut öffnen löst das Problem aber.

Ein Blick in die Datei- und Ordnerstruktur zeigt dann jedoch anhand der schieren Unmenge an Zusatz Modulen, dass es so eigentlich nicht bleiben kann. Und so habe ich für mich und hoffentlich auch für Euch hilfreich das Ganze zu einem deutlich kleineren Template wie folgt zusammen gekürzt:

Ordner Struktur:
└───custom-cocos-build-template     
             │   package-lock.json                // NodeJS Paket Definition
             │   package.json                     // Template Paket Definition
             │   readme.md                        // Dokumentation Dummy
             │
             └───dist              
                     builder.js                   // Builder Definition
                     hooks.js                     // Build Event Call Backs
package-lock.json:
  • Wird durch npm install erstellt.
package.json: 
  • Legt unter contributions.builder das Build Script fest.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    "name": "custom-cocos-build-template",
    "title": "custom-cocos-build-template",
    "package_version": 2,
    "version": "1.0.0",
    "author": "Mike Ziebeck",
    "description": "custom-cocos-build-template",
    "contributions": {
        "builder": "./dist/builder.js"
    }
}
builder.js:
  • Initialisiert die Builder Hooks load und unload zunächst mit void 0 [3] und legt dann mit hooks [6] das Script: "./hooks" für deren eigentliche Implementierung fest.
 1
 2
 3
 4
 5
 6
 7
 8
"use strict";
Object.defineProperty(exports, "__esModule", { value: !0 }),
    (exports.configs = exports.unload = exports.load = void 0),
    (exports.configs = {
        "*": {
            hooks: "./hooks",
        },
    });
hooks.js:
  • Stellt nun die eigentliche Funktionalität her. Neben den Hooks, wird hier auch eine minifizierte __awaiter Funktion [4-34] definiert, die ich aber für uns mit Hilfe der Vorlage aus: CocosDashboard_1.0.11\resources\.editors\Creator\3.2.0\resources\app.asar.unpacked\node_modules\v-unzip\dist\index.js wieder aussagekräftig zurück entwickelt habe. Was sie im Detail tut, soll an anderer Stelle näher erläutert werden. Eventuell ja sogar von Dir geneigtem Leser. ;-)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
"use strict";

var fs = require("fs");
var __awaiter =
    (this && this.__awaiter) ||
    function (thisArg, _arguments, P, generator) {
        return new (P || (P = Promise))(function (resolve, reject) {
            function fulfilled(value) {
                try {
                    step(generator.next(value));
                } catch (e) {
                    reject(e);
                }
            }
            function rejected(value) {
                try {
                    step(generator.throw(value));
                } catch (e) {
                    reject(e);
                }
            }
            function step(result) {
                result.done
                    ? resolve(result.value)
                    : (result.value instanceof P
                        ? result.value
                        : new P(function (resolve) {
                            resolve(result.value);
                        })
                    ).then(fulfilled, rejected);
            }
            step((generator = generator.apply(thisArg, _arguments || [])).next());
        });
    };

Object.defineProperty(exports, "__esModule", { value: !0 }),
(exports.unload =
 exports.onAfterBuild =
 exports.onAfterCompressSettings =
 exports.onBeforeCompressSettings =
 exports.onBeforeBuild =
 exports.load =
 exports.throwError =
 void 0);

const PACKAGE_NAME = "custom-cocos-build-template";

function load() {
    return __awaiter(this, void 0, void 0, function* () {
        console.debug(`[${PACKAGE_NAME}] Load cocos plugin example.`);
    });
}

function onBeforeBuild(e) {
    return __awaiter(this, void 0, void 0, function* () {
        const url = "db://assets/app.json";
        var asset = yield Editor.Message.request("asset-db", "query-asset-info", 
                                                 url);
        var file = fs.readFileSync(asset.file);
        var json = JSON.parse(file);

        json.buildID += 1;
        fs.writeFileSync(asset.file, JSON.stringify(json, null, 4));
        yield Editor.Message.request("asset-db", "refresh-asset", url);
        console.debug(`${PACKAGE_NAME} onBeforeBuild: buildID: ${json.buildID}`);
    });
}

function onBeforeCompressSettings(e, o) {
    return __awaiter(this, void 0, void 0, function* () {
        console.debug(`${PACKAGE_NAME} onBeforeCompressSettings`);
    });
}

function onAfterCompressSettings(e, o) {
    return __awaiter(this, void 0, void 0, function* () {
        console.debug(`${PACKAGE_NAME} onAfterCompressSettings`);
    });
}

function onAfterBuild(e, o) {
    return __awaiter(this, void 0, void 0, function* () {
        console.debug(`${PACKAGE_NAME} onAfterBuild`);
    });
}

function unload() {
    return __awaiter(this, void 0, void 0, function* () {
        console.debug(`[${PACKAGE_NAME}] Unload cocos plugin example.`);
    });
}

(exports.throwError = !0),
(exports.load = void 0),
(exports.onBeforeBuild = onBeforeBuild),
(exports.onBeforeCompressSettings = void 0),
(exports.onAfterCompressSettings = void 0),
(exports.onAfterBuild = void 0),
(exports.unload = void 0);
  • Die für uns interessanten Zeilen sind:
    • [3]:           FileSystem Bibliothek importieren.
    • [54-66]:   onBeforeBuild(e) Hook implementieren.
    • [95]:         onBeforeBuild Hook festlegen.

 


Und als Sahnehäubchen oben drauf, möchte ich Euch noch von nachfolgender Zeile Quell-Code berichten.

63
        yield Editor.Message.request("asset-db", "refresh-asset", url);

Dieses kleine, aber äußerst wichtige Code-Fragment sorgt dafür, dass die Änderungen, die wir im onBeforeBuild(e) Hook am app.json JSON-Asset vornehmen, auch tatsächlich in den Build-Prozess integriert werden. Ohne - hinkt nämlich die per Build-Prozess erzeugte Version genau einen Schritt der eigentlich aktuellen - hinterher.


Leider ist die Dokumentation diesbezüglich aber ganz dünn besetzt. Sodass man nicht umhin kommt, direkt in den Cocos Creator Dateien nach: "refresh-asset" zu suchen.

Und unter: CocosDashboard_1.0.11\resources\.editors\Creator\3.2.0\resources fündig geworden, gilt es nun die vom Framework Electron erzeugte app.asar via:

npx asar extract app.asar a

zu entpacken.

Hier findet sich dann auch unter a/package.json die Quelle unserer asset-db Nachricht. Und man bekommt einen Eindruck von den weiteren Möglichkeiten, die Asset Datenbank von Cocos programmatisch anzusprechen. Für weiteres Forschen in die Richtung habe ich einmal ein paar mögliche Varianten aufgeführt und mit .. mögliche Parameter frei gelassen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
yield Editor.Message.request( "asset-db", "asset-db:asset-add" ..);
yield Editor.Message.request( "asset-db", "asset-db:asset-change" ..);
yield Editor.Message.request( "asset-db", "asset-db:asset-delete" ..);
yield Editor.Message.request( "asset-db", "asset-db:close" ..);
yield Editor.Message.request( "asset-db", "asset-db:ready" ..);
yield Editor.Message.request( "asset-db", "copy-asset" ..);
yield Editor.Message.request( "asset-db", "create-asset" ..);
yield Editor.Message.request( "asset-db", "create-asset-dialog" ..);
yield Editor.Message.request( "asset-db", "delete-asset" ..);
yield Editor.Message.request( "asset-db", "execute-script" ..);
yield Editor.Message.request( "asset-db", "generate-available-url" ..);
yield Editor.Message.request( "asset-db", "import-asset" ..);
yield Editor.Message.request( "asset-db", "init-asset" ..);
yield Editor.Message.request( "asset-db", "move-asset" ..);
yield Editor.Message.request( "asset-db", "notice-reload-editor" ..);
yield Editor.Message.request( "asset-db", "open-asset" ..);
yield Editor.Message.request( "asset-db", "open-devtools" ..);
yield Editor.Message.request( "asset-db", "query-all-asset-types" ..);
yield Editor.Message.request( "asset-db", "query-all-importer" ..);
yield Editor.Message.request( "asset-db", "query-asset-info" ..);
yield Editor.Message.request( "asset-db", "query-asset-meta" ..);
yield Editor.Message.request( "asset-db", "query-asset-mtime" ..);
yield Editor.Message.request( "asset-db", "query-assets" ..);
yield Editor.Message.request( "asset-db", "query-db-info" ..);
yield Editor.Message.request( "asset-db", "query-db-list" ..);
yield Editor.Message.request( "asset-db", "query-path" ..);
yield Editor.Message.request( "asset-db", "query-ready" ..);
yield Editor.Message.request( "asset-db", "query-url" ..);
yield Editor.Message.request( "asset-db", "query-uuid" ..);
yield Editor.Message.request( "asset-db", "refresh" ..);
yield Editor.Message.request( "asset-db", "refresh-asset" ..);
yield Editor.Message.request( "asset-db", "refresh-default-user-data-config" ..);
yield Editor.Message.request( "asset-db", "reimport-asset" ..);
yield Editor.Message.request( "asset-db", "save-asset" ..);
yield Editor.Message.request( "asset-db", "save-asset-meta" ..);

Viel Spaß beim Erweitern des Build-Prozesses von Cocos Creator 3.2 !