Skip to main content

Best Practices for Games on Arcadible

Players judge games on Arcadible the way they judge games on any other platform: how fast they load, how they feel, and how polished they are. This guide covers what's specific to shipping a game on the web platform, in the order a player meets your game. It assumes you already know how to make a game.

Arcadible requires only what arc validate checks. Everything here is advice: take what fits your game.

Load fast​

There's no install step: a new player downloads your game when they first open it. Set your targets for a phone on a mobile connection.

The menu and first level download before play; later levels stream in while the player plays menu level 1 level 2 level 3 First download Before play Playing While you play Streamed in

  • Paint within 1 second. Put a small loading screen directly in index.html, with inline CSS, so it renders before any script loads. Use the same art as your splash so the transition from the store matches.
  • Be playable within about 5 seconds. Load only the first screen, the first level, and their assets, and stream the rest during play, from your build or your own CDN.
  • Compress what you ship. Choose compressed formats for textures, models, audio, and fonts, and minify your code. Most engines make these choices for you.
  • Show progress. Base it on bytes loaded where you can, such as for a bundled file loaded with fetch. Browsers don't report progress for ES module imports, so for those, animate the loading screen so it doesn't look stuck.
  • Handle failure. Time out and retry requests, and show an error with a retry option if loading fails.

Test with your browser's network panel throttled to a slow mobile connection and the cache disabled, which matches a new player's first load.

Make the first touch part of the game​

Browsers block audio, fullscreen, and pointer lock until the player interacts with the page. A common workaround is a "Tap to play" screen that exists only to collect that interaction. Instead, design the opening so the player's first natural input provides it:

  • Open on something interactive, such as an attract-mode title, a controllable character, or a menu. The player's first action, whether a jump, a steer, or selecting "New Game", is also the interaction the browser requires.
  • Start audio on that input by calling audioContext.resume() in your first pointerdown or keydown handler, and fade the music in.
  • Design the opening to work without sound. Nothing plays before the first input, so use motion to prompt it.

Get to play quickly​

  • Give control early. Make logos skippable, and keep story sequences and settings out of the way of the first moment of play.
  • Teach through play. A new player should understand the goal within a few seconds without reading instructions. Introduce one mechanic at a time, and show gestures with animation.
  • Let players play before signing up. Ask for an account once they have progress to keep.

Fit every screen​

Players use phones in both orientations, tablets, ultrawide monitors, and resizable desktop windows.

A phone held sideways: the game fills the screen, and the hearts and score stay inside the safe area, clear of the notch 0420 Safe area The HUD stays inside it

  • Handle resizing with a ResizeObserver on the canvas, rather than reading the size once at startup.
  • Choose a scaling strategy, whether that's letterboxing, extending the view, or reflowing the UI, and test the extremes: tall phones, square windows, 21:9 monitors.
  • Keep UI inside the safe area, away from notches and home indicators, with env(safe-area-inset-*). Let the game world extend behind them.
  • Declare your orientation, so the Arcadible desktop app can shape its window for you. In a browser, show a "rotate your device" screen if your game only works one way.

A canvas can't use env(). Copy the insets into custom properties, and read them in JavaScript when the canvas resizes:

:root {
--safe-top: env(safe-area-inset-top, 0px);
--safe-bottom: env(safe-area-inset-bottom, 0px);
}
const style = getComputedStyle(document.documentElement);
const safeTop = parseFloat(style.getPropertyValue("--safe-top"));
const safeBottom = parseFloat(style.getPropertyValue("--safe-bottom"));

Support every input you declare​

Declare your inputs accurately, and support each one fully, menus included. Switch prompts to whichever device the player used last.

  • Keyboard: use event.code, which names the physical key, so WASD stays in place on AZERTY and other layouts. Let players rebind keys.
  • Mouse: for mouse-look, use pointer lock with requestPointerLock({ unadjustedMovement: true }) for raw input. Escape always releases the lock, so pause the game when pointerlockchange reports it's released.
  • Gamepad: the Gamepad API has no button events, so poll navigator.getGamepads() every frame. With the "standard" mapping, button 0 is the bottom face button. Show Xbox, PlayStation, or Switch glyphs based on gamepad.id.
  • Touch: use Pointer Events for multi-touch. Make targets at least 44 CSS pixels, keep them clear of screen edges, which are reserved for system gestures, and hide touch controls on devices without touch.

Turn off browser defaults​

Browsers scroll the page, select text on a double tap, open a context menu on right-click, and zoom on a pinch, which a game rarely wants. The starters turn these off already, in their index.html and main.js. If you started elsewhere, here's how.

In CSS, stop selection, scrolling, bouncing, and browser gestures:

html,
body {
margin: 0;
height: 100%;
overflow: hidden;
overscroll-behavior: none;
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
-webkit-tap-highlight-color: transparent;
}

canvas {
touch-action: none;
}

In the viewport tag, turn off zoom, and extend the page behind the notch and other display cutouts:

<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
/>

In JavaScript, suppress the context menu, and keep the keys your game uses from scrolling the page:

const GAME_KEYS = new Set([
"Space",
"ArrowUp",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
]);

addEventListener("contextmenu", (e) => e.preventDefault());
addEventListener("keydown", (e) => {
if (GAME_KEYS.has(e.code)) e.preventDefault();
});
  • Set a background color that matches your loading screen, so players never see a white flash.
  • Style every element, so no default browser buttons, links, or form controls show.
  • Hide the cursor when it isn't in use, such as during gamepad play.

Run smoothly​

Aim for 60 frames per second, with a floor of 30 on the lowest-end device you support. Consistent frame times matter more than a high average.

  • Drive the loop with requestAnimationFrame, and never assume 60 Hz. Displays run at 90, 120, 144 Hz and more, so step your simulation at a fixed rate and interpolate when drawing.
  • Avoid per-frame allocation. Garbage collection pauses cause dropped frames. Pool objects, reuse arrays, and avoid creating closures in the loop.
  • Warm up during loading. Compile shaders, upload textures, and decode audio before play, not the first time they're needed.
  • Move heavy work off the main thread with Web Workers, and rendering too with OffscreenCanvas. Arcadible games are cross-origin isolated, so threads and SharedArrayBuffer are available.
  • Budget memory. Browsers on low-end phones can terminate a page using a few hundred megabytes. A 2048 × 2048 RGBA texture uses 16 MB of GPU memory, regardless of its file size.
  • Mind the GPU. Most engines pick WebGPU or WebGL for you. In WebGL, avoid calls that stall the GPU, such as getError() and synchronous readPixels(). See MDN's WebGL best practices.

Scale resolution​

The same circle at pixel ratios 1, 2, and 3, with 2 marked as the cap cap here 1x 2x 4 times the pixels 3x 9 times the pixels

Phones have a device pixel ratio of up to 3, which is nine times the pixels of a ratio of 1. Above 2, the visual gain rarely justifies the fill-rate cost.

  • Cap the pixel ratio at 2, or lower for heavy 3D.
  • Scale dynamically. Lower the render resolution when frame time rises, and raise it when there's headroom. Render the UI at full resolution.
  • For pixel art, scale by whole numbers instead. Render to a small buffer, such as 320 × 180, and draw it at the largest whole-number multiple that fits the canvas at its full device resolution, with imageSmoothingEnabled off. Leave the pixel ratio uncapped: a small buffer costs little fill rate, and fractional scaling makes pixels uneven.

Test on real hardware​

Development machines are much faster than typical player devices. Test on the phones you have, including an older or cheaper one if you can.

Pause, save, and resume​

A page can be hidden or closed at any time, without warning.

  • Pause on visibilitychange and blur, audio included, and resume to a pause menu. In the desktop app, your game keeps running in the background unless you pause it.
  • Save continuously, at checkpoints and when the page is hidden. visibilitychange and pagehide are the last events you can count on.
  • Keep progress in saves, which carry over to your next version, where browser storage doesn't.

Other recommendations​

  • Accessibility: reduce screen shake and flashing when prefers-reduced-motion is set, don't convey information by color alone, and offer subtitles and control remapping.
  • Language: pick a default from navigator.languages, let players change it, and format numbers and dates with Intl.
  • Network: let the game start without a server, retry with backoff, and show players a message they can act on rather than a raw error (Networking).

Next steps​