UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

267 lines (264 loc) 10.3 kB
import { Debug } from '../../core/debug.js'; import { EventHandler } from '../../core/event-handler.js'; import { SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE } from './constants.js'; /** * @import { AppBase } from '../app-base.js' * @import { Entity } from '../entity.js' */ /** * The `Script` class is the fundamental base class for all scripts within PlayCanvas. It provides * the minimal interface required for a script to be compatible with both the Engine and the * Editor. * * At its core, a script is simply a collection of methods that are called at various points in the * Engine's lifecycle. These methods are: * * - `Script#initialize` - Called once when the script is initialized. * - `Script#postInitialize` - Called once after all scripts have been initialized. * - `Script#update` - Called every frame, if the script is enabled. * - `Script#postUpdate` - Called every frame, after all scripts have been updated. * - `Script#swap` - Called when a script is redefined. * * These methods are entirely optional, but provide a useful way to manage the lifecycle of a * script and perform any necessary setup and cleanup. * * Below is a simple example of a script that rotates an entity every frame. * @example * ```javascript * import { Script } from 'playcanvas'; * * export class Rotator extends Script { * static scriptName = 'rotator'; * * update(dt) { * this.entity.rotateLocal(0, 1, 0); * } * } * ``` * * When this script is attached to an entity, the update will be called every frame, slowly * rotating the entity around the Y-axis. * * For more information on how to create scripts, see the [Scripting Overview](https://developer.playcanvas.com/user-manual/scripting/). * * @category Script */ class Script extends EventHandler { static{ /** * Fired when a script instance becomes enabled. * * @event * @example * export class PlayerController extends Script { * static scriptName = 'playerController'; * initialize() { * this.on('enable', () => { * // Script Instance is now enabled * }); * } * }; */ this.EVENT_ENABLE = 'enable'; } static{ /** * Fired when a script instance becomes disabled. * * @event * @example * export class PlayerController extends Script { * static scriptName = 'playerController'; * initialize() { * this.on('disable', () => { * // Script Instance is now disabled * }); * } * }; */ this.EVENT_DISABLE = 'disable'; } static{ /** * Fired when a script instance changes state to enabled or disabled. The handler is passed a * boolean parameter that states whether the script instance is now enabled or disabled. * * @event * @example * export class PlayerController extends Script { * static scriptName = 'playerController'; * initialize() { * this.on('state', (enabled) => { * console.log(`Script Instance is now ${enabled ? 'enabled' : 'disabled'}`); * }); * } * }; */ this.EVENT_STATE = 'state'; } static{ /** * Fired when a script instance is destroyed and removed from component. * * @event * @example * export class PlayerController extends Script { * static scriptName = 'playerController'; * initialize() { * this.on('destroy', () => { * // no longer part of the entity * // this is a good place to clean up allocated resources used by the script * }); * } * }; */ this.EVENT_DESTROY = 'destroy'; } static{ /** * Fired when script attributes have changed. This event is available in two forms. They are as * follows: * * 1. `attr` - Fired for any attribute change. The handler is passed the name of the attribute * that changed, the value of the attribute before the change and the value of the attribute * after the change. * 2. `attr:[name]` - Fired for a specific attribute change. The handler is passed the value of * the attribute before the change and the value of the attribute after the change. * * @event * @example * export class PlayerController extends Script { * static scriptName = 'playerController'; * initialize() { * this.on('attr', (name, newValue, oldValue) => { * console.log(`Attribute '${name}' changed from '${oldValue}' to '${newValue}'`); * }); * } * }; * @example * export class PlayerController extends Script { * static scriptName = 'playerController'; * initialize() { * this.on('attr:speed', (newValue, oldValue) => { * console.log(`Attribute 'speed' changed from '${oldValue}' to '${newValue}'`); * }); * } * }; */ this.EVENT_ATTR = 'attr'; } static{ /** * Fired when a script instance had an exception. The script instance will be automatically * disabled. The handler is passed an Error object containing the details of the * exception and the name of the method that threw the exception. * * @event * @example * export class PlayerController extends Script { * static scriptName = 'playerController'; * initialize() { * this.on('error', (err, method) => { * // caught an exception * console.log(err.stack); * }); * } * }; */ this.EVENT_ERROR = 'error'; } /** * Create a new Script instance. * * @param {object} args - The input arguments object. * @param {AppBase} args.app - The {@link AppBase} that is running the script. * @param {Entity} args.entity - The {@link Entity} that the script is attached to. */ constructor(args){ super(); this.initScript(args); } /** * True if the instance of this script is in running state. False when script is not running, * because the Entity or any of its parents are disabled or the {@link ScriptComponent} is * disabled or the Script Instance is disabled. When disabled, no update methods will be called * on each tick. `initialize` and `postInitialize` methods will run once when the script * instance is in the `enabled` state during an app tick. * * @type {boolean} */ set enabled(value) { this._enabled = !!value; if (this.enabled === this._enabledOld) return; this._enabledOld = this.enabled; this.fire(this.enabled ? 'enable' : 'disable'); this.fire('state', this.enabled); // initialize script if not initialized yet and script is enabled if (!this._initialized && this.enabled) { this._initialized = true; this.fire('preInitialize'); if (this.initialize) { this.entity.script._scriptMethod(this, SCRIPT_INITIALIZE); } } // post initialize script if not post initialized yet and still enabled // (initialize might have disabled the script so check this.enabled again) // Warning: Do not do this if the script component is currently being enabled // because in this case post initialize must be called after all the scripts // in the script component have been initialized first if (this._initialized && !this._postInitialized && this.enabled && !this.entity.script._beingEnabled) { this._postInitialized = true; if (this.postInitialize) { this.entity.script._scriptMethod(this, SCRIPT_POST_INITIALIZE); } } } get enabled() { return this._enabled && !this._destroyed && this.entity.script.enabled && this.entity.enabled; } /** * @typedef {object} ScriptInitializationArgs * @property {boolean} [enabled] - True if the script instance is in running state. * @property {AppBase} app - The {@link AppBase} that is running the script. * @property {Entity} entity - The {@link Entity} that the script is attached to. */ /** * @param {ScriptInitializationArgs} args - The input arguments object. * @protected */ initScript(args) { const script = this.constructor; // get script type, i.e. function (class) Debug.assert(args && args.app && args.entity, `script [${script.__name}] has missing arguments in constructor`); this.app = args.app; this.entity = args.entity; this._enabled = typeof args.enabled === 'boolean' ? args.enabled : true; this._enabledOld = this.enabled; this.__destroyed = false; this.__scriptType = script; this.__executionOrder = -1; } static{ /** * Name of a Script Type. * * @type {string} * @private */ this.__name = null; // Will be assigned when calling createScript or registerScript. } static{ /** * @param {*} constructorFn - The constructor function of the script type. * @returns {string} The script name. * @private */ this.__getScriptName = getScriptName; } /** * Name of a Script Type. * * @type {string|null} */ static get scriptName() { return this.__name; } } // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/no-useless-escape const funcNameRegex = /^\s*function(?:\s|\s*\/\*.*\*\/\s*)+([^(\s\/]*)\s*/; /** * @param {Function} constructorFn - The constructor function of the script type. * @returns {string|undefined} The script name. */ function getScriptName(constructorFn) { if (typeof constructorFn !== 'function') return undefined; if (constructorFn.scriptName) return constructorFn.scriptName; if ('name' in Function.prototype) return constructorFn.name; if (constructorFn === Function || constructorFn === Function.prototype.constructor) return 'Function'; const match = `${constructorFn}`.match(funcNameRegex); return match ? match[1] : undefined; } export { Script, getScriptName };