主题切换
游戏 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
}
}性能注意事项
A* 在密集网格中的开销不可忽视。微信小游戏中的优化策略:
- 分层寻路 — 大区域内用粗粒度网格,局部用细粒度
- 路径缓存 — 相同起终点的路径直接复用
- 异步计算 — 将 A* 拆分到多帧完成(每帧只扩展 N 个节点)
- Flow Field — 大量单位同目标时使用流场寻路,只需计算一次
程序化生成入门
程序化生成可以让小体量游戏拥有更多的内容变化。以下是一个简单的随机地牢生成器:
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} 个房间`)NavMesh 导航网格
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 是离线烘焙的——在编辑器中预先计算可行走区域。这意味着:
- 静态地形可以直接烘焙
- 动态障碍物(如可破坏的墙壁)需要额外的动态避障算法(如 RVO/ORCA)
- 对于微信小游戏中大多数 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 状态以上开始复杂 | 可视化编辑工具友好 | 概念简单,调试困难 |
| 微信小游戏适用性 | ✅ 主体方案 | ✅ 中型游戏 | ⚠️ 仅大项目/特殊需求 |
GOAP 在微信小游戏中的实用性
实话实说:对于大多数微信小游戏,GOAP 是过度工程。一个行为树 + 简单的 Utility AI 评分系统在 95% 的场景下足够使用。GOAP 真正的价值体现在:
- 开放世界游戏(AI 需要自主寻找资源、装备、路径)
- 需要 AI 表现出「创造性」行为的场景(玩家无法预测 AI 会怎么组合动作)
- 大规模 NPC 需要不同行为模式的模拟类游戏
如果你的游戏 AI 需求只是「巡逻 → 发现玩家 → 追击 → 攻击」,用 FSM 就足够了。
📚 相关阅读
- 游戏设计模式 — 状态模式和组件模式在 AI 中的应用
- 03 游戏开发核心概念 — 游戏循环与 AABB 碰撞检测
- 08 性能优化与包体控制 — 大量实体场景下的 AI 性能优化
📝 课后练习
练习 1:为塔防游戏设计敌人 AI
题目: 设计一个塔防游戏的敌人 AI 系统,要求:
- 敌人沿预定路线移动,遇到岔路时选择最短路径到终点
- 被「减速塔」攻击后,速度降低 50%,持续 3 秒
- 被「眩晕塔」攻击后,原地停顿 2 秒
- 不同类型的敌人有不同行为:普通(直线移动)、飞行(忽略地面障碍)、Boss(免疫减速)
参考答案: 使用状态机,核心状态:Walking → Slowed → Stunned → Walking。飞行敌人和 Boss 通过检查 enemyType 跳过不适用的状态转换(如飞行敌人不响应地面障碍的路径重算)。
练习 2:实现异步寻路
题目: 将 A* 寻路改为异步执行——每帧只扩展 50 个节点,避免阻塞游戏循环。
参考答案: 将 while(openList.length > 0) 主循环改为可恢复的迭代器模式(capture openList 和 closedSet 的状态),每帧调用时继续上一次的迭代。或者使用 Web Worker 在独立线程中计算(微信小游戏基础库 ≥ 2.20.0 支持 Worker)。