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
- Download the starter game from the developer page and unzip it.
- In the folder, run:
npm install
npm run dev
- 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):0for everyone,10for ages 10 and up,13for teens (casino games are always13). 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 leaveslevelsout (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. Thedefaultboard is the one shown on your game page, with its champion. Setminandmaxfrom your real game: staff check them, and values outside are refused.achievements: arulelets 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:
scoresare whole numbers, keyed by leaderboard ID, within each board'sminandmax. Only send the boards that apply (a failed level usually has no time).timeMsis game time: count your fixed updates (frames × 1000 / 60). It can never be more than the real time sincelevelStart.- 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
- Sign in at app.pixeljs.com and create your channel (adults; you sign the Creator Agreement).
- Create a game, fill in its page (title, description, category, up to five sub-genres, cover and preview clip) and upload your ZIP.
- The upload is checked. If
pixeljs.jsonhas a problem, the studio shows it with its place in the file, for exampleleaderboards[1].max: must be greater than min. - 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.
- 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.