Creator best practices

How to build web games that load instantly and play well on PLAY, on computers and phones alike. Everything here comes from how PLAY actually checks, hosts and runs games. Already know the rules? Jump to the prompts for your AI tools.

The short checklist

What you can build with

PLAY runs anything a modern browser runs as a static page. These are recognised and tested automatically:

Plain HTML5 and JavaScript

Canvas, WebGL, DOM and text games, including a single .html file. Phaser, PixiJS, Three.js, Babylon.js and PlayCanvas all work.

JavaScript projects

Upload the built folder, or the project itself: PLAY installs and builds npm, Vite and Next.js projects for you (Vite with relative paths, Next.js as a static export). Builds have 12 minutes.

Godot

Godot 4 web exports with Thread Support off, or upload the project folder with project.godot and PLAY exports it for you. Godot 3 web exports work too.

Unity, GameMaker, Construct

Upload the exported web build: Unity WebGL, GameMaker HTML5, Construct 3 Web (HTML5). PLAY can't build these from project files and will tell you how to export.

Twine, Ink, RPG Maker and others

Any tool with an HTML export. RPG Maker's www folder and similar export folders are found automatically.

Rust, C++ and WebAssembly

Build for the web (Emscripten, trunk, wasm-pack) single-threaded and upload the folder with index.html. Python games can be packaged with pygbag.

Not supported: anything that needs a server you run (PHP, Node servers, databases), desktop or phone app builds (.exe, .apk, .ipa), and multi-threaded builds that need SharedArrayBuffer. For multiplayer, use PLAY lobbies in the SDK.

Packaging and upload

Design for every screen size

Your game runs inside the PLAY player, which can be anything from an upright phone to a wide monitor in full screen. PLAY rescues fixed-size games by scaling them to fit, but a game that fills its own window always looks and plays better.

<meta name="viewport" content="width=device-width, initial-scale=1">
<style>html, body { margin: 0; height: 100%; overflow: hidden; background: #000; }
canvas { display: block; width: 100%; height: 100%; touch-action: none; }</style>
<canvas id="game"></canvas>
<script>
const canvas = document.getElementById("game");
function fit() {
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
  canvas.width = Math.round(canvas.clientWidth * dpr);
  canvas.height = Math.round(canvas.clientHeight * dpr);
  // re-layout your UI and on-screen controls here
}
addEventListener("resize", fit);
fit();
</script>

Controls that work on desktop and phones

PLAY adapts many games to touch screens automatically, but games designed for both play far better. Here is what PLAY does for you, and what to build yourself.

What PLAY does automatically

PLAY's adapters switch off when your game handles touch or pointer events itself, so it never fights your own controls. If you add touch support, make it complete.

What to build

const pointers = new Map();
canvas.addEventListener("pointerdown", (e) => { canvas.setPointerCapture(e.pointerId); pointers.set(e.pointerId, e); startAudio(); });
canvas.addEventListener("pointermove", (e) => { if (pointers.has(e.pointerId)) pointers.set(e.pointerId, e); });
canvas.addEventListener("pointerup", (e) => pointers.delete(e.pointerId));
canvas.addEventListener("pointercancel", (e) => pointers.delete(e.pointerId));
const isTouch = matchMedia("(pointer: coarse)").matches;  // show on-screen buttons

Sound

const audio = new (window.AudioContext || window.webkitAudioContext)();
function startAudio() { if (audio.state !== "running") audio.resume(); }
addEventListener("keydown", startAudio, { once: true });

Speed and memory

PLAY tests every upload and gives it an Instant Play Score. The best scores go to games that:

Phones have much less memory than computers, and iPhone Safari reloads pages that use too much. Keep textures at 2048 pixels or smaller, use compressed textures in Unity and Godot, reuse objects instead of creating new ones every frame, and free levels you've left.

Saves and progress

async function save(data) {
  try { await PLAY.storage.save(data); } catch { localStorage.setItem("save", JSON.stringify(data)); }
}
async function load() {
  try { return await PLAY.storage.load(); } catch { return JSON.parse(localStorage.getItem("save") || "null"); }
}

What the sandbox blocks

Every game runs in a sandboxed frame on its own domain, so it can't reach players' accounts or other games. That means:

Test before you publish

Prompts for your AI tools

Building with Claude, ChatGPT, Cursor, Copilot or another AI tool? Copy one of these into it. Each carries PLAY's rules, so the game it writes loads and plays well here. You can also start in Create on PLAY.

Build a new game for PLAY

Starting from nothing. Replace the bracketed part with your idea.

You are helping me build a browser game that I will publish on PLAY, an instant-play web game platform where people play on both computers and phones.

My game idea: [describe your game in a few sentences].

Build it as a single-page HTML5 game (plain JavaScript and a <canvas>, or Phaser bundled into the build) that follows every rule below. Keep it in as few files as practical: index.html, a JS file, and an assets folder.

PLAY platform rules (playsmorgi.com):
- Output a static web build: an index.html at the top of the folder plus its files. No server code, no PHP, no backend. All paths relative ("./assets/hero.png", never "/assets/hero.png" or "C:\..."), and file names must match their case exactly.
- No outside network at play time. Every library, font, image, sound and data file ships inside the build. Don't call external APIs, analytics, ad networks or WebSocket servers; they are blocked. (Libraries linked from big public CDNs such as jsDelivr, unpkg, cdnjs or Google Fonts are copied in by PLAY at upload, but bundling them is safer.)
- The game runs inside a sandboxed iframe on its own domain. alert(), confirm() and prompt() don't show, window.open() and pop-ups are blocked, HTML forms don't submit, nested iframes are blocked, and camera, microphone and location are off. Draw your own dialogs and menus in the game.
- No SharedArrayBuffer or multi-threaded WebAssembly (no cross-origin isolation). Use single-threaded builds.
- Fill the window it is given and keep filling it when it resizes or the phone turns: size the canvas from window.innerWidth/innerHeight (or its container), listen for "resize", cap devicePixelRatio at 2, and keep the page itself from scrolling (html, body { margin:0; height:100%; overflow:hidden }). Include <meta name="viewport" content="width=device-width, initial-scale=1">.
- Must be fully playable with a mouse or keyboard on desktop AND with a finger on phones. Use Pointer Events (pointerdown/pointermove/pointerup) so one code path serves mouse, touch and pen; set touch-action: none on the play area. On-screen buttons at least 48x48 CSS pixels, kept away from the screen edges. No hover-only actions, no right-click-only actions, no double-click-only actions.
- Never require pointer lock (mouse look) to play: phones don't have it. Offer drag-to-look or on-screen sticks instead.
- Keyboard controls: use standard keys (arrow keys or WASD to move, Space/Enter/Z/X for actions, Escape or P for pause). Write the controls in one short line, e.g. "Arrows: move · Space: jump · X: dash".
- Start audio only after the first tap or key press inside the game (call audioContext.resume() on the first pointerdown/keydown). Browsers keep sound off until then.
- First frame fast: draw a title or loading screen within 2 seconds and keep the download before play under 10 MB. Load later levels and music after the game starts.
- Be light on phones: textures 2048 px or smaller, compressed audio (MP3/OGG/M4A), no huge single files, free what you no longer use. iPhone Safari reloads pages that use too much memory.
- Saves: localStorage works but is tied to one published version of the game and one browser. For progress that must survive updates and devices, use PLAY's cloud saves (window.PLAY.storage) when window.PLAY exists, and fall back to localStorage when it doesn't.
- Pausing: PLAY can pause any game and freezes timers and animation frames for you. If the game runs on a Web Worker or its own clock, also listen for PLAY.on("pause") and PLAY.on("resume").
- Guard every PLAY SDK call with try/catch so the game still runs when opened outside PLAY.

When you're done:
1. List the controls for desktop and for phones in one short line each.
2. Tell me the orientation it is designed for (landscape, portrait or any).
3. Tell me exactly which folder to ZIP and upload (the one with index.html at its top).

Make my existing game ready for PLAY

You already have a game. Paste this with your project open in Cursor, Claude Code or similar.

I want to publish this web game on PLAY, an instant-play game platform where it runs in a sandboxed iframe and people play on both computers and phones.

Review the whole project against the rules below. For each rule, tell me whether the game already follows it, and fix every one it breaks. Change as little as possible and keep the gameplay the same.

PLAY platform rules (playsmorgi.com):
- Output a static web build: an index.html at the top of the folder plus its files. No server code, no PHP, no backend. All paths relative ("./assets/hero.png", never "/assets/hero.png" or "C:\..."), and file names must match their case exactly.
- No outside network at play time. Every library, font, image, sound and data file ships inside the build. Don't call external APIs, analytics, ad networks or WebSocket servers; they are blocked. (Libraries linked from big public CDNs such as jsDelivr, unpkg, cdnjs or Google Fonts are copied in by PLAY at upload, but bundling them is safer.)
- The game runs inside a sandboxed iframe on its own domain. alert(), confirm() and prompt() don't show, window.open() and pop-ups are blocked, HTML forms don't submit, nested iframes are blocked, and camera, microphone and location are off. Draw your own dialogs and menus in the game.
- No SharedArrayBuffer or multi-threaded WebAssembly (no cross-origin isolation). Use single-threaded builds.
- Fill the window it is given and keep filling it when it resizes or the phone turns: size the canvas from window.innerWidth/innerHeight (or its container), listen for "resize", cap devicePixelRatio at 2, and keep the page itself from scrolling (html, body { margin:0; height:100%; overflow:hidden }). Include <meta name="viewport" content="width=device-width, initial-scale=1">.
- Must be fully playable with a mouse or keyboard on desktop AND with a finger on phones. Use Pointer Events (pointerdown/pointermove/pointerup) so one code path serves mouse, touch and pen; set touch-action: none on the play area. On-screen buttons at least 48x48 CSS pixels, kept away from the screen edges. No hover-only actions, no right-click-only actions, no double-click-only actions.
- Never require pointer lock (mouse look) to play: phones don't have it. Offer drag-to-look or on-screen sticks instead.
- Keyboard controls: use standard keys (arrow keys or WASD to move, Space/Enter/Z/X for actions, Escape or P for pause). Write the controls in one short line, e.g. "Arrows: move · Space: jump · X: dash".
- Start audio only after the first tap or key press inside the game (call audioContext.resume() on the first pointerdown/keydown). Browsers keep sound off until then.
- First frame fast: draw a title or loading screen within 2 seconds and keep the download before play under 10 MB. Load later levels and music after the game starts.
- Be light on phones: textures 2048 px or smaller, compressed audio (MP3/OGG/M4A), no huge single files, free what you no longer use. iPhone Safari reloads pages that use too much memory.
- Saves: localStorage works but is tied to one published version of the game and one browser. For progress that must survive updates and devices, use PLAY's cloud saves (window.PLAY.storage) when window.PLAY exists, and fall back to localStorage when it doesn't.
- Pausing: PLAY can pause any game and freezes timers and animation frames for you. If the game runs on a Web Worker or its own clock, also listen for PLAY.on("pause") and PLAY.on("resume").
- Guard every PLAY SDK call with try/catch so the game still runs when opened outside PLAY.

Then give me:
1. A short list of what you changed and why.
2. The controls for desktop and for phones in one short line each.
3. The exact build command (if any) and which folder to ZIP and upload.

Add controls that work on desktop and phones

Your game works with a keyboard or mouse but is awkward or impossible with a finger.

Add touch controls to this browser game so it plays well on phones, without changing how it plays with a keyboard, mouse or controller on desktop.

Requirements:
- Use Pointer Events (pointerdown, pointermove, pointerup, pointercancel) for all mouse and touch input, so one code path serves both. Set touch-action: none on the play area and prevent default on touches there so the page never scrolls or zooms.
- Show on-screen controls only on touch devices (matchMedia("(pointer: coarse)").matches, or after the first touch), and hide them again if a keyboard or mouse is used.
- Movement: a virtual thumbstick or D-pad in the bottom-left. Actions: up to 3 large buttons in the bottom-right. Every button at least 48x48 CSS pixels with space between them, semi-transparent, and kept away from the screen edges. Support multi-touch so moving and pressing a button work at the same time.
- Lay the controls out again when the window resizes or the phone turns.
- Replace any hover-only, right-click-only or double-click-only action with a tap or a visible button. Never require pointer lock; if the game uses mouse look, add drag-to-look.
- If the game uses the Gamepad API, keep it working.
- Keep keyboard controls on standard keys (arrows or WASD, Space, Enter, Escape) and give me the controls as one short line, e.g. "Arrows: move · Space: jump".

Make it fit every screen

Your game is a fixed size, gets cut off on phones, or looks blurry.

Make this browser game fit any window size, from a phone held upright to a wide desktop monitor, and keep fitting when the window resizes or the phone turns.

Requirements:
- The page never scrolls: html, body { margin: 0; height: 100%; overflow: hidden; } and <meta name="viewport" content="width=device-width, initial-scale=1">.
- Pick a design resolution and scale the game to the largest size that fits the window while keeping its aspect ratio (letterbox the rest), or lay the game out responsively. Nothing important may ever be cut off.
- Size the canvas backing store to its CSS size times devicePixelRatio, capped at 2, so it stays sharp without wasting memory on phones.
- Listen for "resize" (which also fires when a phone turns) and re-layout UI and on-screen controls.
- Keep UI and text readable on a 360-pixel-wide phone screen: at least 14 CSS pixels for text, and buttons at least 48x48.
- Tell me which orientation the game works best in (landscape, portrait or any).

Add cloud saves, leaderboards and pause with the PLAY SDK

Your game works and you want saves that follow players, achievements or high scores.

Integrate the PLAY SDK into this browser game. Wrap every call in try/catch and make the game behave exactly as before when window.PLAY is missing or its calls fail (for example when opened from my own disk).

PLAY SDK (window.PLAY is injected automatically on PLAY; calls reject outside PLAY, so wrap them in try/catch):
- const player = await PLAY.ready  // { playerId, name, signedIn, preview }
- await PLAY.storage.save(data) / save("slot", data); await PLAY.storage.load() / load("slot")  // JSON, up to 1 MB per slot, 20 slots
- await PLAY.achievements.unlock("key")
- await PLAY.leaderboards.submit("board", score); await PLAY.leaderboards.top("board", 10)
- PLAY.game.started("mode"); PLAY.game.completed({ mode, score })  // call completed only when a run is truly over: PLAY may offer the next game
- PLAY.analytics.event("name", { flat: "props" })
- PLAY.on("pause", fn); PLAY.on("resume", fn); PLAY.on("mute", (muted) => ...)
- Achievements and leaderboards are declared in a play.json next to index.html:
  { "achievements": [{ "key": "first_win", "name": "First win", "points": 10 }],
    "leaderboards": [{ "key": "high_score", "name": "High score", "order": "desc", "format": "number" }] }

What to add:
- Cloud saves: save progress with PLAY.storage at sensible checkpoints (level complete, settings changed), load it at start, and keep the existing localStorage save as a fallback.
- Call PLAY.game.started when a run or level begins and PLAY.game.completed only when a run is truly over.
- [Optional: add a "high_score" leaderboard and these achievements: ...] and write the matching play.json next to index.html.
- Listen for PLAY.on("pause") and PLAY.on("resume") and pause the game loop, timers and music, and PLAY.on("mute").
List the changes you made and show me the play.json.

Check my build before I upload

Right before you upload, to catch the problems that stop a game from loading.

I'm about to upload this game's web build to PLAY as a ZIP. Check the build output (not the source) and fix or tell me about anything that would break it:
- index.html is at the top of the folder I will ZIP (or the build folder is the only folder with an index.html).
- Every asset path is relative and matches the file's name exactly, including upper and lower case.
- Nothing is loaded from the internet at play time: no CDN scripts, web fonts, APIs, analytics or ads. Bundle anything that is.
- No use of alert/confirm/prompt, window.open, form submits, iframes, camera, microphone, geolocation, SharedArrayBuffer or pointer lock that the game can't play without.
- The download before the first screen is under 10 MB; list the 10 largest files and suggest how to shrink them.
- No node_modules, source maps, .git folder or secret files (.env, keys) in the build.
Then tell me the exact folder to ZIP.

Set up Godot, Unity or another engine's web export

You use a game engine and want the right export settings for PLAY.

I use [Godot 4 / Unity / GameMaker / Construct 3 / other: ...] and want to publish my game on PLAY, a web platform where games run in a sandboxed iframe on computers and phones, with no outside network access and no cross-origin isolation.

Walk me through the exact web export settings, step by step, for my engine version:
- Godot 4: Web export preset with Thread Support turned OFF (single-threaded), export to an empty folder.
- Unity: WebGL build, a template whose canvas fills the browser window and follows resizes, compressed textures, memory kept low for iPhones, no multi-threading.
- GameMaker: HTML5 target, scaling set to fill the window and keep aspect ratio.
- Construct 3: Web (HTML5) export, fullscreen mode "letterbox scale" or "scale outer".
Also tell me how to add touch controls in this engine so the game plays with a finger as well as a keyboard or mouse, and which folder or ZIP to upload.

More questions? See the creator FAQ or the PLAY SDK docs.

Creator best practices · PLAY