MOODUL 12 · TASE 5 — SPETSIALIST

🤖 Tehisintellekt Mängudes

Behavior Trees, Finite State Machines, GOAP, Utility AI, flocking — loo nutikad vaenlased ja NPC-d!

⏱️ ~8-10 tundi 📝 6 harjutust 🟣 Spetsialist

🌳 Samm 1 — Behavior Tree (Käitumispuu)

ℹ️ Miks Behavior Tree?

Behavior Trees on mängutööstuse standard NPC AI jaoks. Need on modulaarsed, taaskasutatavad ja visuaalselt loetavad. Kasutusel AAA mängudes nagu Halo, Unreal Engine jne.

behaviorTree.ts — Käitumispuu
type NodeStatus = 'success' | 'failure' | 'running';

// Baasnood
abstract class BTNode {
  abstract tick(context: AIContext): NodeStatus;
}

// Kontekst (jagatud andmed)
interface AIContext {
  entity: Enemy;
  target: Player | null;
  deltaTime: number;
  blackboard: Map<string, any>;
}

// ---- COMPOSITE NODES ----

// Sequence: käivita järjest, esimesel failure'il peatu
class Sequence extends BTNode {
  constructor(private children: BTNode[]) { super(); }

  tick(ctx: AIContext): NodeStatus {
    for (const child of this.children) {
      const status = child.tick(ctx);
      if (status !== 'success') return status;
    }
    return 'success';
  }
}

// Selector: proovi järjest, esimesel success'il peatu
class Selector extends BTNode {
  constructor(private children: BTNode[]) { super(); }

  tick(ctx: AIContext): NodeStatus {
    for (const child of this.children) {
      const status = child.tick(ctx);
      if (status !== 'failure') return status;
    }
    return 'failure';
  }
}

// ---- DECORATOR NODES ----

// Inverter: pöörab tulemuse
class Inverter extends BTNode {
  constructor(private child: BTNode) { super(); }

  tick(ctx: AIContext): NodeStatus {
    const status = this.child.tick(ctx);
    if (status === 'success') return 'failure';
    if (status === 'failure') return 'success';
    return 'running';
  }
}

// Repeater: korda N korda
class Repeater extends BTNode {
  private count = 0;
  constructor(private child: BTNode, private times: number) { super(); }

  tick(ctx: AIContext): NodeStatus {
    if (this.count < this.times) {
      const status = this.child.tick(ctx);
      if (status === 'success') this.count++;
      return this.count >= this.times ? 'success' : 'running';
    }
    return 'success';
  }
}

// ---- LEAF NODES (Tegevused ja tingimused) ----

class IsTargetInRange extends BTNode {
  constructor(private range: number) { super(); }

  tick(ctx: AIContext): NodeStatus {
    if (!ctx.target) return 'failure';
    const dist = ctx.entity.position.distanceTo(ctx.target.position);
    return dist <= this.range ? 'success' : 'failure';
  }
}

class MoveToTarget extends BTNode {
  tick(ctx: AIContext): NodeStatus {
    if (!ctx.target) return 'failure';
    const dir = ctx.target.position.clone().sub(ctx.entity.position).normalize();
    ctx.entity.position.add(dir.multiplyScalar(ctx.entity.speed * ctx.deltaTime));
    const dist = ctx.entity.position.distanceTo(ctx.target.position);
    return dist < 1 ? 'success' : 'running';
  }
}

class AttackTarget extends BTNode {
  tick(ctx: AIContext): NodeStatus {
    if (!ctx.target) return 'failure';
    ctx.target.takeDamage(ctx.entity.attackDamage);
    ctx.blackboard.set('lastAttackTime', Date.now());
    return 'success';
  }
}

class Patrol extends BTNode {
  tick(ctx: AIContext): NodeStatus {
    const waypoints = ctx.entity.patrolPoints;
    const currentWP = ctx.blackboard.get('waypointIndex') ?? 0;
    const target = waypoints[currentWP];
    const dir = target.clone().sub(ctx.entity.position).normalize();
    ctx.entity.position.add(dir.multiplyScalar(ctx.entity.speed * 0.5 * ctx.deltaTime));

    if (ctx.entity.position.distanceTo(target) < 0.5) {
      ctx.blackboard.set('waypointIndex', (currentWP + 1) % waypoints.length);
    }
    return 'running';
  }
}
Käitumispuu koostamine
// Vaenlase AI puu
const enemyBT = new Selector([
  // 1. Kui mängija lähedal → ründa!
  new Sequence([
    new IsTargetInRange(2),
    new AttackTarget(),
  ]),

  // 2. Kui mängija nähtusel → jälita!
  new Sequence([
    new IsTargetInRange(15),
    new MoveToTarget(),
  ]),

  // 3. Muidu → patrulli
  new Patrol(),
]);

// Mängutsüklis:
// enemyBT.tick(context);

🎯 Samm 2 — Utility AI (Kasulikkusel Põhinev)

utilityAI.ts — Skooripõhine otsustamine
interface AIAction {
  name: string;
  score(ctx: AIContext): number;  // 0..1
  execute(ctx: AIContext): void;
}

class UtilityAI {
  private actions: AIAction[] = [];

  addAction(action: AIAction): void {
    this.actions.push(action);
  }

  decide(ctx: AIContext): AIAction | null {
    let bestAction: AIAction | null = null;
    let bestScore = -Infinity;

    for (const action of this.actions) {
      const score = action.score(ctx);
      if (score > bestScore) {
        bestScore = score;
        bestAction = action;
      }
    }
    return bestAction;
  }

  update(ctx: AIContext): void {
    const action = this.decide(ctx);
    if (action) {
      action.execute(ctx);
    }
  }
}

// Tegevused:
const healAction: AIAction = {
  name: 'heal',
  score(ctx) {
    const healthRatio = ctx.entity.hp / ctx.entity.maxHp;
    if (healthRatio > 0.5) return 0;
    return (1 - healthRatio) * 0.9; // Madal HP → kõrge skoor
  },
  execute(ctx) {
    ctx.entity.heal(20);
  }
};

const fleeAction: AIAction = {
  name: 'flee',
  score(ctx) {
    if (!ctx.target) return 0;
    const healthRatio = ctx.entity.hp / ctx.entity.maxHp;
    const dist = ctx.entity.position.distanceTo(ctx.target.position);
    return healthRatio < 0.3 && dist < 5 ? 0.95 : 0;
  },
  execute(ctx) {
    if (!ctx.target) return;
    const away = ctx.entity.position.clone().sub(ctx.target.position).normalize();
    ctx.entity.position.add(away.multiplyScalar(ctx.entity.speed * ctx.deltaTime));
  }
};

🐦 Samm 3 — Flocking (Parvealgoritm)

flocking.ts — Boid'id
class Boid {
  position: THREE.Vector2;
  velocity: THREE.Vector2;
  acceleration: THREE.Vector2 = new THREE.Vector2();

  constructor(x: number, y: number) {
    this.position = new THREE.Vector2(x, y);
    this.velocity = new THREE.Vector2(Math.random() - 0.5, Math.random() - 0.5).normalize().multiplyScalar(2);
  }

  // 3 põhireeglit:
  flock(boids: Boid[]): void {
    const separation = this.separate(boids).multiplyScalar(1.5);
    const alignment = this.align(boids).multiplyScalar(1.0);
    const cohesion = this.cohere(boids).multiplyScalar(1.0);

    this.acceleration.add(separation).add(alignment).add(cohesion);
  }

  // 1. Separation — hoia distantsi
  private separate(boids: Boid[], radius = 25): THREE.Vector2 {
    const steer = new THREE.Vector2();
    let count = 0;
    for (const other of boids) {
      const d = this.position.distanceTo(other.position);
      if (d > 0 && d < radius) {
        const diff = this.position.clone().sub(other.position).normalize().divideScalar(d);
        steer.add(diff);
        count++;
      }
    }
    if (count > 0) steer.divideScalar(count);
    return steer;
  }

  // 2. Alignment — ühtlusta suund
  private align(boids: Boid[], radius = 50): THREE.Vector2 {
    const avg = new THREE.Vector2();
    let count = 0;
    for (const other of boids) {
      if (this.position.distanceTo(other.position) < radius) {
        avg.add(other.velocity);
        count++;
      }
    }
    if (count > 0) avg.divideScalar(count).normalize().multiplyScalar(2).sub(this.velocity);
    return avg;
  }

  // 3. Cohesion — liigu grupi keskele
  private cohere(boids: Boid[], radius = 50): THREE.Vector2 {
    const center = new THREE.Vector2();
    let count = 0;
    for (const other of boids) {
      if (this.position.distanceTo(other.position) < radius) {
        center.add(other.position);
        count++;
      }
    }
    if (count > 0) {
      center.divideScalar(count).sub(this.position).normalize().multiplyScalar(2).sub(this.velocity);
    }
    return center;
  }

  update(maxSpeed = 3): void {
    this.velocity.add(this.acceleration);
    this.velocity.clampLength(0, maxSpeed);
    this.position.add(this.velocity);
    this.acceleration.set(0, 0);
  }
}

📝 Harjutused

  • Behavior Tree raamistik

    Loo BT: Sequence, Selector, Inverter, Repeater. Vaenlane: patrullib → märkab mängijat → jälitab → ründab. Visualiseeri puu debug HTML-is.

  • Utility AI süsteem

    5 tegevust: ründa, paranda, põgene, patrulli, kutsu abi. Iga tegevuse skoor sõltub HP-st, kaugusest ja liitlastest. Lisa slider'id testimiseks.

  • Flocking simulatsioon

    100 boid'i Canvas2D-s. Separation + alignment + cohesion. Lisa hiire tõmbejõud ja tõukejõud. Värvid sõltuvad kiirusest.

  • Stealth AI

    Vaenlased nägemis-koonusega (FOV). Mängija saab hiilida selja taga. Alert/Search/Patrol olekud. Helid meelitavad vaenlasi. Minimap.

  • GOAP (Goal-Oriented Action Planning)

    Agent: eesmärk "tapa mängija" → plaan: "leia relv → laadi → lähene → tulista". Dünaamiline planeerimine. Lisa uus eesmärk "jää ellu".

  • Boss AI

    Bossivaenlase AI 3 faasiga. Faas 1: lihtsad rünnakud. Faas 2 (HP < 60%): uued rünnakumustrid. Faas 3 (HP < 30%): kiire + ohtlik. Enrage timer.

← Moodul 11b Moodul 13: AI Arendus →