Skip to content
Olivier Biot edited this page Nov 14, 2026 · 2 revisions

Poki

Poki is a website of free browser games, played on desktop and mobile. Every game on it is hand-picked: developers work with Poki through Poki for Developers, which playtests the game with real players before releasing it. Poki asks for web exclusivity: the game can't also be on other web portals, but Steam, app stores and consoles are fine.

This guide covers melonJS 20.15 or later with Poki's HTML5 SDK (v2). See Platforms for the pattern shared with the other portals.

The SDK

Load the SDK in index.html, before the game:

<script src="https://game-cdn.poki.com/scripts/v2/poki-sdk.js"></script>

It defines a global PokiSDK. Calls made before it has finished loading are queued, so the game can use it straight away.

On localhost, the SDK runs in debug mode and logs each call in the console, such as POKI: PokiSDK.gameplayStart().

Starting the game

import { Application, audio, input, loader, state, video } from "melonjs";
import { PlayScreen } from "./screens/play.js";
import { resources } from "./resources.js";

// the game starts even when the SDK can't, behind an ad blocker for one
try {
    await PokiSDK.init();
} catch {
    console.warn("Poki SDK failed to start");
}

const app = new Application(1280, 720, { parent: "screen", scaleMethod: "fit", renderer: video.AUTO });
await app.init();

// the arrow keys and Space must not scroll Poki's page
input.bindKey(input.KEY.UP, "up");
input.bindKey(input.KEY.DOWN, "down");
input.bindKey(input.KEY.LEFT, "left");
input.bindKey(input.KEY.RIGHT, "right");
input.bindKey(input.KEY.SPACE, "accept");

await loader.preload(resources);
PokiSDK.gameLoadingFinished();

state.set(state.PLAY, new PlayScreen());
state.change(state.PLAY, true);

The sections below explain each part.

What Poki asks for, in melonJS

Say when the game is played

  • PokiSDK.gameLoadingFinished() once the game has loaded, as above.
  • PokiSDK.gameplayStart() on the player's first input, not when the game loads, and again each time play resumes.
  • PokiSDK.gameplayStop() at every break: the game's own pause, a menu, the end of a level, a death.

Never send the same one twice in a row, and none while an ad plays.

// a level starts: the player pressed Play
PokiSDK.gameplayStart();

// the level is over
PokiSDK.gameplayStop();
await showAd("commercial");    // the player chose to play again
PokiSDK.gameplayStart();

A game played with the keyboard must pause and resume with Escape or Space, and send the same calls:

import { event, input, state } from "melonjs";

input.bindKey(input.KEY.ESC, "pause");
// on the key's release: a key held down repeats its KEYDOWN
event.on(event.KEYUP, async (action) => {
    if (action !== "pause") {
        return;
    }
    if (!state.isPaused()) {
        PokiSDK.gameplayStop();
        state.pause(true);
    } else {
        await showAd("commercial");
        state.resume(true);
        PokiSDK.gameplayStart();
    }
});

Ads

PokiSDK.commercialBreak() asks for an ad at a natural break, just before going back to play. PokiSDK.rewardedBreak() shows one the player chose to watch, for a reward; it resolves true when they watched it. Poki decides when an ad is due, so most commercialBreak() calls show nothing and resolve straight away.

While an ad plays, the game must be paused and silent, and its buttons must not answer. Both calls take a function, which Poki calls when the ad starts. Pause and mute there, and undo it only if it ran:

import { audio, state, UIBaseElement } from "melonjs";

// play an ad with the game paused and silent: resolves true once a rewarded ad played
async function showAd(type) {
    let mutedBefore;   // set once the ad starts
    let pausedBefore;
    let focused;
    const adStarted = () => {
        mutedBefore = audio.muted();
        pausedBefore = state.isPaused();
        audio.muteAll();
        state.pause(true);
        // a paused game still takes key presses: Enter would press the focused button
        focused = UIBaseElement.focusOwner;
        focused?.blur();
    };
    const adEnded = () => {
        if (mutedBefore !== undefined) {
            // a game already paused, by its pause menu say, stays paused
            if (!pausedBefore) {
                state.resume(true);
            }
            if (!mutedBefore) {
                audio.unmuteAll();
            }
            focused?.focus();
        }
    };
    const rewarded = type === "rewarded"
        ? await PokiSDK.rewardedBreak(adStarted)
        : await PokiSDK.commercialBreak(adStarted);
    adEnded();
    return rewarded === true;
}
// a rewarded ad, from a button that says it shows one
PokiSDK.gameplayStop();
if (await showAd("rewarded")) {
    progress.lives += 1;
}
PokiSDK.gameplayStart();
  • state.pause(true) stops the game and its music, audio.muteAll() the rest. Poki doesn't touch the game's sound: muting it is the game's job.
  • When no ad plays, Poki never calls adStarted, so nothing was paused and nothing is undone. A sound the player had muted stays muted after an ad, and a game paused before the ad stays paused.
  • A tab switch during the ad doesn't resume the game under it: on focus, melonJS only resumes a pause it made itself.
  • If the game locks the pointer, release it in adStarted with input.exitPointerLock().
  • A reward button carries a 🎬 icon. Next to it, at the same time and at least as large, there must be a plain way to continue. Give the reward only when showAd("rewarded") resolves true, so never behind an ad blocker.
  • Don't add ad timers of your own: Poki decides how often.

Saving progress

melonJS's save keeps the player's progress in localStorage, which works on Poki: use it as it is.

  • Where localStorage can't be used, in some private windows for one, save keeps the progress in memory and the game still runs. Poki asks for exactly that.
  • For players logged in to Poki, Poki's User Accounts copy localStorage to their account and restore it on their other devices, with nothing to add to the game. That copy is limited to 1 MB once compressed.

Keyboard, mouse and touch

The game runs in a frame on a page that scrolls, and the arrow keys and Space must not scroll that page. melonJS stops the browser's own action for the keys bound with input.bindKey(). The UI's keyboard focus reads the arrow keys and Space without binding them, so bind them, as Starting the game does. Space still presses the focused button. Leave input.setPreventDefault() at its default, true: a key bound after input.setPreventDefault(false) scrolls the page.

The mouse wheel over the canvas doesn't scroll the page, once melonJS listens to the pointer (UI widgets and pointer events do).

The game must work on desktop, mobile and tablet, with the controls of each: touch controls on a phone or a tablet.

Screen size

On desktop, Poki shows the game in a 16:9 frame: 640 x 360, 836 x 470 or 1031 x 580. A 16:9 game with scaleMethod: "fit" fills each of them. On a phone, the game must fill the screen in portrait, in landscape, or both:

  • With "fit", a 16:9 game fills a phone held sideways (a long phone shows narrow bars at the sides), and is a strip across the top of one held upright.
  • To fill both, use a scale method that resizes the game's view, such as "flex", and lay the game out from app.viewport's size.

Language

PokiSDK.getLanguage() gives the player's language as a two-letter code, such as "fr" for fr-FR. Start in that language when the game has it, and let the player change it.

Files and size

  • Bundle everything: fonts, images, sounds and libraries. Poki blocks requests to other sites, except a few it approves, such as a multiplayer server.
  • Poki recommends no more than 5 MB before the game starts and 8 MB in all. A small melonJS game with one MP3 measured 708 KB in the Inspector.
  • Build with relative paths (base: "./" with Vite), and leave out debug code.

Testing

On your own machine

On localhost, the SDK runs in debug mode: build the game, serve the build (vite build then vite preview) and open it. Each SDK call is logged in the console. Ads mostly don't play there: test them in the Inspector.

In the Poki Inspector

The Poki Inspector runs the game as Poki will. It needs no account:

  1. Open it, and drop the build folder (dist/, with index.html in it) on "Upload your game". The Inspector uploads it to Poki's test server and runs it from there.
  2. Play the game. The Event Log lists each SDK call as it happens: "Game loading finished", "Gameplay start", "Commercial break", "Rewarded break".
  3. Tick Force show ads to get an ad on every commercialBreak() and rewardedBreak(): check that the game pauses and goes silent, and that Space and Enter do nothing until the ad is over.
  4. Go through the QA Modules on the left: 22 checks in six groups, from the SDK events to mobile and ads. The Inspector ticks some itself, such as "Has the game initialized the SDK?"; tick the others as you try them.
  5. Game details under the game show the loading time and the initial download size.
  6. Scaling Tests shows the game at Poki's desktop sizes and on phones and tablets, upright or rotated.
  7. Warnings lists external requests, images that could be smaller, and anything that gets in the way of the SDK.
  8. Mobile shows a QR code that opens the game on your phone, with its events in the log. Switching to it starts the session again.

Publishing

  1. Sign in to Poki for Developers and add the game. Upload a build from the game's "Versions" tab, or with Poki's command-line tool.
  2. Run it through the Inspector, then ask for a playtest. The build doesn't need to be finished.
  3. Poki playtests it with players, then reviews it before release.

Known issues

Symptom Cause
the game's buttons answer Space or Enter during an ad a paused game still takes key presses: blur the focus while the ad plays, as showAd() above does
the arrow keys scroll Poki's page (and the Inspector's) while they move the UI focus the keys are not bound, or were bound after input.setPreventDefault(false): bind them with input.bindKey(), with input.setPreventDefault() left on
the game is frozen and silent in the Inspector after a click on the Inspector's panels melonJS pauses the game when its frame loses the focus (pauseOnBlur): click the game and it resumes
on a phone held upright, the game is a strip across the top a 16:9 game with scaleMethod: "fit": use a scale method that resizes the view, such as "flex"
on localhost, rewardedBreak() resolves false straight away, with "POKI: a video ad is already running" the ad asked for just before is still pending: test ads in the Inspector, with "Force show ads"
the Inspector's Warnings show "Image optimization" with a percentage and a file that image can be made that much smaller: compress it

See also

Clone this wiki locally