Skip to content

YouTube Playables

Olivier Biot edited this page Nov 13, 2026 · 1 revision

YouTube Playables

YouTube Playables are games played right inside the YouTube app and website. Google lists melonJS among the engines that can make them.

This guide covers melonJS 20.15 or later, which added what a Playable needs: save.setStorage(), save.createCloudStorage() and device.setFocusEvents(). See Platforms for the pattern shared with the other portals.

The SDK

The Playables SDK must load before any game code, so it goes first in index.html:

<script src="https://www.youtube.com/game_api/v1"></script>

It defines a global ytgame. Outside YouTube it does nothing, so check where the game runs before using it:

const inPlayables = typeof ytgame !== "undefined" && ytgame.IN_PLAYABLES_ENV;

What a Playable must do, in melonJS

Save progress in YouTube's cloud save

A Playable may only keep the player's progress in YouTube's cloud save, and must load it before writing to it. Load it before the game starts, then hand it to save:

const cloud = save.createCloudStorage({
    data: await ytgame.game.loadData(),
    write: (items) => ytgame.game.saveData(JSON.stringify(items)),
});
save.setStorage(cloud);
ytgame.system.onPause(() => cloud.flush());   // save at once when YouTube pauses the game

The game then just uses save (save.add(), then plain assignments). The storage keeps everything in memory and saves in the background, one save at a time. The save is limited to 3 MiB.

Pause only when YouTube says so

A Playable must stop completely when YouTube calls onPause (game loop, rendering, sound and input), resume only on onResume, and must not pause on the browser's own focus or visibility events:

device.setFocusEvents(false);   // the browser window no longer reports focus
app.stopOnBlur = true;          // a pause stops the loop and the rendering
app.pauseOnBlur = false;
ytgame.system.onPause(() => event.emit(event.BLUR));
ytgame.system.onResume(() => event.emit(event.FOCUS));

setFocusEvents() decides who reports that the player left; the flags decide what the game does. See Platforms.

Follow YouTube's mute

YouTube's mute always wins over the game's sound:

const setSound = (enabled) => (enabled ? audio.unmuteAll() : audio.muteAll());
setSound(ytgame.system.isAudioEnabled());
ytgame.system.onAudioEnabledChange(setSound);

YouTube is then the mute switch: don't add a mute button of your own. Separate music and effects volumes are fine.

Say when the game is ready

YouTube shows a spinner until ytgame.game.firstFrameReady(), which must come while a loading screen is visible. Then it waits for ytgame.game.gameReady(), which must only come once the game can be played.

// the first frame of melonJS's loading screen
const onFrame = () => {
    if (state.isCurrent(state.LOADING)) {
        event.off(event.GAME_AFTER_DRAW, onFrame);
        ytgame.game.firstFrameReady();
    }
};
event.on(event.GAME_AFTER_DRAW, onFrame);
await loader.preload(resources);

// then, in the first playable screen's onResetEvent():
ytgame.game.gameReady();

Start from a fixed size

YouTube can load the game in a frame that has no height yet. new Application(window.innerWidth, window.innerHeight, ...) then fails with "width or height was zero". Start from a fixed size, and let the scaling fill the frame once it has its real size:

const app = new Application(360, 640, { parent: "screen", scaleMethod: "flex" });

Score, ads and language

  • ytgame.engagement.sendScore({ value }) takes a whole number. The best score sent must match the best score in the save.
  • ytgame.ads.requestInterstitialAd() and requestRewardedAd(rewardId) can fail: carry on when they do. A rewarded ad uses a fixed ID per kind of reward, and the reward is only given when it resolves to true.
  • ytgame.system.getLanguage() gives the player's YouTube language ("fr-FR"), which a Playable should use rather than the browser's.

Input

A Playable must work with touch and a mouse, and should work with a keyboard. melonJS's UI buttons handle all three: give one the focus with button.focus(), and the arrow keys move between buttons while Enter or Space presses them.

Testing

The SDK does nothing outside YouTube. Google's SDK Test Suite loads your local build, checks the integration as it loads, logs every SDK call, and lets you pause, mute, change the language, clear the save and answer ad requests.

  1. Build the game and serve the build, for example with vite build then vite preview.
  2. Open the Test Suite and enter your local address, such as http://localhost:8080.
  3. Chrome asks whether the page may access your local network: allow it.

Test the production build: a development server serves files such as /@vite/client, whose names fail the Test Suite's file name check.

YouTube's security policy

YouTube serves a Playable with a strict Content-Security-Policy: scripts only from the game and the SDK, and network requests only to the game's own origin. Google's Test Suite guide gives the exact header. Sending it from your local server (with Vite, server.headers and preview.headers) makes anything YouTube would block fail in your console first.

Publishing

  • Upload the build in the Playables Developer Portal. Build with relative paths (base: "./" with Vite).
  • Sizes: under 30 MiB downloaded before gameReady() (15 MiB recommended), each file under 30 MiB (512 KiB recommended), 250 MiB in total.
  • File names: letters, digits, -, _ and . only.

Known issues

Symptom Cause
"width or height was zero, Canvas could not be initialized" at start the application was sized from a frame that has no height yet: start from a fixed size
Cannot read properties of null (reading 'getItem') at boot (before 20.15) the Test Suite's frame has localStorage set to null; fixed in melonJS 20.15
the game pauses when you switch tabs the browser window still reports focus: device.setFocusEvents(false)
"filenames must only contain supported characters" in the Test Suite a development server: test the production build
the Test Suite never shows the game Chrome blocked the local address: allow local network access when asked

See also

Clone this wiki locally