← Yodacon Ship Pack · Sample projects · Playground

Examples

One small page for each way of drawing the ships. Each loads one ship (any ?edition=&ship= from the catalogue works) and lets you turn or fly it. Frameworks are pinned to exact versions on jsDelivr; nothing needs installing. Every folder has a README that walks through the code, and the tutorial note below explains how to start your own project.

Want more? The demo game is the full version, the sample projects are complete little apps, and the playground runs any of this in your browser.

Yodacon Ship Pack · Tutorial Note 1 · October 2026

From Sprite Sheet to Moving Ship: Starting a Game With the Yodacon Ship Pack

Abstract—The Yodacon Ship Pack publishes the starships of the 1997–1998 ConEx Escape Velocity plugins as GPL-licensed PNG sprite sheets with a JSON description of each ship. This note shows a newcomer how to start a project with them: where the files are, the one convention that governs every sprite, the three functions that turn a heading into pixels on screen, and how to carry those three functions into an engine of their own. Every listing is taken from, and cites, the pack's own code.

Index Terms—sprite sheets, 2-D rendering, game loops, pixel art, asset pipelines.

I. Introduction

A ship in a 1990s top-down space game is not rotated at run time. It is drawn 36 times in advance, once per 10° of heading, and the game shows whichever drawing is closest to the way the ship points. The pack keeps that design [1]. Rendering a ship is therefore not image rotation but frame selection: compute one integer from the heading, find that frame's rectangle in a sheet, and copy the rectangle to the screen. The rest of this note builds those steps (Section III), puts them in a game loop (Section IV), moves them to other engines (Section V), and adds your own content (Section VI).

II. What Is in the Pack

Each ship lives in a folder <edition>/ships/<slug>/, where the edition is conex (1997), override (1998) or konex. Table I lists the files. catalog.json at the root lists every edition and ship, and is the only file a program needs to discover the rest [2].

Table I. Files in one ship folder
FileUse
sheet-alpha.png36 frames, 6 × 6, transparent; the file to draw from
sheet.png, mask.pngThe 1997 opaque sheet and its 1-bit mask
frames/NN.pngThe same 36 frames as single files, 00–35
target, yard, comm, thumbTargeting, shipyard, comm and thumbnail pictures
ship.jsonName, frame size, file names and the 1997 statistics

The part of ship.json a renderer needs is the sprite object (Listing 1). Frame sizes differ between ships (36 to 120 pixels), so never hard-code them; read them here.

Listing 1. The sprite block of conex/ships/dart/ship.json
"sprite": { "frame_width": 80, "frame_height": 80,
            "columns": 6, "rows": 6, "frames": 36,
            "rotation": "frame 00 nose-up, 10 degrees
                         clockwise per frame" }

III. The Core: Heading to Pixels

A. The Convention

Heading θ is measured in degrees clockwise from straight up, which matches screen coordinates whose y axis points down. Frame 00 is nose-up and each following frame is 10° further clockwise [3, lines 12–17]. The frame to show and its place in the sheet follow directly:

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

x = (f mod 6) · w,   y = ⌊f / 6⌋ · h(2)

where w and h are frame_width and frame_height. The modulo in (1) must wrap negative headings too, which JavaScript's % does not; Listing 2 corrects for it.

Listing 2. Frame selection, lib/yodacon-ship-pack.js:115 and :131
export function frameIndex(headingDeg) {
  const i = Math.floor(headingDeg / 10) % FRAMES;
  return i < 0 ? i + FRAMES : i;      // -10° → 35
}
export function frameRect(ship, i) {
  const { w, h, columns } = spriteOf(ship);
  const f = frameIndex(i * 10);
  return { x: (f % columns) * w,          // (2)
           y: Math.floor(f / columns) * h, w, h };
}

B. Loading

A ship is its ship.json plus one decoded image. loadShip() fetches both (Listing 3) and caches the image, so ten ships of one kind cost one download [3, line 56].

Listing 3. Loading, lib/yodacon-ship-pack.js:156
export async function loadShip(edition = "conex",
    slug = "yodacon", base = PACK_BASE) {
  const ship = await loadShipMeta(edition, slug,
                                  base, "ships");
  ship.sheet = await loadImage(ship.urls.sheet);
  return ship;
}

C. Drawing

Drawing copies the rectangle from (2) to a point centred on the ship (Listing 4). Two details matter for 1997 pixel art: turn off image smoothing, and round the destination to whole pixels; scale only by whole numbers, or the pixels smear.

Listing 4. Drawing, lib/yodacon-ship-pack.js:182
export function drawShip(ctx, ship, headingDeg,
                         x, y, scale = 1) {
  const r = frameRect(ship, frameIndex(headingDeg));
  const w = r.w * scale, h = r.h * scale;
  ctx.imageSmoothingEnabled = false;  // crisp pixels
  ctx.drawImage(ship.sheet, r.x, r.y, r.w, r.h,
    Math.round(x - w / 2), Math.round(y - h / 2), w, h);
}

IV. A First Game Loop

With Listings 2–4, a flyable ship is a page and some 40 lines. Create index.html beside a copy of the pack (or import the library from yodacon.org or jsDelivr [3, lines 6–8]) and serve the folder with any static server, for example python3 -m http.server; ES modules do not load from file://. Listing 5 is the loop of the Canvas example [4]: turn with the arrow keys, thrust along the frame shown so motion matches the picture, integrate by the frame time, and draw.

Listing 5. Game loop, after examples/canvas/index.html:34–84
import { loadShip, drawShip }
  from "./yodacon-ship-pack/lib/yodacon-ship-pack.js";
const ship = await loadShip("conex", "dart");
const s = { x: 380, y: 260, vx: 0, vy: 0, heading: 0 };
const keys = {};
onkeydown = (e) => keys[e.key] = true;
onkeyup   = (e) => keys[e.key] = false;
let last = performance.now();
requestAnimationFrame(function frame(now) {
  const dt = Math.min(0.05, (now - last) / 1000);
  last = now;
  if (keys.ArrowLeft)  s.heading -= 180 * dt;
  if (keys.ArrowRight) s.heading += 180 * dt;
  s.heading = (s.heading + 360) % 360;
  if (keys.ArrowUp) {          // along the frame shown
    const a = Math.floor(s.heading / 10) * 10
              * Math.PI / 180;
    s.vx += Math.sin(a) * 200 * dt;    // x: sin θ
    s.vy -= Math.cos(a) * 200 * dt;    // y: −cos θ
  }
  s.x += s.vx * dt;  s.y += s.vy * dt;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawShip(ctx, ship, s.heading, s.x, s.y, 1);
  requestAnimationFrame(frame);
});

The unit vector for heading θ under this convention is (sin θ, −cos θ). The demo and the flying games in projects/ (Orbital Defence, Slingshot, ConEx Courier) use it, so they are worked examples of the same loop.

V. Moving to Another Engine

Each example page ends with its own note, Tutorial Notes 2–10 [8]–[16], which walks through that page's source line by line: the DOM and CSS sprite, Canvas 2D, the <yodacon-ship> element, three.js, Phaser 3, PixiJS, SVG, CSS with no script, and a worker with OffscreenCanvas. This section gives the pattern they share.

Nothing above depends on the browser except drawImage. An engine needs three things: the sheet as a texture, the frame grid from Listing 1, and Equation (1). Do not also rotate the sprite; the frame already shows the rotation.

A. Phaser

Phaser slices the sheet itself when given the frame size, and the frame number is (1) [5], [12]:

Listing 6. examples/phaser/index.html:45 and :71–72
this.load.spritesheet("ship", ship.urls.sheet,
                      { frameWidth, frameHeight });
// each update:
this.ship.setFrame(frameIndex(this.heading));

B. Godot 4

A Sprite2D with the sheet as its texture slices a grid through hframes and vframes; the frame is (1) again. Turn off filtering on the texture for crisp pixels.

Listing 7. GDScript for a Sprite2D
extends Sprite2D
var heading := 0.0       # degrees clockwise from up
func _ready():
    texture = load("res://ships/dart/sheet-alpha.png")
    hframes = 6; vframes = 6
    texture_filter = TEXTURE_FILTER_NEAREST
func _process(delta):
    heading = fposmod(heading + Input.get_axis(
        "ui_left", "ui_right") * 180.0 * delta, 360.0)
    frame = int(heading / 10.0) % 36

C. Any Other Engine

In three.js, pygame, raylib, SDL, Unity or Bevy the pattern is the same: load sheet-alpha.png, compute the source rectangle with (2), and draw it. In UV terms the rectangle is u = (f mod 6)/6, v = ⌊f/6⌋/6 with size 1/6 × 1/6, flipping v in engines whose texture origin is the bottom left [6]. If an engine cannot take a sheet, use the single files frames/NN.png and index them by f.

VI. Building On It

A. Finding Ships

Read catalog.json instead of hard-coding slugs, so new ships appear in your game when the pack grows. loadCatalog() [3, line 77] returns every edition with every ship's file URLs.

B. Using the 1997 Numbers

ship.json keeps each ship's original Escape Velocity statistics under stats: Speed, Accel, Maneuver, Shield, Armor, Holds and more. Scale them into your units rather than inventing new ones; Fleet Tactics and ConEx Courier in projects/ show two such mappings in their READMEs.

C. Licence and Credit

The art is GPL-2.0-or-later. Keep the credit line (CREDIT in the library) in your game, and if you ship it, add it to the pack's leaderboard through USED-IN.md [7].

VII. Conclusion

Rendering a pre-rotated sprite comes down to Equation (1), Equation (2) and one copy of a rectangle. Those three steps are about a dozen lines in the pack's library and carry unchanged into any engine. Start from Listing 5, swap in your engine's draw call, and the 1997 fleet is flying in your project.

References

  1. Yodacon Ship Pack, “README,” 2026. [Online]. Available: github.com/yodacon/yodacon-ship-pack
  2. Yodacon Ship Pack, catalog.json, v2.0.0, 2026.
  3. Yodacon Ship Pack, lib/yodacon-ship-pack.js, v2.0.0, 2026.
  4. Yodacon Ship Pack, “Canvas 2D example,” examples/canvas/, 2026.
  5. Yodacon Ship Pack, “Phaser example,” examples/phaser/, Phaser 3.80.1, 2026.
  6. Yodacon Ship Pack, “three.js example,” examples/threejs/, three.js 0.160.0, 2026.
  7. Yodacon Ship Pack, USED-IN.md and CONTRIBUTING.md, 2026.
  8. Yodacon Ship Pack, “A ship in plain HTML and CSS,” Tutorial Note 2, examples/dom/, 2026.
  9. Yodacon Ship Pack, “A game loop on Canvas 2D,” Tutorial Note 3, examples/canvas/, 2026.
  10. Yodacon Ship Pack, “Ships as HTML: the yodacon-ship element,” Tutorial Note 4, examples/web-component/, 2026.
  11. Yodacon Ship Pack, “Pre-rotated sprites in 3-D,” Tutorial Note 5, examples/threejs/, 2026.
  12. Yodacon Ship Pack, “One scene, two assets: the Phaser 3 example,” Tutorial Note 6, examples/phaser/, 2026.
  13. Yodacon Ship Pack, “Thirty-six textures on one image,” Tutorial Note 7, examples/pixi/, 2026.
  14. Yodacon Ship Pack, “A viewport per ship: the SVG example,” Tutorial Note 8, examples/svg/, 2026.
  15. Yodacon Ship Pack, “Spinning without script: the CSS-only example,” Tutorial Note 9, examples/css/, 2026.
  16. Yodacon Ship Pack, “Drawing off the main thread: the OffscreenCanvas example,” Tutorial Note 10, examples/offscreen/, 2026.

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