Repository navigation
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 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;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 gameThe 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.
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.
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.
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();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" });-
ytgame.engagement.sendScore({ value })takes a whole number. The best score sent must match the best score in the save. -
ytgame.ads.requestInterstitialAd()andrequestRewardedAd(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 totrue. -
ytgame.system.getLanguage()gives the player's YouTube language ("fr-FR"), which a Playable should use rather than the browser's.
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.
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.
- Build the game and serve the build, for example with
vite buildthenvite preview. - Open the Test Suite and enter your local address, such as
http://localhost:8080. - 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 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.
- 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.
| 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 |
- Playables documentation and publishing requirements
- Google's web-game-samples: templates for several engines
- Platforms