Repository: playcanvas/engine
Pull request #6237 (2024-04-10 13:35:05)

Add Script alias
Having some discussions with @willeastcott, with the new ESM Script, we want to keep the default script boilerplate as clean as possible. This PR exports an alias for `ScriptType` named `Script`

```javascript
import { Script } from 'playcanvas';

class Rotator extends Script {
  initialize() {
    ...
  }
}
```

UPDATE:
ScriptType has been added to the deprecation.js file
Improved dicunmentation for Script to include ESM example


I confirm I have read the [contributing guidelines](https://github.com/playcanvas/engine/blob/master/.github/CONTRIBUTING.md) and signed the [Contributor License Agreement](https://docs.google.com/a/playcanvas.com/forms/d/1Ih69zQfJG-QDLIEpHr6CsaAs6fPORNOVnMv5nuo0cjk/viewform).


kpal81xd: What was the reason for this?
willeastcott: > Should we rename ScriptType to Script in that module, and export it as ScriptType for compatibility?

Yeah, because otherwise, the API ref will continue to show `ScriptType`. The aliasing should be done in the backwards compatibility module.
marklundin: @willeastcott are we happy to simply rename the class and keep the `script-type.js` filename?
mvaligursky: Rename filename as well, we keep those in sync.
willeastcott: Maybe do:

* `script.js` -> `script-create.js` (or `script-register.js`)
* `script-type.js` -> `script.js`

willeastcott: BTW, we should consider deprecating and hiding `registerScript`.
mvaligursky: We also need to consider the impact in the Editor. Will the Editor be able to still use at least few previous engine versions? Do we need to add some type checking to it or similar? Maybe it all just works, but we need to check.
marklundin: > * `script.js` -> `script-create.js` (or `script-register.js`)
> * `script-type.js` -> `script.js`

Makes sense, although strictly speaking this would be a breaking change right?

> We also need to consider the impact in the Editor. Will the Editor be able to still use at least few previous engine versions? Do we need to add some type checking to it or similar? Maybe it all just works, but we need to check.

Yeah it should be fine, editor only uses ScriptType when parsing and `pc.Script === pc.ScriptType`


marklundin: Actually maybe not a breaking change since we haven't yet [published the ESM build ](https://www.npmjs.com/package/playcanvas?activeTab=code). Mainly thinking about `import { Script } from 'playcanvas/framework/script/script-type.js'
marklundin: This has not been updated

- Renamed script.js -> script-create.js
- Renamed script-type.js -> script.js
- Updated jsdoc references of {ScriptType} => {Script}

Verified with type docs...
<img width="951" alt="image" src="https://github.com/playcanvas/engine/assets/430764/6c08c208-9fc5-426b-ab35-a29a18f63d7a">

*Note that now ScriptType does not exist, it does not generate a corresponding url. It would be great to retain the original, to indicated to users that it has been renamed
marklundin: jsdocs update

<img width="986" alt="image" src="https://github.com/playcanvas/engine/assets/430764/46e6f766-41a1-48a9-a56d-09d4d24458f7">
marklundin: Any more updates on this @willeastcott? Keen to get this merged

Patch:
diff --git a/src/deprecated/deprecated.js b/src/deprecated/deprecated.js
index 1ad88648e..e2af6dcd4 100644
--- a/src/deprecated/deprecated.js
+++ b/src/deprecated/deprecated.js
@@ -118,6 +118,9 @@ import { RigidBodyComponentSystem } from '../framework/components/rigid-body/sys
 import { basisInitialize } from '../framework/handlers/basis.js';
 import { LitShader } from '../scene/shader-lib/programs/lit-shader.js';
 
+// ScriptType alias
+export { Script as ScriptType } from '../framework/script/script.js';
+
 // CORE
 export const LINEBATCH_WORLD = 0;
 export const LINEBATCH_OVERLAY = 1;
diff --git a/src/framework/components/script/component.js b/src/framework/components/script/component.js
index c182759bf..cc918e721 100644
--- a/src/framework/components/script/component.js
+++ b/src/framework/components/script/component.js
@@ -19,7 +19,7 @@ import { Entity } from '../../entity.js';
  */
 class ScriptComponent extends Component {
     /**
-     * Fired when a {@link ScriptType} instance is created and attached to the script component.
+     * Fired when a {@link Script} instance is created and attached to the script component.
      * This event is available in two forms. They are as follows:
      *
      * 1. `create` - Fired when a script instance is created. The name of the script type and the
@@ -40,7 +40,7 @@ class ScriptComponent extends Component {
     static EVENT_CREATE = 'create';
 
     /**
-     * Fired when a {@link ScriptType} instance is destroyed and removed from the script component.
+     * Fired when a {@link Script} instance is destroyed and removed from the script component.
      * This event is available in two forms. They are as follows:
      *
      * 1. `destroy` - Fired when a script instance is destroyed. The name of the script type and
@@ -109,7 +109,7 @@ class ScriptComponent extends Component {
     static EVENT_STATE = 'state';
 
     /**
-     * Fired when the index of a {@link ScriptType} instance is changed in the script component.
+     * Fired when the index of a {@link Script} instance is changed in the script component.
      * This event is available in two forms. They are as follows:
      *
      * 1. `move` - Fired when a script instance is moved. The name of the script type, the script
@@ -130,7 +130,7 @@ class ScriptComponent extends Component {
     static EVENT_MOVE = 'move';
 
     /**
-     * Fired when a {@link ScriptType} instance had an exception. The handler is passed the script
+     * Fired when a {@link Script} instance had an exception. The handler is passed the script
      * instance, the exception and the method name that the exception originated from.
      *
      * @event
@@ -154,7 +154,7 @@ class ScriptComponent extends Component {
         /**
          * Holds all script instances for this component.
          *
-         * @type {import('../../script/script-type.js').ScriptType[]}
+         * @type {import('../../script/script.js').Script[]}
          * @private
          */
         this._scripts = [];
@@ -191,7 +191,7 @@ class ScriptComponent extends Component {
      * An array of all script instances attached to an entity. This array is read-only and should
      * not be modified by developer.
      *
-     * @type {import('../../script/script-type.js').ScriptType[]}
+     * @type {import('../../script/script.js').Script[]}
      */
     set scripts(value) {
         this._scriptsData = value;
@@ -546,8 +546,8 @@ class ScriptComponent extends Component {
     /**
      * Detect if script is attached to an entity.
      *
-     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
-     * name or type of {@link ScriptType}.
+     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
+     * name or type of {@link Script}.
      * @returns {boolean} If script is attached to an entity.
      * @example
      * if (entity.script.has('playerController')) {
@@ -570,9 +570,9 @@ class ScriptComponent extends Component {
     /**
      * Get a script instance (if attached).
      *
-     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
-     * name or type of {@link ScriptType}.
-     * @returns {import('../../script/script-type.js').ScriptType|null} If script is attached, the
+     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
+     * name or type of {@link Script}.
+     * @returns {import('../../script/script.js').Script|null} If script is attached, the
      * instance is returned. Otherwise null is returned.
      * @example
      * const controller = entity.script.get('playerController');
@@ -594,8 +594,8 @@ class ScriptComponent extends Component {
     /**
      * Create a script instance and attach to an entity script component.
      *
-     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
-     * name or type of {@link ScriptType}.
+     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
+     * name or type of {@link Script}.
      * @param {object} [args] - Object with arguments for a script.
      * @param {boolean} [args.enabled] - If script instance is enabled after creation. Defaults to
      * true.
@@ -605,9 +605,9 @@ class ScriptComponent extends Component {
      * script and attributes must be initialized manually. Defaults to false.
      * @param {number} [args.ind] - The index where to insert the script instance at. Defaults to
      * -1, which means append it at the end.
-     * @returns {import('../../script/script-type.js').ScriptType|null} Returns an instance of a
-     * {@link ScriptType} if successfully attached to an entity, or null if it failed because a
-     * script with a same name has already been added or if the {@link ScriptType} cannot be found
+     * @returns {import('../../script/script.js').Script|null} Returns an instance of a
+     * {@link Script} if successfully attached to an entity, or null if it failed because a
+     * script with a same name has already been added or if the {@link Script} cannot be found
      * by name in the {@link ScriptRegistry}.
      * @example
      * entity.script.create('playerController', {
@@ -699,8 +699,8 @@ class ScriptComponent extends Component {
     /**
      * Destroy the script instance that is attached to an entity.
      *
-     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
-     * name or type of {@link ScriptType}.
+     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
+     * name or type of {@link Script}.
      * @returns {boolean} If it was successfully destroyed.
      * @example
      * entity.script.destroy('playerController');
@@ -756,8 +756,8 @@ class ScriptComponent extends Component {
     /**
      * Swap the script instance.
      *
-     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
-     * name or type of {@link ScriptType}.
+     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
+     * name or type of {@link Script}.
      * @returns {boolean} If it was successfully swapped.
      * @private
      */
@@ -927,8 +927,8 @@ class ScriptComponent extends Component {
     /**
      * Move script instance to different position to alter update order of scripts within entity.
      *
-     * @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
-     * name or type of {@link ScriptType}.
+     * @param {string|typeof import('../../script/script.js').Script} nameOrType - The
+     * name or type of {@link Script}.
      * @param {number} ind - New position index.
      * @returns {boolean} If it was successfully moved.
      * @example
diff --git a/src/framework/entity.js b/src/framework/entity.js
index 08b808127..e4ca2cb4b 100644
--- a/src/framework/entity.js
+++ b/src/framework/entity.js
@@ -400,8 +400,8 @@ class Entity extends GraphNode {
     /**
      * Search the entity and all of its descendants for the first script instance of specified type.
      *
-     * @param {string|typeof import('./script/script-type.js').ScriptType} nameOrType - The name or type of {@link ScriptType}.
-     * @returns {import('./script/script-type.js').ScriptType|undefined} A script instance of specified type, if the entity or any of its descendants
+     * @param {string|typeof import('./script/script.js').Script} nameOrType - The name or type of {@link Script}.
+     * @returns {import('./script/script.js').Script|undefined} A script instance of specified type, if the entity or any of its descendants
      * has one. Returns undefined otherwise.
      * @example
      * // Get the first found "playerController" instance in the hierarchy tree that starts with this entity
@@ -415,8 +415,8 @@ class Entity extends GraphNode {
     /**
      * Search the entity and all of its descendants for all script instances of specified type.
      *
-     * @param {string|typeof import('./script/script-type.js').ScriptType} nameOrType - The name or type of {@link ScriptType}.
-     * @returns {import('./script/script-type.js').ScriptType[]} All script instances of specified type in the entity or any of its
+     * @param {string|typeof import('./script/script.js').Script} nameOrType - The name or type of {@link Script}.
+     * @returns {import('./script/script.js').Script[]} All script instances of specified type in the entity or any of its
      * descendants. Returns empty array if none found.
      * @example
      * // Get all "playerController" instances in the hierarchy tree that starts with this entity
diff --git a/src/framework/handlers/script.js b/src/framework/handlers/script.js
index d77f0f71f..c63caab4e 100644
--- a/src/framework/handlers/script.js
+++ b/src/framework/handlers/script.js
@@ -1,8 +1,8 @@
 import { platform } from '../../core/platform.js';
 import { script } from '../script.js';
-import { ScriptType } from '../script/script-type.js';
+import { Script } from '../script/script.js';
 import { ScriptTypes } from '../script/script-types.js';
-import { registerScript } from '../script/script.js';
+import { registerScript } from '../script/script-create.js';
 import { ResourceLoader } from './loader.js';
 
 import { ResourceHandler } from './handler.js';
@@ -154,7 +154,7 @@ class ScriptHandler extends ResourceHandler {
 
             for (const key in module) {
                 const scriptClass = module[key];
-                const extendsScriptType = scriptClass.prototype instanceof ScriptType;
+                const extendsScriptType = scriptClass.prototype instanceof Script;
 
                 if (extendsScriptType) {
 
diff --git a/src/framework/script/script-attributes.js b/src/framework/script/script-attributes.js
index 0d0dc1d90..7a718b401 100644
--- a/src/framework/script/script-attributes.js
+++ b/src/framework/script/script-attributes.js
@@ -150,8 +150,8 @@ function rawToValue(app, args, value, old) {
 
 /**
  * Container of Script Attribute definitions. Implements an interface to add/remove attributes and
- * store their definition for a {@link ScriptType}. Note: An instance of ScriptAttributes is
- * created automatically by each {@link ScriptType}.
+ * store their definition for a {@link Script}. Note: An instance of ScriptAttributes is
+ * created automatically by each {@link Script}.
  *
  * @category Script
  */
@@ -159,7 +159,7 @@ class ScriptAttributes {
     /**
      * Create a new ScriptAttributes instance.
      *
-     * @param {typeof import('./script-type.js').ScriptType} scriptType - Script Type that attributes relate to.
+     * @param {typeof import('./script.js').Script} scriptType - Script Type that attributes relate to.
      */
     constructor(scriptType) {
         this.scriptType = scriptType;
diff --git a/src/framework/script/script-create.js b/src/framework/script/script-create.js
new file mode 100644
index 000000000..cd31cf49d
--- /dev/null
+++ b/src/framework/script/script-create.js
@@ -0,0 +1,156 @@
+import { Debug } from '../../core/debug.js';
+import { EventHandler } from '../../core/event-handler.js';
+
+import { script } from '../script.js';
+import { AppBase } from '../app-base.js';
+
+import { ScriptAttributes } from './script-attributes.js';
+import { Script } from './script.js';
+import { ScriptTypes } from './script-types.js';
+
+const reservedScriptNames = new Set([
+    'system', 'entity', 'create', 'destroy', 'swap', 'move', 'data',
+    'scripts', '_scripts', '_scriptsIndex', '_scriptsData',
+    'enabled', '_oldState', 'onEnable', 'onDisable', 'onPostStateChange',
+    '_onSetEnabled', '_checkState', '_onBeforeRemove',
+    '_onInitializeAttributes', '_onInitialize', '_onPostInitialize',
+    '_onUpdate', '_onPostUpdate',
+    '_callbacks', '_callbackActive', 'has', 'get', 'on', 'off', 'fire', 'once', 'hasEvent'
+]);
+
+function getReservedScriptNames() {
+    return reservedScriptNames;
+}
+
+/**
+ * Create and register a new {@link Script}. It returns new class type (constructor function),
+ * which is auto-registered to {@link ScriptRegistry} using its name. This is the main interface to
+ * create Script Types, to define custom logic using JavaScript, that is used to create interaction
+ * for entities.
+ *
+ * @param {string} name - Unique Name of a Script Type. If a Script Type with the same name has
+ * already been registered and the new one has a `swap` method defined in its prototype, then it
+ * will perform hot swapping of existing Script Instances on entities using this new Script Type.
+ * Note: There is a reserved list of names that cannot be used, such as list below as well as some
+ * starting from `_` (underscore): system, entity, create, destroy, swap, move, scripts, onEnable,
+ * onDisable, onPostStateChange, has, on, off, fire, once, hasEvent.
+ * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
+ * to add a script to. By default it will use `Application.getApplication()` to get current
+ * {@link AppBase}.
+ * @returns {typeof Script|null} A class type (constructor function) that inherits {@link Script},
+ * which the developer is meant to further extend by adding attributes and prototype methods.
+ * Returns null if there was an error.
+ * @example
+ * var Turning = pc.createScript('turn');
+ *
+ * // define 'speed' attribute that is available in Editor UI
+ * Turning.attributes.add('speed', {
+ *     type: 'number',
+ *     default: 180,
+ *     placeholder: 'deg/s'
+ * });
+ *
+ * // runs every tick
+ * Turning.prototype.update = function (dt) {
+ *     this.entity.rotate(0, this.speed * dt, 0);
+ * };
+ * @category Script
+ */
+function createScript(name, app) {
+    if (script.legacy) {
+        Debug.error('This project is using the legacy script system. You cannot call pc.createScript().');
+        return null;
+    }
+
+    if (reservedScriptNames.has(name))
+        throw new Error(`Script name '${name}' is reserved, please rename the script`);
+
+    const scriptType = function (args) {
+        EventHandler.prototype.initEventHandler.call(this);
+        Script.prototype.initScriptType.call(this, args);
+    };
+
+    scriptType.prototype = Object.create(Script.prototype);
+    scriptType.prototype.constructor = scriptType;
+
+    scriptType.extend = Script.extend;
+    scriptType.attributes = new ScriptAttributes(scriptType);
+
+    registerScript(scriptType, name, app);
+    return scriptType;
+}
+
+// Editor uses this - migrate to ScriptAttributes.reservedNames and delete this
+const reservedAttributes = {};
+ScriptAttributes.reservedNames.forEach((value, value2, set) => {
+    reservedAttributes[value] = 1;
+});
+createScript.reservedAttributes = reservedAttributes;
+
+/* eslint-disable jsdoc/check-examples */
+/**
+ * Register a existing class type as a Script Type to {@link ScriptRegistry}. Useful when defining
+ * a ES6 script class that extends {@link Script} (see example).
+ *
+ * @param {typeof Script} script - The existing class type (constructor function) to be
+ * registered as a Script Type. Class must extend {@link Script} (see example). Please note: A
+ * class created using {@link createScript} is auto-registered, and should therefore not be pass
+ * into {@link registerScript} (which would result in swapping out all related script instances).
+ * @param {string} [name] - Optional unique name of the Script Type. By default it will use the
+ * same name as the existing class. If a Script Type with the same name has already been registered
+ * and the new one has a `swap` method defined in its prototype, then it will perform hot swapping
+ * of existing Script Instances on entities using this new Script Type. Note: There is a reserved
+ * list of names that cannot be used, such as list below as well as some starting from `_`
+ * (underscore): system, entity, create, destroy, swap, move, scripts, onEnable, onDisable,
+ * onPostStateChange, has, on, off, fire, once, hasEvent.
+ * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
+ * to register the script type to. By default it will use `Application.getApplication()` to get
+ * current {@link AppBase}.
+ * @example
+ * // define a ES6 script class
+ * class PlayerController extends pc.Script {
+ *
+ *     initialize() {
+ *         // called once on initialize
+ *     }
+ *
+ *     update(dt) {
+ *         // called each tick
+ *     }
+ * }
+ *
+ * // register the class as a script
+ * pc.registerScript(PlayerController);
+ *
+ * // declare script attributes (Must be after pc.registerScript())
+ * PlayerController.attributes.add('attribute1', {type: 'number'});
+ * @category Script
+ */
+function registerScript(script, name, app) {
+    if (script.legacy) {
+        Debug.error('This project is using the legacy script system. You cannot call pc.registerScript().');
+        return;
+    }
+
+    if (typeof script !== 'function')
+        throw new Error(`script class: '${script}' must be a constructor function (i.e. class).`);
+
+    if (!(script.prototype instanceof Script))
+        throw new Error(`script class: '${Script.__getScriptName(script)}' does not extend pc.Script.`);
+
+    name = name || script.__name || Script.__getScriptName(script);
+
+    if (reservedScriptNames.has(name))
+        throw new Error(`script name: '${name}' is reserved, please change script name`);
+
+    script.__name = name;
+
+    // add to scripts registry
+    const registry = app ? app.scripts : AppBase.getApplication().scripts;
+    registry.add(script);
+
+    ScriptTypes.push(script, script.legacy);
+}
+/* eslint-enable jsdoc/check-examples */
+
+export { createScript, registerScript, getReservedScriptNames };
diff --git a/src/framework/script/script-registry.js b/src/framework/script/script-registry.js
index 12f08201c..78db4b531 100644
--- a/src/framework/script/script-registry.js
+++ b/src/framework/script/script-registry.js
@@ -1,7 +1,7 @@
 import { EventHandler } from '../../core/event-handler.js';
 
 /**
- * Container for all {@link ScriptType}s that are available to this application. Note that
+ * Container for all {@link Script}s that are available to this application. Note that
  * PlayCanvas scripts can access the Script Registry from inside the application with
  * {@link AppBase#scripts}.
  *
@@ -9,13 +9,13 @@ import { EventHandler } from '../../core/event-handler.js';
  */
 class ScriptRegistry extends EventHandler {
     /**
-     * @type {Object<string, typeof import('./script-type.js').ScriptType>}
+     * @type {Object<string, typeof import('./script.js').Script>}
      * @private
      */
     _scripts = {};
 
     /**
-     * @type {typeof import('./script-type.js').ScriptType[]}
+     * @type {typeof import('./script.js').Script[]}
      * @private
      */
     _list = [];
@@ -37,12 +37,12 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Add {@link ScriptType} to registry. Note: when {@link createScript} is called, it will add
-     * the {@link ScriptType} to the registry automatically. If a script already exists in
+     * Add {@link Script} to registry. Note: when {@link createScript} is called, it will add
+     * the {@link Script} to the registry automatically. If a script already exists in
      * registry, and the new script has a `swap` method defined, it will perform code hot swapping
      * automatically in async manner.
      *
-     * @param {typeof import('./script-type.js').ScriptType} script - Script Type that is created
+     * @param {typeof import('./script.js').Script} script - Script Type that is created
      * using {@link createScript}.
      * @returns {boolean} True if added for the first time or false if script already exists.
      * @example
@@ -146,10 +146,10 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Remove {@link ScriptType}.
+     * Remove {@link Script}.
      *
-     * @param {string|typeof import('./script-type.js').ScriptType} nameOrType - The name or type
-     * of {@link ScriptType}.
+     * @param {string|typeof import('./script.js').Script} nameOrType - The name or type
+     * of {@link Script}.
      * @returns {boolean} True if removed or False if already not in registry.
      * @example
      * app.scripts.remove('playerController');
@@ -178,10 +178,10 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Get {@link ScriptType} by name.
+     * Get {@link Script} by name.
      *
-     * @param {string} name - Name of a {@link ScriptType}.
-     * @returns {typeof import('./script-type.js').ScriptType} The Script Type if it exists in the
+     * @param {string} name - Name of a {@link Script}.
+     * @returns {typeof import('./script.js').Script} The Script Type if it exists in the
      * registry or null otherwise.
      * @example
      * var PlayerController = app.scripts.get('playerController');
@@ -191,11 +191,11 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Check if a {@link ScriptType} with the specified name is in the registry.
+     * Check if a {@link Script} with the specified name is in the registry.
      *
-     * @param {string|typeof import('./script-type.js').ScriptType} nameOrType - The name or type
-     * of {@link ScriptType}.
-     * @returns {boolean} True if {@link ScriptType} is in registry.
+     * @param {string|typeof import('./script.js').Script} nameOrType - The name or type
+     * of {@link Script}.
+     * @returns {boolean} True if {@link Script} is in registry.
      * @example
      * if (app.scripts.has('playerController')) {
      *     // playerController is in pc.ScriptRegistry
@@ -212,9 +212,9 @@ class ScriptRegistry extends EventHandler {
     }
 
     /**
-     * Get list of all {@link ScriptType}s from registry.
+     * Get list of all {@link Script}s from registry.
      *
-     * @returns {Array<typeof import('./script-type.js').ScriptType>} list of all {@link ScriptType}s
+     * @returns {Array<typeof import('./script.js').Script>} list of all {@link Script}s
      * in registry.
      * @example
      * // logs array of all Script Type names available in registry
diff --git a/src/framework/script/script-type.js b/src/framework/script/script-type.js
deleted file mode 100644
index 3a1ff14a3..000000000
--- a/src/framework/script/script-type.js
+++ /dev/null
@@ -1,393 +0,0 @@
-import { Debug } from '../../core/debug.js';
-import { EventHandler } from '../../core/event-handler.js';
-
-import { SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE } from './constants.js';
-import { ScriptAttributes } from './script-attributes.js';
-
-const funcNameRegex = new RegExp('^\\s*function(?:\\s|\\s*\\/\\*.*\\*\\/\\s*)+([^\\(\\s\\/]*)\\s*');
-
-/**
- * Represents the type of a script. It is returned by {@link createScript}. Also referred to as
- * Script Type.
- *
- * The type is to be extended using its JavaScript prototype. There is a list of methods that will
- * be executed by the engine on instances of this type, such as:
- *
- * - `initialize`
- * - `postInitialize`
- * - `update`
- * - `postUpdate`
- * - `swap`
- *
- * `initialize` and `postInitialize` - are called (if defined) when a script is about to run for
- * the first time - `postInitialize` will run after all `initialize` methods are executed in the
- * same tick or enabling chain of actions.
- *
- * `update` and `postUpdate` - are called (if defined) for enabled (running state) scripts on each
- * tick.
- *
- * `swap` - is called when a ScriptType that already exists in the registry gets redefined. If the
- * new ScriptType has a `swap` method in its prototype, then it will be executed to perform hot-
- * reload at runtime.
- *
- * @category Script
- */
-class ScriptType extends EventHandler {
-    /**
-     * Fired when a script instance becomes enabled.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('enable', () => {
-     *         // Script Instance is now enabled
-     *     });
-     * };
-     */
-    static EVENT_ENABLE = 'enable';
-
-    /**
-     * Fired when a script instance becomes disabled.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('disable', () => {
-     *         // Script Instance is now disabled
-     *     });
-     * };
-     */
-    static EVENT_DISABLE = 'disable';
-
-    /**
-     * 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
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('state', (enabled) => {
-     *         console.log(`Script Instance is now ${enabled ? 'enabled' : 'disabled'}`);
-     *     });
-     * };
-     */
-    static EVENT_STATE = 'state';
-
-    /**
-     * Fired when a script instance is destroyed and removed from component.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('destroy', () => {
-     *         // no longer part of the entity
-     *         // this is a good place to clean up allocated resources used by the script
-     *     });
-     * };
-     */
-    static EVENT_DESTROY = 'destroy';
-
-    /**
-     * 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
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('attr', (name, newValue, oldValue) => {
-     *         console.log(`Attribute '${name}' changed from '${oldValue}' to '${newValue}'`);
-     *     });
-     * };
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('attr:speed', (newValue, oldValue) => {
-     *         console.log(`Attribute 'speed' changed from '${oldValue}' to '${newValue}'`);
-     *     });
-     * };
-     */
-    static EVENT_ATTR = 'attr';
-
-    /**
-     * Fired when a script instance had an exception. The script instance will be automatically
-     * disabled. The handler is passed an {@link Error} object containing the details of the
-     * exception and the name of the method that threw the exception.
-     *
-     * @event
-     * @example
-     * PlayerController.prototype.initialize = function () {
-     *     this.on('error', (err, method) => {
-     *         // caught an exception
-     *         console.log(err.stack);
-     *     });
-     * };
-     */
-    static EVENT_ERROR = 'error';
-
-    /**
-     * The {@link AppBase} that the instance of this type belongs to.
-     *
-     * @type {import('../app-base.js').AppBase}
-     */
-    app;
-
-    /**
-     * The {@link Entity} that the instance of this type belongs to.
-     *
-     * @type {import('../entity.js').Entity}
-     */
-    entity;
-
-    /** @private */
-    _enabled;
-
-    /** @private */
-    _enabledOld;
-
-    /** @private */
-    _initialized;
-
-    /** @private */
-    _postInitialized;
-
-    /** @private */
-    __destroyed;
-
-    /** @private */
-    __attributes;
-
-    /** @private */
-    __attributesRaw;
-
-    /** @private */
-    __scriptType;
-
-    /**
-     * The order in the script component that the methods of this script instance will run
-     * relative to other script instances in the component.
-     *
-     * @type {number}
-     * @private
-     */
-    __executionOrder;
-
-    /**
-     * Create a new ScriptType instance.
-     *
-     * @param {object} args - The input arguments object.
-     * @param {import('../app-base.js').AppBase} args.app - The {@link AppBase} that is running the
-     * script.
-     * @param {import('../entity.js').Entity} args.entity - The {@link Entity} that the script is
-     * attached to.
-     */
-    constructor(args) {
-        super();
-        this.initScriptType(args);
-    }
-
-    /**
-     * True if the instance of this type 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 `enabled` state during 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.__initializeAttributes(true);
-
-            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;
-    }
-
-    /**
-     * @param {{entity: import('../entity.js').Entity, app: import('../app-base.js').AppBase}} args -
-     * The entity and app.
-     * @private
-     */
-    initScriptType(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.__attributes = { };
-        this.__attributesRaw = args.attributes || { }; // need at least an empty object to make sure default attributes are initialized
-        this.__scriptType = script;
-        this.__executionOrder = -1;
-    }
-
-    /**
-     * Name of a Script Type.
-     *
-     * @type {string}
-     * @private
-     */
-    static __name = null; // Will be assigned when calling createScript or registerScript.
-
-    /**
-     * @param {*} constructorFn - The constructor function of the script type.
-     * @returns {string} The script name.
-     * @private
-     */
-    static __getScriptName(constructorFn) {
-        if (typeof constructorFn !== 'function') return undefined;
-        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;
-    }
-
-    /**
-     * Name of a Script Type.
-     *
-     * @type {string|null}
-     */
-    static get scriptName() {
-        return this.__name;
-    }
-
-    /**
-     * The interface to define attributes for Script Types. Refer to {@link ScriptAttributes}.
-     *
-     * @type {ScriptAttributes}
-     * @example
-     * var PlayerController = pc.createScript('playerController');
-     *
-     * PlayerController.attributes.add('speed', {
-     *     type: 'number',
-     *     title: 'Speed',
-     *     placeholder: 'km/h',
-     *     default: 22.2
-     * });
-     */
-    static get attributes() {
-        if (!this.hasOwnProperty('__attributes')) this.__attributes = new ScriptAttributes(this);
-        return this.__attributes;
-    }
-
-    /**
-     * @param {boolean} [force] - Set to true to force initialization of the attributes.
-     * @private
-     */
-    __initializeAttributes(force) {
-        if (!force && !this.__attributesRaw)
-            return;
-
-        // set attributes values
-        for (const key in this.__scriptType.attributes.index) {
-            if (this.__attributesRaw && this.__attributesRaw.hasOwnProperty(key)) {
-                this[key] = this.__attributesRaw[key];
-            } else if (!this.__attributes.hasOwnProperty(key)) {
-                if (this.__scriptType.attributes.index[key].hasOwnProperty('default')) {
-                    this[key] = this.__scriptType.attributes.index[key].default;
-                } else {
-                    this[key] = null;
-                }
-            }
-        }
-
-        this.__attributesRaw = null;
-    }
-
-    /**
-     * Shorthand function to extend Script Type prototype with list of methods.
-     *
-     * @param {object} methods - Object with methods, where key - is name of method, and value - is function.
-     * @example
-     * var PlayerController = pc.createScript('playerController');
-     *
-     * PlayerController.extend({
-     *     initialize: function () {
-     *         // called once on initialize
-     *     },
-     *     update: function (dt) {
-     *         // called each tick
-     *     }
-     * });
-     */
-    static extend(methods) {
-        for (const key in methods) {
-            if (!methods.hasOwnProperty(key))
-                continue;
-
-            this.prototype[key] = methods[key];
-        }
-    }
-
-    /**
-     * @function
-     * @name ScriptType#[initialize]
-     * @description Called when script is about to run for the first time.
-     */
-
-    /**
-     * @function
-     * @name ScriptType#[postInitialize]
-     * @description Called after all initialize methods are executed in the same tick or enabling chain of actions.
-     */
-
-    /**
-     * @function
-     * @name ScriptType#[update]
-     * @description Called for enabled (running state) scripts on each tick.
-     * @param {number} dt - The delta time in seconds since the last frame.
-     */
-
-    /**
-     * @function
-     * @name ScriptType#[postUpdate]
-     * @description Called for enabled (running state) scripts on each tick, after update.
-     * @param {number} dt - The delta time in seconds since the last frame.
-     */
-
-    /**
-     * @function
-     * @name ScriptType#[swap]
-     * @description Called when a ScriptType that already exists in the registry
-     * gets redefined. If the new ScriptType has a `swap` method in its prototype,
-     * then it will be executed to perform hot-reload at runtime.
-     * @param {ScriptType} old - Old instance of the scriptType to copy data to the new instance.
-     */
-}
-
-export { ScriptType };
diff --git a/src/framework/script/script.js b/src/framework/script/script.js
index b25939d38..a136619f1 100644
--- a/src/framework/script/script.js
+++ b/src/framework/script/script.js
@@ -1,156 +1,404 @@
 import { Debug } from '../../core/debug.js';
 import { EventHandler } from '../../core/event-handler.js';
 
-import { script } from '../script.js';
-import { AppBase } from '../app-base.js';
-
+import { SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE } from './constants.js';
 import { ScriptAttributes } from './script-attributes.js';
-import { ScriptType } from './script-type.js';
-import { ScriptTypes } from './script-types.js';
-
-const reservedScriptNames = new Set([
-    'system', 'entity', 'create', 'destroy', 'swap', 'move', 'data',
-    'scripts', '_scripts', '_scriptsIndex', '_scriptsData',
-    'enabled', '_oldState', 'onEnable', 'onDisable', 'onPostStateChange',
-    '_onSetEnabled', '_checkState', '_onBeforeRemove',
-    '_onInitializeAttributes', '_onInitialize', '_onPostInitialize',
-    '_onUpdate', '_onPostUpdate',
-    '_callbacks', '_callbackActive', 'has', 'get', 'on', 'off', 'fire', 'once', 'hasEvent'
-]);
-
-function getReservedScriptNames() {
-    return reservedScriptNames;
-}
+
+const funcNameRegex = new RegExp('^\\s*function(?:\\s|\\s*\\/\\*.*\\*\\/\\s*)+([^\\(\\s\\/]*)\\s*');
 
 /**
- * Create and register a new {@link ScriptType}. It returns new class type (constructor function),
- * which is auto-registered to {@link ScriptRegistry} using its name. This is the main interface to
- * create Script Types, to define custom logic using JavaScript, that is used to create interaction
- * for entities.
+ * The Script class is a base class that you must extend to receive
+ * {@link https://developer.playcanvas.com/user-manual/scripting/anatomy/ various lifecycle updates}
+ * from the engine.
+ *
+ * You can create a Script using either {@link createScript} or by extending the class directly.
+ *
+ * ```javascript
+ * import { Script } from 'playcanvas';
+ * class Rotator extends Script {
+ *     update() {
+ *         this.entity.rotate(0, 0.1, 0);
+ *     }
+ * }
+ * ```
+ * The following methods are called if they exist on the Script instance:
+ *
+ * - `initialize`
+ * - `postInitialize`
+ * - `update`
+ * - `postUpdate`
+ * - `swap`
  *
- * @param {string} name - Unique Name of a Script Type. If a Script Type with the same name has
- * already been registered and the new one has a `swap` method defined in its prototype, then it
- * will perform hot swapping of existing Script Instances on entities using this new Script Type.
- * Note: There is a reserved list of names that cannot be used, such as list below as well as some
- * starting from `_` (underscore): system, entity, create, destroy, swap, move, scripts, onEnable,
- * onDisable, onPostStateChange, has, on, off, fire, once, hasEvent.
- * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
- * to add a script to. By default it will use `Application.getApplication()` to get current
- * {@link AppBase}.
- * @returns {typeof ScriptType|null} A class type (constructor function) that inherits {@link ScriptType},
- * which the developer is meant to further extend by adding attributes and prototype methods.
- * Returns null if there was an error.
- * @example
- * var Turning = pc.createScript('turn');
+ * `initialize` and `postInitialize` - are called (if defined) when a script is about to run for
+ * the first time - `postInitialize` will run after all `initialize` methods are executed in the
+ * same tick or enabling chain of actions.
  *
- * // define 'speed' attribute that is available in Editor UI
- * Turning.attributes.add('speed', {
- *     type: 'number',
- *     default: 180,
- *     placeholder: 'deg/s'
- * });
+ * `update` and `postUpdate` - are called (if defined) for enabled (running state) scripts on each
+ * tick.
  *
- * // runs every tick
- * Turning.prototype.update = function (dt) {
- *     this.entity.rotate(0, this.speed * dt, 0);
- * };
+ * `swap` - is called when a Script that already exists in the registry gets redefined. If the
+ * new Script has a `swap` method in its prototype, then it will be executed to perform hot-
+ * reload at runtime.
+ *
+ * @see {@link https://developer.playcanvas.com/user-manual/scripting/anatomy/} for more information.
  * @category Script
  */
-function createScript(name, app) {
-    if (script.legacy) {
-        Debug.error('This project is using the legacy script system. You cannot call pc.createScript().');
-        return null;
+class Script extends EventHandler {
+    /**
+     * Fired when a script instance becomes enabled.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('enable', () => {
+     *         // Script Instance is now enabled
+     *     });
+     * };
+     */
+    static EVENT_ENABLE = 'enable';
+
+    /**
+     * Fired when a script instance becomes disabled.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('disable', () => {
+     *         // Script Instance is now disabled
+     *     });
+     * };
+     */
+    static EVENT_DISABLE = 'disable';
+
+    /**
+     * 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
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('state', (enabled) => {
+     *         console.log(`Script Instance is now ${enabled ? 'enabled' : 'disabled'}`);
+     *     });
+     * };
+     */
+    static EVENT_STATE = 'state';
+
+    /**
+     * Fired when a script instance is destroyed and removed from component.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('destroy', () => {
+     *         // no longer part of the entity
+     *         // this is a good place to clean up allocated resources used by the script
+     *     });
+     * };
+     */
+    static EVENT_DESTROY = 'destroy';
+
+    /**
+     * 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
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('attr', (name, newValue, oldValue) => {
+     *         console.log(`Attribute '${name}' changed from '${oldValue}' to '${newValue}'`);
+     *     });
+     * };
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('attr:speed', (newValue, oldValue) => {
+     *         console.log(`Attribute 'speed' changed from '${oldValue}' to '${newValue}'`);
+     *     });
+     * };
+     */
+    static EVENT_ATTR = 'attr';
+
+    /**
+     * Fired when a script instance had an exception. The script instance will be automatically
+     * disabled. The handler is passed an {@link Error} object containing the details of the
+     * exception and the name of the method that threw the exception.
+     *
+     * @event
+     * @example
+     * PlayerController.prototype.initialize = function () {
+     *     this.on('error', (err, method) => {
+     *         // caught an exception
+     *         console.log(err.stack);
+     *     });
+     * };
+     */
+    static EVENT_ERROR = 'error';
+
+    /**
+     * The {@link AppBase} that the instance of this type belongs to.
+     *
+     * @type {import('../app-base.js').AppBase}
+     */
+    app;
+
+    /**
+     * The {@link Entity} that the instance of this type belongs to.
+     *
+     * @type {import('../entity.js').Entity}
+     */
+    entity;
+
+    /** @private */
+    _enabled;
+
+    /** @private */
+    _enabledOld;
+
+    /** @private */
+    _initialized;
+
+    /** @private */
+    _postInitialized;
+
+    /** @private */
+    __destroyed;
+
+    /** @private */
+    __attributes;
+
+    /** @private */
+    __attributesRaw;
+
+    /** @private */
+    __scriptType;
+
+    /**
+     * The order in the script component that the methods of this script instance will run
+     * relative to other script instances in the component.
+     *
+     * @type {number}
+     * @private
+     */
+    __executionOrder;
+
+    /**
+     * Create a new Script instance.
+     *
+     * @param {object} args - The input arguments object.
+     * @param {import('../app-base.js').AppBase} args.app - The {@link AppBase} that is running the
+     * script.
+     * @param {import('../entity.js').Entity} args.entity - The {@link Entity} that the script is
+     * attached to.
+     */
+    constructor(args) {
+        super();
+        this.initScriptType(args);
     }
 
-    if (reservedScriptNames.has(name))
-        throw new Error(`Script name '${name}' is reserved, please rename the script`);
+    /**
+     * True if the instance of this type 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 `enabled` state during app tick.
+     *
+     * @type {boolean}
+     */
+    set enabled(value) {
+        this._enabled = !!value;
 
-    const scriptType = function (args) {
-        EventHandler.prototype.initEventHandler.call(this);
-        ScriptType.prototype.initScriptType.call(this, args);
-    };
+        if (this.enabled === this._enabledOld) return;
 
-    scriptType.prototype = Object.create(ScriptType.prototype);
-    scriptType.prototype.constructor = scriptType;
+        this._enabledOld = this.enabled;
+        this.fire(this.enabled ? 'enable' : 'disable');
+        this.fire('state', this.enabled);
 
-    scriptType.extend = ScriptType.extend;
-    scriptType.attributes = new ScriptAttributes(scriptType);
+        // initialize script if not initialized yet and script is enabled
+        if (!this._initialized && this.enabled) {
+            this._initialized = true;
 
-    registerScript(scriptType, name, app);
-    return scriptType;
-}
+            this.__initializeAttributes(true);
 
-// Editor uses this - migrate to ScriptAttributes.reservedNames and delete this
-const reservedAttributes = {};
-ScriptAttributes.reservedNames.forEach((value, value2, set) => {
-    reservedAttributes[value] = 1;
-});
-createScript.reservedAttributes = reservedAttributes;
+            if (this.initialize)
+                this.entity.script._scriptMethod(this, SCRIPT_INITIALIZE);
+        }
 
-/* eslint-disable jsdoc/check-examples */
-/**
- * Register a existing class type as a Script Type to {@link ScriptRegistry}. Useful when defining
- * a ES6 script class that extends {@link ScriptType} (see example).
- *
- * @param {typeof ScriptType} script - The existing class type (constructor function) to be
- * registered as a Script Type. Class must extend {@link ScriptType} (see example). Please note: A
- * class created using {@link createScript} is auto-registered, and should therefore not be pass
- * into {@link registerScript} (which would result in swapping out all related script instances).
- * @param {string} [name] - Optional unique name of the Script Type. By default it will use the
- * same name as the existing class. If a Script Type with the same name has already been registered
- * and the new one has a `swap` method defined in its prototype, then it will perform hot swapping
- * of existing Script Instances on entities using this new Script Type. Note: There is a reserved
- * list of names that cannot be used, such as list below as well as some starting from `_`
- * (underscore): system, entity, create, destroy, swap, move, scripts, onEnable, onDisable,
- * onPostStateChange, has, on, off, fire, once, hasEvent.
- * @param {AppBase} [app] - Optional application handler, to choose which {@link ScriptRegistry}
- * to register the script type to. By default it will use `Application.getApplication()` to get
- * current {@link AppBase}.
- * @example
- * // define a ES6 script class
- * class PlayerController extends pc.ScriptType {
- *
- *     initialize() {
- *         // called once on initialize
- *     }
- *
- *     update(dt) {
- *         // called each tick
- *     }
- * }
- *
- * // register the class as a script
- * pc.registerScript(PlayerController);
- *
- * // declare script attributes (Must be after pc.registerScript())
- * PlayerController.attributes.add('attribute1', {type: 'number'});
- * @category Script
- */
-function registerScript(script, name, app) {
-    if (script.legacy) {
-        Debug.error('This project is using the legacy script system. You cannot call pc.registerScript().');
-        return;
+        // 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;
     }
 
-    if (typeof script !== 'function')
-        throw new Error(`script class: '${script}' must be a constructor function (i.e. class).`);
+    /**
+     * @param {{entity: import('../entity.js').Entity, app: import('../app-base.js').AppBase}} args -
+     * The entity and app.
+     * @private
+     */
+    initScriptType(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`);
 
-    if (!(script.prototype instanceof ScriptType))
-        throw new Error(`script class: '${ScriptType.__getScriptName(script)}' does not extend pc.ScriptType.`);
+        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.__attributes = { };
+        this.__attributesRaw = args.attributes || { }; // need at least an empty object to make sure default attributes are initialized
+        this.__scriptType = script;
+        this.__executionOrder = -1;
+    }
+
+    /**
+     * Name of a Script.
+     *
+     * @type {string}
+     * @private
+     */
+    static __name = null; // Will be assigned when calling createScript or registerScript.
+
+    /**
+     * @param {*} constructorFn - The constructor function of the script type.
+     * @returns {string} The script name.
+     * @private
+     */
+    static __getScriptName(constructorFn) {
+        if (typeof constructorFn !== 'function') return undefined;
+        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;
+    }
+
+    /**
+     * Name of a Script Type.
+     *
+     * @type {string|null}
+     */
+    static get scriptName() {
+        return this.__name;
+    }
+
+    /**
+     * The interface to define attributes for Script Types. Refer to {@link ScriptAttributes}.
+     *
+     * @type {ScriptAttributes}
+     * @example
+     * var PlayerController = pc.createScript('playerController');
+     *
+     * PlayerController.attributes.add('speed', {
+     *     type: 'number',
+     *     title: 'Speed',
+     *     placeholder: 'km/h',
+     *     default: 22.2
+     * });
+     */
+    static get attributes() {
+        if (!this.hasOwnProperty('__attributes')) this.__attributes = new ScriptAttributes(this);
+        return this.__attributes;
+    }
+
+    /**
+     * @param {boolean} [force] - Set to true to force initialization of the attributes.
+     * @private
+     */
+    __initializeAttributes(force) {
+        if (!force && !this.__attributesRaw)
+            return;
+
+        // set attributes values
+        for (const key in this.__scriptType.attributes.index) {
+            if (this.__attributesRaw && this.__attributesRaw.hasOwnProperty(key)) {
+                this[key] = this.__attributesRaw[key];
+            } else if (!this.__attributes.hasOwnProperty(key)) {
+                if (this.__scriptType.attributes.index[key].hasOwnProperty('default')) {
+                    this[key] = this.__scriptType.attributes.index[key].default;
+                } else {
+                    this[key] = null;
+                }
+            }
+        }
+
+        this.__attributesRaw = null;
+    }
+
+    /**
+     * Shorthand function to extend Script Type prototype with list of methods.
+     *
+     * @param {object} methods - Object with methods, where key - is name of method, and value - is function.
+     * @example
+     * var PlayerController = pc.createScript('playerController');
+     *
+     * PlayerController.extend({
+     *     initialize: function () {
+     *         // called once on initialize
+     *     },
+     *     update: function (dt) {
+     *         // called each tick
+     *     }
+     * });
+     */
+    static extend(methods) {
+        for (const key in methods) {
+            if (!methods.hasOwnProperty(key))
+                continue;
+
+            this.prototype[key] = methods[key];
+        }
+    }
 
-    name = name || script.__name || ScriptType.__getScriptName(script);
+    /**
+     * @function
+     * @name Script#[initialize]
+     * @description Called when script is about to run for the first time.
+     */
 
-    if (reservedScriptNames.has(name))
-        throw new Error(`script name: '${name}' is reserved, please change script name`);
+    /**
+     * @function
+     * @name Script#[postInitialize]
+     * @description Called after all initialize methods are executed in the same tick or enabling chain of actions.
+     */
 
-    script.__name = name;
+    /**
+     * @function
+     * @name Script#[update]
+     * @description Called for enabled (running state) scripts on each tick.
+     * @param {number} dt - The delta time in seconds since the last frame.
+     */
 
-    // add to scripts registry
-    const registry = app ? app.scripts : AppBase.getApplication().scripts;
-    registry.add(script);
+    /**
+     * @function
+     * @name Script#[postUpdate]
+     * @description Called for enabled (running state) scripts on each tick, after update.
+     * @param {number} dt - The delta time in seconds since the last frame.
+     */
 
-    ScriptTypes.push(script, script.legacy);
+    /**
+     * @function
+     * @name Script#[swap]
+     * @description Called when a Script that already exists in the registry
+     * gets redefined. If the new Script has a `swap` method in its prototype,
+     * then it will be executed to perform hot-reload at runtime.
+     * @param {Script} old - Old instance of the script to copy data to the new instance.
+     */
 }
-/* eslint-enable jsdoc/check-examples */
 
-export { createScript, registerScript, getReservedScriptNames };
+export { Script };
diff --git a/src/index.js b/src/index.js
index f4d29cf8f..29579aec0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -349,10 +349,10 @@ export { ElementInput, ElementInputEvent, ElementMouseEvent, ElementSelectEvent,
 export { JsonStandardMaterialParser } from './framework/parsers/material/json-standard-material.js';
 
 // FRAMEWORK /SCRIPTS
-export { createScript, registerScript, getReservedScriptNames } from './framework/script/script.js';
+export { createScript, registerScript, getReservedScriptNames } from './framework/script/script-create.js';
 export { ScriptAttributes } from './framework/script/script-attributes.js';
 export { ScriptRegistry } from './framework/script/script-registry.js';
-export { ScriptType } from './framework/script/script-type.js';
+export { Script } from './framework/script/script.js';
 
 // FRAMEWORK / LOCALIZATION
 export { I18n } from './framework/i18n/i18n.js';
diff --git a/utils/plugins/rollup-types-fixup.mjs b/utils/plugins/rollup-types-fixup.mjs
index d33e81551..8f2a445ab 100644
--- a/utils/plugins/rollup-types-fixup.mjs
+++ b/utils/plugins/rollup-types-fixup.mjs
@@ -209,7 +209,7 @@ import { Texture } from '../../platform/graphics/texture.js';
 `
     }
 }, {
-    path: `${TYPES_PATH}/framework/script/script-type.d.ts`,
+    path: `${TYPES_PATH}/framework/script/script.d.ts`,
     replacement: {
         from: 'get enabled(): boolean;',
         to: `get enabled(): boolean;
@@ -232,12 +232,12 @@ import { Texture } from '../../platform/graphics/texture.js';
      */
     postUpdate?(dt: number): void;
     /**
-     * Called when a ScriptType that already exists in the registry gets redefined. If the new
-     * ScriptType has a \`swap\` method in its prototype, then it will be executed to perform
+     * Called when a Script that already exists in the registry gets redefined. If the new
+     * Script has a \`swap\` method in its prototype, then it will be executed to perform
      * hot-reload at runtime.
-     * @param old - Old instance of the scriptType to copy data to the new instance.
+     * @param old - Old instance of the script to copy data to the new instance.
      */
-    swap?(old: ScriptType): void;
+    swap?(old: Script): void;
 `
     }
 }];


Test patch:
diff --git a/test/framework/entity.test.mjs b/test/framework/entity.test.mjs
index 4bbd61bc0..38f5d80c8 100644
--- a/test/framework/entity.test.mjs
+++ b/test/framework/entity.test.mjs
@@ -1,4 +1,4 @@
-import { createScript } from '../../src/framework/script/script.js';
+import { createScript } from '../../src/framework/script/script-create.js';
 import { Color } from '../../src/core/math/color.js';
 
 import { AnimComponent } from '../../src/framework/components/anim/component.js';