
Interactive Tutorials
Teach the game while it's being played. The SDK's tutorial overlay gives you fingers, highlight rings, swipe demos, and text pills, plus the gating that shows them to new players only.
What It Is
The tutorial overlay is a DOM layer over your game that can point at things, demonstrate gestures, and (sparingly) say something.
It lives in the same UI entry point as the HUD toolkit:
import { shouldShowTutorial, createTutorialOverlay } from '@minit-games/sdk/ui'
No rendering library required: the overlay is pure DOM and works over any canvas, WebGL context, or DOM game. All artwork and sound effects are embedded in the package, so nothing is fetched at runtime.
Warning: Like the rest of
@minit-games/sdk/ui, this is npm-package only: there is nowindow.minitequivalent, and the Unity, Godot, Defold, and PlayCanvas facades do not expose it.
Always Gate on shouldShowTutorial() First
Never show tutorial UI without checking shouldShowTutorial(): a returning player who sees the finger every launch leaves.
// Capture ONCE at boot; do not re-read mid-session.
const tutorialMode = shouldShowTutorial()
if (tutorialMode) {
const tutorial = createTutorialOverlay({ container: gameRoot })
// ... wire steps to game events
}
How the Decision Is Made
| Priority | Condition | Result |
|---|---|---|
| 1 | ?tutorial=1 or ?tutorial=true in the URL | Force show (for QA) |
| 2 | ?tutorial=0 or ?tutorial=false in the URL | Force hide |
| 3 | The player's stored userData is a non-empty string | Hide: this player has been here before |
| 4 | (default) | Show: new player |
Persisting Completion
The overlay does not remember anything by itself. Rule 3 above only fires if your game has written something to the player's userData slot, so add userData to every reportResult call site: game over, win, timeout, give-up.
import { reportResult } from '@minit-games/sdk'
reportResult(score, {
flavorText: 'Cleared wave 6 without a single miss',
userData: 'true',
})
- Write it unconditionally: don't wrap it in
if (tutorialMode). Writing'true'repeatedly is harmless, and doing it every time self-heals a session that crashed before it could report. - Omitting
userDataleaves the slot untouched, which means the tutorial shows again next launch. - If your game already stores richer save state in that slot, any non-empty string satisfies the check. You don't need a dedicated flag.
Warning: The
userDataslot must be enabled for your Game in the Creator Console. If it isn't,getUserData()always returnsundefined, writes are silently ignored, and the tutorial will show on every launch. See Saving User Data.
Creating the Overlay
const GAME_W = 960
const GAME_H = 560
const tutorial = createTutorialOverlay({
container: gameRoot,
width: GAME_W,
height: GAME_H,
})
| Option | Type | Default | Purpose |
|---|---|---|---|
container | HTMLElement | document.body | The element the overlay covers. Tutorial UI is clipped to it; nothing renders outside your game's bounds. |
width | number | none | Logical canvas width, when your game renders to a fixed coordinate space. |
height | number | none | Logical canvas height. |
zIndex | number | theme default | Stacking order, if the overlay collides with your own UI. |
Pass width and height when your game uses a fixed logical canvas (e.g. 960×560) that CSS scales to fit the screen. Coordinates you give the primitives are then in that logical space, and the overlay scales gestures and pills to match. Omit both when your (x, y) values are already in the container's displayed pixel space.
Call tutorial.destroy() to tear the whole overlay down, for example when the tutorial sequence finishes, or at game over.
The Primitives
| Method | Use it for | Blocks input? |
|---|---|---|
highlight({ x, y }) | First choice: expanding rings that draw the eye to an element without covering it | No |
showFinger({ x, y, gesture?, direction? }) | A looping tap or long-press demo. Pair with a highlight on the same target | No |
showSwipe({ from, to }) or showSwipe({ path }) | A looping drag or swipe demo | No |
showPill(text, { onClose, delay? }) | Modal text: a last resort, when a gesture genuinely can't convey the rule | Yes |
destroy() | Remove the overlay entirely | n/a |
Each of highlight, showFinger, and showSwipe returns a handle with a remove() method. That's how a step ends:
const ring = tutorial.highlight({ x: tile.x, y: tile.y })
const finger = tutorial.showFinger({ x: tile.x, y: tile.y, gesture: 'tap' })
// later, when the player does the thing:
ring.remove()
finger.remove()
All coordinates are in game viewport space: the logical width/height you passed to createTutorialOverlay, or the container's own client size if you didn't. Pills are always centred by the SDK and take no position argument.
Design: Live Guide, Not a Briefing
The overlay runs alongside the game and reacts to what the player actually does.
Step by Step, Never Upfront
- The game begins immediately: no paused "tutorial mode", no wall of text.
- When the game reaches a state that calls for guidance, show one step: a highlight and/or a finger on the thing that matters right now.
- Wait for the player to perform the action.
- Only then reveal the next step.
A player should never be reading. They should be doing.
State-Reactive, Not Timed
Wire every hint to game events and state, never to setTimeout or a linear script running independently of play:
- Show the finger on the first enemy when that enemy spawns, not two seconds after load.
- Remove the swipe hint the moment the player completes a swipe, not on a timeout.
- Advance to "now avoid the hazard" only when a hazard actually appears.
- If the player figures it out before the hint would appear, skip that step entirely.
The step machine lives in your game code. The SDK primitives are stateless tools you call and remove; deciding when is your job.
let step = 0
let pending = null // appeared before its step was live
function onEnemySpawned(enemy) {
if (step !== 0) return // already past this step
const ring = tutorial.highlight({ x: enemy.x, y: enemy.y })
const finger = tutorial.showFinger({ x: enemy.x, y: enemy.y, gesture: 'tap' })
enemy.once('tapped', () => {
ring.remove()
finger.remove()
enterStep1() // next step waits for its own event
})
}
function onBonusTileAppeared(tile) {
if (step < 1) {
// Too early. Hold it, and keep tracking it: input is open, so the player
// can collect it before step 1 ever starts.
pending = { tile, collected: false }
tile.once('collected', () => { if (pending) pending.collected = true })
return
}
hintBonusTile(tile)
}
function enterStep1() {
step = 1
if (!pending) return
const { tile, collected } = pending
pending = null
if (collected) {
step = 2 // already done; skip the step
return
}
hintBonusTile(tile) // replay what arrived early
}
function hintBonusTile(tile) {
const ring = tutorial.highlight({ x: tile.x, y: tile.y })
tile.once('collected', () => { ring.remove(); step = 2 })
}
Hold an event that arrives early, keep tracking it, and settle it on step entry. The game doesn't wait for your tutorial. Two things can happen while the player is still on step 0, and both stall the tutorial if you ignore them:
- The tile appears. If the handler just returns because the step isn't live yet, the tile is already on screen when step 1 starts and no second appearance event is coming. The hint silently never shows.
- The tile gets collected. Input is open the whole time, so the player can
finish the step before it begins. Attach the
collectedlistener when you hold the event, not when you replay it; otherwise you install it after the event already fired andstepnever advances.
Settling on entry is what makes both safe: hint the tile if it's still there, skip the step outright if it's already done. That second branch is the gestures-first rule from earlier applied to your own state: if the player acts correctly before the hint appears, skip that step entirely.
Gestures First, Text Last, Never Both
Every step is either a show step or a tell step:
| Step type | What's visible | Can the player act? |
|---|---|---|
| Show | highlight + showFinger / showSwipe | Yes; input is open |
| Tell | showPill only; remove active gesture hints first | No; input is blocked |
Priority Order
highlight: mark the important element (button, tile, target, hazard) without covering it. Use on every show step.showFinger/showSwipe: demonstrate the exact gesture. Pair with a highlight on the same target whenever you can.showPill: only when a rule genuinely isn't guessable from a gesture. One short sentence. Remove all gesture hints first.
A Typical Step
// ── SHOW step: gesture + highlight, input open ─────────────────────────
const ring = tutorial.highlight({ x: targetX, y: targetY })
const finger = tutorial.showFinger({ x: targetX, y: targetY, gesture: 'tap' })
function onTargetTapped() {
ring.remove()
finger.remove()
// advance: another show step, or a tell step if a rule must be stated
}
// ── TELL step (only if needed): clear gestures, then the pill ──────────
function showRuleStep() {
ring.remove()
finger.remove()
tutorial.showPill('One wrong tap ends the run.', {
onClose: () => {
// start the next show step once the player dismisses
},
})
}
Use highlight Alone When…
…the element should be noticed but not touched yet: a hazard appearing, a power-up slot, an exit tile the player will reach later. Add the finger once interaction is actually expected.
When Text Earns Its Place
| Use a pill | Skip the pill |
|---|---|
| A non-obvious rule no gesture can convey ("One wrong tap ends the run") | A tap, swipe, or jump the finger already demonstrates |
| Puzzle or strategy framing, before the first move can make sense | Arcade games where the core verb is obvious |
| Stating the goal after the first successful action (a reward, not a toll booth) | Restating what the looping hand already shows ("Tap to jump!") |
Arcade vs Puzzle
- Arcade / reflex (tap, swipe, jump): gesture only,
highlight+showFinger/showSwipefrom the first frame. No intro pill. Optionally one short pill after the first successful action, to state the goal. - Puzzle / strategy: one short intro pill may be needed to frame the rules, but follow it immediately with a highlight and finger on the first interactive element. The player should be acting within a second.
Fixed Styling: Don't Override It
Pill colours, finger glyphs, ring sizes, fonts, and timings all come from the SDK's bundled theme, so tutorials look the same across every Game. Position things; leave the look alone.
| Fine to pass | Leave alone |
|---|---|
{ x, y } on highlight, showFinger, showSwipe | fontSize, glyph, color, radius, pulseScale |
gesture, direction, from / to / path | Custom HTML/CSS overlays of your own |
onClose, delay on pills | Repositioning or restyling a pill (it's always centred) |
createTutorialOverlay({ container, width, height, zIndex }) | Any per-call colour or size override |
Testing Locally
The two URL params make both paths reachable without a host:
| URL | Expected |
|---|---|
/?tutorial=1 | Tutorial always shows |
/?tutorial=0 | Tutorial never shows |
/ (nothing stored) | Tutorial shows (the new-player path) |
/?userData=true | Tutorial hidden (simulates a returning player) |
To simulate a host-injected value specifically (the real app path, which
outranks the URL param), the stub has to exist before your game boots. Typing it
into the DevTools console and reloading does not work: the reload creates a fresh
page global and discards the assignment long before shouldShowTutorial() runs.
Inject it ahead of your bundle instead, in your dev HTML only:
<!-- dev only; never ship this in your uploaded build -->
<script>window.minit = { userData: 'true' }</script>
<script type="module" src="/src/main.js"></script>
Order matters: the stub <script> must come before the one that loads your
game.
Common Mistakes
- Skipping the
shouldShowTutorial()check: returning players get the tutorial forever. - Forgetting
userDataon somereportResultcall sites: the one path you missed (time-up, give-up) never marks the tutorial complete. - A wall of text or several pills before play begins: one step at a time, in the moment it's needed.
- Timer-driven steps: hints wired to
setTimeout, or steps that advance automatically after N milliseconds instead of when the player acts. - A gesture and a pill on screen at once: remove the highlight and finger before opening a pill.
- Opening with a pill when a finger would do.
- Text that narrates the obvious ("Tap the button!") while a finger already points at it.
- A start menu or a "Play again" screen: a Game is one session (load, play, result). See Reporting Results.
- Rolling your own HTML/CSS tutorial overlay: you lose the consistent look and the correct input blocking.
Deeper Dives
- Game UI: HUD, Feedback & Rewards: the rest of
@minit-games/sdk/ui - Saving User Data: the
userDataslot the gating depends on, and how to enable it - Reporting Results: where you persist tutorial completion
- Install the Minit SDK: installing the package