MOODUL 06b · TASE 2 — EDASIJÕUDNUD

🔷 TypeScript Mängudes

Tüübiturvalisus, interfaced, generics ja OOP mustrid — TypeScript muudab su mängukoodi usaldusväärsemaks ja hooldatavamaks.

⏱️ ~4-5 tundi 📝 5 harjutust 🟡 Edasijõudnud

⚡ Samm 1 — TypeScript Seadistamine

Terminal
# Paigalda TypeScript
npm install -D typescript

# Loo tsconfig.json
npx tsc --init

# Vite + TypeScript töötab juba automaatselt!
tsconfig.json — Mänguprojektile
{
  "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

src/types.ts — Mängu tüübid
// 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

src/entities/GameObject.ts
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 };
src/entities/Player.ts
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

src/systems/ObjectPool.ts
// 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

src/systems/EventBus.ts
// 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 GameObject abstraktne klass. Laienda sellest Player, Enemy ja Bullet. Igal on oma update() ja draw().

  • 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.

← Moodul 06 Moodul 07: Arhitektuur →