Skip to main content

ESM Scripts

ESM Scripts use modern ES Module syntax and provide the recommended way to write PlayCanvas scripts. They offer better code organization, static imports, improved bundling, and a more familiar development experience for modern JavaScript developers.

Why Choose ESM Scripts?

  • Modern JavaScript: Use ES6+ features like classes, arrow functions, and destructuring
  • Better Tooling: Enhanced IDE support with better autocomplete and error detection
  • Modular Code: Import and export functionality between scripts
  • Improved Performance: Static imports enable better bundling and dead code elimination
  • Future-Proof: Built on web standards that continue to evolve

Creating ESM Scripts

ESM scripts must have the .mjs file extension:

  1. In the Asset Panel, create a new script
  2. Name it with .mjs extension (e.g., PlayerController.mjs)
  3. The editor will provide ESM boilerplate code
import { Script } from 'playcanvas';

export class PlayerController extends Script {
static scriptName = 'playerController';

initialize() {
// Setup code here
}

update(dt) {
// Frame update code here
}
}
tip

Multiple Scripts Per File: A single .mjs file can export multiple script classes, but each must be exported to be available in the editor.

Module Imports and Exports

Share code between scripts using standard ES Module syntax:

// config.mjs - Shared configuration
export const GAME_SETTINGS = {
playerSpeed: 5,
jumpHeight: 10,
gravity: -9.8
};

export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// PlayerController.mjs - Using shared code
import { Script } from 'playcanvas';
import { GAME_SETTINGS, clamp } from './config.mjs';

export class PlayerController extends Script {
static scriptName = 'playerController';

update(dt) {
const speed = GAME_SETTINGS.playerSpeed;
// Use clamp function...
}
}

Registering Scripts (Engine-only)

In the Editor, ESM scripts are registered automatically when their .mjs asset is loaded. In an engine-only project there is no asset pipeline doing this for you, so you must register each script class with the application's ScriptRegistry yourself before you can attach it to an entity by name.

Use registerScript (or the registry's add method) to register the class:

import { registerScript } from 'playcanvas';
import { Rotator } from './rotator.mjs';

// register the class with the application's script registry
registerScript(Rotator, undefined, app);

// equivalently:
// app.scripts.add(Rotator);

The registry uses the class's static scriptName as its key. Once registered, you can add the script to any entity by that name — including procedurally, when cloning, or when instantiating templates:

const entity = new pc.Entity();
entity.addComponent('script');

// attach the script by its registered name
entity.script.create('rotator');
tip

Registering by name is what makes asynchronous and procedural workflows possible — for example dynamically import()-ing a script module at runtime and registering it, or instantiating entities from templates that reference scripts by name. If you only ever pass the class reference directly to entity.script.create(Rotator), registration is not strictly required, but name-based lookup will not be available.