{ 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:
- Set up a TypeScript game project.
- Apply OOP design to organize game logic.
- Implement humanoid character physics and running animation.
- Create dynamic obstacles and collision detection.
- Design continuous parallax backgrounds with clouds and ground.
- Track scores and increase game difficulty progressively.
- 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/
2.2 TypeScript Configuration (tsconfig.json
)
{
"compilerOptions": {
"target": "ES6",
"module": "ES6",
"outDir": "./dist",
"strict": true
},
"include": ["src/**/*"]
}
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>
3.2 CSS (style.css
)
body { margin: 0; overflow: hidden; background: linear-gradient(to bottom, #74ebd5, #ACB6E5); }
canvas { display: block; margin: 0 auto; }
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;
}
}
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); }
}
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);
}
}
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;
}
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
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();
- This entry point sets up the canvas and rendering context.
- Initializes the
Game
class with the updated humanoid character, obstacles, and parallax background. -
gameLoop
usesrequestAnimationFrame
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)