Puneet Sharma - Frontend Developer & UI Engineer
Puneet Sharma
Frontend Dev & UI Engineer · 16+ yrs · pixel-perfect HTML, React & WordPress

9 Copy-Paste Mobile Games That Teach Touch Controls, Canvas Physics & Game Loops

Nine free copy-paste canvas games with mobile touch controls — shooter, platformer, runner, Asteroids, Frogger & Pac-Man. Vanilla JS.
9 Copy-Paste Mobile Games That Teach Touch Controls, Canvas Physics & Game Loops

The hard part of a browser game isn't the game — it's the thumb. A dodge game on a desktop is ten lines of keyboard handling; the same game on a phone has to answer questions the keyboard never asks. What happens when a finger slides off the button mid-press? Does holding "fire" on a 120Hz display shoot twice as fast as on a 60Hz one? Is "jump" the same kind of input as "move left", or something else entirely? Get those wrong and the game feels broken in a way no amount of art fixes — the controls fight the player instead of disappearing.

Below are nine free, self-contained canvas games built specifically to get the touch layer right, and each one deliberately picks a different control scheme so the set is a tour of the whole space. The first four: a wave-based space shooter with a D-pad and a hold-to-fire button, a side-scrolling platformer with real gravity and one-way platforms, a top-down tank battle with a four-way cross D-pad and destructible cover, and an endless three-lane runner controlled entirely by taps. The next four push into control schemes the first didn't cover: an Asteroids clone flown by rotate-and-thrust, a Doodle-Jump-style climber steered with just two buttons while it bounces on its own, a Frogger crossing of discrete grid hops, and a one-button cave flyer that's nothing but hold-to-rise. And the ninth is the big one — a full Pac-Man-style maze-chase with buffered turning and three ghosts that each hunt you differently. Every one is a live, interactive preview — play them right here in the article on desktop or mobile — and each exports to React, Vue, Angular, or Tailwind in one click from the snippet page. They're pure vanilla JavaScript with zero dependencies and no CDN scripts.

What these nine get right about controls

  • One input state, two input devices. Nothing moves the player inside an event handler. Keyboard listeners and on-screen buttons both write to a single keys object (or a single flying flag, in the cave flyer) that the game reads once per frame. That's why a held D-pad button feels exactly like a held arrow key, and why there's no second "touch version" of the logic to keep in sync.
  • The right input type for each action. Moving is a held state; firing and thrusting are held states (throttled by a cooldown for the guns); jumping in the platformer, every control in the runner, and every hop in Frogger are discrete taps fired on pointerdown. Choosing held-vs-tap per action is the difference between a jump you can spam into a double-jump and one that behaves.
  • Frame-rate independence, done two ways. Most of these scale movement by delta-time so speed is identical on any display. The platformer, tank game and sky hopper run a fixed timestep instead — because collision against thin platforms and walls, or a precise bounce height, breaks if a single step moves the player too far. Same goal, two correct tools.
  • The finger-slides-off case is handled. Every hold button releases on pointerup, pointercancel and pointerleave, so the ship never gets stuck moving — or the cave flyer stuck rising — when your thumb drifts past the button edge, the single most common mobile-control bug.
  • The control scheme is chosen to fit the game, not the reverse. A D-pad for the shooter, rotate-and-thrust for Asteroids, two steer buttons for an auto-bouncer, a four-way cross for grid hops, one button for the flyer. Each is the minimum input the game actually needs, which is why none of them feel cramped on a phone.

1. Space Blaster Arcade Game

A wave-based fixed shooter: move your ship with the on-screen D-pad, hold FIRE to shoot, and clear formations of enemies that march sideways, drop down, and shoot back. Three lives, wave-scaled scoring, and a persisted high score.

How it works: the control model is the whole point. Both the keyboard handlers and the on-screen buttons only flip booleans in a shared keys object — keys.left, keys.right, keys.fire — which update() reads once per frame, so the ship never moves inside an event handler. A tiny hold() helper wires each button through Pointer Events and binds pointerup, pointercancel and pointerleave together, which is what stops the ship gliding forever when a thumb slides off. Firing is the subtle one: holding FIRE sets a flag read every frame, so without a FIRE_COOLDOWN timestamp check a 120Hz display would emit twice the bullets of a 60Hz one — the cooldown makes the shot cadence a function of real milliseconds, not frames. The enemy formation is pure Space Invaders: every living enemy is moved sideways in one pass, and if any of them crosses a screen margin a hitEdge flag reverses the whole block and steps it down in the next pass.

Best for: a games-portal demo that has to feel good on a phone, and as a reference for the "shared input state" pattern. Tip: difficulty lives entirely in buildWave(n) — it scales column count, row count and per-enemy hit points off the wave number, so one function is the entire difficulty curve.

Grab the code: Space Blaster Arcade Game

2. Pixel Platformer Game

A side-scroller with real gravity: run with ◀ ▶, tap JUMP to leap, collect coins, dodge spikes, and reach the flag — through a level wider than the screen, with a camera that follows you.

How it works: this one deliberately does not scale movement by delta-time. Platformer collision is sensitive to how far you move per step — a big variable step lets a fast fall tunnel straight through a thin platform between two frames — so the loop drains an accumulator and runs update() in fixed 1/60-second steps, drawing once per rendered frame. The platforms are one-way: a landing only registers when the player is moving downward (vy >= 0) and their previous-frame bottom edge was at or above the platform surface, which is the single guard that lets you jump up through a floating platform and land on it coming down. Run and jump are split by input type on purpose — left/right are held flags, but JUMP fires once on pointerdown and only when onGround is true, so holding it can't double-jump. The camera x-offset centres the player and is clamped to the level bounds, and the whole world is drawn through one ctx.translate(-cam, 0).

Best for: teaching 2D game physics — gravity integration, one-way collision, and a follow-camera are all here in readable form. Tip: the level is plain data. The platforms, spikes and coins arrays and the flag object sit at the top of the file in world coordinates; rearrange them and you've designed a new level without touching the engine.

Grab the code: Pixel Platformer Game

3. Tank Arena Game

Top-down tank combat: drive with a four-way cross D-pad, tap FIRE to shoot in the direction you're facing, and fight waves of AI tanks across an arena of solid and destructible walls.

How it works: movement is resolved one axis at a time in moveTank() — the horizontal step is applied only if the new x is clear, then the vertical step is checked separately. That axis-separation is what lets a tank slide along a wall it's pressed against instead of catching on the corner, and it's three lines. Walls carry hit points: the border is solid and infinite, interior cover blocks start at two and are filtered out of the walls array once a bullet drops them to zero, so cover genuinely erodes and you can shoot a firing lane through it. The enemy AI is intentionally shallow but not random noise — on a timer each tank half the time picks the axis that points most directly at the player (compare horizontal vs vertical distance, move on the larger) and half the time picks a random direction, which makes them pursue without all converging on one path. Like the platformer it runs on a fixed 1/60-second timestep, and the four-way D-pad is a CSS grid so the buttons form a real cross.

Best for: a four-direction touch template, and a compact reference for tile collision plus a bias-plus-random enemy controller. Tip: the spots array in buildWalls() is the arena layout — a list of destructible-block grid cells. Change those coordinates and you've designed a new map; the collision and AI don't care.

Grab the code: Tank Arena Game

4. Lane Runner Endless Game

An endless three-lane runner: switch lanes with ◀ ▶, JUMP the green barriers, SLIDE under the amber gates, and dodge red blocks by changing lanes — as the whole thing speeds up and packs tighter.

How it works: every control is a discrete tap — there's no held state anywhere, which is why all four buttons just fire their function on pointerdown through a two-line tap() helper with no release handling to manage. The lane is a logical integer 0–2 used for collision, but the runner is drawn at a separate x-position that eases toward the target lane centre each frame (curX += (targetX - curX) * k), so an instant index change reads as a quick slide rather than a teleport — a clean split worth stealing for any grid-snapped-but-fluid movement. The three obstacle types each demand a different verb: a barrier must be jumped, an overhead gate must be slid under, and a block fills every lane but one so it must be dodged. Jump and slide are timed windows (jumpT/slideT countdowns), so mistiming fails exactly like a real runner — jump too early and the window's expired by the time the barrier arrives. Speed and spawn density are both continuous functions of distance travelled, so it never stops getting harder.

Best for: the most touch-native genre there is, and a demo of discrete-tap controls with zero press-and-hold complexity. Tip: the game feel is four numbers — speed's growth term, JUMP_MS, SLIDE_MS, and the spawn gap floor. Nudge those and you've retuned the entire difficulty ramp.

Grab the code: Lane Runner Endless Game

5. Asteroids Blaster Game

The vector classic, on touch: rotate with ↺ ↻, hold THRUST to build momentum, and tap FIRE to break the rocks into smaller, faster ones. Fly off one edge and you appear on the other.

How it works: this is the one game here where you don't steer position at all — you steer acceleration. The ship keeps a heading angle and a velocity vector (vx, vy) that are deliberately independent: the rotate buttons change only the angle, THRUST adds a small push along that angle, and every frame the velocity is multiplied by a DRAG factor just under 1 so the ship coasts and slowly bleeds speed rather than stopping dead. That decoupling is the entire feel — you can drift backwards while firing forwards — and it's why the control layout needs a separate thrust button instead of a directional pad. Everything that moves (ship, bullets, rocks) runs through one wrap() function that teleports it across the screen edges, making the field a torus with no walls. And the signature mechanic is three lines: when a bullet is inside a rock's radius, splitRock() awards size-scaled points and, unless the rock is already the smallest, replaces it with two smaller ones on fresh headings — so one slow rock becomes the swarm.

Best for: a rotational-control template that's genuinely different from a D-pad, and the cleanest small example of momentum-based movement. Tip: the whole game feel is three constants — THRUST, DRAG and MAX_V. Lower the drag for an icier, more floaty ship; raise the thrust for a twitchier one.

Grab the code: Asteroids Blaster Game

6. Sky Hopper Game

A vertical climber that bounces on its own — you only steer. Hold ◀ ▶ to move, land on platforms to auto-bounce, and climb through breakable, moving and spring platforms as far as you can.

How it works: there is no jump button, and that's the point — reducing the whole control surface to two buttons. The hopper is always under gravity and bounces to a fixed upward velocity whenever it lands, using the same one-way landing test as the platformer: the check only fires while falling (vy > 0) and only when the feet cross a platform's top edge, so you rise straight up through platforms and bounce coming down. Two systems make the endless climb work. The camera is one-directional — it scrolls up when you climb past a line at 42% of the canvas but never scrolls back down, so every bounce is permanent progress and falling below the view is the fail condition. And the platforms are an object pool: ones that scroll off the bottom are filtered out and new ones spawned above the current highest, keeping a fixed, small number in memory no matter how high you get. makePlatform() rolls a type on spawn — normal, breakable (gives way after one bounce), moving, or a purple spring for a super-bounce.

Best for: the minimal two-button touch game, and a compact reference for a follow-camera plus object pooling. Tip: the difficulty is in the spawn odds in makePlatform() and the GAP between platforms — more breakables and a wider gap is a much harder climb.

Grab the code: Sky Hopper Game

7. Frog Crossing Game

Frogger, on a four-way D-pad of discrete hops: dodge the traffic lanes, ride the logs across the river, and fill all four homes at the top without drowning or getting flattened.

How it works: the clever part is that the two hazards use opposite collision rules on the same board, chosen by the row the frog is on. Each row carries a type, and update() branches on it: on a road row the frog dies if its rectangle overlaps any car, but on a water row the logic inverts — it drowns unless its centre is on a log, and when it is, the log's velocity is added to the frog every frame so it rides along (and it drowns if a log carries it off the edge). That single inversion is the whole risk/reward of the river. Movement is discrete: each press is one committed hop of exactly one tile fired on pointerdown, so timing your hop into a gap is the skill — you can't nudge or cancel it. Each lane is configured with a signed speed, length and gap, and its items scroll and wrap seamlessly, with alternating directions creating the staggered timing windows that turn the crossing into a puzzle.

Best for: a grid-hop control template for any tile-based touch game, and a neat lesson in how one flipped rule produces completely different play. Tip: the entire board tuning is the LANES config object — speeds, gaps and piece lengths per row. Widen the gaps or slow the lanes for an easier crossing.

Grab the code: Frog Crossing Game

8. Cave Flyer Game

One button, nothing else: hold to rise, release to fall, and thread a procedurally generated cave that narrows and speeds up the deeper you fly. The purest test of a single-input game.

How it works: the physics is gravity versus lift on a single axis. Every frame, if flying is true an upward LIFT acceleration is added to the velocity; if false, a downward GRAV is added instead — the craft is never set to a height, it's always accelerating one way, so hovering means feathering the button exactly like a real helicopter game. Because it's one boolean, the same control is routed through a hold button, the keyboard, and a press on the canvas via one setFly() call, with release bound on pointerup/pointercancel/pointerleave everywhere so the thrust can't stick on. The cave is a list of thin vertical slices, each with a ceiling and floor height; makeSlice() meanders the centre with a bounded random walk so the tunnel curves smoothly, narrows the gap as distance grows, and occasionally juts a stalactite into the passage. Slices scroll off the left and new ones append on the right, and collision only ever tests the single slice under the craft — cheap no matter how long the run.

Best for: the one-button touch game, and a tidy reference for hold-to-thrust physics plus procedural, recycling terrain. Tip: GRAV, LIFT and the starting gap plus its narrowing rate are the entire difficulty. A smaller gap that tightens faster turns a gentle glide into a white-knuckle thread.

Grab the code: Cave Flyer Game

9. Dot Muncher Maze Game

The maze-chase classic, on touch: eat every dot with a four-way D-pad, grab a power pellet to turn the three ghosts blue and chomp them, and don't get caught. Buffered turning, real ghost AI, and a maze that fixes its own mistakes.

How it works: this is the most engine-heavy of the nine, and three ideas carry it. First, tile-locked movement: every mover — the muncher and each ghost — is a current tile plus a prog value from 0 to 1 toward the next one, with pixel position derived as tile-centre + dir * prog * TS. Turns and wall checks only happen when prog crosses a tile boundary, so nothing can ever clip a wall or turn off-grid no matter the frame rate — the property a maze game lives or dies on. Second, buffered input: the D-pad and keyboard set a queued want direction that's applied at the next tile where it's open (with reversal special-cased to fire instantly), which is why pressing a corner slightly early still turns. Third, the ghosts each minimise distance to a different target tile — one chases you directly, one aims a few tiles ahead to cut you off, one wavers — under a scatter/chase timer, flipping to slow random flight when you eat a pellet. The trick worth stealing: at load a BFS flood-fill from the player start clears any unreachable dots and picks a reachable ghost spawn, so a hand-drawn maze is always winnable even if you draw a sealed pocket.

Best for: a grid-maze control template, and by far the best reference here for tile-locked movement plus target-tile chase AI. Tip: the board is the MAZE array of strings at the top of the file — redraw it however you like and the BFS cleanup guarantees you can't accidentally make it unsolvable.

Grab the code: Dot Muncher Maze Game

How to drop these into your project

  1. Open any snippet and hit View / Edit Code to see the HTML, CSS, and JS in separate tabs — every one is pure vanilla JavaScript with zero dependencies and no CDN scripts required.
  2. Paste the HTML into your markup, the CSS into your stylesheet, and the JS before your closing </body> tag, or use the one-click export to React, Vue, Angular, or Tailwind right from the snippet page.
  3. Copy the control layer, not just the game. The shared keys object read once per frame, the hold() helper that releases on pointerup/pointercancel/pointerleave, and the tap() helper for discrete actions are the reusable parts — they turn any keyboard game into a mobile one without a second code path.
  4. Match the loop to the game. Use delta-time scaling (the shooter, the runner, Asteroids, Frogger, the cave flyer and the muncher) when speed just needs to be consistent; use a fixed timestep (the platformer, the tank game, the sky hopper) when collision against thin walls or platforms — or a precise bounce height — would break under a large variable step. Both patterns are in these files to copy from.
  5. Each game keeps its content as editable data at the top of the file — buildWave() in the shooter, the level arrays in the platformer, the spots layout in the tank arena, the LANES config in Frogger, the MAZE grid in the muncher, and the tuning constants in the runner, the hopper and the flyer — deliberately separated from the engine below. That's the intended place to adapt them, and it needs no changes to the logic.
  6. If you wrap one in a React or Vue component, drive the loop from useRef/useEffect and cancel the animation frame in the cleanup function on unmount — and guard the localStorage best-score read/write in try/catch, which these already do, so the game still renders in a sandboxed iframe where storage access throws.

Final thought

What these nine share isn't the arcade format — they run from a shooter to a grid-hopper to a one-button flyer to a full maze-chase — it's that the controls were designed first and the game was built on top of them. A single input state so keyboard and touch can't diverge, the right held-vs-tap choice per action, a loop matched to what its collision needs, and the finger-slides-off case handled before it becomes a bug report. That's the unglamorous layer that decides whether a browser game feels like a game or feels like a webpage pretending to be one — and it's a few dozen lines you can lift straight out of any of these.

Play any of them right here, retune the constants, and export to your framework of choice in one click. Browse the full collection at FWD Tools UI Snippets — it's free and runs entirely in your browser.

About the author

Puneet Sharma
Puneet Sharma is a freelance web developer and the creator of FWD Tools and WebDevPuneet. Follow him on X/Twitter

Post a Comment