Skip to main content
Unlisted page
This page is unlisted. Search engines will not index it, and only users having a direct link can access it.

Migration Guide

ESM Scripts replace the legacy Scripting system as the recommended way to develop PlayCanvas applications. Whilst legacy scripts will continue to be work in existing projects amd will be supported for the foreseeable future, we recommend using the newer ESM format for your projects.

Gradual Migration

Using ESM Scripts within your project is entirely optional and allows you to gradually migrate your projects over to the newer ESM based format in your own time, without affecting existing projects.

tip

Projects can contain both ESM Scripts and legacy scripts

You do not need to update all your scripts together. We recommend gradually migrating scripts and iteratively testing


Codemod

In order to migrate legacy PlayCanvas Scripts, we've provided a codemod that will automatically update your code to the newer ESM Format.

You can find the codemods in our github repository and you can run the codemod using the following command:

npx codemod playcanvas-esm-scripts

Known differences

In general ESM Scripts provide a more expressive and flexible way of creating projects. Whilst we have attempted to keep the migration process as seamless as possible, there are some note-able differences that you should bare in mind.

Module Scope

ESM Scripts have module scope, classic scripts have global scope. This means modules cannot implicitly access variables defined in other files. Often this is used as a way to define a global settings or configuration. The config has a higher loading order than the script, and so the SPEED is accessible globally.

See code example
// config.js
var SPEED = 10;

// script.js
// ❌ This will not work. `SPEED` is scoped to config.js
console.log(SPEED)

This is a hidden dependency which breaks if the loading order changes. Instead, use import/export syntax to explicitly define the dependency.

// config.mjs
export const SPEED = 10

// script.mjs
import { SPEED } from './config.mjs';
// ✅ Works!
console.log(SPEED);

You can learn more about the other difference between ES Modules and standard scripts here

Loading Order

note

ESM Script do not have a loading order.

The loading order of scripts was introduced as a way to organize dependencies between scripts and guarantee certain code would execute before others. With ES modules, these relationships can be explicitly defined through import/export syntax. As such ESM Scripts do not have an loading order and they should not be relied upon to load in a certain way. Instead we encourage you to use the import/export to set up dependencies.

The new Script class

With ESM Scripts the new Script base class replaces the existing ScriptType class as the default base class. The Script class represents the minimal set of features necessary, but omits a couple of features present in the original ScriptType class.

It's worth noting that although Script is now the default base class, it's still possible to use ScriptType as the base class (internally ScriptType extends Script), however we do not recommend doing this for ESM Scripts, due to some of the reasons listed below.

Attribute Events

note

ESM Script do not fire Attribute Events.

The Script class does not support attributes events in the format of attr:[name]. The reason behind removing this is that internally the engine would override class members, and in practice this would create difficult to debug scenarios as it's not fully compatible with ES6 class syntax.

Instead you can define your own events around class attribute members using something like the following;

See code example
const watch = (target, prop) => {
const privateProp = `#{prop}`;
target[privateProp] = target[prop];

Object.defineProperty(target, prop, {
set(value) {
if (target[privateProp] !== value) {
target.fire(`changed:${prop}`, value);
target[privateProp] = value;
}
},
get() {
return this[privateProp];
}
});
}

import { Script } from 'playcanvas'
export class Rotate extends Script {
/** attribute */
speed = 10;

initialize() {
watch(this, 'speed');

this.on('changed:speed', console.log)
}
}

This also means you can have events for any class members too, not only script attributes.

Attribute Copying

note

ESM Script Attributes are not copied, they are passed by reference.

Attributes are no longer copied, they are passed by reference. The reasons this was changed was also due a bug in ScriptType that was incompatible with ES6 classes.

Instead, if you do need to copy values, we recommend you do it manually and explicitly via getters and setters. Whilst this is more verbose, it's clear and explicit

See code example
import { Script, Vec3 } from 'playcanvas';
class Rotate extends Script {

_speed = new Vec3();

set speed(value) {
this._speed.copy(value)
}

get speed() {
return this_.speed'
}
}