Retro games are making a comeback—sometimes for nostalgia, sometimes for their delightful simplicity. In this article, we’ll walk through how to build a small-scale 2D game using HTML5 Canvas and vanilla JavaScript. We’ll cover setting up the project, drawing shapes and sprites, handling input, detecting collisions, and more.

Table of Contents
- Project Setup
- Basic Canvas and Game Loop
- Sprites and Movement
- Input Handling
- Collision Detection and Game States
- Simple Retro Effects
- Sound Effects and Music (Optional)
- Enhancements and Next Step
- Complete Example
- Conclusion
1. Project Setup
Create Your Project Folder
- Create a new folder, for example, retro-game.
- Inside, add:
- index.html
- main.js
- style.css(optional for styling or UI elements).
 
index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Retro 2D Game</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <canvas id="gameCanvas" width="400" height="400"></canvas>
  <script src="main.js"></script>
</body>
</html>style.css (Optional)
body {
  margin: 0;
  padding: 0;
  background-color: #333;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}
#gameCanvas {
  background-color: #000;
  /* This ensures pixelated scaling for a retro feel */
  image-rendering: pixelated;
}2. Basic Canvas and Game Loop
Access the Canvas
In main.js, grab references to your canvas and its 2D context:
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');Game Loop Overview
A core component of any 2D game is the game loop, which continuously:
- Updates the game state and logic (positions, collisions, etc.)
- Draws the current frame
We can use requestAnimationFrame() to keep this loop running smoothly:
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);Clearing the Canvas
Each time we draw a new frame, we should clear out the old one:
function clearCanvas() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
}3. Sprites and Movement
Loading Sprites (Optional)
If you’d rather not rely on solid shapes, load a sprite sheet or simple image:
const sprite = new Image();
sprite.src = 'path/to/sprite.png';But for a minimalistic retro look, rectangles and pixels often suffice.
Tracking Positions
Here’s a simple player object:
const player = {
  x: 50,
  y: 50,
  width: 16,
  height: 16,
  speed: 2,
};Updating Movement
In our update() function, we can move the player:
function update() {
  // Example: Move player automatically to the right
  player.x += player.speed;
  // Wrap around screen for a "retro" effect
  if (player.x > canvas.width) {
    player.x = -player.width;
  }
}Drawing the Player
Inside draw(), we can visualize the player:
Option A: Rectangle
ctx.fillStyle = '#0f0';
ctx.fillRect(player.x, player.y, player.width, player.height);Option B: Sprite
ctx.drawImage(sprite, player.x, player.y, player.width, player.height);4. Input Handling
Keyboard Events
We can track keyboard presses using event listeners and a keys object:
const keys = {};
window.addEventListener('keydown', (e) => {
  keys[e.code] = true;
});
window.addEventListener('keyup', (e) => {
  keys[e.code] = false;
});
function update() {
  if (keys['ArrowUp'] || keys['KeyW']) {
    player.y -= player.speed;
  }
  if (keys['ArrowDown'] || keys['KeyS']) {
    player.y += player.speed;
  }
  if (keys['ArrowLeft'] || keys['KeyA']) {
    player.x -= player.speed;
  }
  if (keys['ArrowRight'] || keys['KeyD']) {
    player.x += player.speed;
  }
  // Additional logic like collision checks, etc.
}5. Collision Detection and Game States
Basic Collision
A quick rectangle collision check between two objects:
function isColliding(a, b) {
  return (
    a.x < b.x + b.width &&
    a.x + a.width > b.x &&
    a.y < b.y + b.height &&
    a.y + a.height > b.y
  );
}Game States
It’s common to have multiple game states—like “start,” “playing,” “over.”
let gameState = 'start';
function update() {
  if (gameState === 'playing') {
    // Update positions, collisions, etc.
  }
}
function draw() {
  clearCanvas();
  if (gameState === 'start') {
    // Draw start screen
  } else if (gameState === 'playing') {
    // Draw main game
  } else if (gameState === 'over') {
    // Draw game over screen
  }
}6. Simple Retro Effects
Pixel Scaling
To emulate an older-style pixel look:
- Use small sprite sizes (e.g. 16×16).
- Scale up the displayed size using CSS:
#gameCanvas {
  width: 400px;  /* displayed size */
  height: 400px;
  image-rendering: pixelated;
}- Keep the canvas’s actual resolution relatively small (e.g., 200×200).
Fixed Frame Rate
Old-school games often ran at fixed frame rates (e.g., 60 FPS). You can mimic this:
let lastTime = 0;
const fps = 60;
const frameDuration = 1000 / fps;
function gameLoop(timestamp) {
  const delta = timestamp - lastTime;
  if (delta > frameDuration) {
    lastTime = timestamp - (delta % frameDuration);
    update();
    draw();
  }
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);This ensures each update occurs at a stable rhythm.
7. Sound Effects and Music (Optional)
Loading Audio
const jumpSound = new Audio('sounds/jump.wav');
const backgroundMusic = new Audio('sounds/music.mp3');
backgroundMusic.loop = true;Playing Sounds
function jump() {
  jumpSound.currentTime = 0;
  jumpSound.play();
}
// Start the music if needed
backgroundMusic.play();Use events like “key press” or “collision” to trigger a sound.
8. Enhancements in Code
- Enemies – Store multiple enemies in an array, each with its own position and movement logic.
- Scoring – Increment a score counter on successful events (e.g., hitting a target).
- UI Elements – Display messages, menus, or a HUD by drawing text on the canvas or using HTML overlays.
- Refactoring – As your code grows, consider using classes or modules to separate logic (e.g., Player,Enemy,GameWorld).
9. Complete Example
Here’s a more cohesive snippet demonstrating the ideas mentioned:
// main.js
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const keys = {};
let gameState = 'start';
let score = 0;
const player = {
  x: 50,
  y: 50,
  width: 16,
  height: 16,
  speed: 2,
};
window.addEventListener('keydown', (e) => {
  keys[e.code] = true;
  // If we're on the start screen, press Enter to begin
  if (e.code === 'Enter' && gameState === 'start') {
    gameState = 'playing';
  }
});
window.addEventListener('keyup', (e) => {
  keys[e.code] = false;
});
function isColliding(a, b) {
  return (
    a.x < b.x + b.width &&
    a.x + a.width > b.x &&
    a.y < b.y + b.height &&
    a.y + a.height > b.y
  );
}
function clearCanvas() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function update() {
  if (gameState === 'playing') {
    // Player movement
    if (keys['ArrowUp'] || keys['KeyW']) player.y -= player.speed;
    if (keys['ArrowDown'] || keys['KeyS']) player.y += player.speed;
    if (keys['ArrowLeft'] || keys['KeyA']) player.x -= player.speed;
    if (keys['ArrowRight'] || keys['KeyD']) player.x += player.speed;
    // Screen wrap
    if (player.x < 0) player.x = canvas.width;
    if (player.x > canvas.width) player.x = 0;
    if (player.y < 0) player.y = canvas.height;
    if (player.y > canvas.height) player.y = 0;
    // Simple score increment
    score++;
  }
}
function draw() {
  clearCanvas();
  if (gameState === 'start') {
    ctx.fillStyle = '#fff';
    ctx.font = '20px monospace';
    ctx.fillText('Press Enter to Start', 50, 100);
  } else if (gameState === 'playing') {
    // Draw player
    ctx.fillStyle = '#0f0';
    ctx.fillRect(player.x, player.y, player.width, player.height);
    // Draw score
    ctx.fillStyle = '#fff';
    ctx.font = '16px monospace';
    ctx.fillText(`Score: ${score}`, 10, 20);
  } else if (gameState === 'over') {
    ctx.fillStyle = '#fff';
    ctx.font = '20px monospace';
    ctx.fillText('Game Over!', 100, 100);
  }
}
function gameLoop() {
  update();
  draw();
  requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);Conclusion
By understanding the basics of the HTML5 Canvas, requestAnimationFrame loops, and key input handling, you have the foundation for creating a wide variety of 2D games—from small arcade clones to more ambitious retro-inspired adventures. Experiment with sprite art, physics, sound design, and user interface to bring your game to life. As you enhance and expand your code, consider adopting object-oriented patterns or game development libraries (e.g., Phaser or PixiJS) to streamline your workflow.