DEV Community

Cover image for Build a Browser-Based Endless Runner Game in TypeScript: Step-by-Step Guide for Beginners
Md Mahbubur Rahman
Md Mahbubur Rahman

Posted on

Build a Browser-Based Endless Runner Game in TypeScript: Step-by-Step Guide for Beginners

{ Download Full Source code of GAME | Play this Interactive GAME Online }

Key Takeaways

Beginners following this tutorial will learn:

  • How to set up a TypeScript project and compile it for browser-based games.
  • Structuring game logic using Object-Oriented Programming (OOP) principles.
  • Creating a humanoid player character with arms, legs, torso, and head.
  • Implementing jump physics and gravity simulation for realistic movement.
  • Designing and managing dynamic obstacles with collision detection.
  • Building continuous parallax backgrounds including moving clouds and scrolling ground.
  • Tracking score and implementing progressive difficulty to enhance gameplay.
  • Handling keyboard input for player control.
  • Optimizing animation using requestAnimationFrame for smooth, efficient performance.
  • Understanding real-time rendering loops, event handling, and modular game architecture for maintainable code.

Abstract

This tutorial presents a comprehensive approach to building an Endless Runner game using TypeScript, HTML5, and CSS3. It now incorporates a humanoid player character with arms, legs, torso, and head, continuous cloud generation, and dynamic parallax backgrounds. Readers will explore object-oriented programming, physics simulation, real-time rendering loops, and collision detection through a fully functional browser game.

1. Introduction

Browser-based gaming has evolved with TypeScript and modern Web APIs, enabling high-performance interactive applications. The Endless Runner genre, where a character runs infinitely while avoiding obstacles, is ideal for beginners and covers:

  • Real-time animation (requestAnimationFrame)
  • Humanoid character animation
  • Collision detection with dynamic obstacles
  • Keyboard input handling (jump mechanics)
  • Continuous parallax scrolling with clouds and ground
  • Progressive difficulty and scoring

Learning Objectives

By the end of this tutorial, learners will:

  1. Set up a TypeScript game project.
  2. Apply OOP design to organize game logic.
  3. Implement humanoid character physics and running animation.
  4. Create dynamic obstacles and collision detection.
  5. Design continuous parallax backgrounds with clouds and ground.
  6. Track scores and increase game difficulty progressively.
  7. Deploy the game for browser execution using TypeScript, HTML, and CSS.

2. Project Setup

2.1 Folder Structure

endless-runner/
├── index.html
├── style.css
├── tsconfig.json
├── src/
│   ├── main.ts
│   ├── game.ts
│   ├── player.ts
│   ├── obstacle.ts
│   └── background.ts
└── dist/
Enter fullscreen mode Exit fullscreen mode

2.2 TypeScript Configuration (tsconfig.json)

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ES6",
    "outDir": "./dist",
    "strict": true
  },
  "include": ["src/**/*"]
}
Enter fullscreen mode Exit fullscreen mode

3. HTML and CSS

3.1 HTML (index.html)

<canvas id="gameCanvas" width="900" height="450"></canvas>
<script type="module" src="dist/main.js"></script>
Enter fullscreen mode Exit fullscreen mode

3.2 CSS (style.css)

body { margin: 0; overflow: hidden; background: linear-gradient(to bottom, #74ebd5, #ACB6E5); }
canvas { display: block; margin: 0 auto; }
Enter fullscreen mode Exit fullscreen mode

4. Game Architecture

Class Responsibility
Game Manages game loop, updates, rendering, obstacles, and score
Player Humanoid character with arms, legs, torso, and head; jump physics
Obstacle Moving obstacles and collision detection
Background Continuous clouds and ground for parallax effect

5. Core Classes

5.1 Player (player.ts)

Humanoid character with running animation and jump physics.

export class Player {
  x: number; y: number; width = 30; height = 50;
  velocityY = 0; gravity = 0.8; jumpForce = -14;
  ground: number; runningStep = 0;

  constructor(x: number, y: number) {
    this.x = x; this.y = y; this.ground = y;
  }

  jump() { if (this.y === this.ground) this.velocityY = this.jumpForce; }

  update() {
    this.y += this.velocityY; this.velocityY += this.gravity;
    if (this.y > this.ground) this.y = this.ground;
    this.runningStep += 0.2;
  }

  draw(ctx: CanvasRenderingContext2D) {
    // torso
    ctx.fillStyle = '#3498db';
    ctx.fillRect(this.x, this.y - this.height, this.width, this.height);
    // legs
    ctx.fillStyle = '#2c3e50';
    ctx.fillRect(this.x - 5, this.y, 10, 20);
    ctx.fillRect(this.x + 15, this.y + Math.sin(this.runningStep)*5, 10, 20);
    // arms
    ctx.fillStyle = '#f1c27d';
    ctx.fillRect(this.x - 10, this.y - this.height + 10 + Math.sin(this.runningStep)*5, 10, 30);
    ctx.fillRect(this.x + this.width, this.y - this.height + 10 - Math.sin(this.runningStep)*5, 10, 30);
    // head
    ctx.beginPath();
    ctx.arc(this.x + this.width/2, this.y - this.height - 10 + Math.sin(this.runningStep*2)*2, 10, 0, Math.PI*2);
    ctx.fill();
  }

  collidesWith(obstacle: any): boolean {
    return this.x < obstacle.x + obstacle.width && this.x + this.width > obstacle.x &&
           this.y - this.height < obstacle.y && this.y > obstacle.y - obstacle.height;
  }
}
Enter fullscreen mode Exit fullscreen mode

5.2 Obstacle (obstacle.ts)

export class Obstacle {
  constructor(public x: number, public y: number, public width: number, public height: number, public speed: number) {}

  update() { this.x -= this.speed; }
  draw(ctx: CanvasRenderingContext2D) { ctx.fillStyle = '#ff6b6b'; ctx.fillRect(this.x, this.y - this.height, this.width, this.height); }
}
Enter fullscreen mode Exit fullscreen mode

5.3 Background with Clouds (background.ts)

export interface Cloud { x: number; y: number; radiusX: number; radiusY: number; }
export class Background {
  clouds: Cloud[] = [];
  groundOffset = 0;

  constructor(public ctx: CanvasRenderingContext2D, public speed: number) {
    for (let i=0;i<5;i++) this.clouds.push({x: Math.random()*900, y: 50+Math.random()*100, radiusX: 20+Math.random()*20, radiusY: 15+Math.random()*15});
  }

  update() {
    this.clouds.forEach(cloud => { cloud.x -= this.speed*0.3; if(cloud.x+cloud.radiusX*2<0) { cloud.x=900+cloud.radiusX*2; cloud.y=50+Math.random()*100; } });
    this.groundOffset -= this.speed;
  }

  draw() {
    // sky
    const gradient = this.ctx.createLinearGradient(0,0,0,450);
    gradient.addColorStop(0,'#74ebd5'); gradient.addColorStop(1,'#ACB6E5');
    this.ctx.fillStyle = gradient; this.ctx.fillRect(0,0,900,450);
    // clouds
    this.ctx.fillStyle='rgba(255,255,255,0.8)';
    this.clouds.forEach(cloud => { this.ctx.beginPath(); this.ctx.ellipse(cloud.x,cloud.y,cloud.radiusX,cloud.radiusY,0,0,Math.PI*2); this.ctx.ellipse(cloud.x+cloud.radiusX,cloud.y,cloud.radiusX+10,cloud.radiusY+5,0,0,Math.PI*2); this.ctx.fill(); });
    // ground
    this.ctx.fillStyle='#7ec850';
    this.ctx.fillRect(this.groundOffset%900, 450-40, 900, 40);
    this.ctx.fillRect((this.groundOffset%900)+900,450-40,900,40);
  }
}
Enter fullscreen mode Exit fullscreen mode

5.4 Utility (utils.ts)

export function isColliding(a:any,b:any):boolean{
  return a.x < b.x+b.width && a.x+a.width > b.x && a.y-a.height < b.y && a.y > b.y-b.height;
}
Enter fullscreen mode Exit fullscreen mode

6. Game Controller (game.ts)

import { Player } from './player.js';
import { Obstacle } from './obstacle.js';
import { Background } from './background.js';
import { isColliding } from './utils.js';

export class Game {
  player: Player;
  obstacles: Obstacle[]=[];
  background: Background;
  score=0;
  speed=4;
  gameOver=false;

  constructor(private ctx: CanvasRenderingContext2D){
    this.player=new Player(100,380);
    this.background=new Background(ctx,this.speed);
    this.spawnObstacle();
    this.listenForInput();
  }

  listenForInput(){
    window.addEventListener('keydown',e=>{if(e.code==='Space') this.player.jump();});
  }

  spawnObstacle(){
    setInterval(()=>{if(!this.gameOver) this.obstacles.push(new Obstacle(900,450-40,20,20+Math.random()*40,this.speed));},1500);
  }

  checkCollisions(){this.obstacles.forEach(o=>{if(this.player.collidesWith(o)) this.gameOver=true;});}

  update(){
    if(this.gameOver) return;
    this.background.update();
    this.player.update();
    this.obstacles.forEach(o=>o.update());
    this.obstacles=this.obstacles.filter(o=>o.x+o.width>0);
    this.checkCollisions();
    this.score++;
    if(this.score%5===0) this.speed+=0.5;
  }

  draw(){
    this.background.draw();
    this.player.draw(this.ctx);
    this.obstacles.forEach(o=>o.draw(this.ctx));
    this.ctx.fillStyle='#2d3436'; this.ctx.font='24px Arial';
    this.ctx.fillText('Score: '+this.score,20,40);
    if(this.gameOver){this.ctx.fillStyle='red'; this.ctx.font='40

Enter fullscreen mode Exit fullscreen mode

7. Entry Point (main.ts)

import { Game } from './game.js';

const canvas = document.getElementById('gameCanvas') as HTMLCanvasElement;
const ctx = canvas.getContext('2d')!;

const game = new Game(ctx);

function gameLoop() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  game.update();
  game.draw();
  requestAnimationFrame(gameLoop);
}

gameLoop();
Enter fullscreen mode Exit fullscreen mode
  • This entry point sets up the canvas and rendering context.
  • Initializes the Game class with the updated humanoid character, obstacles, and parallax background.
  • gameLoop uses requestAnimationFrame for smooth animation, clears the canvas each frame, updates game state, and renders all elements.

8. Physics, Optimization, and Reflection

  • Gravity: Simulates realistic jump and fall mechanics for the humanoid character.
  • AABB Collision Detection: Efficiently detects collisions between player and obstacles.
  • Dynamic Difficulty Adjustment: Increases obstacle speed progressively as the score increases.
  • Parallax Scrolling: Clouds and ground move at different speeds to enhance depth perception.
  • Performance Optimizations: Uses minimal per-frame allocations and requestAnimationFrame to maintain 60fps and reduce memory usage.

9. Conclusion

The Endless Runner game with a humanoid player, continuous cloud generation, and dynamic parallax backgrounds demonstrates TypeScript's strength for browser-based interactive applications. Beginners gain hands-on experience with modular OOP design, real-time physics, animation loops, collision detection, and progressive gameplay mechanics. This project serves as a robust foundation for building more complex browser games and reinforces practical software engineering principles.

Top comments (0)