How to create a retro 2D game using JavaScript and HTML5 Canvas

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.

How to create a retro 2D game using JavaScript and HTML5 Canvas
Create 2D Game with JavaScript

Table of Contents

  1. Project Setup
  2. Basic Canvas and Game Loop
  3. Sprites and Movement
  4. Input Handling
  5. Collision Detection and Game States
  6. Simple Retro Effects
  7. Sound Effects and Music (Optional)
  8. Enhancements and Next Step
  9. Complete Example
  10. Conclusion

1. Project Setup

Create Your Project Folder

  1. Create a new folder, for example, retro-game.
  2. 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:

  1. Updates the game state and logic (positions, collisions, etc.)
  2. 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

  1. Enemies – Store multiple enemies in an array, each with its own position and movement logic.
  2. Scoring – Increment a score counter on successful events (e.g., hitting a target).
  3. UI Elements – Display messages, menus, or a HUD by drawing text on the canvas or using HTML overlays.
  4. 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.

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

    Comments