Make a file
Open any text editor โ Notepad, TextEdit (set it to plain text), or VS Code if you want colours. Save an empty file as game.html. The .html ending is what matters.
Zero to playable ยท no installs ยท no build tools
Ten short lessons take you from your very first <h1> to a finished arcade game you can play, share and keep improving. Nearly every lesson has a live editor โ change the code, hit Run, see it happen instantly.
Open any text editor โ Notepad, TextEdit (set it to plain text), or VS Code if you want colours. Save an empty file as game.html. The .html ending is what matters.
Every page starts the same way. Type the block in lesson 1 once by hand โ muscle memory beats copy-paste for the first one.
Your browser opens the file. That's it โ you are running your own program. Edit, save, press F5 to reload. That loop is the whole job.
Press F12 (or Cmd+Option+I) and click Console. Red text there tells you exactly which line broke. Check it every single time something doesn't work.
Everything on this page runs in the little editors below too, so you can learn first and set up your editor later.
Work top to bottom. Each one adds a piece of the game you'll play at the bottom of this page.
HTML is a set of tags that wrap content: <tag>content</tag>. A tag plus its content is an element. Elements nest inside each other like boxes. Here is the shape of every page you will ever write:
<!DOCTYPE html> โ tells the browser "modern HTML".<head> โ invisible setup: title, styles, metadata.<body> โ everything people actually see.<script> โ your JavaScript, the part that makes things move.Try it: change the <h1> text, add a second <p>, then press Run.
There are about a hundred HTML tags. You need roughly twelve. These are them:
| Tag | What it's for |
|---|---|
<h1>โ<h6> | Headings, biggest to smallest. One h1 per page. |
<p> | A paragraph of text. |
<div> | A generic box. Your main layout tool. |
<span> | A generic bit of inline text, e.g. a score number. |
<button> | Something clickable. Use this, not a clickable div. |
<img src="โฆ" alt="โฆ"> | A picture. alt describes it for screen readers. |
<canvas> | A blank rectangle you draw pixels on. The game lives here. |
<audio> | Sound effects and music. |
<a href="โฆ"> | A link. |
<ul> / <li> | A list โ great for high-score tables. |
<input> | A text box, slider, checkboxโฆ |
<style> / <script> | CSS and JavaScript, inline in the page. |
Tags carry attributes โ extra settings inside the opening tag. The two you'll use constantly are id (a unique name so JavaScript can find the element) and class (a label so CSS can style many elements at once).
CSS styles your HTML. A rule is a selector plus declarations:
h1 { color: tomato; } /* every h1 */
.card { padding: 16px; } /* class="card" */
#score { font-weight: 700; } /* id="score" */
For games you mostly need: background, color, padding, margin, border, border-radius, display:flex for laying things in a row, and position for sticking a score on top of the canvas.
HTML is the nouns, CSS is the adjectives, JavaScript is the verbs. You need these six ideas and almost nothing else:
| Idea | Looks like | Means |
|---|---|---|
| Variable | let x = 10; | A named box holding a value. const if it never changes. |
| Function | function jump() { โฆ } | A named chunk of steps you can run later. |
| Condition | if (y > 300) { โฆ } | Do this only when that is true. |
| Loop | for (let i = 0; i < 5; i++) | Repeat something a set number of times. |
| Array | let enemies = []; | A numbered list. enemies.push(e) adds, enemies[0] reads. |
| Object | { x: 10, y: 20, hp: 3 } | A labelled bundle. Every game entity is one of these. |
Try it: add a fourth enemy to the array and run it again. Nothing else needs to change โ that's why we use arrays.
A <canvas> is an empty rectangle. You get a context from it and issue drawing commands. Coordinates start at 0,0 in the top-left, and y grows downwards โ the single most common beginner surprise.
ctx.fillStyle = '#f00' โ set the colour for the next shape.ctx.fillRect(x, y, w, h) โ filled rectangle. 90% of your drawing.ctx.clearRect(0, 0, W, H) โ wipe the whole canvas.ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI*2); ctx.fill() โ a circle.ctx.fillText('Score', x, y) โ text, after setting ctx.font.ctx.drawImage(img, x, y) โ a sprite.Try it: move the hero by changing 40, 120. Make the sun bigger by changing the radius 24.
A game is one function run ~60 times a second. Each pass is a frame, and each frame does three things in order:
requestAnimationFrame(loop) is how you ask. Never use setInterval for this; requestAnimationFrame syncs to the screen and pauses in background tabs.
Multiply movement by delta time (seconds since the last frame) so the game runs at the same speed on a 60 Hz laptop and a 144 Hz monitor.
The trick nobody tells beginners: don't move the player inside the key event. Key events fire irregularly. Instead record which keys are currently held in an object, and read that object in the loop.
Add e.preventDefault() in keydown for arrows and space, or the page will scroll while you play.
Two rectangles overlap when they overlap on both axes. This four-line check ("AABB") covers almost every 2D game:
function hits(a, b) {
return a.x < b.x + b.w && a.x + a.w > b.x &&
a.y < b.y + b.h && a.y + a.h > b.y;
}
Gravity is just a constant added to vertical speed every frame. Jumping is setting that speed to a negative number once, while standing on the ground.
Sprites. Load an image once, draw it every frame. Wait for onload before drawing, or you get nothing.
const hero = new Image();
hero.src = 'hero.png';
// in draw(): ctx.drawImage(hero, p.x, p.y, p.w, p.h);
For an animation strip, draw one slice of the sheet: ctx.drawImage(sheet, frame*32, 0, 32, 32, p.x, p.y, 32, 32) and advance frame every few hundredths of a second.
Sound. Browsers block audio until the player interacts, so start sound on the first click or keypress.
const coinSfx = new Audio('coin.wav');
function play(a) { a.currentTime = 0; a.play().catch(() => {}); }
States. A game is never only "playing". Keep one variable and branch on it โ this is what stops your code turning into spaghetti.
localStorage keeps small values on the player's machine, forever, with no server. Values are always strings, so wrap numbers with Number() and objects with JSON.stringify / JSON.parse.
Once a file passes ~300 lines, split it: index.html for structure, style.css for looks, game.js for logic. Link them with <link rel="stylesheet" href="style.css"> and <script src="game.js"></script> placed just before </body>.
A structure that scales to a real game:
const game = {
state: 'menu',
player: { โฆ },
enemies: [],
score: 0,
};
function update(dt) { /* move + collide, branch on game.state */ }
function draw() { /* clear + render, branch on game.state */ }
function reset() { /* put game back to a fresh run */ }
function loop(now) { update(dt); draw(); requestAnimationFrame(loop); }
Everything from the ten lessons, assembled. Arrow keys or A/D to move, Space to jump, or tap the two buttons on a phone. Collect coins, dodge the red blocks, beat your high score.
Shipping version 1 is the easy half. Here's the workflow that keeps version 12 from collapsing.
Edit a single value, save, press F5. If it broke, you know exactly what did it. Changing five things at once and then debugging is how evenings disappear.
Keep tunable numbers together as named constants โ GRAVITY, JUMP, SPAWN_RATE, PLAYER_SPEED. Balancing then means editing a tidy list, not hunting through logic.
Describe levels and enemy types as arrays of objects. Adding content becomes adding a line of data: { type:'bat', hp:2, speed:120 }.
Before a risky change, duplicate the file as game-v3.html. Better: learn git init, git add ., git commit -m "added bats" โ then every version is recoverable.
Browsers cache aggressively. If an update "didn't apply", press Ctrl/Cmd+Shift+R before you start debugging code that's already correct.
console.log('player', p.x, p.y) inside the loop prints every frame. Red errors name the file and line. debugger; freezes execution so you can inspect everything.
| Symptom | Cause | Fix |
|---|---|---|
| Blank canvas | Script ran before the canvas existed | Put <script> just before </body>, or use defer |
| Everything smears into trails | You never cleared the canvas | ctx.clearRect(0,0,W,H) at the top of draw() |
| Player teleports / sprints | requestAnimationFrame called twice, so two loops run | Start the loop exactly once |
| Speed differs per computer | Movement not multiplied by delta time | x += speed * dt |
| Blurry or stretched canvas | Sized with CSS instead of attributes | Set width/height attributes; use CSS only to scale |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width,
initial-scale=1">
<title>My Game</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<canvas id="game" width="640"
height="360"></canvas>
<script src="game.js"></script>
</body>
</html>
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#3ddc97';
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.fillRect(x, y, w, h);
ctx.strokeRect(x, y, w, h);
ctx.clearRect(0, 0, W, H);
ctx.beginPath();
ctx.arc(x, y, r, 0, Math.PI * 2);
ctx.fill();
ctx.font = 'bold 20px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Score', x, y);
ctx.drawImage(img, x, y, w, h);
ctx.save(); ctx.translate(x, y);
ctx.rotate(angle); ctx.restore();
Math.random() // 0โ1
Math.floor(Math.random()*n)
Math.max(lo, Math.min(hi, v)) // clamp
Math.hypot(bx-ax, by-ay) // distance
Math.sin(t), Math.cos(t) // waves
Math.PI * 2 // full turn
Math.round(v), Math.abs(v)
addEventListener('keydown', fn);
addEventListener('keyup', fn);
el.addEventListener('click', fn);
el.addEventListener('pointerdown', fn);
addEventListener('resize', fn);
// e.key 'ArrowLeft' 'a' ' '
// e.code 'Space' 'KeyA' 'ArrowUp'
const el = document.getElementById('hud');
const b = document.querySelector('.btn');
el.textContent = 'Score: ' + score;
el.classList.add('hidden');
el.classList.toggle('paused');
el.style.display = 'none';
game.html file and get it running from your own desktop.