ignifx0.x · unpublished
GitHub

Recipes·skills/ignifx/references/recipes/tween-a-transform.md

Tween a transform

<!-- Generated by `pnpm docs:recipes`. Do not edit by hand. -->

app.tweens is core: it interpolates any number, Vec2, Vec3 or Quat property of any object, on the game clock, in PostUpdate — so a tween honours time.timeScale and stops with app.pause() unless it asks for updateWhenPaused. That is what makes it the right tool instead of a coroutine that lerps by hand.

A tween latches its start value when its delay elapses, not when it is created, so a chain built from onComplete starts from wherever the previous one finished. Two tweens on one property fight each other; one property, one tween.

A tween keeps a reference to its target, so a script that starts tweens stops them again in onDestroy with stopAllOf — otherwise they keep writing to a transform whose entity is gone.

typescript
import { Quat, Script, createApp, f32 } from "@ignifx/core";import type { ScriptCallbacks, Tween } from "@ignifx/core";/** Lifts a portcullis, then swings it, then reports that it is open. */class Portcullis extends Script.define({ travel: f32(3), seconds: f32(0.8) }) implements ScriptCallbacks {  static typeId = "recipes/Portcullis";  #tween: Tween | null = null;  start(): void {    this.#tween = this.app.tweens.to(      this.transform,      { position: { x: 0, y: this.travel, z: 0 } },      {        duration: this.seconds,        // `ease` takes any of EASING_NAMES, or a function of your own: (t) => t * t.        ease: "cubicInOut",        onComplete: (): void => {          this.#swing();        },      },    );  }  /** The second link of the chain: it starts where the first one left the transform. */  #swing(): void {    this.#tween = this.app.tweens.to(      this.transform,      { rotation: Quat.fromEulerDegrees(0, 90, 0) },      {        duration: 0.4,        ease: "backOut",        onComplete: (): void => {          this.app.log.info("open");        },      },    );  }  onDestroy(): void {    this.app.tweens.stopAllOf(this.transform);    this.#tween = null;  }  // How far along the current link is, in `[0, 1]`; a HUD or a test reads it.  get progress(): number {    return this.#tween?.progress ?? 0;  }}const app = await createApp({ headless: true });app.registerComponents([Portcullis]);const gate = app.world.createEntity("Portcullis");const portcullis = gate.addComponent(Portcullis, { travel: 3, seconds: 0.8 });await app.start();// One `app.step(0.4)` would not advance the tween by 0.4 s: `time.maximumDeltaTime` clamps a raw// frame delta to 0.1 s, so a long step is a short frame. Pump real frames instead.for (let step = 0; step < 24; step += 1) {  app.step(1 / 60);}app.log.info("0.4 s in, y and progress:", gate.transform.position.y, portcullis.progress);for (let step = 0; step < 90; step += 1) {  app.step(1 / 60);}app.log.info("done y, tweens running:", gate.transform.position.y, app.tweens.count);// Destroying the entity stops its tweens through `onDestroy`; nothing keeps writing afterwards.gate.destroy();app.step(1 / 60);app.dispose();

Source: examples/recipes/tween-a-transform/main.ts