Platformer Game Tutorial using JavaScript

Platformer games are one of the most beloved video game genres, with classic series like Super Mario Bros., Sonic the Hedgehog, and Mega Man that have captivated players for decades. The core gameplay involves guiding a character to jump between platforms, avoid obstacles, and reach the end of each level.

In this in-depth tutorial, we‘ll learn how to build a complete platformer game from scratch using vanilla JavaScript and HTML5 canvas. Whether you‘re a seasoned developer looking to get into game development or a beginner eager to create your first game, this guide will walk you through the process step-by-step.

We‘ll cover all the essential aspects of platformer game development, including:

  • Setting up the project structure
  • Architecting the game with the Model-View-Controller (MVC) pattern
  • Designing levels with tile maps
  • Implementing smooth player movement and jumping
  • Creating engaging enemies and obstacles
  • Detecting collisions for precise gameplay
  • Animating characters and backgrounds
  • Adding collectibles, powerups, and a lives system
  • Transitioning between levels
  • Polishing the game with menus, sound, and effects

The final result will be a fully playable platformer game that showcases the fundamental techniques used in JavaScript game development. You‘ll gain a solid understanding of the core concepts and be able equip with the skills to extend the game further or build your own games from the ground up.

Let‘s jump in!

Project Setup

To get started, create a new folder for the project with the following structure:

platformer-game/
  index.html
  style.css
  js/
    game.js
    player.js 
    enemy.js
    tilemap.js
  img/
    player.png
    enemies.png
    tiles.png

The index.html file will contain the game‘s markup and canvas element, while the style.css file will hold any necessary CSS styles.

Inside the js folder, we‘ll split our JavaScript code into multiple files following the MVC architecture:

  • game.js will handle the overall game logic and loop
  • player.js will define the player object and its behavior
  • enemy.js will implement the various enemy types
  • tilemap.js will load and render the level design

Finally, the img folder will store the sprite sheets and tile sets used for the game‘s graphics.

Game Architecture with MVC

To keep our code organized and maintainable, we‘ll structure the game following the Model-View-Controller (MVC) architectural pattern. This separates the game into three interconnected components:

  1. The Model represents the game‘s data and logic, such as the player, enemies, and level layout. It‘s independent of the user interface.

  2. The View is responsible for rendering the game‘s visual elements on the screen, such as drawing the level and animating sprites.

  3. The Controller handles user input and updates the model based on the game‘s rules. It acts as an intermediary between the Model and View.

Separating concerns in this way promotes a cleaner codebase that‘s easier to reason about and extend over time. We‘ll create classes for our game entities and systems using Object-Oriented Programming (OOP) principles.

Implementing the Game Model

At the heart of every platformer game is the level design. We‘ll use a tile-based approach where the game world is represented as a grid of tiles. Each tile is assigned a specific type, such as a solid block, a platformer, a hazard, or a collectable.

To define our levels, we can use a 2D array where each element corresponds to a tile type. For example:

const LEVELS = [
  [
    ‘                 ‘,
    ‘                 ‘, 
    ‘                 ‘,
    ‘      c          ‘,
    ‘  c     c    c   ‘,
    ‘xxxxxxxxxxxxxxxx ‘,
  ],
  // ... more levels
];

Here, x represents a solid tile,‘‘ an empty space, and c a collectable. We can parse this array in our tilemap.js module to generate the level geometry.

Next, let‘s define our Player class to encapsulate the player object‘s properties and behavior:

class Player {
  constructor(x, y) {
    this.x = x;
    this.y = y;
    this.width = 32;
    this.height = 32;
    this.velocity = { x: 0, y: 0 };

    this.jumping = false;
    this.grounded = false;
  }

  update(dt) {
    this.velocity.y += GRAVITY * dt;

    if (!this.grounded) {
      this.grounded = this.y >= GROUND_Y;     
    }

    if (controller.jump && this.grounded) {
      this.velocity.y = JUMP_VELOCITY;
      this.grounded = false;
    }

    this.x += this.velocity.x * dt;
    this.y += this.velocity.y * dt;
  }
}

The player has properties for position, size, velocity, and flags for jumping and grounded state. The update method is called each frame to apply gravity and handle jumping based on user input.

We can define similar classes for different enemy types, such as patrolling or flying enemies, each with their own movement patterns and collision behavior.

Creating the Game View

With our game model in place, let‘s bring it to life with visuals. We‘ll use HTML5 canvas to render our game world and entities.

First, set up a canvas element in index.html:

<canvas id="gameCanvas" width="800" height="600"></canvas>

In our game.js module, we‘ll get a reference to the canvas and its 2D rendering context:

const canvas = document.getElementById(‘gameCanvas‘);
const ctx = canvas.getContext(‘2d‘);

To draw our tiled level, we‘ll load the tile set image and render each tile based on the level data:

const tileSize = 32;

function drawLevel() {
  for (let row = 0; row < level.length; row++) {
    for (let col = 0; col < level[row].length; col++) {
      const tile = level[row][col];

      if (tile === ‘x‘) {
        ctx.drawImage(
          tilesetImage, 
          0, 0, tileSize, tileSize,
          col * tileSize, row * tileSize, tileSize, tileSize
        );
      } else if (tile === ‘c‘) {
        // Draw collectable
      }
    }
  }
}

To render the player and enemies, we‘ll load their sprite sheets and use canvas‘s drawImage method to draw the appropriate frame based on their current animation state.

const playerSprite = new Image();
playerSprite.src = ‘img/player.png‘;

function drawPlayer() {  
  ctx.drawImage(
    playerSprite,
    0, 0, player.width, player.height,
    player.x, player.y, player.width, player.height
  );
}

For more dynamic visuals, we can incorporate sprite animations by cycling through different regions of the sprite sheet over time. This gives the illusion of walking, running, or attacking.

To add depth to the scene, we can implement parallax scrolling where background layers move at different speeds relative to the camera. This creates a sense of depth and immersion.

Handling Player Input

Engaging controls are essential for a fun platformer experience. We‘ll listen for keyboard events to control the player character‘s movement and actions.

In our game.js module, we can set up event listeners for keydown and keyup events:

const controller = {};

document.addEventListener(‘keydown‘, ({ code }) => {
  controller[code] = true;
});

document.addEventListener(‘keyup‘, ({ code }) => {
  controller[code] = false;
});

The controller object keeps track of which keys are currently pressed.

In the player‘s update method, we can check the controller state to determine movement:

if (controller.ArrowLeft) {
  this.velocity.x = -PLAYER_SPEED;
} else if (controller.ArrowRight) {
  this.velocity.x = PLAYER_SPEED;
} else {
  this.velocity.x = 0;
}

if (controller.Space) {
  this.jump();
}

For a mobile-friendly experience, we can also implement touch controls with on-screen buttons or swipe gestures.

Collision Detection

Precise collision detection is crucial for responsive platforming gameplay. We need to check for collisions between the player, platforms, enemies, and collectables.

A simple approach is to use axis-aligned bounding box (AABB) collision. We check if the rectangles of two entities are overlapping:

function checkCollision(rect1, rect2) {
  return (
    rect1.x < rect2.x + rect2.width &&
    rect1.x + rect1.width > rect2.x &&
    rect1.y < rect2.y + rect2.height &&
    rect1.y + rect1.height > rect2.y
  );
}

For pixel-perfect collisions, we can use the separating axis theorem (SAT) or bitmap collision detection for more complex shapes.

When a collision is detected, we‘ll resolve it by adjusting the player‘s position and velocity accordingly. This prevents the player from passing through solid tiles or enemies.

Assembling the Game Loop

With all our game components in place, it‘s time to bring them together in the main game loop. This is the heartbeat of our game where we continuously update the game state and render the results.

In game.js, we‘ll set up a requestAnimationFrame loop:

function gameLoop() {
  const dt = 1 / 60; // Fixed time step of 60 FPS

  update(dt);
  render();

  requestAnimationFrame(gameLoop);
}

function update(dt) {
  player.update(dt);
  enemies.forEach(enemy => enemy.update(dt));
  checkCollisions();
  checkWinCondition();
}

function render() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawLevel();
  drawPlayer();
  enemies.forEach(enemy => enemy.draw());
}

gameLoop();

Each frame, we update the game state by a fixed time step, detect collisions, and check win/lose conditions. Then we render the updated game world and entities. This loop runs indefinitely to animate the game.

Adding Polish and Juice

To take our platformer from functional to delightful, we can add various polish and "juice" to enhance the game feel:

  • Create a title screen and level select menu for easy navigation
  • Add sound effects for actions like jumping, collecting items, and taking damage
  • Implement background music and transition tracks between levels
  • Use particle effects for collectables, explosions, and other visual flourishes
  • Provide clear feedback for player actions through animations and screen shake
  • Incorporate parallax scrolling for a sense of depth in the background
  • Animate level transitions and screen wipes for a cohesive experience

Optimization and Deployment

As our game grows in complexity, it‘s important to optimize performance to ensure a smooth framerate. Some techniques include:

  • Batch rendering to minimize expensive draw calls
  • Culling off-screen entities to reduce unnecessary updates
  • Pooling objects to avoid garbage collection overhead
  • Lazy loading assets to speed up initial load times

When our game is ready for the world, we can package it for distribution on the web, desktop, or mobile platforms. Tools like Cordova or Electron make it easy to wrap our HTML5 game into native apps.

Extending the Platformer

Congratulations! You‘ve built a complete platformer game in JavaScript. But the fun doesn‘t stop here. You can extend the game with new features and mechanics:

  • Add power-ups like invincibility, double jump, or projectile weapons
  • Create moving platforms, disappearing blocks, or other interactive elements
  • Implement a level editor for players to create and share their own challenges
  • Explore procedural level generation for endless replayability

There‘s no limit to the creativity you can pour into your platformer project. Use this foundation as a springboard to build the game of your dreams.

Conclusion

In this comprehensive tutorial, we‘ve covered the essential techniques for building a platformer game in JavaScript. From project setup to gameplay mechanics to polish and optimization, you now have a solid understanding of the game development process.

Remember, game development is an iterative journey of experimentation and refinement. Don‘t be afraid to prototype ideas, playtest often, and seek feedback from players. The more you practice and explore, the more your skills will grow.

I hope this guide has ignited your passion for game development and equipped you with the knowledge to create your own JavaScript games. Go forth and build amazing experiences that players will love.

Happy coding!

Similar Posts