🏗️ Mängu Arhitektuur & Disainimustrid
ECS (Entity Component System), State Machine, Observer — mustrid mis hoiavad su koodi puhtana ka 10 000 rida koodi juures.
🧠 Samm 1 — State Machine (Olekumasin)
Olekumasin haldab objekti käitumist (idle, patrolling, attacking). Iga olek teab mida teha ja millal vahetada.
interface State {
name: string;
enter(): void;
update(dt: number): void;
exit(): void;
}
class StateMachine {
private states = new Map<string, State>();
private current: State | null = null;
add(state: State): void {
this.states.set(state.name, state);
}
change(name: string): void {
if (this.current) this.current.exit();
this.current = this.states.get(name) || null;
if (this.current) this.current.enter();
}
update(dt: number): void {
if (this.current) this.current.update(dt);
}
get currentState(): string {
return this.current?.name || 'none';
}
}
// Vaenlase olekud
class IdleState implements State {
name = 'idle';
private enemy: Enemy;
private timer = 0;
constructor(enemy: Enemy) { this.enemy = enemy; }
enter() { this.timer = 0; }
update(dt: number) {
this.timer += dt;
// Mängija lähedal? → jahtimine!
const dist = distance(this.enemy.pos, player.pos);
if (dist < 200) {
this.enemy.fsm.change('chase');
}
// Igav? → patrulli
if (this.timer > 3) {
this.enemy.fsm.change('patrol');
}
}
exit() {}
}
class ChaseState implements State {
name = 'chase';
private enemy: Enemy;
constructor(enemy: Enemy) { this.enemy = enemy; }
enter() { /* äratuse animatsioon */ }
update(dt: number) {
// Liigu mängija suunas
const dir = normalize(subtract(player.pos, this.enemy.pos));
this.enemy.pos.x += dir.x * this.enemy.speed * dt;
this.enemy.pos.y += dir.y * this.enemy.speed * dt;
// Piisavalt lähedal? → ründa!
const dist = distance(this.enemy.pos, player.pos);
if (dist < 40) {
this.enemy.fsm.change('attack');
}
// Liiga kaugel? → tagasi idle
if (dist > 400) {
this.enemy.fsm.change('idle');
}
}
exit() {}
}
🧱 Samm 2 — Entity Component System (ECS)
ECS lahutab andmed (Components) käitumisest (Systems). Entiteedid on lihtsalt ID-d.
// Komponendid — puhas andmed, mitte loogika!
interface PositionComponent {
type: 'position';
x: number;
y: number;
}
interface VelocityComponent {
type: 'velocity';
vx: number;
vy: number;
}
interface SpriteComponent {
type: 'sprite';
image: HTMLImageElement;
width: number;
height: number;
}
interface HealthComponent {
type: 'health';
current: number;
max: number;
}
interface ColliderComponent {
type: 'collider';
width: number;
height: number;
layer: string; // 'player' | 'enemy' | 'bullet'
}
type Component = PositionComponent | VelocityComponent | SpriteComponent
| HealthComponent | ColliderComponent;
// Entity — lihtsalt ID + komponentide kogu
type EntityId = number;
class World {
private nextId: EntityId = 0;
private entities = new Map<EntityId, Map<string, Component>>();
private systems: System[] = [];
createEntity(): EntityId {
const id = this.nextId++;
this.entities.set(id, new Map());
return id;
}
addComponent(entity: EntityId, component: Component): void {
this.entities.get(entity)?.set(component.type, component);
}
getComponent<T extends Component>(entity: EntityId, type: string): T | undefined {
return this.entities.get(entity)?.get(type) as T | undefined;
}
// Leia kõik entiteedid kellel on nõutud komponendid
query(...types: string[]): EntityId[] {
const result: EntityId[] = [];
for (const [id, components] of this.entities) {
if (types.every(t => components.has(t))) {
result.push(id);
}
}
return result;
}
removeEntity(entity: EntityId): void {
this.entities.delete(entity);
}
addSystem(system: System): void {
this.systems.push(system);
}
update(dt: number): void {
for (const system of this.systems) {
system.update(this, dt);
}
}
}
// Süsteemid — loogika!
interface System {
update(world: World, dt: number): void;
}
class MovementSystem implements System {
update(world: World, dt: number): void {
const entities = world.query('position', 'velocity');
for (const id of entities) {
const pos = world.getComponent<PositionComponent>(id, 'position')!;
const vel = world.getComponent<VelocityComponent>(id, 'velocity')!;
pos.x += vel.vx * dt;
pos.y += vel.vy * dt;
}
}
}
class RenderSystem implements System {
constructor(private ctx: CanvasRenderingContext2D) {}
update(world: World, dt: number): void {
const entities = world.query('position', 'sprite');
for (const id of entities) {
const pos = world.getComponent<PositionComponent>(id, 'position')!;
const spr = world.getComponent<SpriteComponent>(id, 'sprite')!;
this.ctx.drawImage(spr.image, pos.x, pos.y, spr.width, spr.height);
}
}
}
// Kasutamine:
// const world = new World();
// world.addSystem(new MovementSystem());
// world.addSystem(new RenderSystem(ctx));
// const player = world.createEntity();
// world.addComponent(player, { type: 'position', x: 100, y: 200 });
// world.addComponent(player, { type: 'velocity', vx: 0, vy: 0 });
👀 Samm 3 — Observer Muster
// Observer — objektid kuulavad sündmusi
class EventEmitter<Events extends Record<string, any>> {
private listeners = new Map<string, Set<Function>>();
on<K extends keyof Events>(event: K, fn: (data: Events[K]) => void): () => void {
if (!this.listeners.has(event as string))
this.listeners.set(event as string, new Set());
this.listeners.get(event as string)!.add(fn);
// Tagasta unsubscribe funktsioon
return () => this.off(event, fn);
}
off<K extends keyof Events>(event: K, fn: Function): void {
this.listeners.get(event as string)?.delete(fn);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners.get(event as string)?.forEach(fn => fn(data));
}
}
// Mängu sündmused
interface Events {
'score:add': number;
'player:hit': { damage: number };
'enemy:die': { x: number; y: number; reward: number };
'level:up': { level: number };
}
const events = new EventEmitter<Events>();
// UI kuulab skoori muutusi
events.on('score:add', (points) => {
scoreDisplay.textContent = `Skoor: ${totalScore += points}`;
});
// Audio kuulab vaenlase surma
events.on('enemy:die', (data) => {
audio.play('explosion');
spawnParticles(data.x, data.y);
});
// Mänguloogika emiteerib sündmusi
function killEnemy(enemy: Enemy) {
enemy.destroy();
events.emit('enemy:die', {
x: enemy.pos.x, y: enemy.pos.y, reward: 10
});
events.emit('score:add', 10);
}
🏭 Samm 4 — Scene Manager
interface Scene {
name: string;
init?(data?: any): void;
update(dt: number): void;
draw(ctx: CanvasRenderingContext2D): void;
destroy?(): void;
}
class SceneManager {
private scenes = new Map<string, Scene>();
private stack: Scene[] = [];
register(scene: Scene): void {
this.scenes.set(scene.name, scene);
}
// Vaheta stseeni (eelmine kaob)
goto(name: string, data?: any): void {
const current = this.stack.pop();
if (current?.destroy) current.destroy();
const scene = this.scenes.get(name);
if (scene) {
if (scene.init) scene.init(data);
this.stack.push(scene);
}
}
// Lisa stseen peale (pause overlay)
push(name: string, data?: any): void {
const scene = this.scenes.get(name);
if (scene) {
if (scene.init) scene.init(data);
this.stack.push(scene);
}
}
// Eemalda ülemine stseen
pop(): void {
const scene = this.stack.pop();
if (scene?.destroy) scene.destroy();
}
update(dt: number): void {
// Ainult ülemine stseen saab update'i
const top = this.stack[this.stack.length - 1];
if (top) top.update(dt);
}
draw(ctx: CanvasRenderingContext2D): void {
// Joonista kõik stseenid (alumised nähtavad pause ajal)
for (const scene of this.stack) {
scene.draw(ctx);
}
}
}
// const scenes = new SceneManager();
// scenes.register(new MenuScene());
// scenes.register(new GameScene());
// scenes.register(new PauseScene());
// scenes.goto('menu');
// // Kui vajutad ESC:
// scenes.push('pause');
// // Kui jätkad:
// scenes.pop();
📝 Harjutused
-
State Machine vaenlasele
Implementeeri StateMachine ja loo vaenlane kolme olekuga: Idle (seisab), Patrol (liigub edasi-tagasi), Chase (jälitab mängijat). Visualiseeri olek värviga.
-
ECS süsteemiga mäng
Ehita lihtne ECS — World, Entity, Component, System. Loo MovementSystem, RenderSystem, CollisionSystem. 50 entiteeti mis liiguvad ja põrkavad.
-
Observer sündmuste süsteem
Ehita EventEmitter ja integreeri mängu: collision → sound + particles + score. Eraldi moodulid ei tea teineteisest — ainult sündmustest!
-
Scene Manager
Implementeeri SceneManager stack'iga. Loo 4 stseeni: Menu, Game, Pause (overlay), GameOver. Andmete edastamine stseenide vahel (skoor, tase).
-
Object Pool muster
Implementeeri ObjectPool kuulide ja partiklite jaoks. Võrdlus: mõõda FPS 1000 partikli puhul pooliga ja ilma (new/destroy). Visualiseeri erinevus.
-
Kombineeri kõik mustrid
Ehita väike mäng mis kasutab kõiki mustreid: ECS entiteetide jaoks, StateMachine AI-le, Observer sündmuste jaoks, SceneManager stseenidele, ObjectPool kuulidele.