Sprites & Animation
For most game art — characters, props, tiles, backgrounds — you’ll be
displaying a texture. SpriteComponent is the right primitive; reach for
AnimatedSpriteComponent + AnimationController when frames change over
time.
Sprites
Section titled “Sprites”SpriteComponent displays a texture on an entity. It automatically syncs with
the entity’s Transform.
import { SpriteComponent } from "@yagejs/renderer";
entity.add( new SpriteComponent({ texture: playerTexture, anchor: { x: 0.5, y: 0.5 }, layer: "characters", tint: 0xffffff, alpha: 1, }),);All properties are optional except texture. The layer property controls
z-ordering — see Camera & Layers.
Escape hatch: .sprite is the underlying pixi Sprite. Reach for it
when you need a pixi feature SpriteComponent doesn’t surface — full pixi
API is available. See the pixi Sprite docs.
Animated Sprites
Section titled “Animated Sprites”For frame-based sprite animations, use AnimatedSpriteComponent together with
an AnimationController.
import { AnimatedSpriteComponent, AnimationController,} from "@yagejs/renderer";
entity.add( new AnimatedSpriteComponent({ spritesheet: heroSheet, defaultAnimation: "idle", }),);
entity.add( new AnimationController({ animations: { idle: { frames: [0, 1, 2, 3], speed: 0.1 }, run: { frames: [4, 5, 6, 7, 8, 9], speed: 0.15 }, jump: { frames: [10, 11, 12], speed: 0.12, loop: false }, }, }),);Switch animations at runtime:
const anim = entity.get(AnimationController);anim.play("run");anim.play("jump", { onComplete: () => anim.play("idle") });Escape hatch: .animatedSprite is the underlying pixi AnimatedSprite.
Typing the controller
Section titled “Typing the controller”AnimationController<T extends string = string> is generic on the
animation-name union, which gives play() / playOneShot() strict
autocomplete and typo-checking. But the runtime class is non-generic —
AnimationController<HeroAnim> doesn’t exist as a value at runtime, so
there’s nothing to pass to Component.sibling() or entity.get(). A
default AnimationController<string> isn’t sound-assignable to
AnimationController<HeroAnim> either: the current: T | "" getter is
covariant on T, so a string-returning instance can’t substitute for one
promising the narrow union. Cast at the field declaration to recover the
narrow type — every call site downstream picks it up for free:
import { Component } from "@yagejs/core";import { AnimationController } from "@yagejs/renderer";
type HeroAnim = "idle" | "walk" | "attack";
class HeroController extends Component { private readonly _anim = this.sibling(AnimationController) as AnimationController<HeroAnim>;
update() { this._anim.play("walk"); // typed — a typo here is a compile error }}Annotate the field once; the cast is required because the type parameter is
type-only and sibling() can only narrow by the runtime class.
Layered characters (head + body + outfit)
Section titled “Layered characters (head + body + outfit)”When a character is built from multiple sprite layers — head + body +
outfit, each with its own AnimatedSpriteComponent and
AnimationController — every playOneShot call computes its lock
duration from frames.length / speed, rounded to whole frame-ms. When
the layers have different frame counts or speeds, those durations
disagree by a millisecond or two, the locks expire on different
frames, and one sprite snaps back to idle while the others are still
mid-swing. The visual giveaway is a single layer flickering at the
tail of every attack animation.
The recommended fix is LayeredAnimationController, which wraps the
per-layer controllers, computes the duration once on a lead controller,
and cascades lock release via a single master timer:
import { AnimatedSpriteComponent, AnimationController, LayeredAnimationController,} from "@yagejs/renderer";
class Hero extends Entity { setup() { this.add(new Transform()); const body = this.spawnChild("body", HeroLayer, { sheet: "body.png" }); const head = this.spawnChild("head", HeroLayer, { sheet: "head.png" });
this.add( new LayeredAnimationController<"idle" | "attack">({ controllers: [ body.get(AnimationController) as AnimationController<"idle" | "attack">, head.get(AnimationController) as AnimationController<"idle" | "attack">, ], }), ); }}
// Call once, both layers stay in sync:hero.get(LayeredAnimationController).playOneShot("attack", { onComplete: () => hero.get(LayeredAnimationController).play("idle"),});The wrapper computes a single lock duration (from the first controller, or an
explicit duration option), passes Number.POSITIVE_INFINITY to each child
so child timers can never expire independently, and fires onComplete
exactly once when the master timer expires. All layers unlock together
regardless of per-layer frame counts.
Manual workaround (when you don’t want a wrapper)
Section titled “Manual workaround (when you don’t want a wrapper)”For prototypes — or when each layer already has its own custom controller
managing more than just animation — the same insight can live as a one-line
helper. Precompute the duration on a “lead” controller and broadcast via
options.duration:
import { AnimationController } from "@yagejs/renderer";
function playOneShotLayered( controllers: AnimationController<string>[], name: string, onComplete?: () => void,) { const duration = controllers[0]!.calcDuration(name); controllers[0]!.playOneShot(name, { duration, onComplete }); for (let i = 1; i < controllers.length; i++) { controllers[i]!.playOneShot(name, { duration }); }}
playOneShotLayered([bodyAnim, headAnim, outfitAnim], "attack", () => { // every layer is back from the one-shot at the same instant});AnimationController.calcDuration(name) is the same calculation
playOneShot does internally — calling it once and broadcasting the result
is the cheapest way to keep layered characters in lockstep without bringing
in LayeredAnimationController.
Asset Factories
Section titled “Asset Factories”YAGE provides helper functions to load and define render assets:
import { texture, spritesheet, renderAsset } from "@yagejs/renderer";
// Load a single textureconst bg = texture("assets/background.png");
// Load a spritesheet (atlas JSON; the JSON references the texture image)const heroSheet = spritesheet("assets/hero.json");
// Generic render asset (auto-detects type)const asset = renderAsset("assets/tileset.png");These return handles that are resolved during scene loading, so textures are
available by the time setup() runs.