🔷 TypeScript Mängudes
Tüübiturvalisus, interfaced, generics ja OOP mustrid — TypeScript muudab su mängukoodi usaldusväärsemaks ja hooldatavamaks.
⚡ Samm 1 — TypeScript Seadistamine
# Paigalda TypeScript
npm install -D typescript
# Loo tsconfig.json
npx tsc --init
# Vite + TypeScript töötab juba automaatselt!
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true, // ALATI strict mode!
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}
📦 Samm 2 — Tüübid ja Interfaced
// Lihtsad tüübid
type PlayerState = 'idle' | 'running' | 'jumping' | 'falling' | 'dead';
type Direction = 'left' | 'right' | 'up' | 'down';
// Interface — objekti kuju
interface Vector2D {
x: number;
y: number;
}
interface Bounds {
x: number;
y: number;
width: number;
height: number;
}
// Mängu entity interface
interface Entity {
id: string;
position: Vector2D;
velocity: Vector2D;
bounds: Bounds;
active: boolean;
update(deltaTime: number): void;
draw(ctx: CanvasRenderingContext2D): void;
destroy(): void;
}
// Mängu seadistus
interface GameConfig {
width: number;
height: number;
gravity: number;
debug: boolean;
audio: {
masterVolume: number;
musicVolume: number;
sfxVolume: number;
};
}
// Enum - mängu olekud
enum GameState {
Loading = 'loading',
Menu = 'menu',
Playing = 'playing',
Paused = 'paused',
GameOver = 'gameover',
}
// Ekspordi
export { PlayerState, Direction, Vector2D, Bounds, Entity, GameConfig, GameState };
🏗️ Samm 3 — Klassid ja Pärimine
import { Entity, Vector2D, Bounds } from '../types';
// Abstraktne baasklass — ei saa otse luua
abstract class GameObject implements Entity {
public id: string;
public position: Vector2D;
public velocity: Vector2D;
public active: boolean = true;
public width: number;
public height: number;
constructor(x: number, y: number, width: number, height: number) {
this.id = crypto.randomUUID();
this.position = { x, y };
this.velocity = { x: 0, y: 0 };
this.width = width;
this.height = height;
}
get bounds(): Bounds {
return {
x: this.position.x,
y: this.position.y,
width: this.width,
height: this.height,
};
}
// Abstraktsed meetodid — alamklassid PEAVAD implementeerima
abstract update(deltaTime: number): void;
abstract draw(ctx: CanvasRenderingContext2D): void;
destroy(): void {
this.active = false;
}
// Kokkupõrke kontroll
collidesWith(other: GameObject): boolean {
const a = this.bounds;
const b = other.bounds;
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
);
}
}
export { GameObject };
import { GameObject } from './GameObject';
import { PlayerState } from '../types';
class Player extends GameObject {
public state: PlayerState = 'idle';
public health: number = 100;
public maxHealth: number = 100;
private speed: number = 200;
private jumpForce: number = -400;
private isGrounded: boolean = false;
constructor(x: number, y: number) {
super(x, y, 32, 48);
}
update(deltaTime: number): void {
// Gravitatsioon
this.velocity.y += 980 * deltaTime;
// Liikumine
this.position.x += this.velocity.x * deltaTime;
this.position.y += this.velocity.y * deltaTime;
// Oleku uuendamine
if (!this.isGrounded && this.velocity.y > 0) {
this.state = 'falling';
} else if (!this.isGrounded && this.velocity.y < 0) {
this.state = 'jumping';
} else if (Math.abs(this.velocity.x) > 0.1) {
this.state = 'running';
} else {
this.state = 'idle';
}
}
draw(ctx: CanvasRenderingContext2D): void {
// Värv vastavalt olekule
const colors: Record<PlayerState, string> = {
idle: '#22c55e',
running: '#3b82f6',
jumping: '#eab308',
falling: '#f97316',
dead: '#ef4444',
};
ctx.fillStyle = colors[this.state];
ctx.fillRect(this.position.x, this.position.y, this.width, this.height);
// Tervis riba
const barWidth = this.width;
const healthPct = this.health / this.maxHealth;
ctx.fillStyle = '#374151';
ctx.fillRect(this.position.x, this.position.y - 8, barWidth, 4);
ctx.fillStyle = healthPct > 0.3 ? '#22c55e' : '#ef4444';
ctx.fillRect(this.position.x, this.position.y - 8, barWidth * healthPct, 4);
}
moveLeft(): void { this.velocity.x = -this.speed; }
moveRight(): void { this.velocity.x = this.speed; }
stopX(): void { this.velocity.x = 0; }
jump(): void {
if (this.isGrounded) {
this.velocity.y = this.jumpForce;
this.isGrounded = false;
}
}
land(groundY: number): void {
this.position.y = groundY - this.height;
this.velocity.y = 0;
this.isGrounded = true;
}
takeDamage(amount: number): void {
this.health = Math.max(0, this.health - amount);
if (this.health <= 0) this.state = 'dead';
}
}
export { Player };
🧩 Samm 4 — Generics
// Generic Object Pool — töötab iga tüübiga!
class ObjectPool<T extends { active: boolean }> {
private pool: T[] = [];
private factory: () => T;
private maxSize: number;
constructor(factory: () => T, maxSize: number = 100) {
this.factory = factory;
this.maxSize = maxSize;
// Eeltäida pool
for (let i = 0; i < maxSize; i++) {
const obj = this.factory();
obj.active = false;
this.pool.push(obj);
}
}
get(): T | null {
const obj = this.pool.find(o => !o.active);
if (obj) {
obj.active = true;
return obj;
}
return null;
}
release(obj: T): void {
obj.active = false;
}
getActive(): T[] {
return this.pool.filter(o => o.active);
}
get activeCount(): number {
return this.pool.filter(o => o.active).length;
}
}
// Kasutamine:
// const bulletPool = new ObjectPool<Bullet>(() => new Bullet(), 50);
// const bullet = bulletPool.get();
// if (bullet) { /* configure and use */ }
// bulletPool.release(bullet);
export { ObjectPool };
🎮 Samm 5 — Event System
// Tüübitud sündmused
interface GameEvents {
'player:damage': { amount: number; source: string };
'player:death': { position: { x: number; y: number } };
'enemy:killed': { id: string; score: number };
'score:changed': { score: number; combo: number };
'level:complete': { level: number; time: number };
}
class EventBus {
private listeners = new Map<string, Function[]>();
on<K extends keyof GameEvents>(
event: K,
callback: (data: GameEvents[K]) => void
): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(callback);
}
off<K extends keyof GameEvents>(
event: K,
callback: (data: GameEvents[K]) => void
): void {
const listeners = this.listeners.get(event);
if (listeners) {
const index = listeners.indexOf(callback);
if (index >= 0) listeners.splice(index, 1);
}
}
emit<K extends keyof GameEvents>(event: K, data: GameEvents[K]): void {
const listeners = this.listeners.get(event);
if (listeners) {
listeners.forEach(cb => cb(data));
}
}
}
// Globaalne event bus
export const eventBus = new EventBus();
// Kasutamine:
// eventBus.on('enemy:killed', (data) => {
// console.log(`+${data.score} points!`);
// });
// eventBus.emit('enemy:killed', { id: 'enemy-1', score: 100 });
📝 Harjutused
-
TypeScript projekti seadistamine
Loo uus Vite + TypeScript projekt mängule. Seadista tsconfig.json strict mode'iga. Konverteeri üks varasem JavaScript mäng TypeScript'iks.
-
Interface'id ja tüübid
Defineeri kõik mängu tüübid: Entity, Player, Enemy, Bullet, GameState. Kasuta union tüüpe olekute jaoks ja võimaldatu tüübikaitseid (type guards).
-
Abstract klass ja pärimine
Loo
GameObjectabstraktne klass. Laienda sellestPlayer,EnemyjaBullet. Igal on omaupdate()jadraw(). -
Generic ObjectPool
Implementeeri generic ObjectPool<T> mis töötab kuulide, vaenlaste ja partiklitega. Testi et poolist saab objekte ja tagastab need. Lisa statistika (active/total).
-
Tüübitud EventBus
Ehita tüübitud EventBus süsteem. Defineeri GameEvents interface kõigi mängu sündmustega. Integreeri mängu nii et mängija surm, skoor jne kasutavad event bus'i.