Decide two things before you build: where your game will be opened, and how it occupies the screen there. They change how the game is built, from how big the text can be to what happens to a long list, and they are expensive to change later.
npx gamestage create --name "Kick Off" --format hunt --host app-webviewcreate records both in gamestage.yaml, and verify holds the game to them.
Where it runs
--host | Where the game is opened | Default layout |
|---|---|---|
fullscreen | Full screen, from a carousel, a Hub or a link. The default | screen (page for predict) |
article-launch | A card in an article that opens the game full screen over it | launch |
article-embed | A box inside an article, played where it sits | card |
app-webview | Inside a club's or publisher's own app, in a web view | screen (page for predict) |
How it sits on the screen
--layout | What the player gets | Use it for |
|---|---|---|
screen | Fills the phone, nothing scrolls, and the layout adapts to the phone's size | Almost every game |
canvas | Drawn at one size and scaled to fit, with bars where the shape differs | Games that need exact positions |
launch | A preview in the host page; tapping it opens the game full screen, and closing it returns the reader to where they were | Any game shown in an article |
card | A fixed box inside a page that scrolls, with no scrolling inside it | Only where the host can't open a game full screen |
page | Scrolls like a web page | Long question lists and results tables |
sheet | Slides up over part of a host app | One quick question inside an app |
Choose full screen. Players come back to a game they can play properly. Full screen, a game feels like an app and gets played; a widget in an article reads as throwaway and gets scrolled past. In an article, use launch, and keep card for a host that can't allow anything else: a box in an article is small, easy to scroll past, and the page's own scrolling fights swipes inside the game.
Gamestage UI's Stage takes the same words as fit, so a game built with it says this once.
Load smoothly
A fan should see your game's colour from the first frame, then a loading mark, then the game in one step. Never a white page, bare "Loading…" text, or a board that jumps in.
Scaffolds from create do all of this already. If you wrote your own page:
- Paint the game's colour before any script. In the head, before the first
<script>:
html <meta name="color-scheme" content="dark" /> <meta name="theme-color" content="#0B1B2B" /> <style>html, body { background: #0B1B2B; margin: 0; }</style>
- Draw a loading state, never text. A centred spinner, or a progress bar when you know how far through you are, in a box the size of the game. Gamestage UI's
Loadingis exactly this. - Say when the game is playable. When the round is drawn, or has failed, call
window.gamestageReady()(scaffolds define it), or set the mark yourself:performance.mark("gamestage:playable"). Remove the loading state then, not before.
Intro screen
Every game create scaffolds opens on an intro screen: a full-screen colour gradient, your game's name in its own font, a progress bar while it loads, then a Play button. The tap on Play is what starts the game, which is also the gesture a browser needs before it will let a page play sound. A game scaffolded before this feature shipped keeps its old start.
The window.gamestageReady() call from the previous section is also what drives the intro screen. Call it with an argument to say more than "ready":
| Call | What the fan sees |
|---|---|
window.gamestageReady() | The round is drawn; the button reads "Play" |
window.gamestageReady({ resume: true }) | The fan has a round already in progress; the button reads "Continue" |
window.gamestageReady({ failed: true }) | The game could not load; the screen shows its error text and a retry button |
Wait for the tap before you open anything that asks the fan something, such as How to play. window.gamestageStart is a promise that resolves on Play, and gamestage:start is the matching event; opening a dialog before then would be asking the fan before they have agreed to start.
The colours and words are Studio settings, under Brand and Wording. Set them there, or seed them in gamestage.settings.json before your first deploy the same as any other field (see Beside the manifest: `gamestage.settings.json`); create already seeds the four button and error labels for you.
| Setting | Field key | What it changes |
|---|---|---|
| Intro screen colour, top | splash_gradient_start | Top of the gradient, and the colour the page paints before anything else loads. Empty uses your background colour |
| Intro screen colour, bottom | splash_gradient_end | Bottom of the gradient. Empty uses your background container colour |
| Intro screen button | splash_play_label | What Play says. Empty says "Play" |
| Intro screen button, part way through | splash_continue_label | What the button says when the fan has an unfinished round. Empty says "Continue" |
| Intro screen, when loading fails | splash_error_text | Empty says "The game didn't load. Check your connection and try again." |
| Intro screen, try again button | splash_retry_label | Empty says "Try again" |
The name is drawn in its own typeface here too, from the same Brand settings that style it everywhere else on the page: see Theming: the title's own typeface for the typeface, weight, letter spacing, case, colour, and the separate logo text and accent colour settings.
Inside a native app
Tell the app's team two things, because they are in the app rather than your page:
- Set the web view's own background to your game's background colour. A web view is white until the page paints, so without this the player sees a white flash first.
- Create the web view before the player taps, and load the game into it then. Starting it on the tap adds the whole start-up to the wait.
Load fast
gamestage verify opens your game on a mid-range phone over 4G (the processor slowed four times, 9 Mbit/s down, 60 ms round trips, nothing cached) and reports what it downloaded and how long it took. Over a budget, it warns and names the heaviest files. It never fails a game for being slow.
| Layout | JavaScript | CSS | Fonts | Everything | First render | Playable |
|---|---|---|---|---|---|---|
screen | 350 KB | 60 KB | 150 KB | 900 KB | 1.8 s | 3.5 s |
canvas | 500 KB | 60 KB | 150 KB | 1,500 KB | 1.8 s | 4.5 s |
launch | 300 KB | 50 KB | 120 KB | 700 KB | 1.5 s | 3.0 s |
card | 300 KB | 50 KB | 120 KB | 700 KB | 1.5 s | 3.0 s |
page | 400 KB | 80 KB | 150 KB | 1,100 KB | 1.8 s | 4.0 s |
sheet | 250 KB | 40 KB | 100 KB | 600 KB | 1.2 s | 2.5 s |
Sizes are what crossed the network, compressed. First render is when the fan first sees something that isn't blank, so painting your colour and a loading mark from the HTML counts. Playable is your gamestage:playable mark, or when verify first finds the board if you set none.
What usually makes a game heavy, and the fix:
- Fonts: load one family in two weights, not five, and subset it to the characters the game uses.
- Components you don't use: install only the Gamestage UI components the game draws.
- Images: size them for a phone and use WebP.
verify also checks a screen, canvas, card or sheet game fits at 360×640, 390×844 and landscape without the page scrolling. If a list must scroll, let it scroll inside the game, never the page.
