Skip to content

Make the game
you keep thinking about.

Flixel-Pixi handles the game loop, rendering, input, collisions, sound, and all the fiddly browser work. You get to spend that time making the game feel good.

npm install flixel-pixi@next pixi.js@^8.19.0

The core is just 137 kB gzipped.

  • Move sprites and animate them
  • Handle collisions and physics
  • Use keyboard, touch, pointer, or gamepad
  • Add cameras, sound, particles, and effects
  • Build tilemaps and preload assets
  • Save progress, record replays, and debug
Starting gameTap / Enter
TS
import { FlxBackdrop, FlxG, FlxSprite, FlxState, FlxText } from 'flixel-pixi';
import {
  FLOOR_Y,
  makeCollectibleGraphic,
  makeGroundTile,
  makeRunnerSheet,
  makeSkyTile,
  PLAYER_FLOOR_Y,
  PLAYER_HEIGHT,
  PLAYER_WIDTH,
  VIEW_HEIGHT,
  VIEW_WIDTH,
} from './art';

export class HeroRunnerState extends FlxState {
  player!: FlxSprite;
  scoreText!: FlxText;

  #collectibles: FlxSprite[] = [];
  #jumpRequested = false;
  #score = 0;
  #nextCollectibleGap = 0;

  requestJump(): void {
    this.#jumpRequested = true;
  }

  override create(): void {
    super.create();
    FlxG.camera.bgColor = 0xff090d16;

    const sky = new FlxBackdrop(makeSkyTile(), 0, 0, VIEW_WIDTH, VIEW_HEIGHT);
    sky.repeatY = false;
    sky.scrollVelocity.x = -13;
    this.add(sky);

    const ground = new FlxBackdrop(
      makeGroundTile(),
      0,
      FLOOR_Y,
      VIEW_WIDTH,
      VIEW_HEIGHT - FLOOR_Y,
    );
    ground.repeatY = false;
    ground.scrollVelocity.x = -92;
    this.add(ground);

    this.player = new FlxSprite(52, PLAYER_FLOOR_Y);
    this.player.loadGraphic(
      makeRunnerSheet(),
      true,
      false,
      PLAYER_WIDTH,
      PLAYER_HEIGHT,
    );
    this.player.addAnimation('run', [0, 1, 2, 3], 11, true);
    this.player.play('run', { loop: true, speed: 11 / 60 });
    this.player.acceleration.y = 680;
    this.player.maxVelocity.y = 420;
    this.add(this.player);

    this.#makeCollectible(224, 102);
    this.#makeCollectible(292, 78);
    this.#makeCollectible(366, 102);

    this.scoreText = new FlxText(9, 8, 150, 'GEMS 000');
    this.scoreText.setFormat(undefined, 9, 0xfff8fafc, 'left');
    this.scoreText.scrollFactor.make(0, 0);
    this.add(this.scoreText);
  }

  override update(): void {
    const grounded = this.player.y >= PLAYER_FLOOR_Y - 0.5;
    if (this.#jumpRequested && grounded) {
      this.player.velocity.y = -285;
      this.player.play('run', { loop: true, speed: 7 / 60 });
    }
    this.#jumpRequested = false;

    super.update();

    if (this.player.y >= PLAYER_FLOOR_Y) {
      this.player.y = PLAYER_FLOOR_Y;
      this.player.velocity.y = 0;
      this.player.play('run', { loop: true, speed: 11 / 60 });
    }

    for (const collectible of this.#collectibles) {
      if (collectible.x + collectible.width < 0) {
        this.#recycleCollectible(collectible);
      }
      if (this.player.overlaps(collectible)) {
        this.#score += 1;
        this.scoreText.text = `GEMS ${this.#score.toString().padStart(3, '0')}`;
        FlxG.camera.flash(0x4412d9e6, 0.12);
        this.#recycleCollectible(collectible);
      }
    }
  }

  #makeCollectible(x: number, y: number): void {
    const collectible = new FlxSprite(x, y);
    collectible.loadGraphic(makeCollectibleGraphic());
    collectible.velocity.x = -92;
    collectible.angularVelocity = 90;
    this.#collectibles.push(collectible);
    this.add(collectible);
  }

  #recycleCollectible(collectible: FlxSprite): void {
    this.#nextCollectibleGap += 37;
    collectible.x = VIEW_WIDTH + 54 + (this.#nextCollectibleGap % 76);
    collectible.y = this.#nextCollectibleGap % 2 === 0 ? 102 : 78;
  }
}
Engine readyTypeScript

There is more than one way into a game.

Take the guided route, learn from a working project, or look up the one thing blocking you right now.

The everyday tools of making a game.

The useful parts are already connected, so a quick prototype can grow into a real project without a rewrite halfway through.

A world that stays manageable

Keep menus, levels, entities, groups, cameras, and collisions in a lifecycle that is easy to reason about.

Physics that serves the game feel

Add gravity and velocity, separate solid objects, tune elasticity, detect overlaps, and keep busy worlds quick with spatial collision.

Movement worth tuning

Animate sprites, shape motion with tweens, shake the camera, and keep adjusting until the jump feels right.

Particles and effects with personality

Burst sparks, dust, smoke, trails, and debris from pooled emitters, then layer in filters, fades, flashes, and camera shake.

Controls players can make their own

Map keyboard, pointer, touch, and gamepad controls to the same game actions, with rebinding when you need it.

Sound and ambience with a sense of place

Mix music and effects in their own groups, loop ambient beds, and attach sounds to waterfalls, engines, or anything moving through the world.

A game that fits the screen it is on

Scale, align, and letterbox the game across phones, tablets, and desktops, with safe-area-aware layouts and fullscreen support.

The unglamorous parts are here too

Preload assets, save progress, record deterministic replays, inspect the game in the debugger, and get the build onto the web.

Don't just read about it. Play it.

This little invaders game is built with the same public API in the guide. Click the canvas, move around, fire a few shots, then open the example and pull it apart.

Flx-Invaders — AS3 clean-room port
🎮

Flx-Invaders — AS3 clean-room port

Use the arrow keys or A and D to steer. Press Spacebar to fire. Click the game first to focus it.

Put something on screen.

One state. One sprite. One idea you want to feel in your hands. We can build outward from there.

Released under the MIT License.