I've finally figured out how to use ES6 classes with PlayCanvas

I ended up writing a simple wrapper for my ES6 classes. Here is the code:

var wrapper = function(obj) {
    var instance = new obj()
    var script = pc.createScript(instance.name);
    var attributes = [];
    if(instance.attributes) {
        for (var attr in instance.attributes) {
            attributes.push(attr)
            script.attributes.add(attr, instance.attributes[attr])
        }
    }
    
    for (var prop in instance) { 
        if (prop === 'attributes' || prop === 'name' || attributes.includes(prop)) {
            // do nothing
        } else {
            script.prototype[prop] = instance[prop];
        }                
    }
}

It allows us to write our scripts like this:

wrapper(class {
    name = 'wassup';
    num = 13;
    word = 'donkey';

    attributes = {
        word: {
            type: 'string',
            default: 'donkey'
        }
    }

    initialize() {
        console.log(this);        
    }

    update(dt) {
        console.log(`the word of the day is ${this.word} and the number is ${this.num}.`)
    }
});

And if your really want to go all in with recent ES features instead of wrapping the class in the wrapper function, use a decorator:

@wrapper
class Wassup {
   ...
}

Notice the use of this in console.log()… it works.

I haven’t tested it extensively yet but so far it all seems to work as intended. I’d love to hear what you think. Let me know if you find a bug or have any ideas to improve the wrapper.

5 Likes

@neoflash Can you access this.app or this.entity in your ES6 class ?

Absolutely. And here is a slightly updated version of my wrapper to allow for static properties:

const createScript = function (obj) {
    const instance = new obj();
    const script = pc.createScript(instance.name);
    const attributes = [];

    // Add public attributes accessible in the editor
    if (instance.attributes) {
        for (let attr in instance.attributes) {
            attributes.push(attr)
            script.attributes.add(attr, instance.attributes[attr])
        }
    }
    // Add intance properties and methods to prototype
    for (let prop in instance) {
        if (prop === 'attributes' || prop === 'name' || attributes.includes(prop)) {
            // do nothing
        } else {
            script.prototype[prop] = instance[prop];
        }
    }

    // Add static properties
    for (let prop in obj) {
        script[prop] = obj[prop];
    }
}

I’ve now tested it in a variety of scripts and all seems to work perfectly. Let me know how things work for you.

1 Like

Ok, I can access instance properties and script properties now. The only problem is that I don’t know how to access static properties…:joy:

Here is my code:

export function script(ScriptConstructor) {
    return function (app) {
        let scriptInstance = new ScriptConstructor();
        let script = pc.createScript(scriptInstance.name, app);

        for (let prop in scriptInstance) {
            if (prop === 'name' || prop === 'attributes') continue;

            script.prototype[prop] = scriptInstance[prop];
        }

        for (let staticProp in ScriptConstructor) {
            if (staticProp === 'extendsFrom') continue;
            script[staticProp] = ScriptConstructor[staticProp];
        }

        return scriptInstance;
    }
}

@script
export class OrbitCamera {
    name = 'orbitCamera';

    static x = 1;

    initialize() {
        // I can not access the static properties...
        // OrbitCamera.x do not work...
    }
}

What kind of error are you getting? Have you tried testing it without your if statement in the for loop? It’s the only difference I see between your code and mine and I can access static properties just fine.

No error, it just return undefined.

for (let staticProp in ScriptConstructor) {
    if (staticProp === 'extendsFrom') continue;

    script[staticProp] = ScriptConstructor[staticProp];

    console.log(staticProp, script, script[staticProp]);
}

The console print correct staticProp and value.

initialize() {
    console.log(this);
}

And I can see the static property in __scriptType in initialize.

Note: I am using typescript instead of ES6. But I don’t think it matters.

I’m also using Typescript so no, it shouldn’t matter. I’ll take a closer look as soon as I get the chance.

Alright, I think I figured out what your problem was. It looks like you incorrectly implemented the decorator factory. The app param needs to be passed to the outer factory function and the ScriptConstructor to the inner closure function. Like so:

export function script(app) {
    return function (ScriptConstructor) {
        let scriptInstance = new ScriptConstructor();
        let script = pc.createScript(scriptInstance.name, app);

        for (let prop in scriptInstance) {
            if (prop === 'name' || prop === 'attributes') continue;

            script.prototype[prop] = scriptInstance[prop];
        }

        for (let staticProp in ScriptConstructor) {
            if (staticProp === 'extendsFrom') continue;
            script[staticProp] = ScriptConstructor[staticProp];
        }

        return scriptInstance;
    }
}

Also, when using the decorator, since it is a factory, you need to add parenthesis:

@script()
export class OrbitCamera {
    name = 'orbitCamera';

    static x = 1;

    initialize() {
        // I can not access the static properties...
        // OrbitCamera.x do not work...
    }
}

Try it and let me know if this fixes your problem.

1 Like

Because I want to create the script base on the specific app, so I write as ScriptConstructor => app => ... , so that I can use it by:

var app = new pc.Application(canvas);
var orbitCamera = new OrbitCamera(app);  // pass the specific app to the script
var cameraEntity = new pc.Entity();
cameraEntity.addComponent('script');
cameraEntity.script.create(orbitCamera.name);

This sounds fantastic – any chance that you could share your ES6 OrbitCamera class with the community?

I failed to rewrite the script by ES6 because of my usage.
If you don’t care the app parameter, you can rewrite it like @neoflash.

I finally find a workaround. Just do not use it as decorator, use it as high order function.

export function createScript(ScriptConstructor) {
    return function (app) {
        var scriptInstance = new ScriptConstructor();
        var script = pc.createScript(scriptInstance.name, app);
        // do stuffs
        return scriptInstance;
    }
}

// in orbtiCamera.js
class OrbitCamera {
   // ...
}

export default createScript(OrbitCamera);

// app.js
var app = new pc.Application();
var orbitCamera = new OrbitCamera(app);

var cameraEntity = new pc.Entity();
cameraEntity.addComponent('script');
cameraEntity.script.create(orbitCamera.name);

It works ! :smile:

1 Like

Decorator version:

export function createScript(ScriptConstructor) {
    return class extends ScriptConstructor {
        constructor(app) {
            super();
            // do stuff
        }
    }
}

@createScript
export default class OrbitCamera {
  // ....
}

Does this still work? I’m trying it and while my script is getting added to the reg and I am able to attach it using create, it never echos the console call from it’s initialize.

can u give me a link to the documentation of class please and thx

Update: this can now be done simply with https://developer.playcanvas.com/en/api/pc.html#registerScript

 class ES6Script extends pc.ScriptType {

        initialize() {
            this.fire('initialize');
        }

        postInitialize() {
            this.fire('postInitialize');
        }

        update(dt) {
            this.fire('update', dt);
        }

        postUpdate(dt) {
            this.fire('postUpdate', dt);
        }

        swap() {
            this.fire('swap');
        }
}

pc.registerScript(ES6Script);
    
ES6Script.attributes.add('attribute', { type: 'boolean', default: false });
3 Likes

Hi @Micheal_Parks and welcome,

Thanks for sharing this!