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
| Asset | Read at | Used for |
|---|---|---|
ship.json | index.html:35 | Frame size, stats, name |
sheet-alpha.png | index.html:45 | Phaser texture "ship", frames 0–35 |
frameIndex() | index.html:31 | Heading 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).
index.html:34–3934const 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.
index.html:42–4542preload() {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.
index.html:63–7763update(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
- Read
ship.json(withloadShipMetaor a plainfetch) before creating the game, or in a boot scene. this.load.spritesheet(key, url, { frameWidth, frameHeight })onsheet-alpha.png.- Set
pixelArt: trueand scale only by whole numbers. - Each update,
sprite.setFrame(frameIndex(heading)); move with (2).
For a ship that simply spins, Phaser's animation system works on the same 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
- Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
- Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.
- Phaser Studio, Phaser 3 API Documentation, v3.80.1, 2024. [Online]. Available: docs.phaser.io