引言
游戏 AI = 「决策 + 移动」。决策层用状态机/行为树决定「下一步干什么」,移动层用寻路算法决定「怎么走到那」。本文把 Defold 游戏 AI 讲透:先讲 A* 寻路原理与在 Lua 里的实现,再讲导航网格(Navmesh)与路径平滑,接着覆盖敌人 AI 的典型模式(追逐、巡逻、视线检测),再给行为树(BT)与状态机的选择与实战,最后讲群体行为与性能优化,让你给 Defold 敌人配上会「思考」的脑子。
前置:/defold-game-engine-complex-logic-state-management/(状态机)、/defold-script-system-lua/(协程与消息)、/defold-physics-collision/(碰撞检测)。Lua 语言见 [[lua]]。
目录
- 1. AI 分层:决策与移动
- 2. A* 寻路算法原理
- 3. Lua 实现 A*:网格寻路实战
- 4. 导航网格(Navmesh)与路径平滑
- 5. 敌人 AI:追逐、巡逻与视线检测
- 6. 有限状态机 AI:三态敌人实战
- 7. 行为树(BT):更复杂的决策
- 8. 群体行为:编队与避让
- 9. AI 性能优化
- 10. 速查表
- 延伸阅读
1. AI 分层:决策与移动
游戏 AI 拆成两层,各司其职:
决策层(脑子):
状态机 / 行为树 → 决定「下一步做什么」
↓ 下达目标
移动层(腿):
寻路算法 → 从 A 点到 B 点的路径
平滑/避让 → 走起来自然
典型 AI 循环:
每帧:
1. 感知(视野/距离/血量)
2. 决策(打/追/跑/待机)
3. 移动(朝目标走/用寻路)
| 层 | 工具 | 问题 |
|---|---|---|
| 决策 | FSM / 行为树 | 干什么 |
| 移动 | A*/Navmesh | 怎么走 |
| 感知 | 距离/射线/视野 | 知道啥 |
心智:AI = 感知 → 决策 → 移动 的循环——别一上来写复杂算法,先分层想清楚。
2. A* 寻路算法原理
A = Dijkstra + 启发式*——用「已走成本 g + 估算成本 h」引导搜索:
f(n) = g(n) + h(n)
g(n) = 起点到 n 的实际成本
h(n) = n 到终点的估算成本(启发式,如曼哈顿距离)
开放列表:待考察节点(按 f 排序,取最小)
关闭列表:已考察节点
过程:从起点出发,扩展邻居,选 f 最小的继续,直到终点
启发式选择:
| 地图 | h 启发式 |
|---|---|
| 四方向网格 | 曼哈顿距离 |
| 八方向网格 | 切比雪夫距离 |
| 任意图 | 欧氏距离 |
为什么 A 快*:启发式把搜索「引向终点」,不用盲搜整张地图。
记忆:A 的核心是 f = g + h*——h 引导方向、g 保证最优,地图越大越省事。
3. Lua 实现 A*:网格寻路实战
网格寻路的 Lua 实现(简化):
-- 网格:1=可通过,0=障碍
local GRID = {
{1,1,1,1,1,1},
{1,0,0,0,1,1},
{1,1,1,0,1,1},
{1,0,1,1,1,1},
}
local DIRS = { {1,0},{-1,0},{0,1},{0,-1} } -- 四方向
local function heuristic(a, b)
return math.abs(a.x - b.x) + math.abs(a.y - b.y) -- 曼哈顿
end
-- 返回从 start 到 goal 的路径(节点列表)
local function astar(grid, start, goal)
local open, closed = { [start] = { g=0, f=heuristic(start, goal), from=nil } }, {}
while next(open) do
-- 取 f 最小的节点
local cur, curf = nil, math.huge
for node, data in pairs(open) do
if data.f < curf then cur, curf = node, data.f end
end
if cur.x == goal.x and cur.y == goal.y then
-- 回溯路径
local path = {}
local n = cur
while n do path[#path+1] = n; n = open[n].from end
return path
end
open[cur] = nil; closed[cur] = true
for _, d in ipairs(DIRS) do
local nx, ny = cur.x+d[1], cur.y+d[2]
if grid[ny] and grid[ny][nx] == 1 and not closed[{x=nx,y=ny}] then
local ng = (open[cur].g or 0) + 1
local key = { x=nx, y=ny }
if not open[key] or ng < open[key].g then
open[key] = { g=ng, f=ng+heuristic(key, goal), from=cur }
end
end
end
end
return nil -- 无路可达
end
在游戏里用:
-- 敌人寻路到玩家
local path = astar(GRID, {x=ex, y=ey}, {x=px, y=py})
if path then
-- 沿路径逐节点移动
self.path = path; self.path_idx = 1
end
记忆:A Lua 实现约 30 行*——开放表取最小 f、扩展邻居、回溯路径,够游戏用;要更高效可换二叉堆。
4. 导航网格(Navmesh)与路径平滑
网格寻路局限:障碍形状复杂时网格粒度粗、路径锯齿。Navmesh(导航网格)更优:
Navmesh:把可行走区域多边形化 → 多边形凸块
寻路在凸多边形之间跑 → 路径更平滑、更高效
Defold 里做 Navmesh 的常见做法:
1. 手工铺设:把地形划分成凸多边形,存到 Lua 表
2. 工具生成:外部工具导出 navmesh 数据(JSON)→ Defold 加载
3. 简化:小型 2D 游戏用「网格 + 路径平滑」足够
路径平滑(消除锯齿走位):
-- 简单平滑:直线可达就跳过中间节点(视线剪枝)
local function smooth(self, path)
local result = {}
for i = 1, #path do
local cur = path[i]
if not self.prev or not line_of_sight(self.prev, cur) then
result[#result+1] = cur
self.prev = cur
end
end
return result
end
| 方案 | 适用 | 平滑度 |
|---|---|---|
| 网格 A* | 小型/格子地图 | 锯齿 |
| 网格 + 平滑 | 常见 2D | 较好 |
| Navmesh | 复杂地形 | 最好 |
记忆:小地图网格 + 平滑够用,复杂地形上 Navmesh——「直线可达就跳点」的剪枝平滑最实用。
5. 敌人 AI:追逐、巡逻与视线检测
追逐(简单跟随):
-- 朝玩家移动
local player = self.player_pos
local dir = vmath.normalize(player - go.get_position("."))
go.set_velocity("." , dir * self.speed) -- 或手动位移
巡逻(来回走 + 边界回退):
-- 巡逻点列表
self.patrol = { vmath.vector3(10,0,0), vmath.vector3(60,0,0) }
self.patrol_idx = 1
local function update_patrol(self)
local target = self.patrol[self.patrol_idx]
local pos = go.get_position(".")
local dist = vmath.length(target - pos)
if dist < 5 then -- 到达 → 换下一个点
self.patrol_idx = (self.patrol_idx % #self.patrol) + 1
else
local dir = vmath.normalize(target - pos)
go.set_position("." , pos + dir * self.speed * dt)
end
end
视线检测(能看到才追):
-- 距离 + 角度 + 射线遮挡
local function has_line_of_sight(self, from, to)
local dist = vmath.length(to - from)
if dist > self.view_range then return false end -- 超出视野
local hit = physics.raycast(from, to) -- 射线
return not hit or hit.distance > dist -- 无遮挡
end
记忆:追逐 = 朝目标走;巡逻 = 点列表循环;视线 = 距离 + 射线遮挡——三个模式拼出有压迫感的敌人。
6. 有限状态机 AI:三态敌人实战
状态机 AI(IDLE → CHASE → ATTACK):
local STATE = { IDLE=1, CHASE=2, ATTACK=3 }
function init(self)
self.state = STATE.IDLE
self.range = 200 -- 警觉距离
self.attack_range = 40
self.attack_cd = 0
end
function update(self, dt)
local player = self.player_pos
local pos = go.get_position(".")
local dist = vmath.length(player - pos)
-- 状态转移
if dist < self.range and self.state ~= STATE.CHASE then
set_state(self, STATE.CHASE)
elseif dist < self.attack_range and self.state ~= STATE.ATTACK then
set_state(self, STATE.ATTACK)
elseif dist > self.range and self.state == STATE.CHASE then
set_state(self, STATE.IDLE)
end
-- 状态行为
if self.state == STATE.CHASE then
chase_player(self, dt) -- 追逐
elseif self.state == STATE.ATTACK then
attack_player(self, dt) -- 攻击 + 冷却
end
end
状态机优点:直观、易调参、好排错——适合状态少、转换清晰的 AI。
记忆:FSM AI 三步——定义状态、判距离转移、按状态执行——状态少时最清晰,别为小 AI 硬上行为树。
7. 行为树(BT):更复杂的决策
行为树把决策组织成树——节点返回 成功/失败/运行:
选择器(Sequence 全成 / Selector 任一)
└─ 条件:玩家在视野?
└─ 序列:
├─ 行动:追击
├─ 条件:距离 < 攻击距离?
└─ 行动:攻击
Defold 里简单实现行为树节点:
-- 行为节点:返回 "success" / "fail" / "running"
local function node_chase(self, ctx)
if not ctx.target then return "fail" end
chase_player(self, ctx.dt)
return vmath.length(ctx.target - go.get_position(".")) > 40 and "running" or "success"
end
local function node_attack(self, ctx)
if vmath.length(ctx.target - go.get_position(".")) > 40 then return "fail" end
attack_player(self)
return "running"
end
-- 选择器:依次尝试,成功即返回
local function selector(...)
return function(self, ctx)
for _, n in ipairs({...}) do
local r = n(self, ctx)
if r ~= "fail" then return r end
end
return "fail"
end
end
FSM vs 行为树:
| 维度 | FSM | 行为树 |
|---|---|---|
| 状态清晰度 | 高 | 树形 |
| 扩展性 | 状态多会乱 | 加节点即可 |
| 复用 | 低 | 节点可复用 |
| 适合 | 简单 AI | 复杂/组合行为 |
记忆:状态多、行为能组合 → 行为树;状态少 → FSM 够用——别拿锤子砸钉子,也别为复杂硬上简单。
8. 群体行为:编队与避让
**群体 AI(敌人波、守卫队)**的经典做法:
避让(分离)——别挤成一团:
-- 对每个邻居施加斥力
local function separation(self)
local push = vmath.vector3(0, 0, 0)
for _, other in ipairs(self.nearby) do
local delta = go.get_position(".") - other.pos
local d = vmath.length(delta)
if d < self.min_sep and d > 0.01 then
push = push + vmath.normalize(delta) / d -- 距离越近斥力越大
end
end
return push * self.sep_strength
end
编队(队列)——跟随队长:
-- 士兵跟随队长,保持队形偏移
local function follow_leader(self, leader_pos, offset)
local target = leader_pos + offset
local dir = vmath.normalize(target - go.get_position("."))
go.set_position(".", go.get_position(".") + dir * self.speed * dt)
end
记忆:群体 AI 两条力——分离斥力防拥挤、编队偏移保队形——简单公式就能模拟出「整齐军队」。
9. AI 性能优化
AI 是 CPU 大头——数量多必须优化:
| 优化 | 做法 | 效果 |
|---|---|---|
| 分帧 | 每帧只更新部分 AI | 均摊开销 |
| 距离剔除 | 远处 AI 降频/不更新 | 省大头 |
| 简化感知 | 定时射线段,别每帧 | 省射线 |
| 预计算路径 | 路径缓存复用 | 省 A* |
| 协程 | 用协程做延迟决策 | 分散开销 |
分帧示例:
-- 每帧只更新 1/3 的敌人(轮流)
function update(self, dt)
local i = self.frame % 3
for j = i, #self.enemies, 3 do
update_enemy_ai(self.enemies[j], dt)
end
self.frame = self.frame + 1
end
记忆:AI 优化三板斧——分帧、距离剔除、路径缓存——几百个敌人也能流畅跑。
10. 速查表
| 需求 | 做法 |
|---|---|
| 找路径 | A*(f=g+h)网格寻路 |
| 平滑路径 | 视线剪枝(直线可达跳点) |
| 复杂地形 | Navmesh 多边形导航 |
| 追逐 | 朝目标 normalize 移动 |
| 巡逻 | 巡逻点列表循环 |
| 视野 | 距离 + 射线遮挡 |
| 决策 | FSM(状态少)/ 行为树(组合多) |
| 避让 | 邻居斥力 separation |
| 编队 | 队长偏移跟随 |
| 优化 | 分帧 + 距离剔除 + 路径缓存 |
一句话记忆:Defold AI = 决策(FSM/行为树)+ 移动(A/Navmesh);A 用 f=g+h 网格寻路、直线可达就平滑剪枝;追逐朝目标、巡逻走点列表、视野看距离加射线;状态少用 FSM、行为组合上行为树;群体靠分离斥力 + 编队偏移;性能用分帧 + 剔除 + 路径缓存——给敌人装上会思考的脑子。**
延伸阅读
- /defold-game-engine-complex-logic-state-management/ — 状态机与 AI 行为基础
- /defold-physics-collision/ — 射线检测与碰撞
- /defold-script-system-lua/ — 协程与消息驱动
- /defold-performance-optimization/ — AI 性能优化
- [[lua]] — Lua 语言深入
- [[game]] — 游戏 AI 方法论
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。