Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to the exciting world of game development with TypeScript! ๐ฎ In this tutorial, weโll explore how to create interactive games using HTML5 Canvas and WebGL, all powered by TypeScriptโs type safety.
Youโll discover how Canvas and WebGL can transform your creative ideas into playable games. Whether youโre building simple 2D puzzles ๐งฉ, action-packed shooters ๐, or stunning 3D experiences ๐, understanding these technologies is essential for modern web game development.
By the end of this tutorial, youโll be creating your own games with confidence! Letโs dive in! ๐โโ๏ธ
๐ Understanding Canvas and WebGL
๐ค What are Canvas and WebGL?
Think of Canvas as your digital drawing board ๐จ and WebGL as your supercharged paintbrush that can create 3D masterpieces! Canvas gives you a 2D drawing surface, while WebGL unleashes the power of your GPU for stunning graphics.
In TypeScript terms, Canvas provides a simple 2D rendering context with methods for drawing shapes, images, and text. WebGL takes it further by giving you access to the GPU for hardware-accelerated 3D graphics. This means you can:
- โจ Create smooth 60 FPS games
- ๐ Render thousands of objects efficiently
- ๐ก๏ธ Build type-safe game engines
๐ก Why Use TypeScript for Game Development?
Hereโs why game developers love TypeScript:
- Type Safety ๐: Catch bugs before they crash your game
- Better IDE Support ๐ป: Autocomplete for game objects and methods
- Code Documentation ๐: Types document your game architecture
- Refactoring Confidence ๐ง: Safely modify complex game systems
Real-world example: Imagine building a space shooter ๐. With TypeScript, you can define exact types for enemies, weapons, and power-ups, preventing runtime errors when a laser collides with an asteroid!
๐ง Basic Syntax and Usage
๐ Simple Canvas Example
Letโs start with a friendly bouncing ball:
// ๐ Hello, Canvas!
interface GameConfig {
width: number; // ๐ Canvas width
height: number; // ๐ Canvas height
fps: number; // ๐ฌ Frames per second
}
// ๐จ Our bouncing ball
interface Ball {
x: number; // ๐ X position
y: number; // ๐ Y position
vx: number; // โก๏ธ X velocity
vy: number; // โฌ๏ธ Y velocity
radius: number; // โญ Ball size
color: string; // ๐จ Ball color
}
// ๐ฎ Simple game class
class BounceGame {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private ball: Ball;
constructor(config: GameConfig) {
// ๐ผ๏ธ Create canvas
this.canvas = document.createElement('canvas');
this.canvas.width = config.width;
this.canvas.height = config.height;
// ๐จ Get drawing context
this.ctx = this.canvas.getContext('2d')!;
// โฝ Initialize ball
this.ball = {
x: config.width / 2,
y: config.height / 2,
vx: 5,
vy: 3,
radius: 20,
color: '#FF6B6B'
};
}
// ๐ฏ Game loop
update(): void {
// ๐ Update position
this.ball.x += this.ball.vx;
this.ball.y += this.ball.vy;
// ๐ Bounce off walls
if (this.ball.x <= this.ball.radius ||
this.ball.x >= this.canvas.width - this.ball.radius) {
this.ball.vx = -this.ball.vx;
}
if (this.ball.y <= this.ball.radius ||
this.ball.y >= this.canvas.height - this.ball.radius) {
this.ball.vy = -this.ball.vy;
}
}
// ๐จ Render everything
render(): void {
// ๐งน Clear canvas
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// ๐จ Draw ball
this.ctx.beginPath();
this.ctx.arc(this.ball.x, this.ball.y, this.ball.radius, 0, Math.PI * 2);
this.ctx.fillStyle = this.ball.color;
this.ctx.fill();
}
}
๐ก Explanation: Notice how TypeScript helps us define exact types for our game objects. The !
after getContext('2d')
tells TypeScript weโre sure it wonโt be null.
๐ฏ WebGL Basics
Hereโs a simple WebGL triangle with types:
// ๐ WebGL shader types
interface ShaderSource {
vertex: string; // ๐ Vertex shader
fragment: string; // ๐จ Fragment shader
}
// ๐ WebGL game class
class WebGLGame {
private gl: WebGLRenderingContext;
private program: WebGLProgram;
constructor(canvas: HTMLCanvasElement) {
// ๐ฎ Get WebGL context
this.gl = canvas.getContext('webgl')!;
// ๐จ Shaders for a colorful triangle
const shaders: ShaderSource = {
vertex: `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`,
fragment: `
precision mediump float;
uniform float time;
void main() {
// ๐ Rainbow colors!
gl_FragColor = vec4(
sin(time) * 0.5 + 0.5,
sin(time + 2.0) * 0.5 + 0.5,
sin(time + 4.0) * 0.5 + 0.5,
1.0
);
}
`
};
// ๐๏ธ Build shader program
this.program = this.createShaderProgram(shaders);
}
// ๐ง Helper to create shaders
private createShaderProgram(source: ShaderSource): WebGLProgram {
// Implementation details...
return this.gl.createProgram()!;
}
}
๐ก Practical Examples
๐ Example 1: Particle System
Letโs build a fun particle explosion effect:
// ๐ Particle definition
interface Particle {
x: number; // ๐ Position X
y: number; // ๐ Position Y
vx: number; // โก๏ธ Velocity X
vy: number; // โฌ๏ธ Velocity Y
life: number; // โฐ Lifetime (0-1)
size: number; // ๐ Particle size
color: string; // ๐จ Particle color
emoji?: string; // ๐ Optional emoji particle!
}
// ๐ Particle system manager
class ParticleSystem {
private particles: Particle[] = [];
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
// ๐ฅ Create explosion
explode(x: number, y: number, count: number = 50): void {
const emojis = ['โจ', 'โญ', '๐ซ', '๐', 'โก'];
for (let i = 0; i < count; i++) {
const angle = (Math.PI * 2 * i) / count;
const speed = 2 + Math.random() * 4;
this.particles.push({
x,
y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 1.0,
size: 3 + Math.random() * 5,
color: `hsl(${Math.random() * 360}, 100%, 50%)`,
emoji: Math.random() > 0.7 ? emojis[Math.floor(Math.random() * emojis.length)] : undefined
});
}
console.log(`๐ฅ Boom! Created ${count} particles!`);
}
// ๐ Update all particles
update(deltaTime: number): void {
this.particles = this.particles.filter(particle => {
// ๐ Apply gravity
particle.vy += 0.2;
// ๐ Update position
particle.x += particle.vx;
particle.y += particle.vy;
// โฐ Reduce lifetime
particle.life -= deltaTime * 0.02;
// ๐๏ธ Remove dead particles
return particle.life > 0;
});
}
// ๐จ Draw particles
render(): void {
this.particles.forEach(particle => {
this.ctx.save();
this.ctx.globalAlpha = particle.life;
if (particle.emoji) {
// ๐ Draw emoji particle
this.ctx.font = `${particle.size * 2}px Arial`;
this.ctx.fillText(particle.emoji, particle.x, particle.y);
} else {
// ๐จ Draw colored circle
this.ctx.beginPath();
this.ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
this.ctx.fillStyle = particle.color;
this.ctx.fill();
}
this.ctx.restore();
});
}
}
// ๐ฎ Usage
const particleCanvas = document.getElementById('game') as HTMLCanvasElement;
const particleSystem = new ParticleSystem(particleCanvas);
// ๐ฑ๏ธ Click to explode!
particleCanvas.addEventListener('click', (e) => {
const rect = particleCanvas.getBoundingClientRect();
particleSystem.explode(e.clientX - rect.left, e.clientY - rect.top);
});
๐ฏ Try it yourself: Add different particle shapes, gravity wells, or color gradients!
๐ฎ Example 2: Simple Game Engine
Letโs create a basic game engine structure:
// ๐ฎ Game object interface
interface GameObject {
id: string;
x: number;
y: number;
width: number;
height: number;
update(deltaTime: number): void;
render(ctx: CanvasRenderingContext2D): void;
}
// ๐โโ๏ธ Player character
class Player implements GameObject {
id = 'player';
x = 100;
y = 100;
width = 32;
height = 32;
speed = 200; // ๐ Pixels per second
emoji = '๐';
private keys: Set<string> = new Set();
constructor() {
// โจ๏ธ Setup input handling
window.addEventListener('keydown', (e) => this.keys.add(e.key));
window.addEventListener('keyup', (e) => this.keys.delete(e.key));
}
update(deltaTime: number): void {
const distance = this.speed * (deltaTime / 1000);
// ๐ฎ WASD controls
if (this.keys.has('w')) this.y -= distance;
if (this.keys.has('s')) this.y += distance;
if (this.keys.has('a')) this.x -= distance;
if (this.keys.has('d')) this.x += distance;
}
render(ctx: CanvasRenderingContext2D): void {
// ๐ Draw player emoji
ctx.font = '32px Arial';
ctx.fillText(this.emoji, this.x, this.y);
}
}
// ๐ Collectible item
class Star implements GameObject {
id: string;
x: number;
y: number;
width = 24;
height = 24;
collected = false;
constructor(x: number, y: number) {
this.id = `star-${Date.now()}-${Math.random()}`;
this.x = x;
this.y = y;
}
update(deltaTime: number): void {
// โจ Sparkle effect
this.width = 24 + Math.sin(Date.now() * 0.005) * 4;
this.height = this.width;
}
render(ctx: CanvasRenderingContext2D): void {
if (!this.collected) {
ctx.font = `${this.width}px Arial`;
ctx.fillText('โญ', this.x, this.y);
}
}
}
// ๐ฎ Game engine
class GameEngine {
private objects: Map<string, GameObject> = new Map();
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private lastTime = 0;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
}
// โ Add game object
addObject(obj: GameObject): void {
this.objects.set(obj.id, obj);
console.log(`โ
Added ${obj.id} to game!`);
}
// ๐ Main game loop
gameLoop = (timestamp: number): void => {
const deltaTime = timestamp - this.lastTime;
this.lastTime = timestamp;
// ๐งน Clear screen
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// ๐ Update and render all objects
this.objects.forEach(obj => {
obj.update(deltaTime);
obj.render(this.ctx);
});
// ๐ฌ Next frame
requestAnimationFrame(this.gameLoop);
};
// ๐ Start the engine!
start(): void {
console.log('๐ฎ Game engine started!');
requestAnimationFrame(this.gameLoop);
}
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Sprite Animation System
When youโre ready to level up, try this sprite animation system:
// ๐ฏ Advanced sprite animation
interface AnimationFrame {
x: number; // ๐ X position in sprite sheet
y: number; // ๐ Y position in sprite sheet
width: number; // ๐ Frame width
height: number; // ๐ Frame height
duration: number; // โฐ Frame duration in ms
}
interface Animation {
name: string;
frames: AnimationFrame[];
loop: boolean;
}
// ๐ช Sprite animator
class SpriteAnimator {
private animations: Map<string, Animation> = new Map();
private currentAnimation: string = 'idle';
private currentFrame = 0;
private frameTime = 0;
// โจ Add animation
addAnimation(animation: Animation): void {
this.animations.set(animation.name, animation);
console.log(`๐ฌ Added animation: ${animation.name}`);
}
// ๐ฎ Play animation
play(name: string): void {
if (this.currentAnimation !== name) {
this.currentAnimation = name;
this.currentFrame = 0;
this.frameTime = 0;
console.log(`โถ๏ธ Playing: ${name}`);
}
}
// ๐ Update animation
update(deltaTime: number): AnimationFrame | null {
const animation = this.animations.get(this.currentAnimation);
if (!animation) return null;
this.frameTime += deltaTime;
const currentFrameData = animation.frames[this.currentFrame];
// โญ๏ธ Next frame?
if (this.frameTime >= currentFrameData.duration) {
this.frameTime = 0;
this.currentFrame++;
// ๐ Loop or stop
if (this.currentFrame >= animation.frames.length) {
if (animation.loop) {
this.currentFrame = 0;
} else {
this.currentFrame = animation.frames.length - 1;
}
}
}
return currentFrameData;
}
}
๐๏ธ Advanced Topic 2: WebGL Shader Effects
For the brave developers, hereโs a WebGL post-processing effect:
// ๐ Post-processing effects
type ShaderEffect = 'normal' | 'blur' | 'pixelate' | 'wave';
interface PostProcessor {
applyEffect(effect: ShaderEffect, time: number): void;
}
// ๐ Shader effect manager
class WebGLEffects implements PostProcessor {
private gl: WebGLRenderingContext;
private shaders: Map<ShaderEffect, WebGLProgram> = new Map();
constructor(gl: WebGLRenderingContext) {
this.gl = gl;
this.initializeShaders();
}
// ๐จ Wave effect shader
private getWaveShader(): string {
return `
precision mediump float;
uniform sampler2D texture;
uniform float time;
varying vec2 vTexCoord;
void main() {
vec2 uv = vTexCoord;
// ๐ Wave distortion
uv.x += sin(uv.y * 10.0 + time) * 0.02;
uv.y += sin(uv.x * 10.0 + time) * 0.02;
gl_FragColor = texture2D(texture, uv);
}
`;
}
// โจ Apply selected effect
applyEffect(effect: ShaderEffect, time: number): void {
const program = this.shaders.get(effect);
if (program) {
this.gl.useProgram(program);
const timeLocation = this.gl.getUniformLocation(program, 'time');
this.gl.uniform1f(timeLocation, time);
console.log(`โจ Applied ${effect} effect!`);
}
}
private initializeShaders(): void {
// Initialize all shader effects
console.log('๐จ Initializing shader effects...');
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Forgetting RequestAnimationFrame
// โ Wrong way - burns CPU and looks choppy!
class BadGame {
gameLoop(): void {
while (true) {
this.update();
this.render();
}
}
}
// โ
Correct way - smooth 60 FPS!
class GoodGame {
private lastTime = 0;
gameLoop = (timestamp: number): void => {
const deltaTime = timestamp - this.lastTime;
this.lastTime = timestamp;
this.update(deltaTime);
this.render();
requestAnimationFrame(this.gameLoop); // ๐ฌ Smooth animation!
};
}
๐คฏ Pitfall 2: Not Handling Canvas Resize
// โ Dangerous - stretched graphics!
const canvas = document.getElementById('game') as HTMLCanvasElement;
canvas.style.width = '100%';
canvas.style.height = '100%';
// โ
Safe - proper resolution!
function resizeCanvas(canvas: HTMLCanvasElement): void {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
const ctx = canvas.getContext('2d')!;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
console.log('๐ Canvas resized properly!');
}
window.addEventListener('resize', () => resizeCanvas(canvas));
๐ ๏ธ Best Practices
- ๐ฏ Use TypeScript Interfaces: Define types for all game objects
- ๐ Separate Logic and Rendering: Keep update() and render() methods distinct
- ๐ก๏ธ Handle Edge Cases: Always check canvas context exists
- ๐จ Optimize Drawing: Batch similar draw calls together
- โจ Use Object Pools: Reuse objects instead of creating new ones
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Space Invaders Clone
Create a simple space shooter game:
๐ Requirements:
- โ Player spaceship that moves left/right
- ๐ท๏ธ Enemy invaders that move in formation
- ๐ค Shooting mechanics with collision detection
- ๐ Score tracking and lives system
- ๐จ Each enemy type needs a unique emoji!
๐ Bonus Points:
- Add power-ups with special effects
- Implement particle explosions
- Create multiple levels with increasing difficulty
๐ก Solution
๐ Click to see solution
// ๐ฏ Space Invaders in TypeScript!
interface SpaceObject {
x: number;
y: number;
width: number;
height: number;
emoji: string;
alive: boolean;
}
interface Bullet extends SpaceObject {
vy: number;
}
class SpaceInvaders {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private player: SpaceObject;
private enemies: SpaceObject[][] = [];
private bullets: Bullet[] = [];
private score = 0;
private lives = 3;
private enemyDirection = 1;
constructor(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
// ๐ Initialize player
this.player = {
x: canvas.width / 2 - 16,
y: canvas.height - 60,
width: 32,
height: 32,
emoji: '๐',
alive: true
};
// ๐พ Create enemy grid
this.createEnemies();
// ๐ฎ Controls
this.setupControls();
}
// ๐พ Create enemy formation
private createEnemies(): void {
const enemyEmojis = ['๐พ', '๐ฝ', '๐ธ'];
for (let row = 0; row < 5; row++) {
this.enemies[row] = [];
for (let col = 0; col < 11; col++) {
this.enemies[row][col] = {
x: col * 50 + 50,
y: row * 40 + 50,
width: 30,
height: 30,
emoji: enemyEmojis[Math.floor(row / 2)],
alive: true
};
}
}
console.log('๐พ Enemy formation ready!');
}
// ๐ฎ Setup keyboard controls
private setupControls(): void {
window.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft' && this.player.x > 0) {
this.player.x -= 20;
}
if (e.key === 'ArrowRight' && this.player.x < this.canvas.width - this.player.width) {
this.player.x += 20;
}
if (e.key === ' ') {
this.shoot();
}
});
}
// ๐ซ Fire bullet
private shoot(): void {
this.bullets.push({
x: this.player.x + this.player.width / 2 - 4,
y: this.player.y,
width: 8,
height: 16,
emoji: '๐ฅ',
vy: -10,
alive: true
});
console.log('๐ซ Pew pew!');
}
// ๐ Update game state
update(): void {
// ๐ Update bullets
this.bullets = this.bullets.filter(bullet => {
bullet.y += bullet.vy;
// ๐ฅ Check enemy collisions
for (const row of this.enemies) {
for (const enemy of row) {
if (enemy.alive && this.checkCollision(bullet, enemy)) {
enemy.alive = false;
bullet.alive = false;
this.score += 10;
console.log(`๐ฅ Hit! Score: ${this.score}`);
}
}
}
return bullet.alive && bullet.y > 0;
});
// ๐พ Move enemies
this.moveEnemies();
}
// ๐พ Enemy movement
private moveEnemies(): void {
let shouldDescend = false;
// Check boundaries
for (const row of this.enemies) {
for (const enemy of row) {
if (enemy.alive) {
if ((enemy.x <= 0 && this.enemyDirection < 0) ||
(enemy.x >= this.canvas.width - enemy.width && this.enemyDirection > 0)) {
shouldDescend = true;
}
}
}
}
// Move enemies
for (const row of this.enemies) {
for (const enemy of row) {
if (shouldDescend) {
enemy.y += 20;
} else {
enemy.x += this.enemyDirection * 2;
}
}
}
if (shouldDescend) {
this.enemyDirection *= -1;
}
}
// ๐ฅ Collision detection
private checkCollision(a: SpaceObject, b: SpaceObject): boolean {
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;
}
// ๐จ Render everything
render(): void {
// ๐งน Clear screen
this.ctx.fillStyle = '#000033';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// ๐ Draw player
this.ctx.font = '32px Arial';
this.ctx.fillText(this.player.emoji, this.player.x, this.player.y);
// ๐พ Draw enemies
for (const row of this.enemies) {
for (const enemy of row) {
if (enemy.alive) {
this.ctx.font = '30px Arial';
this.ctx.fillText(enemy.emoji, enemy.x, enemy.y);
}
}
}
// ๐ฅ Draw bullets
this.ctx.font = '16px Arial';
for (const bullet of this.bullets) {
this.ctx.fillText(bullet.emoji, bullet.x, bullet.y);
}
// ๐ Draw UI
this.ctx.fillStyle = 'white';
this.ctx.font = '20px Arial';
this.ctx.fillText(`Score: ${this.score} ๐`, 10, 30);
this.ctx.fillText(`Lives: ${'โค๏ธ'.repeat(this.lives)}`, this.canvas.width - 100, 30);
}
}
// ๐ฎ Start the game!
const gameCanvas = document.getElementById('game') as HTMLCanvasElement;
const game = new SpaceInvaders(gameCanvas);
function gameLoop(): void {
game.update();
game.render();
requestAnimationFrame(gameLoop);
}
gameLoop();
console.log('๐ฎ Space Invaders started! Use arrow keys and space to play!');
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Create Canvas games with smooth animations ๐ช
- โ Use WebGL for advanced graphics effects ๐ก๏ธ
- โ Build game engines with TypeScriptโs type safety ๐ฏ
- โ Handle input and collisions like a pro ๐
- โ Optimize performance for 60 FPS gameplay! ๐
Remember: Game development is about experimentation and fun! Donโt be afraid to try wild ideas. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered game development with Canvas and WebGL!
Hereโs what to do next:
- ๐ป Build your own game using the concepts learned
- ๐๏ธ Explore game physics libraries like Matter.js
- ๐ Learn about game design patterns and architectures
- ๐ Share your games with the world!
Remember: Every great game started with a simple prototype. Keep creating, keep learning, and most importantly, have fun! ๐
Happy gaming! ๐ฎ๐โจ