🤖 Tehisintellekt Mängudes
Behavior Trees, Finite State Machines, GOAP, Utility AI, flocking — loo nutikad vaenlased ja NPC-d!
🌳 Samm 1 — Behavior Tree (Käitumispuu)
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.
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';
}
}
// 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)
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)
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.