Publish

Make games for PixelJS

Tutorial: make a single-player game for PixelJS

In this tutorial you turn the PixelJS starter into your own game and publish it on PixelJS with levels, leaderboards, stars, achievements and cloud saves. You need Node.js, a code editor and a Google account. Allow an afternoon for the first time.

The portal does the hard parts: player accounts, storing results, ranking, anti-cheat checks, achievements, saves on every device and the result messages under your game. Your game only tells the portal what happens: a level started, a level ended with this score.

1. Get the starter

  1. Download the starter game from the developer page and unzip it.
  2. In the folder, run:
npm install
npm run dev
  1. Open http://localhost:5173. You see a small platformer with three levels. Play it: the arrows or A/D move, Space jumps.

Outside the portal the game still works: every portal call answers at once, results are not recorded and saves last until you reload. The top of the screen says so.

2. Look around

File What it does
public/pixeljs.json The manifest: levels, leaderboards, achievements, saves. Read by the portal when you upload.
src/main.js Connects to the portal, starts the engine, handles pause and mute.
src/game.js Menus and play. Every portal call is marked PORTAL:.
src/levels.js The levels, drawn as text.
src/physics.js Movement in whole numbers, 60 steps per second.
src/progress.js The player's progress, from the cloud save and the portal.
src/portal-bridge.js The portal bridge. Keep it as it is.

3. Describe your game in pixeljs.json

The manifest says what the portal should expect. Open public/pixeljs.json:

{
  "manifest_version": 2,
  "min_age": 0,
  "width": 320, "height": 180, "scale_mode": "integer", "orientation": "landscape",
  "inputs": ["keyboard", "touch"],
  "controls_md": "- Arrows or A/D: move\n- Space: jump",
  "capabilities": ["pause", "mute", "levels", "scores", "achievements", "save", "level-select"],
  "worlds": [{ "id": "world-1", "name": "First Steps" }],
  "levels": [
    { "id": "1-1", "name": "Hello World", "world": "world-1" },
    { "id": "1-2", "name": "Up We Go", "world": "world-1", "par_time_ms": 45000 },
    { "id": "1-3", "name": "Secret Path", "world": "world-1", "hidden": true, "unlock": { "stars": 6 } }
  ],
  "leaderboards": [
    { "id": "level-score", "name": "Best score", "scope": "level", "sort": "desc", "type": "points",
      "min": 0, "max": 100000, "periods": ["all", "week"] },
    { "id": "fastest", "name": "Fastest", "scope": "level", "sort": "asc", "type": "time",
      "min": 1000, "max": 3600000, "periods": ["all", "week"] },
    { "id": "total", "name": "Total score", "scope": "game", "source": "sum_levels", "of": "level-score",
      "sort": "desc", "type": "points", "periods": ["all", "month", "week", "day"], "default": true }
  ],
  "achievements": [
    { "id": "first-steps", "name": "First Steps", "description": "Complete the first level.", "points": 10,
      "rule": { "complete": "1-1" } },
    { "id": "all-stars", "name": "All Stars", "description": "Collect every star.", "points": 50,
      "rule": { "stars": 9 } }
  ],
  "save": { "slots": 3, "max_bytes": 65536, "schema": 1 },
  "integrity": { "min_run_ms": 2000 }
}

What to change for your game:

  • min_age (required): 0 for everyone, 10 for ages 10 and up, 13 for teens (casino games are always 13). It becomes your game's age rating. Adult content is not accepted.
  • play_modes (optional): if your game can also be played by two people on the same device, or online, list the modes, for example ["solo", "local"] with "local_players": 2. The portal then asks players how they want to play. The multiplayer tutorial shows how.
  • levels: one entry per level, in display order. IDs are permanent: progress and results are stored by ID, so never rename an ID after publishing; add new levels with new IDs. A game without levels simply leaves levels out (section 9).
  • leaderboards: here each level has a best-score board and a fastest-time board, and the game has a total board that adds up each player's best level scores. The default board is the one shown on your game page, with its champion. Set min and max from your real game: staff check them, and values outside are refused.
  • achievements: a rule lets the server unlock the achievement from recorded results (complete a level, a world, every level, reach a score, collect stars). Rules cannot be faked from the browser, so use them whenever you can.
  • save: how many save slots the game uses and how big a save can be.

The full list of fields is in the reference.

4. Connect to the portal

src/main.js does this once, at startup:

import { connectPortal } from './portal-bridge.js';

const portal = await connectPortal({ engine: '@pixeljs/[email protected]' });
portal.loading(0, 2);            // the portal shows a loading bar
// ... create the engine, load your images and sounds ...
portal.on('pause', () => engine.pause());
portal.on('resume', () => engine.resume());
portal.on('mute', ({ muted }) => engine.audio.setVolume(muted ? 0 : 1));
portal.on('visibility', () => game.releaseKeys());     // forget keys held when the tab was left
portal.on('select', ({ level }) => game.selectLevel(level));
portal.ready();                  // the game accepts input

The portal pauses your game when its page is hidden, mutes it from its own button and can open a level from a link (?level=1-2).

5. Report every attempt

A run is one attempt at one level. Start it when the player starts playing (never on menus) and end it when the level ends:

const run = portal.levelStart('1-2');     // a declared level ID

// ... the player plays ...

const answer = await portal.levelEnd(await run, {
  outcome: 'complete',                    // "complete", "fail" or "quit"
  scores: { 'level-score': 4200, fastest: 51320 },
  timeMs: 51320,                          // game time, without pauses
  stars: 3,                               // 0 to 3
  stats: { coins: 37, deaths: 1 },        // optional numbers for your statistics
});
if (answer.newBest && Object.values(answer.newBest).some(Boolean)) showNewBest();

Rules that keep your leaderboards fair:

  • scores are whole numbers, keyed by leaderboard ID, within each board's min and max. Only send the boards that apply (a failed level usually has no time).
  • timeMs is game time: count your fixed updates (frames × 1000 / 60). It can never be more than the real time since levelStart.
  • When the player leaves a level, end it with outcome: "quit". Quits are never ranked.

The portal shows its own message under the game: the result, "New best!", the player's rank and new achievements. For guests it offers "Sign in to save this score" and keeps the result for 10 minutes. Do not build name entry or ranking tables.

6. Show progress in your level menu

const progress = await portal.levels();
// { "1-1": { "completed": true, "stars": 3, "best": { "level-score": 4200, "fastest": 49800 } } }

The starter uses it to draw stars and to lock the secret level until the player has 6 stars. The portal's own level list (under the game) follows the unlock rules of your manifest; your game enforces its own.

7. Save the player's data

const { data, rev } = await portal.load('slot-1');          // data is a string, or null
const state = data ? JSON.parse(data) : newGame();
// ... later, at a checkpoint or the end of a level:
const saved = await portal.save('slot-1', JSON.stringify(state), { rev });
if (!saved.ok && saved.reason === 'conflict') { /* saved on another device: load, merge, save again */ }

Signed-in players get their saves on every device; guests keep them in the browser, and the portal offers to move them into the account when they sign in. Save at checkpoints, not every frame.

8. Achievements without a rule

For something a rule cannot express (for example "finish a level without being hit"), declare the achievement without rule and unlock it from the game:

await portal.unlock('untouched');   // during the run, or within 10 seconds after levelEnd

9. A game without levels

For an endless arcade game, leave levels out of the manifest (the game gets one implicit level called main), declare one leaderboard such as high-score with "default": true, and report each game:

const run = await portal.levelStart();                    // when a game starts
// ... until the player loses ...
await portal.gameOver({ scores: { 'high-score': score }, timeMs });

10. Build and package

npm run build
cd dist && zip -r ../my-game.zip . && cd ..

The ZIP must have index.html and pixeljs.json at its root. Everything loads from your own files with relative URLs: games on PixelJS cannot make network requests or use browser storage.

11. Publish

  1. Sign in at app.pixeljs.com and create your channel (adults; you sign the Creator Agreement).
  2. Create a game, fill in its page (title, description, category, up to five sub-genres, cover and preview clip) and upload your ZIP.
  3. The upload is checked. If pixeljs.json has a problem, the studio shows it with its place in the file, for example leaderboards[1].max: must be greater than min.
  4. Open the preview. It runs in test mode: results are checked like on the public page and kept only on test leaderboards. The bridge inspector below the game lists every message between your game and the portal.
  5. Submit the version. Staff review it; once approved, your game is live with its leaderboards, champion, level list and achievements.

After publishing, the studio shows per-level statistics: how many players start, complete and fail each level, and the median time. When you add levels later, players who played your game are told.

12. Before you submit

Go through the review checklist of the reference, and play your game once signed in and once as a guest.