Lesson 23

Mobile-first game layout: touch controls are part of the game.

A game that works on desktop but hides action behind phone controls is not mobile-ready. Design the canvas, HUD and touch zones together.

IntermediateMobileTouch Controls35-55 minutes

1. Reserve space for controls

Touch buttons need physical space. If they cover enemies, objectives, dialogue or a platform edge, the player loses information. Decide where controls live before finalizing the camera and HUD. Many games work well with movement on the left, actions on the right and critical HUD at the top.

.touch-controls {
    display: none;
    position: fixed;
    left: 0;
    right: 0;
    bottom: env(safe-area-inset-bottom, 0);
    padding: 12px 18px;
    justify-content: space-between;
    pointer-events: none;
}

.touch-pad,
.touch-actions {
    pointer-events: auto;
}

@media (pointer: coarse) {
    .touch-controls { display: flex; }
}

Source pattern: Supagames mobile fixes use coarse-pointer media queries, touch buttons and pointer events so controls appear on phones without disrupting desktop play.

`pointer-events: none` on the wrapper lets empty areas pass through while actual buttons still work. This helps prevent invisible overlays from blocking the game.

2. Map touch to the same input state

Do not create a separate mobile movement system. Touch, keyboard and gamepad should all set the same `keys` or `input` state. Then gameplay code does not care where the input came from.

const keys = {};

function bindTouchHold(element, key) {
    element.addEventListener("pointerdown", function (event) {
        event.preventDefault();
        keys[key] = true;
        if (element.setPointerCapture) element.setPointerCapture(event.pointerId);
    });
    ["pointerup", "pointercancel", "pointerleave"].forEach(type => {
        element.addEventListener(type, function () {
            keys[key] = false;
        });
    });
}

bindTouchHold(document.getElementById("btnLeft"), "left");
bindTouchHold(document.getElementById("btnJump"), "jump");

This also makes testing easier. If a desktop key and a touch button both set `keys.jump`, the jump code has one path.

3. Build a simple virtual joystick

For games with smooth movement, a joystick can feel better than four buttons. Track the pointer from the stick center, clamp the distance and convert it into directional input. Keep a dead zone so small accidental movement does not drift the player.

function updateStick(dx, dy, radius) {
    const dead = radius * 0.25;
    const dist = Math.hypot(dx, dy);
    touch.left = dist > dead && dx < -radius * 0.28;
    touch.right = dist > dead && dx > radius * 0.28;
    touch.up = dist > dead && dy < -radius * 0.28;
    touch.down = dist > dead && dy > radius * 0.28;
}

function releaseStick() {
    touch.left = touch.right = touch.up = touch.down = false;
}

Show the knob moving so the player sees input. If the stick is invisible or too small, players overcorrect and blame the game physics.

4. Responsive canvas without distorted physics

The canvas can scale visually while the game world keeps fixed logical dimensions. Use CSS to fit the screen and compute pointer coordinates by converting from CSS pixels back into canvas pixels.

function canvasPointer(event, canvas) {
    const rect = canvas.getBoundingClientRect();
    return {
        x: (event.clientX - rect.left) * canvas.width / rect.width,
        y: (event.clientY - rect.top) * canvas.height / rect.height
    };
}

canvas.addEventListener("pointerdown", function (event) {
    const p = canvasPointer(event, canvas);
    handleGameTap(p.x, p.y);
});

This prevents the classic mobile bug where the drawn button is in one place but the clickable coordinate is somewhere else after scaling.

5. Prevent page scroll only during gameplay

Space and arrows can scroll the page. Touch can drag the page. Prevent default behavior when the event is part of gameplay, but do not block forms, menus or comments. The browser page is still part of the experience.

document.addEventListener("keydown", function (event) {
    const typing = event.target.matches("input, textarea, select");
    if (typing) return;
    const gameKey = [" ", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key);
    if (gameKey) event.preventDefault();
});

This pattern protects both game control and site usability. A player should be able to jump, then write a comment with spaces.

6. Mobile QA checklist

  • Can the player start, play, pause and restart without a keyboard?
  • Do controls avoid the most important action area?
  • Are buttons large enough for thumbs?
  • Does pointer coordinate conversion still work after resizing?
  • Can forms, comments and search fields still type spaces?
  • Does the game remain readable in portrait and landscape, or does it clearly request one orientation?

Mobile-first does not mean mobile-only. It means the hardest layout constraint is solved first, so desktop becomes the easy version.

7. Tune control opacity and hit targets

Touch controls should be visible enough to find, but not so solid that they hide danger. A common pattern is a translucent control surface with a brighter active state. The hit target can be larger than the visible icon, which helps thumbs without making the UI look huge.

.touch-btn {
    min-width: 64px;
    min-height: 64px;
    border-radius: 18px;
    border: 1px solid rgba(255,255,255,0.24);
    background: rgba(10, 16, 35, 0.48);
    color: #fff;
    touch-action: none;
    user-select: none;
}

.touch-btn:active {
    background: rgba(255, 217, 61, 0.26);
    transform: scale(0.94);
}

Use `touch-action: none` on controls that should not scroll the page. Do not apply it globally to the whole document unless the entire page is dedicated to full-screen play, because players still need normal page behavior around comments, navigation and forms.

Previous: Economy and upgrades. Next: Phaser 2D game basics.