← Examples · Ship Pack · How this page works

Phaser 3

Arrow keys: ← → turn, ↑ thrust, ↓ brake. Arcade physics; the frame follows the heading.

Ships from the Yodacon Ship Pack by Paul Richeson (ConEx, 1997), GPL-2.0-or-later — github.com/yodacon/yodacon-ship-pack

Yodacon Ship Pack · Tutorial Note 6 · October 2026

One Scene, Two Assets: How the Phaser 3 Example Uses the Pack

Abstract—The Phaser 3 example flies a pack ship in a single Arcade-physics scene. This note follows the scene's source: reading ship.json before the game starts, loading sheet-alpha.png as a Phaser spritesheet, selecting a frame from the heading each update, and converting between the pack's and Phaser's angle conventions.

Index Terms—Phaser 3, sprite sheets, Arcade physics, pixel art, asset loading.

I. Introduction

The Phaser example is one scene, Flight, in which a pack ship flies under Arcade physics. It is a useful model because Phaser has its own loader, its own frame numbering and its own angle convention, and the page shows where each meets the pack's. Phaser 3.80.1 is pinned on jsDelivr (index.html:18); the page is examples/phaser/index.html [1].

II. Assets Used

Table I. What the scene takes from the pack
AssetRead atUsed for
ship.jsonindex.html:35Frame size, stats, name
sheet-alpha.pngindex.html:45Phaser texture "ship", frames 0–35
frameIndex()index.html:31Heading to frame

Nothing else is needed: not the opaque sheet, the mask, the single frames nor the pictures.

III. Two-Stage Loading

Phaser's load.spritesheet must be told the frame size up front, and frame sizes differ between ships. So the page reads ship.json with the library's loadShipMeta() before the game exists, using a module's top-level await (Listing 1). Only then is new Phaser.Game created (index.html:84).

Listing 1. Metadata before the game, index.html:34–39
34const q = new URLSearchParams(location.search);35const ship = await loadShipMeta(q.get("edition") || "conex", q.get("ship") || "yodacon");36const { frame_width: frameWidth, frame_height: frameHeight } = ship.sprite;37const stats = ship.meta.stats ?? {};38const MAX = (stats.Speed || 300) * 0.6, ACCEL = (stats.Accel || 300) * 0.6, TURN = (stats.Maneuver || 3) * 40;39const SCALE = frameWidth > 64 ? 1 : 2;               // whole numbers only

Phaser's loader then fetches the image itself in preload() (Listing 2). Phaser numbers a sheet's cells left to right, top to bottom, which is exactly the pack's order, so pack frame f is Phaser frame f. setCORS("anonymous") lets WebGL use the image when the pack is served from another origin, such as a CDN.

Listing 2. preload(), index.html:42–45
42preload() {43  this.load.setCORS("anonymous");                   // needed when the pack is on a CDN44  // sheet-alpha.png is a 6 × 6 grid; Phaser numbers its cells 0–35 the same way the pack does.45  this.load.spritesheet("ship", ship.urls.sheet, { frameWidth, frameHeight });

IV. Creating the Sprite

The ship is an Arcade physics sprite on texture "ship", frame 0 (nose up), scaled by a whole number (index.html:56). The game config's pixelArt: true (index.html:90) switches Phaser to nearest-neighbour sampling, without which the 1997 pixels blur when scaled. The 1997 Speed becomes the body's maximum speed (index.html:57).

V. Each Update: Frame, Then Force

Listing 3 is the whole asset logic of the scene. The heading turns continuously; the frame is

f = ⌊θ / 10⌋ mod 36(1)

and setFrame(f) shows it. The sprite's own rotation is never touched: the 36 frames already are the rotation, and rotating as well would turn the ship twice.

Listing 3. update(), index.html:63–77
63update(time, delta) {64  const dt = delta / 1000, k = this.keys, body = this.ship.body;65  if (k.left.isDown) this.heading -= TURN * dt;66  if (k.right.isDown) this.heading += TURN * dt;67  this.heading = Phaser.Math.Wrap(this.heading, 0, 360);6869  // Show the frame for the heading; never rotate the sprite itself —70  // the 36 frames already are the rotation.71  const frame = frameIndex(this.heading);72  this.ship.setFrame(frame);7374  // Phaser's angle 0 points right (+x); the pack's heading 0 points up.75  if (k.up.isDown) this.physics.velocityFromAngle(frame * 10 - 90, ACCEL, body.acceleration);76  else body.setAcceleration(0, 0);77  body.setDrag(k.down.isDown ? ACCEL : 0);          // drag only acts while not accelerating

Phaser measures angles from +x (pointing right), while the pack's heading 0 points up, so thrust uses the frame's heading minus 90°:

αPhaser = 10 f − 90°(2)

Using f rather than θ makes the ship accelerate exactly the way it appears to point.

VI. Handling From the 1997 Record

Speed, acceleration and turn rate come from ship.meta.stats (index.html:38): Speed and Accel scaled by 0.6 and Maneuver by 40°/s. Swap the ship in the query string and the handling changes with it.

VII. Using It in Your Own Phaser Game

  1. Read ship.json (with loadShipMeta or a plain fetch) before creating the game, or in a boot scene.
  2. this.load.spritesheet(key, url, { frameWidth, frameHeight }) on sheet-alpha.png.
  3. Set pixelArt: true and scale only by whole numbers.
  4. Each update, sprite.setFrame(frameIndex(heading)); move with (2).

For a ship that simply spins, Phaser's animation system works on the same frames:

Listing 4. A spin animation over the 36 frames
this.anims.create({ key: "spin", frameRate: 12, repeat: -1,
  frames: this.anims.generateFrameNumbers("ship",
                              { start: 0, end: 35 }) });
sprite.play("spin");

VIII. Conclusion

The scene uses one image and one JSON file. The only adaptations Phaser needs are loading the metadata first and the 90° offset in (2); frame numbering and pixel-art filtering line up with the pack directly.

References

  1. Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
  2. Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.
  3. Phaser Studio, Phaser 3 API Documentation, v3.80.1, 2024. [Online]. Available: docs.phaser.io