Skip to content

Examples

Complete, runnable ScriptBehavior classes you can paste into the Code panel, Compile, and attach - a spinner, a first-person controller, a scene-loading trigger, and a savable pickup.

Each is self-contained. Paste it, press Compile, add a Script component to an object, and pick it. Public fields (speed, camera, …) appear as Inspector controls.

Spinner

The "hello world" of Awaken scripting - rotate an object every frame. Tune the speed and axis per instance.

ts
// Spins this object every frame.
export default class implements ScriptBehavior {
  speed = 90;                     // degrees/second 0..720   (slider)
  axis: "x" | "y" | "z" = "y";    // rotation axis            (dropdown)
  clockwise = true;               // reverse direction        (checkbox)

  onUpdate(api: ScriptApi, dt: number) {
    const rad = (this.speed * Math.PI / 180) * dt * (this.clockwise ? 1 : -1);
    api.rotate(this.axis === "x" ? 1 : 0, this.axis === "y" ? 1 : 0, this.axis === "z" ? 1 : 0, rad);
  }
}

Attach it to a pickup, a fan, a coin - anything that should turn. To spin about the object's visual centre when its origin is off-centre, read api.getMeshCenter() before and after the rotate and translate by the difference.

First-person controller

A complete WASD + mouse-look + jump controller with real capsule collision. This is the first-person-controller design as importable content - the engine only provides mouse input and moveAndSlide; all the feel lives here.

The rig: this script attaches to a player object that has a capsule Collider. You assign your scene camera to the camera field; the script keeps the camera at eye height and applies pitch to it, while the player body takes the yaw.

ts
// First-person controller. Attach to a player object with a capsule Collider.
// Assign your scene camera to the `camera` field.
function q(axis: "x" | "y" | "z", a: number) {
  const s = Math.sin(a / 2), c = Math.cos(a / 2);
  return { x: axis === "x" ? s : 0, y: axis === "y" ? s : 0, z: axis === "z" ? s : 0, w: c };
}
function qMul(a: any, b: any) {
  return {
    x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
    y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
    z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
    w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
  };
}

export default class implements ScriptBehavior {
  camera: Camera;                 // the scene camera to drive
  moveSpeed = 5;                  // m/s 0..12
  mouseSensitivity = 0.0025;      // radians per pixel
  jumpHeight = 1.2;               // metres 0..3
  eyeHeight = 1.6;                // camera height above the player origin (m)
  gravity = 20;                   // m/s^2
  private yaw = 0;
  private pitch = 0;
  private vy = 0;

  onUpdate(api: ScriptApi, dt: number) {
    // 1. Look - only while the pointer is locked (click the game view to lock).
    if (api.input.pointerLocked) {
      this.yaw   -= api.input.mouseDX * this.mouseSensitivity;
      this.pitch  = Math.max(-1.55, Math.min(1.55, this.pitch - api.input.mouseDY * this.mouseSensitivity));
    }
    // Player body takes the yaw (turns the mesh); camera takes yaw * pitch.
    const yawQ = q("y", this.yaw);
    api.setField("Transform", "rotation", yawQ);

    // 2. Move - WASD in the yaw frame.
    const f = (api.input.isDown("w") ? 1 : 0) - (api.input.isDown("s") ? 1 : 0);
    const s = (api.input.isDown("d") ? 1 : 0) - (api.input.isDown("a") ? 1 : 0);
    const mx = (Math.cos(this.yaw) * s + Math.sin(this.yaw) * f) * this.moveSpeed;
    const mz = (-Math.sin(this.yaw) * s + Math.cos(this.yaw) * f) * this.moveSpeed;

    // 3. Gravity + jump.
    this.vy -= this.gravity * dt;

    // 4. Collide - slide the capsule; get grounded back.
    const { grounded } = api.moveAndSlide(mx * dt, this.vy * dt, mz * dt);
    if (grounded) {
      if (this.vy < 0) this.vy = 0;
      if (api.input.isDown(" ")) this.vy = Math.sqrt(2 * this.gravity * this.jumpHeight);
    }

    // 5. Camera - eye height above the player, composed yaw * pitch.
    if (this.camera) {
      const T = api.world.registry.get("Transform");
      const cam = api.world.get(this.camera, T);
      if (cam) {
        const p = api.getPosition();
        cam.position = { x: p.x, y: p.y + this.eyeHeight, z: p.z };
        cam.rotation = qMul(yawQ, q("x", this.pitch));
        api.world.markMoved(this.camera);
      }
    }
  }
}

Notes on how it works:

  • api.setField("Transform", "rotation", …) writes this object's rotation (the player body). The camera is a different entity, so it is driven through api.world - get its Transform via world.registry.get("Transform"), write position/rotation, then markMoved so the renderer syncs it.
  • The capsule becomes a kinematic character body automatically on the first moveAndSlide call - no extra component beyond the capsule Collider.
  • pointerLocked gates look so the camera does not swing before the player clicks in. See Input.

Trigger that loads a scene

A level exit. Put a trigger Collider on the goal; when the player walks in, load the next scene.

ts
// Level exit - loads a scene when the player enters this trigger.
export default class implements ScriptBehavior {
  nextScene = "Level2";           // scene to load  (edit per instance)
  playerTag = "player";           // which object counts as the player

  onCollisionEnter(api: ScriptApi, other: Entity) {
    const isPlayer = api.findByTag(this.playerTag).includes(other)
      || api.world.getName(other) === "Player";
    if (isPlayer) api.loadScene(this.nextScene);
  }
}

loadScene is deferred to the next frame, so the swap is clean. Tag your player object (a Tag component with value player) for a rename-proof check, or fall back to the name. See Scenes & Persistence.

Pickup with save

A collectible that adds to the score, remembers a lifetime total across reloads, plays a sound, and removes itself.

ts
// Collectible coin - collect on touch, persist the running total.
export default class implements ScriptBehavior {
  value = 1;                      // points this pickup is worth 1..100
  sound = "beep";                 // clip to play on pickup

  onCollisionEnter(api: ScriptApi, other: Entity) {
    if (api.world.getName(other) !== "Player") return;

    // Session score (survives scene loads).
    api.state.score = (api.state.score as number ?? 0) + this.value;
    api.setHud(`Score: ${api.state.score}`);

    // Lifetime total (survives page reloads).
    const total = (api.load("coinsCollected") as number ?? 0) + this.value;
    api.save("coinsCollected", total);

    api.playSound(this.sound, { volume: 0.6 });
    api.destroy();               // remove the coin; onDestroy + timers auto-clean
  }
}

Two lifetimes at work: api.state.score carries the run's score across level loads, while api.save("coinsCollected", …) banks a total that outlives the browser session. See Scenes & Persistence.

See also

Awaken — browser-native WebGPU game engine.