Skip to content

游戏 AI

预计阅读 25 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0

有前端背景的开发者对 AI 并不陌生——但游戏 AI 与 Web AI(机器学习、推荐系统)有本质区别。游戏 AI 的核心目标不是"智能",而是创造有趣的对抗体验。本章聚焦轻量级游戏 AI 技术,适合微信小游戏的性能约束。

区分「游戏 AI」与「生成式 AI」

本章讲的「游戏 AI」是运行时行为控制——状态机、行为树、寻路、效用系统等,用于控制 NPC、敌人、关卡难度等。

而「生成式 AI」是开发期内容生产工具——用 LLM、扩散模型、音频生成模型辅助写代码、画美术、做音乐。后者在 13 AI 辅助独立游戏开发 中有系统讲解。

两者并不冲突:你可以用生成式 AI 快速生成 FSM/行为树的代码模板,再用运行时游戏 AI 驱动角色行为。


前端 AI vs 游戏 AI

前端对比:Web AI vs 游戏 AI
维度前端 AI(ML/推荐)游戏 AI(行为控制)
目标精准预测/推荐创造挑战感和趣味性
运行时服务端 / WASM客户端实时运行
每帧开销不关注< 2ms(避免掉帧)
典型技术TensorFlow.js、ONNX状态机、行为树、效用系统
可调试性关注模型准确率关注行为可观察性

关键差异: 游戏 AI 需要在每帧 16ms 的预算内完成所有敌人的决策——你没有时间跑一个神经网络推理。好消息是,90% 的游戏 AI 需求都可以用简单的状态机和行为树解决。


有限状态机 (FSM) — 游戏 AI 的基石

状态机已在 游戏设计模式 中详细讲解。在 AI 场景中,最常见的状态模式是巡逻 → 追击 → 攻击 → 返回

typescript
// PatrolChaseAI.ts — 巡逻-追击 AI
enum EnemyState {
  Patrol,
  Chase,
  Attack,
  Return,
}

class PatrolChaseAI {
  private state = EnemyState.Patrol
  private target: { x: number; y: number } | null = null
  private homePosition: { x: number; y: number }
  private patrolPoints: Array<{ x: number; y: number }>
  private patrolIndex = 0

  // 可调参数
  detectionRange = 150 // 发现玩家的距离
  attackRange = 30 // 攻击范围
  chaseSpeed = 120 // 追击速度
  patrolSpeed = 50 // 巡逻速度
  forgetTime = 3000 // 丢失目标后继续追击的时间
  private lostTargetTime = 0

  update(dt: number, playerPos: { x: number; y: number }): void {
    const distToPlayer = this.distance(this.getPosition(), playerPos)

    switch (this.state) {
      case EnemyState.Patrol:
        this.patrol(dt)
        // 发现玩家 → 追击
        if (distToPlayer < this.detectionRange) {
          this.state = EnemyState.Chase
          this.target = playerPos
        }
        break

      case EnemyState.Chase:
        this.chase(dt, playerPos)
        // 进入攻击范围 → 攻击
        if (distToPlayer < this.attackRange) {
          this.state = EnemyState.Attack
        }
        // 玩家跑远了,计时忘记
        else if (distToPlayer > this.detectionRange * 1.5) {
          this.lostTargetTime += dt * 1000
          if (this.lostTargetTime > this.forgetTime) {
            this.state = EnemyState.Return
            this.lostTargetTime = 0
          }
        }
        break

      case EnemyState.Attack:
        this.attack()
        // 玩家逃离攻击范围 → 继续追击
        if (distToPlayer > this.attackRange * 1.5) {
          this.state = EnemyState.Chase
        }
        break

      case EnemyState.Return:
        this.returnToHome(dt)
        // 回到巡逻起点 → 恢复巡逻
        if (this.distance(this.getPosition(), this.homePosition) < 5) {
          this.state = EnemyState.Patrol
        }
        // 途中发现玩家 → 追击
        if (distToPlayer < this.detectionRange) {
          this.state = EnemyState.Chase
        }
        break
    }
  }

  private patrol(dt: number): void {
    const target = this.patrolPoints[this.patrolIndex]
    this.moveToward(target, this.patrolSpeed * dt)
    if (this.distance(this.getPosition(), target) < 5) {
      this.patrolIndex = (this.patrolIndex + 1) % this.patrolPoints.length
    }
  }

  private chase(dt: number, playerPos: { x: number; y: number }): void {
    this.moveToward(playerPos, this.chaseSpeed * dt)
  }

  private attack(): void {
    // 执行攻击动画 / 造成伤害
    console.log('敌人攻击!')
  }

  private returnToHome(dt: number): void {
    this.moveToward(this.homePosition, this.patrolSpeed * dt)
  }

  private getPosition() {
    return { x: 0, y: 0 }
  } // 简化
  private moveToward(_target: { x: number; y: number }, _dist: number): void {}
  private distance(a: { x: number; y: number }, b: { x: number; y: number }): number {
    return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2)
  }
}

行为树 (Behavior Tree)

当状态类型变多(超过 5-6 种),状态机变得难以维护。行为树通过组合节点提供更灵活的 AI 编排。

行为树基础节点

Selector (选择节点)     — 依次执行子节点,任一成功则成功(OR 逻辑)
Sequence (顺序节点)     — 依次执行子节点,全部成功才成功(AND 逻辑)
Condition (条件节点)    — 检测某个条件,返回成功/失败
Action (动作节点)       — 执行某个行为,返回成功/失败/运行中

巡逻-追击 AI 的行为树版本

Root (Selector)
├── Sequence #1: 攻击玩家
│   ├── Condition: 玩家在攻击范围内?
│   └── Action: 攻击
├── Sequence #2: 追击玩家
│   ├── Condition: 玩家在侦测范围内?
│   └── Action: 向玩家移动
├── Sequence #3: 返回巡逻起点
│   ├── Condition: 不在巡逻起点附近?
│   └── Action: 返回巡逻起点
└── Action: 巡逻(沿巡逻点移动)
typescript
// BehaviorTree.ts — 轻量行为树实现
type BTStatus = 'success' | 'failure' | 'running'

interface BTNode {
  tick(dt: number, context: AIContext): BTStatus
}

interface AIContext {
  enemy: { x: number; y: number; speed: number }
  player: { x: number; y: number }
  patrolPoints: Array<{ x: number; y: number }>
  patrolIndex: number
}

// 条件:检测玩家距离
class IsPlayerInRange implements BTNode {
  constructor(private range: number) {}
  tick(_dt: number, ctx: AIContext): BTStatus {
    const dist = Math.sqrt((ctx.enemy.x - ctx.player.x) ** 2 + (ctx.enemy.y - ctx.player.y) ** 2)
    return dist <= this.range ? 'success' : 'failure'
  }
}

// 动作:向玩家移动
class MoveToPlayer implements BTNode {
  tick(dt: number, ctx: AIContext): BTStatus {
    const dx = ctx.player.x - ctx.enemy.x
    const dy = ctx.player.y - ctx.enemy.y
    const dist = Math.sqrt(dx ** 2 + dy ** 2)

    if (dist < 2) return 'success'

    const step = ctx.enemy.speed * dt
    ctx.enemy.x += (dx / dist) * step
    ctx.enemy.y += (dy / dist) * step
    return 'running'
  }
}

// 选择节点
class Selector implements BTNode {
  constructor(private children: BTNode[]) {}
  tick(dt: number, ctx: AIContext): BTStatus {
    for (const child of this.children) {
      const status = child.tick(dt, ctx)
      if (status !== 'failure') return status // 第一个非失败的结果
    }
    return 'failure'
  }
}

// 顺序节点
class Sequence implements BTNode {
  constructor(private children: BTNode[]) {}
  tick(dt: number, ctx: AIContext): BTStatus {
    for (const child of this.children) {
      const status = child.tick(dt, ctx)
      if (status !== 'success') return status // 任一非成功就返回
    }
    return 'success'
  }
}

// 构建行为树
const patrolAI = new Selector([
  // 优先尝试攻击
  new Sequence([
    new IsPlayerInRange(30),
    new MoveToPlayer(), // 简化:攻击 = 靠近玩家
  ]),
  // 其次追击
  new Sequence([new IsPlayerInRange(150), new MoveToPlayer()]),
  // 否则巡逻(省略,实际实现中作为 fallback)
])
行为树 vs 状态机 选择指南
场景推荐方案原因
敌人 < 5 种、状态 < 5 个状态机实现简单,调试直观
敌人 > 5 种、行为复杂行为树可组合性高,便于策划调整
微信小游戏(休闲)状态机性能开销小,够用
需要可视化 AI 编辑行为树工具链成熟

A* 寻路算法

当敌人需要在有障碍物的地图中寻找路径时,A* 是最经典的选择。

typescript
// AStar.ts — A* 寻路(网格版本)
interface GridNode {
  x: number
  y: number
  g: number // 从起点到此节点的实际代价
  h: number // 启发式估计值(到此节点的直线距离到终点)
  f: number // g + h
  parent: GridNode | null
  walkable: boolean
}

class AStar {
  private grid: GridNode[][]
  private width: number
  private height: number

  constructor(grid: boolean[][]) {
    this.height = grid.length
    this.width = grid[0].length
    this.grid = grid.map((row, y) =>
      row.map((walkable, x) => ({
        x,
        y,
        walkable,
        g: Infinity,
        h: 0,
        f: Infinity,
        parent: null,
      }))
    )
  }

  findPath(
    startX: number,
    startY: number,
    endX: number,
    endY: number
  ): Array<{ x: number; y: number }> | null {
    const openList: GridNode[] = []
    const closedSet = new Set<string>()

    const startNode = this.grid[startY][startX]
    const endNode = this.grid[endY][endX]

    if (!startNode.walkable || !endNode.walkable) return null

    startNode.g = 0
    startNode.h = this.heuristic(startNode, endNode)
    startNode.f = startNode.h
    openList.push(startNode)

    while (openList.length > 0) {
      // 取 f 最小的节点
      openList.sort((a, b) => a.f - b.f)
      const current = openList.shift()!

      const key = `${current.x},${current.y}`
      if (closedSet.has(key)) continue
      closedSet.add(key)

      // 到达终点
      if (current.x === endX && current.y === endY) {
        return this.reconstructPath(current)
      }

      // 检查 4 邻域
      const neighbors = this.getNeighbors(current)
      for (const neighbor of neighbors) {
        if (!neighbor.walkable) continue
        const neighborKey = `${neighbor.x},${neighbor.y}`
        if (closedSet.has(neighborKey)) continue

        const tentativeG = current.g + 1
        if (tentativeG < neighbor.g) {
          neighbor.g = tentativeG
          neighbor.h = this.heuristic(neighbor, endNode)
          neighbor.f = neighbor.g + neighbor.h
          neighbor.parent = current
          openList.push(neighbor)
        }
      }
    }

    return null // 无可达路径
  }

  private heuristic(a: GridNode, b: GridNode): number {
    // 曼哈顿距离(4 方向移动)
    return Math.abs(a.x - b.x) + Math.abs(a.y - b.y)
  }

  private getNeighbors(node: GridNode): GridNode[] {
    const neighbors: GridNode[] = []
    const dirs = [
      [0, 1],
      [0, -1],
      [1, 0],
      [-1, 0],
    ]
    for (const [dx, dy] of dirs) {
      const nx = node.x + dx,
        ny = node.y + dy
      if (nx >= 0 && nx < this.width && ny >= 0 && ny < this.height) {
        neighbors.push(this.grid[ny][nx])
      }
    }
    return neighbors
  }

  private reconstructPath(node: GridNode): Array<{ x: number; y: number }> {
    const path: Array<{ x: number; y: number }> = []
    let current: GridNode | null = node
    while (current) {
      path.unshift({ x: current.x, y: current.y })
      current = current.parent
    }
    return path
  }
}

程序化生成入门

程序化生成可以让小体量游戏拥有更多的内容变化。以下是一个简单的随机地牢生成器:

typescript
// DungeonGenerator.ts — BSP(二叉空间分割)地牢生成
interface Room {
  x: number
  y: number
  width: number
  height: number
}

class BSPNode {
  left: BSPNode | null = null
  right: BSPNode | null = null
  room: Room | null = null

  constructor(
    public x: number,
    public y: number,
    public width: number,
    public height: number
  ) {}

  /** 递归分割空间 */
  split(minRoomSize: number = 8, maxDepth: number = 4, depth: number = 0): boolean {
    if (depth >= maxDepth) return false
    if (this.left || this.right) return false // 已分割

    // 决定横向还是纵向分割
    const splitH = Math.random() > 0.5

    // 检查是否有空间分割
    const maxSize = (splitH ? this.height : this.width) - minRoomSize
    if (maxSize <= minRoomSize) return false

    const splitPoint = minRoomSize + Math.floor(Math.random() * (maxSize - minRoomSize))

    if (splitH) {
      this.left = new BSPNode(this.x, this.y, this.width, splitPoint)
      this.right = new BSPNode(this.x, this.y + splitPoint, this.width, this.height - splitPoint)
    } else {
      this.left = new BSPNode(this.x, this.y, splitPoint, this.height)
      this.right = new BSPNode(this.x + splitPoint, this.y, this.width - splitPoint, this.height)
    }

    return true
  }

  /** 在叶子节点中生成房间 */
  generateRooms(): Room[] {
    if (this.left && this.right) {
      return [...this.left.generateRooms(), ...this.right.generateRooms()]
    }

    // 在区域内随机生成房间
    const roomWidth = 4 + Math.floor(Math.random() * (this.width - 6))
    const roomHeight = 4 + Math.floor(Math.random() * (this.height - 6))
    const roomX = this.x + Math.floor(Math.random() * (this.width - roomWidth - 1)) + 1
    const roomY = this.y + Math.floor(Math.random() * (this.height - roomHeight - 1)) + 1

    this.room = { x: roomX, y: roomY, width: roomWidth, height: roomHeight }
    return [this.room]
  }
}

// 使用:生成一个 5 层深度的地牢
const root = new BSPNode(0, 0, 80, 80)

// 递归分割
function recursiveSplit(node: BSPNode, depth = 0): void {
  if (node.split(8, 5, depth)) {
    recursiveSplit(node.left!, depth + 1)
    recursiveSplit(node.right!, depth + 1)
  }
}
recursiveSplit(root)

const rooms = root.generateRooms()
console.log(`生成了 ${rooms.length} 个房间`)

A* 寻路在网格地图上表现良好,但在连续地形中会遇到问题:网格太粗 → 路径不自然;网格太细 → 性能爆炸。导航网格(NavMesh) 将可行走区域表示为凸多边形集合,相比网格寻路有显著优势。

网格 vs NavMesh 对比

维度网格 A*NavMesh
地图表示二维数组(离散)凸多边形(连续)
寻路精度受网格分辨率限制平滑路径(可自然拐弯)
内存占用大(网格越大内存越多)小(只存储多边形顶点)
动态障碍容易(直接标记格子)困难(需要重新烘焙)
引擎支持自行实现Cocos/Unity 内置
适合场景塔防、战棋、回合制动作游戏、开放世界、射击

Cocos Creator 中的 NavMesh

Cocos Creator 3.8.x 内置了基于 Recast 的导航网格系统:

typescript
import { _decorator, Component, NavMesh, Vec3 } from 'cc'
const { ccclass } = _decorator

@ccclass('NavMeshAgent')
export class NavMeshAgent extends Component {
  private navMesh: NavMesh | null = null
  private path: Vec3[] = []
  private pathIndex: number = 0
  private speed: number = 100

  start() {
    // NavMesh 通常在场景中作为静态资源烘焙好
    this.navMesh = this.node.scene.getComponentInChildren(NavMesh)
  }

  /** 设置导航目标(findPath 是异步方法) */
  async setDestination(target: Vec3) {
    if (!this.navMesh) return
    // ⚠️ findPath 在 Cocos Creator 3.x 中返回 Promise,必须 await
    const result = await this.navMesh.findPath(this.node.worldPosition, target)
    this.path = result.paths || []
    this.pathIndex = 0
  }

  update(dt: number) {
    if (this.pathIndex >= this.path.length) return

    const target = this.path[this.pathIndex]
    const direction = target.clone().subtract(this.node.worldPosition)
    const distance = direction.length()

    if (distance < 5) {
      // 到达当前路径点,前往下一个
      this.pathIndex++
      return
    }

    // 向目标移动
    direction.normalize()
    this.node.worldPosition = this.node.worldPosition.add(direction.multiplyScalar(this.speed * dt))
  }
}
NavMesh 烘焙

NavMesh 是离线烘焙的——在编辑器中预先计算可行走区域。这意味着:

  1. 静态地形可以直接烘焙
  2. 动态障碍物(如可破坏的墙壁)需要额外的动态避障算法(如 RVO/ORCA)
  3. 对于微信小游戏中大多数 2D 场景,网格 A* 通常更简单直接

Steering Behaviors 转向行为

网格寻路给出的是"全局路径"(从 A 到 B 的一系列点),而转向行为(Steering Behaviors) 解决的是"局部运动"——如何在每个时刻调整实体的速度和方向,实现平滑、自然的移动。

Craig Reynolds 在 1999 年提出的经典转向行为模型至今仍是游戏 AI 运动的基础。

核心行为

typescript
// ai/steering.ts — 转向行为实现
interface Vector2D {
  x: number
  y: number
}

// 工具函数
function vec(x: number, y: number): Vector2D {
  return { x, y }
}
function add(a: Vector2D, b: Vector2D): Vector2D {
  return vec(a.x + b.x, a.y + b.y)
}
function sub(a: Vector2D, b: Vector2D): Vector2D {
  return vec(a.x - b.x, a.y - b.y)
}
function scale(v: Vector2D, s: number): Vector2D {
  return vec(v.x * s, v.y * s)
}
function length(v: Vector2D): number {
  return Math.sqrt(v.x ** 2 + v.y ** 2)
}
function normalize(v: Vector2D): Vector2D {
  const len = length(v)
  return len > 0 ? scale(v, 1 / len) : vec(0, 0)
}
function clamp(v: Vector2D, maxLen: number): Vector2D {
  const len = length(v)
  return len > maxLen ? scale(normalize(v), maxLen) : v
}

// --- 行为函数(返回 steering force,即期望速度的修正量)---

/** Seek:向目标点移动 */
function seek(
  agent: { pos: Vector2D; vel: Vector2D; maxSpeed: number },
  target: Vector2D
): Vector2D {
  const desired = sub(target, agent.pos)
  const desiredVel = scale(normalize(desired), agent.maxSpeed)
  return sub(desiredVel, agent.vel) // steering = desired - current
}

/** Flee:逃离目标点 */
function flee(
  agent: { pos: Vector2D; vel: Vector2D; maxSpeed: number },
  threat: Vector2D
): Vector2D {
  const desired = sub(agent.pos, threat) // 与 seek 方向相反
  const desiredVel = scale(normalize(desired), agent.maxSpeed)
  return sub(desiredVel, agent.vel)
}

/** Arrival:向目标移动,靠近时减速 */
function arrival(
  agent: { pos: Vector2D; vel: Vector2D; maxSpeed: number },
  target: Vector2D,
  slowingRadius: number = 100
): Vector2D {
  const offset = sub(target, agent.pos)
  const dist = length(offset)

  if (dist < 1) return vec(0, 0) // 已到达

  // 在减速半径内:速度 = maxSpeed × (dist / slowingRadius)
  const speed = dist < slowingRadius ? agent.maxSpeed * (dist / slowingRadius) : agent.maxSpeed

  const desiredVel = scale(normalize(offset), speed)
  return sub(desiredVel, agent.vel)
}

/** Wander:随机漫游(通过旋转一个"前方圆圈"上的点来实现) */
function wander(
  agent: { pos: Vector2D; vel: Vector2D; maxSpeed: number },
  wanderAngle: number,
  wanderRadius: number = 50,
  wanderDistance: number = 100,
  wanderJitter: number = 0.3
): { force: Vector2D; newAngle: number } {
  // 在圆周上添加随机偏移
  const newAngle = wanderAngle + (Math.random() * 2 - 1) * wanderJitter

  // 计算圆周上的目标点
  const circleCenter = scale(normalize(agent.vel), wanderDistance)
  const displacement = vec(Math.cos(newAngle) * wanderRadius, Math.sin(newAngle) * wanderRadius)

  const target = add(add(agent.pos, circleCenter), displacement)
  return { force: seek(agent, target), newAngle }
}

Flocking(集群/鸟群行为)

将三个基本行为组合即可实现鸟群/鱼群效果:

typescript
/** Separation(分离):与邻近同伴保持距离 */
function separation(
  agent: { pos: Vector2D; vel: Vector2D; maxSpeed: number },
  neighbors: { pos: Vector2D }[],
  sepRadius: number
): Vector2D {
  const steer = vec(0, 0)
  let count = 0
  for (const n of neighbors) {
    const d = length(sub(agent.pos, n.pos))
    if (d > 0 && d < sepRadius) {
      const diff = normalize(sub(agent.pos, n.pos))
      steer.x += diff.x / d // 越近排斥力越大
      steer.y += diff.y / d
      count++
    }
  }
  return count > 0 ? scale(normalize(steer), agent.maxSpeed) : vec(0, 0)
}

/** Alignment(对齐):与邻近同伴保持相同方向 */
function alignment(
  agent: { pos: Vector2D; vel: Vector2D; maxSpeed: number },
  neighbors: { vel: Vector2D }[],
  alignRadius: number
): Vector2D {
  const avg = vec(0, 0)
  let count = 0
  for (const n of neighbors) {
    avg.x += n.vel.x
    avg.y += n.vel.y
    count++
  }
  return count > 0 ? sub(scale(normalize(avg), agent.maxSpeed), agent.vel) : vec(0, 0)
}

/** Cohesion(凝聚):向邻近同伴的中心移动 */
function cohesion(
  agent: { pos: Vector2D; vel: Vector2D; maxSpeed: number },
  neighbors: { pos: Vector2D }[]
): Vector2D {
  const center = vec(0, 0)
  for (const n of neighbors) ((center.x += n.pos.x), (center.y += n.pos.y))
  if (neighbors.length === 0) return vec(0, 0)
  center.x /= neighbors.length
  center.y /= neighbors.length
  return seek(agent, center)
}

这三个行为的组合权重决定了群体行为的视觉效果——分离权重高 → 松散、对齐权重高 → 整齐划一、凝聚权重高 → 紧密抱团。


Utility AI 效用系统

状态机和行为树在给定条件时做出二元决策("玩家在攻击范围内?→ 是:攻击 / 否:巡逻")。但当决策涉及多个竞争因素的权衡时("我现在应该攻击、逃跑、还是收集资源?"),效用系统更合适。

核心思想

不为行为设置触发条件,而是为每个行为计算一个"效用分数"(Utility Score),选择分数最高的行为执行。

typescript
// ai/utility-ai.ts — 简单效用系统示例

interface UtilityAction {
  name: string
  /** 计算该行为的效用分数 (0~1) */
  score(context: GameContext): number
  /** 执行该行为 */
  execute(context: GameContext): void
}

interface GameContext {
  health: number // 当前生命值 (0~100)
  ammo: number // 剩余弹药 (0~100)
  enemyDistance: number // 最近敌人距离
  hasPowerUp: boolean // 是否有增益道具
}

// 定义行为
const actions: UtilityAction[] = [
  {
    name: 'attack',
    score: (ctx) => {
      if (ctx.ammo <= 0) return 0 // 没弹药无法攻击
      if (ctx.enemyDistance > 200) return 0 // 太远打不到
      // 距离越近、弹药越多 → 攻击效用越高
      const distFactor = 1 - ctx.enemyDistance / 200
      const ammoFactor = ctx.ammo / 100
      return distFactor * 0.7 + ammoFactor * 0.3
    },
    execute: (ctx) => console.log('发动攻击!'),
  },
  {
    name: 'flee',
    score: (ctx) => {
      // 生命值越低 → 逃跑效用越高
      const healthFactor = 1 - ctx.health / 100
      // 有道具时不轻易逃跑(道具可能是无敌)
      const courageFactor = ctx.hasPowerUp ? 0.3 : 1.0
      return healthFactor * courageFactor
    },
    execute: (ctx) => console.log('撤退!'),
  },
  {
    name: 'collect',
    score: (ctx) => {
      // 满血满弹药时不需要收集
      if (ctx.health > 80 && ctx.ammo > 80) return 0.1
      // 血量弹药越低 → 收集效用越高
      const needFactor = (1 - ctx.health / 100) * 0.5 + (1 - ctx.ammo / 100) * 0.5
      return needFactor
    },
    execute: (ctx) => console.log('搜集补给!'),
  },
]

// 每帧决策
function makeDecision(ctx: GameContext): void {
  // 计算所有行为的效用分数
  const scored = actions.map((action) => ({
    action,
    score: action.score(ctx),
  }))

  // 选最高分
  scored.sort((a, b) => b.score - a.score)
  const best = scored[0]

  if (best.score > 0.1) {
    best.action.execute(ctx)
  }
}

何时用哪种 AI 架构

场景推荐方案理由
敌人有明确状态转换(巡逻→追击→攻击)状态机 (FSM)状态边界清晰,易调试
AI 行为需要复杂的条件组合行为树 (BT)条件子树复用性强
多种行为竞争,需动态权衡效用 AI (Utility)无硬编码阈值,行为平滑过渡
开放世界 NPC 日常行为GOAP(目标导向行动规划)自动生成行动计划,NPC 更"聪明"
分层混合使用

实际项目中很少只用一种 AI 方案,常见做法:

  • 战术层(选择做什么):效用 AI 或 GOAP
  • 执行层(具体怎么做):行为树或状态机
  • 运动层(怎么移动):A* 寻路 + Steering Behaviors

举例:效用 AI 决定"逃跑"→ 行为树执行"寻找掩体 → 跑到掩体后"→ Steering Behaviors 控制平滑移动。


层次状态机 (HFSM)

基本 FSM 在状态数量增加时会面临状态爆炸问题——每个状态组合都需要一个独立状态。HFSM 通过将状态组织为树状层次结构,让子状态继承父状态的行为和转换。

概念对比

特性平铺 FSM层次状态机 (HFSM)
状态组织所有状态同级状态嵌套(子状态 ≠ 独立状态)
共享转换每个状态都要定义父状态定义一次,所有子状态继承
适用场景简单敌人 AI(巡逻 → 追击 → 攻击)复杂角色(移动+战斗复合状态)
调试难度中等

HFSM 实现

typescript
// ai/HFSM.ts — 层次状态机
interface HFSMState {
  name: string
  parent?: HFSMState // 父状态,根状态为 undefined
  children: Map<string, HFSMState> // 子状态
  transitions: Array<{ to: string; condition: () => boolean }>
  onEnter?: () => void
  onUpdate?: (dt: number) => void
  onExit?: () => void
}

class HFSM {
  private states: Map<string, HFSMState> = new Map()
  private currentPath: HFSMState[] = [] // 从根到当前激活状态的路径

  addState(state: HFSMState): void {
    this.states.set(state.name, state)
    state.children.forEach((child) => {
      child.parent = state
    })
  }

  /** 设置当前状态(完整路径) */
  setState(stateName: string): void {
    const state = this.states.get(stateName)
    if (!state) return

    // 找到公共祖先,从旧路径退出到公共祖先,再进入新路径
    const newPath = this.buildPath(state)

    // 退出旧路径中不在新路径上的状态
    while (
      this.currentPath.length > 0 &&
      !newPath.includes(this.currentPath[this.currentPath.length - 1])
    ) {
      const exiting = this.currentPath.pop()!
      exiting.onExit?.()
    }

    // 进入新路径中不在旧路径上的状态
    const startIdx = this.currentPath.length
    for (let i = startIdx; i < newPath.length; i++) {
      this.currentPath.push(newPath[i])
      newPath[i].onEnter?.()
    }
  }

  update(dt: number): void {
    // 从最深层子状态开始检查转换
    for (let i = this.currentPath.length - 1; i >= 0; i--) {
      const state = this.currentPath[i]
      state.onUpdate?.(dt)

      // 检查是否有转换被触发
      for (const trans of state.transitions) {
        if (trans.condition()) {
          this.setState(trans.to)
          return
        }
      }
    }
  }

  private buildPath(state: HFSMState): HFSMState[] {
    const path: HFSMState[] = [state]
    let current = state
    while (current.parent) {
      path.unshift(current.parent)
      current = current.parent
    }
    return path
  }
}

// ===== 使用示例:Boss 敌人的 HFSM =====

// 父状态:移动(包含 Walk、Run 子状态)
const moveState: HFSMState = {
  name: 'move',
  children: new Map(),
  transitions: [
    { to: 'attack', condition: () => isPlayerInRange(50) },
    { to: 'dead', condition: () => boss.hp <= 0 },
  ],
}

const walkState: HFSMState = {
  name: 'walk',
  parent: moveState, // walk 是 move 的子状态
  children: new Map(),
  transitions: [
    { to: 'run', condition: () => boss.hp < boss.maxHp * 0.3 }, // 低血量开始跑
  ],
  onEnter: () => {
    boss.speed = 2
  },
  onUpdate: (dt) => {
    patrol(dt)
  },
}

const runState: HFSMState = {
  name: 'run',
  parent: moveState,
  children: new Map(),
  transitions: [],
  onEnter: () => {
    boss.speed = 5
  },
  onUpdate: (dt) => {
    patrol(dt)
  },
}

moveState.children.set('walk', walkState)
moveState.children.set('run', runState)

// 父状态:攻击(包含 Melee、Ranged 子状态)
const attackState: HFSMState = {
  name: 'attack',
  children: new Map(),
  transitions: [
    { to: 'move', condition: () => !isPlayerInRange(50) },
    { to: 'dead', condition: () => boss.hp <= 0 },
  ],
}

// ... MeleeAttack, RangedAttack 子状态类似

// 启动
const bossHFSM = new HFSM()
bossHFSM.addState(moveState)
bossHFSM.addState(walkState)
bossHFSM.addState(runState)
bossHFSM.addState(attackState)
bossHFSM.setState('walk') // 从 walk 子状态开始
HFSM 使用时机
  • 使用 HFSM:当你的 FSM 超过 10 个状态,且多个状态共享相同的外部转换条件时。
  • 不用 HFSM:3-5 个状态的简单 AI,平铺 FSM 更清晰。
  • 与行为树分工:HFSM 做高层决策(巡逻/战斗/逃跑),行为树做当前状态下的具体行为编排。

GOAP 目标导向行动规划

GOAP(Goal-Oriented Action Planning)是 FSM 和 Behavior Tree 之外的另一条路——AI 不按预设的转换规则行动,而是根据当前世界状态目标,动态规划出一系列动作来实现目标。

GOAP 核心概念

世界状态 (World State) → 规划器 (Planner) → 动作序列 (Action Sequence) → 执行
         ↑                                              |
         └──────────── 执行结果反馈 ←────────────────────┘
  • 世界状态:键值对集合(如 { hasWeapon: false, playerVisible: true, hp: 80 }
  • 目标:期望达到的世界状态(如 { playerDead: true }
  • 动作:有前置条件和效果的操作(如「捡武器」:前置 hasWeapon == false,效果 hasWeapon = true
  • 规划器:使用 A* 算法在动作空间中搜索,找到从当前状态到目标状态的动作序列

轻量 GOAP 实现

typescript
// ai/GOAP.ts — 轻量 GOAP 系统(适合小游戏)
interface WorldState {
  [key: string]: boolean | number
}

interface GOAPAction {
  name: string
  cost: number // 执行代价
  preconditions: WorldState // 前置条件
  effects: WorldState // 执行效果
  perform: () => void // 实际执行函数
}

interface GOAPGoal {
  name: string
  priority: number // 优先级(数值越大越优先)
  desiredState: WorldState // 目标世界状态
}

class GOAPPlanner {
  /**
   * 规划动作序列(简化的贪心搜索,适合小规模动作空间)
   * 对于大规模动作空间,应使用 A* 搜索
   */
  plan(currentState: WorldState, goal: GOAPGoal, actions: GOAPAction[]): GOAPAction[] | null {
    const plan: GOAPAction[] = []
    let state = { ...currentState }
    const maxDepth = 10 // 防止无限搜索

    for (let depth = 0; depth < maxDepth; depth++) {
      // 检查是否已达成目标
      if (this.isStateSatisfied(state, goal.desiredState)) {
        return plan
      }

      // 找到第一个可执行的动作(贪心:选代价最低的可行动作)
      const bestAction = actions
        .filter((a) => this.canExecute(a, state))
        .sort((a, b) => a.cost - b.cost)[0]

      if (!bestAction) return null // 无可行动作,规划失败

      plan.push(bestAction)
      state = this.applyEffects(state, bestAction.effects)
    }

    return null // 超过最大深度,规划失败
  }

  private isStateSatisfied(current: WorldState, desired: WorldState): boolean {
    return Object.entries(desired).every(([key, value]) => {
      return current[key] === value
    })
  }

  private canExecute(action: GOAPAction, state: WorldState): boolean {
    return Object.entries(action.preconditions).every(([key, value]) => {
      return state[key] === value
    })
  }

  private applyEffects(state: WorldState, effects: WorldState): WorldState {
    return { ...state, ...effects }
  }
}

// ===== 使用示例:敌人 AI =====

const planner = new GOAPPlanner()

// 定义世界状态
const worldState: WorldState = {
  playerVisible: true,
  hasWeapon: false,
  ammoRemaining: 0,
}

// 可用动作
const actions: GOAPAction[] = [
  {
    name: '拾取武器',
    cost: 1,
    preconditions: { hasWeapon: false },
    effects: { hasWeapon: true },
    perform: () => console.log('→ 捡起地上的武器'),
  },
  {
    name: '装填弹药',
    cost: 2,
    preconditions: { hasWeapon: true, ammoRemaining: 0 },
    effects: { ammoRemaining: 30 },
    perform: () => console.log('→ 装填弹药...'),
  },
  {
    name: '攻击玩家',
    cost: 1,
    preconditions: { playerVisible: true, hasWeapon: true, ammoRemaining: 1 },
    effects: { playerVisible: false },
    perform: () => console.log('→ 向玩家开火!'),
  },
]

// 目标
const goal: GOAPGoal = {
  name: '消灭玩家',
  priority: 10,
  desiredState: { playerVisible: false },
}

// 规划
const plan = planner.plan(worldState, goal, actions)
// 输出: [拾取武器 → 装填弹药 → 攻击玩家]
console.log('规划结果:', plan?.map((a) => a.name).join(' → '))

GOAP vs 行为树 vs FSM

维度FSM行为树GOAP
行为定义状态 + 转换条件树节点(行为/Action/Condition)目标 + 动作前/后条件
AI 可预测性高(所有转换已定义)高(树结构确定路径)低(动态规划,行为可能不同)
新人友好度5 状态以上开始复杂可视化编辑工具友好概念简单,调试困难
微信小游戏适用性✅ 主体方案✅ 中型游戏⚠️ 仅大项目/特殊需求

📚 相关阅读


📝 课后练习

练习 1:为塔防游戏设计敌人 AI

题目: 设计一个塔防游戏的敌人 AI 系统,要求:

  1. 敌人沿预定路线移动,遇到岔路时选择最短路径到终点
  2. 被「减速塔」攻击后,速度降低 50%,持续 3 秒
  3. 被「眩晕塔」攻击后,原地停顿 2 秒
  4. 不同类型的敌人有不同行为:普通(直线移动)、飞行(忽略地面障碍)、Boss(免疫减速)

参考答案: 使用状态机,核心状态:Walking → Slowed → Stunned → Walking。飞行敌人和 Boss 通过检查 enemyType 跳过不适用的状态转换(如飞行敌人不响应地面障碍的路径重算)。

练习 2:实现异步寻路

题目: 将 A* 寻路改为异步执行——每帧只扩展 50 个节点,避免阻塞游戏循环。

参考答案:while(openList.length > 0) 主循环改为可恢复的迭代器模式(capture openListclosedSet 的状态),每帧调用时继续上一次的迭代。或者使用 Web Worker 在独立线程中计算(微信小游戏基础库 ≥ 2.20.0 支持 Worker)。

用心学习,持续实践