Scripting Overview

Write custom game logic in JavaScript or TypeScript. Learn how to create, attach, and run scripts in Web Engine.

Scripts in Web Engine let you add custom behavior to entities. You write JavaScript (or TypeScript) functions that run every frame, with access to transforms, physics, input, and more.

Script Types#

Web Engine supports two scripting styles:

Function-Based

Simple closure pattern. Great for straightforward behaviors with minimal state.

Class-Based

Object-oriented pattern with lifecycle methods. Better for complex behaviors with local state.

Function-Based Scripts#

The simplest way to write a script. Export a factory function that takes theapi object and returns an update function or lifecycle object.

Rotator.js
javascript
// Function-based script (simple)
export default (api) => {
// Initialization code runs once
const speed = 1.0;
api.log("Rotator initialized!");
// Return the update function
return (dt, time) => {
// This runs every frame
const rot = api.rotation;
rot.y += speed * dt;
api.rotation = rot;
};
};

The outer function (factory) runs once when the entity enters Play Mode. The inner function runs every frame. You can also return an object with lifecycle methods:

AdvancedRotator.js
javascript
// Function-based script with lifecycle hooks
export default (api) => {
const speed = 1.0;
let isPaused = false;
return {
onStart: (time) => {
api.log("Starting at time: " + time);
},
onUpdate: (dt, time) => {
if (!isPaused) {
const rot = api.rotation;
rot.y += speed * dt;
api.rotation = rot;
}
},
onCollisionEnter: (otherEid) => {
isPaused = true;
api.log("Collision with entity " + otherEid);
},
onCollisionExit: (otherEid) => {
isPaused = false;
},
onDestroy: (time) => {
api.log("Destroyed at time: " + time);
}
};
};

Factory Pattern

The factory pattern allows you to create local state (closures) that persist between frames. Variables declared in the outer function are private to your script instance.

Class-Based Scripts#

For complex behaviors, extend ScriptComponent which provides a structured class-based pattern with explicit lifecycle methods.

PlayerController.ts
typescript
export default class PlayerController extends ScriptComponent {
speed = 5.0;
jumpForce = 10.0;
// Called immediately when entity becomes active
onAwake(time: number) {
this.api.log("Player awakened!");
}
// Called once after all entities have awakened
onStart(time: number) {
this.api.log("Player ready!");
}
// Called every frame
onUpdate(dt: number, time: number) {
const { move, jumpDown, sprint } = this.api.input;
const currentSpeed = sprint ? this.speed * 2 : this.speed;
// Movement
if (move.x !== 0 || move.y !== 0) {
this.api.controller.move({
x: move.x * currentSpeed * dt,
y: 0,
z: move.y * currentSpeed * dt
});
}
// Jumping
if (jumpDown && this.api.controller.isGrounded) {
this.api.applyImpulse({ x: 0, y: this.jumpForce, z: 0 });
}
}
// Physics collision event
onCollisionEnter(otherEid: number) {
this.api.log("Hit entity " + otherEid);
}
// Called when script is destroyed
onDestroy(time: number) {
this.api.log("Player destroyed");
}
}

The ScriptComponent base class providesthis.api and this.eidautomatically. All lifecycle methods are optional.

Lifecycle Methods#

Scripts support several lifecycle hooks that fire at specific times during the entity's lifetime:

  • onAwake(time) — Called immediately when entity becomes active, before any other logic. Use for critical initialization.
  • onStart(time) — Called once on the first frame after onAwake. Safe to reference other entities that have awakened.
  • onUpdate(dt, time) — Called every frame. dt is delta time in seconds, time is total elapsed time.
  • onDestroy(time) — Called when the entity is destroyed or script is removed. Use for cleanup.
  • onTriggerEnter(otherEid) — Called when a trigger sensor overlap begins. Requires Sensor component.
  • onTriggerExit(otherEid) — Called when a trigger sensor overlap ends.
  • onCollisionEnter(otherEid) — Called when physics collision begins. Requires RigidBody component.
  • onCollisionExit(otherEid) — Called when physics collision ends.
  • onAnimationEvent(clipName, eventName) — Called when an animation event fires.

Lifecycle Order

The execution order is: onAwake onStartonUpdate (every frame) → onDestroy. Physics and animation events can fire at any time after onStart.

The Script API#

Scripts receive an api object that provides access to the entity and game systems:

// Entity ID
api.eid // number
// Transform
api.position // Vector3 (read/write)
api.rotation // Euler (read/write)
api.lookAt(pos) // Face a point
// Physics
api.velocity // Vector3 (read/write)
api.applyImpulse(force)
api.raycast(origin, direction)
// Character Controller
api.controller.move(velocity)
api.controller.isGrounded
// Input
api.input.move // Vector2 (WASD)
api.input.look // Vector2 (mouse delta)
api.input.jumpDown
api.input.sprint
// Audio
api.audio.play()
api.audio.stop()
// Animation
api.animation.play("Walk")
api.animation.crossFade("Run", 0.3)
// Spawning
await api.spawn("Bullet", position, rotation)
await api.loadScene("/scenes/level2.json")
// Debug
api.log("Message")

Pooled Objects

api.position and similar properties return pooled objects. Don't store references between frames — copy values if needed.

Attaching Scripts#

To attach a script to an entity:

  1. Select an entity in the Hierarchy panel.
  2. In the Inspector, click Add Component.
  3. Select Script from the list.
  4. Choose or create a script file.

Scripts are stored in your project's scripts/folder and can be reused across multiple entities.

Common Patterns#

State Machines#

export default (api) => {
let state = "idle";
const states = {
idle: (dt) => {
if (api.input.move.length() > 0.1) {
state = "walking";
api.animation.crossFade("Walk", 0.2);
}
},
walking: (dt) => {
// Movement logic...
if (api.input.move.length() < 0.1) {
state = "idle";
api.animation.crossFade("Idle", 0.2);
}
}
};
return (dt) => states[state](dt);
};

Timers & Cooldowns#

export default (api) => {
let fireCooldown = 0;
const fireRate = 0.25; // 4 shots per second
return (dt) => {
fireCooldown -= dt;
if (api.input.fire && fireCooldown <= 0) {
fireCooldown = fireRate;
api.spawn("Bullet", api.position, api.rotation);
api.audio.play();
}
};
};
Scripting Overview | Web Engine Docs