想象你第一天当外卖骑手:城市很大,你得先搞清楚哪条路能骑车、哪条路是死胡同——这叫导航网格;打开手机导航,App 帮你规划路线、逐段提醒——这叫导航代理;你一天的工作按“等单→取餐→送餐→休息”轮转——这叫状态机;下雨时你会先穿雨衣再出发,订单超时时你会优先送急单——这叫行为树;晚高峰路口上百个骑手互不碰撞,因为大家都遵守几条默契规则——这叫群体AI。Godot 这款开源游戏引擎把这些工具全部内置,本文就用骑手的一天,带你把这些“城市生存技能”一个个安装进游戏角色的大脑。
学完本文,你能在 Godot 中用导航网格、状态机、行为树和群体 AI 快速搭建独立游戏所需的 NPC 智能,这是游戏 AI 方向最实用的上手技能。
可视化演示
展示Godot引擎NavigationServer导航网格烘焙、行为树决策遍历与有限状态机切换的交互过程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
NavigationServer导航网格 可行走区域 障碍物 NavMesh Bake 多边形裁剪合并
导航网格概念
第一天跑外卖,老骑手递给你一张手绘地图:“红色区域是人行道和商场中庭,电动车进不去;蓝色路线才是能骑车的路。”你瞬间明白,能走和不能走本身就是最重要的信息。游戏里这张“地图”叫导航网格(NavigationMesh),它把游戏地面切割成一块块凸多边形,只保留角色可以站立的区域,其余全部剔除。
导航网格不是一张贴图,而是一份结构化数据:每个多边形记录顶点坐标、相邻关系、通行代价。Godot 使用 NavigationMesh 资源保存这份数据,通过 NavigationRegion3D 节点挂进场景;寻路时引擎在路网上执行 A* 搜索——就像导航 App 在路网上寻找最快路线,而不是在卫星照片上硬走。
网格的精细度直接影响性能与手感:格子太粗,角色会“穿墙”或绕远路;格子太细,烘焙时间暴涨。多边形的权重(cost)代表路段的通过难度,例如泥地、陡坡的权重高,寻路会自动绕开。一条路径 P 的总代价,就是每一段代价的累加:
有了“可骑行地图”,骑手才知道哪条路省钱省力。导航网格就是游戏 AI 的第一块地基:它不负责做决定,只负责回答“哪里能走”。地图越准,后面所有的导航与决策才不会跑偏。
- 路径 P一条从起点到终点的路线,由 $n$ 条路段拼接而成。
- 路段权重 $w_i$第 $i$ 段路的通行难度系数,泥地高、柏油路低。
- 路段距离 $d_i$第 $i$ 段路的几何长度,由两端点坐标求得。
- 总代价累加每段路的“权重 × 距离”逐项相加,得到整条路径的代价。
导航服务器烘焙
地图不是骑手自己画的,而是测绘公司的扫描车一点点扫出来的。在 Godot 里,NavigationServer3D 就是那家测绘公司:你把场景里的地形、墙体、平台交给它,它会自动分析哪些表面“可站立”,最终输出一份导航网格。
这个过程叫烘焙(Baking)。Godot 读取 MeshInstance3D 等几何体,再结合代理半径(agent radius)、最大爬坡角(max climb)、单元尺寸(cell size)等参数,把高低起伏的地形“翻译”成一层平整、连续、可查询的可行走网格。烘焙结果挂在 NavigationRegion3D 节点上,多个 Region 可以拼成一张完整的导航地图。
烘焙时引擎会逐面片检查坡度——太陡的坡不能走:
测绘车不是只扫一次。场景改了,可以用 NavigationRegion3D 局部重烘焙;突然出现的路障,则用 NavigationObstacle 动态绕行。记住:烘焙质量决定骑手体验,参数调得好,导航才顺畅。
- 面片法向量 $\vec{n}$地形小面片的朝向,垂直于该表面,如同屋顶的斜坡指向。
- 竖直向量 $\vec{u}$世界坐标的“上”方向。
- 夹角 $\theta$面片与水平面的夹角,用点积公式求得。
- 可走判定夹角小于等于最大爬坡角才保留为可行走区域。
NavigationAgent
光有地图,人不会自己认路。骑手还要掏出手机导航 App,输入目的地,App 规划路线、逐段播报。NavigationAgent3D 就是挂在角色身上的导航 App:它不替你骑车,但负责“接下来往哪走”。
Agent 是一个 Node 节点,通常挂在 CharacterBody3D 下面。你只需要设置 target_position,它就会向 NavigationServer 查询路径,然后通过 get_next_path_position() 把“下一个路点”交给你。角色不需要理解整条路线,只需像听导航一样逐段执行,走到一个路点再取下一点。
Agent 还会给出期望速度:方向指向下一个路点,大小不超过移动上限:
导航 App 只负责播报,真正拧油门的是骑手。Agent 也一样:它给出速度建议和路径信息,但最后驱动身体的还是物理节点。把“大脑”和“手脚”分开,AI 才灵活可控、便于替换。
- 当前位置 $\vec{p}_{\text{current}}$角色当前在世界空间中的坐标。
- 下一个路点 $\vec{p}_{\text{next}}$从 Agent 的路径队列中取出的下一站坐标。
- 方向向量路点坐标减去当前位置,得到“指向目标”的向量。
- 单位化除以其模长,变成长度为一的方向向量,只保留朝向。
- 乘最大速度方向乘以 $v_{\max}$,得到大小封顶的期望速度。
GDScript集成
导航 App 播报完“前方右转”,骑手还得真正拧车把、打转向灯。游戏里这套“神经”就是 GDScript:它把 Agent 的决策与 CharacterBody3D 的物理身体连接起来,让“想法”变成“动作”。
标准写法是:在 _physics_process 中先从 Agent 取路点、计算速度,把结果赋给 velocity,再调用 move_and_slide() 推动身体移动;同时用信号监听“到达目标”“路径更新”等事件,和动画状态机联动——移动时播放跑步动画,停下时播放待机动画。
实际运行时,最终速度往往来自多个来源的叠加:导航给出的期望速度,加上避障系统的修正速度,合成为最终速度,再按物理帧时长积分成新位置:
大脑发指令、身体执行、动画反馈,三者通过 GDScript 的节点树与信号协作。就像骑手“看导航→拧油门→车身响应”,每一步都闭环,AI 角色才显得真实、灵活。
- 导航速度 $\vec{v}_{\text{nav}}$Agent 根据路径给出的推荐前进速度。
- 避障修正 $\vec{v}_{\text{avoid}}$RVO 避障算出的偏移量,避免与同伴或障碍重叠。
- 速度合成两者相加,得到最终执行速度。
- 位置积分用最终速度乘物理帧时长,更新坐标。
有限状态机
骑手的一天其实就那么几个状态:等单、取餐、送餐、休息。再忙也逃不出这几件事来回切换。把 AI 行为也这样“分格”管理,就是有限状态机(Finite State Machine,FSM)。
FSM 把行为拆成有限个互斥状态:idle(待机)、patrol(巡逻)、chase(追击)、attack(攻击)。每个状态只负责自己的行为逻辑——巡逻时沿路点走,追击时朝玩家移动——状态之间用转移条件切换,例如“玩家进入视野 → 进入追击”,“玩家丢失 3 秒 → 回到巡逻”。
Godot 没有内置 FSM 节点,但可以用几个 Node 轻松搭建:每个状态一个节点,公共的“状态机”节点维护当前状态和转移表。转移逻辑可以抽象成一个纯函数:
状态机像骑手的排班表,把复杂的日常压缩成几个格子,每个格子只做一件事。游戏里绝大多数 NPC 用 FSM 就够:简单、直观、好调试,是独立游戏性价比最高的 AI 起点。
- 当前状态 $S_t$$t$ 时刻角色所处的行为状态,如“巡逻”。
- 触发事件 $E$会引发状态改变的外部信号或条件,如“发现玩家”。
- 转移函数 $\delta$根据当前状态与事件,查表得到下一状态。
- 下一状态 $S_{t+1}$转移完成后进入的新状态。
行为树编排
老骑手的车上贴着一张决策手册:“下雨先穿雨衣;订单超时优先送急单;单量少就去商圈蹲单。”这种“如果……就……否则……”的分层决策结构,就是行为树(Behavior Tree)。
行为树是一棵从上往下“咨询”的树:根节点逐级下探,叶子节点是具体动作或条件判断。组合节点决定遍历方式——序列节点要求全部子节点成功才算成功,选择节点只要有一个成功就返回成功。例如“追击敌人”这个序列:先“检查距离条件”(成功)→ 再“朝敌人移动”(成功)→ 最后“播放攻击动画”(成功),全部完成才算真成功。
装饰节点负责包装子节点,添加“限时”“取反”“重试”等规则;黑板(Blackboard)则是共享数据区,让整棵树随时读写“敌人位置”“血量”等变量。数学上,序列与选择的逻辑分别就是“与”和“或”:
决策手册让骑手碰到任何情况都不慌。行为树特别适合 Boss 战、队友 AI 这类复杂角色:招式组合、阶段变化、救援条件都能直观编排,改起来比状态机更灵活,也更容易让策划读懂。
- 子节点结果 $R_i$第 $i$ 个子节点执行后的结果,成功记 1,失败记 0。
- 序列“与”逻辑$\bigwedge$ 表示全部成功才算成功。
- 选择“或”逻辑$\bigvee$ 表示有一个成功即成功。
- 短路执行序列遇到失败立即返回,选择遇到成功立即返回,不再评估后续节点。
群体AI协同
晚高峰十字路口,几十个骑手同时穿行,没有交警却很少相撞——因为大家遵守默契:别贴太近、跟着人流方向、别掉队。这套默契写进代码就是群体 AI(Flocking)。
经典 Boids 模型的三大向量:分离(Separation)推开过近的同伴,对齐(Alignment)匹配周围的平均方向,聚拢(Cohesion)引向群体中心。三者加权求和,得到角色每一帧的最终移动方向:
Godot 的 NavigationServer3D 原生支持 RVO(速度障碍)避障:给 Agent 设置半径后,多个 Agent 之间会自动互相避让,无需手写碰撞判断。编队时还能给每个成员加一个相对“队长”的偏移量,实现整齐阵列。简单规则涌现复杂智慧——几百个骑手遵守三条默契,城市街道就流动起来了。
- 分离向量 $\vec{v}_{\text{sep}}$把过近同伴往外推的力,防止重叠。
- 对齐向量 $\vec{v}_{\text{align}}$趋向周围同伴的平均方向,保持步调一致。
- 聚拢向量 $\vec{v}_{\text{coh}}$引向群体几何中心,避免掉队。
- 加权合成三个权重 $w_s$、$w_a$、$w_c$ 控制侧重,求和得到最终速度。
AI 应用场景
独立游戏《小镇夜巡》NPC 智能
阶段:游戏 AI 行为设计
解决的问题:传统做法靠大量手写 if-else,NPC 一多逻辑就乱成一团;用导航网格加有限状态机,让每个居民拥有“回家→巡逻→睡觉”的日常节奏,还能对玩家入侵做出反应,代码量却少了一半。
具体案例:一款像素风小镇游戏里,30 个 NPC 各自拥有巡逻路径与作息状态。玩家深夜闯入商店时,守卫会从“巡逻”切换到“追击”,丢失目标 3 秒后进入“搜索”,再回到“巡逻”。全程由 NavigationAgent 提供路径、FSM 管理切换,角色行为自然流畅。
动作游戏 Boss 战行为编排
阶段:战斗 AI 决策
解决的问题:Boss 需要多种攻击招式组合与阶段切换,状态机嵌套会让代码爆炸;行为树把“选招式→前摇→攻击→后摇”组织成可复用的决策树,程序与策划都能看懂、能调整。
具体案例:一款横版动作游戏 Boss“铁壁将军”:血量高于 60% 时用序列节点执行“重锤三连击”;血量在 30%~60% 时用选择节点在“冲锋”与“投枪”间随机;低于 30% 时用限时装饰器强制进入狂化状态,每 5 秒必须释放一次大招。
城市交通流人群模拟
阶段:群体智能模拟
解决的问题:模拟几百个行人或车辆同时过路口时,两两互碰会让队伍卡死;RVO 避障加上分离、对齐、聚拢三规则,让群体自然分流,呈现真实的流动感。
具体案例:一个城市规划演示项目用 Godot 生成十字路口人群:200 个 NavigationAgent 从四个方向横穿马路,开启 RVO 后约 30 秒内全部安全通过;关闭 RVO 后,行人会在路口中央互相推挤,队伍彻底堵死。这一结果直接用于验证红绿灯配时方案。
Python 演示
import heapqimport math # 迷你城市地图:0 可通行,1 障碍MAP = [ [0, 0, 0, 0, 1, 0, 0, 0], [1, 1, 0, 0, 1, 0, 1, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 1, 1, 1, 0, 0, 1, 0], [0, 1, 0, 0, 0, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],] def astar(start, goal): """A* 寻路:简化版导航网格路径查询""" rows, cols = len(MAP), len(MAP[0]) def heuristic(a, b): return math.hypot(a[0] - b[0], a[1] - b[1]) open_set = [(0, start)] came_from = {} g_score = {start: 0} while open_set: _, cur = heapq.heappop(open_set) if cur == goal: path = [] while cur in came_from: path.append(cur) cur = came_from[cur] path.append(start) return path[::-1] for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1), (-1, -1), (-1, 1), (1, -1), (1, 1)]: nx, ny = cur[0] + dx, cur[1] + dy if 0 <= nx < rows and 0 <= ny < cols and MAP[nx][ny] == 0: nxt = (nx, ny) step_cost = 1.414 if dx != 0 and dy != 0 else 1.0 new_cost = g_score[cur] + step_cost if new_cost < g_score.get(nxt, float('inf')): g_score[nxt] = new_cost came_from[nxt] = cur heapq.heappush(open_set, (new_cost + heuristic(nxt, goal), nxt)) return [] class Courier: """骑手状态机:等单 -> 派送 -> 完成""" def __init__(self, start, goal): self.pos = start self.goal = goal self.route = astar(start, goal) self.progress = 0 self.state = "等单" def update(self): if self.state == "等单" and self.route: self.state = "派送" elif self.state == "派送": if self.progress < len(self.route): self.pos = self.route[self.progress] self.progress += 1 else: self.state = "完成" return self.state if __name__ == "__main__": courier = Courier((0, 0), (5, 7)) print("规划路线:", courier.route) while courier.state != "完成": print("状态:", courier.state, "位置:", courier.pos) courier.update() print("最终:", courier.state, "位置:", courier.pos)
def astar(start, goal):实现 A* 寻路,等价于 Godot 导航服务器在 NavigationMesh 上查询路径。open_set = [(0, start)]优先队列,以“已走代价 + 预估代价”排序,保证每次都先探索最有希望的方向。step_cost = 1.414 if dx != 0 and dy != 0 else 1.0斜向移动代价约 1.414,直向为 1,模拟不同路段的通行权重。if new_cost < g_score.get(nxt, float('inf')):只有找到更短的路径才更新记录,确保算法收敛到最优解。self.pos = self.route[self.progress]状态机“派送”状态下,沿规划路线逐格更新位置,对应 Agent 逐段路点跟随。while courier.state != "完成":主循环持续驱动状态机,直到骑手送达,对应 Godot 每帧的_physics_process更新。
常见误区
误区我只要在 Godot 里烘焙好导航网格,角色就会自己动起来找路,什么都不用写了。
正解导航网格只是“地图”——它只回答“哪里能走”;问路、走路还要靠 NavigationAgent 查询路径,再靠 CharacterBody3D 的物理驱动真正移动。就像手机地图 App 不等于你会骑电动车:地图负责告诉路,骑车还得自己握把。
为何常见:很多教程演示“烘焙完就能寻路”时省略了 agent 和移动代码,新手误以为导航网格一步到位。
误区设置了 `target_position`,角色就应该自动走向目标,不需要我再写移动逻辑。
正解Agent 只负责查询路径并给出推荐速度,真正移动必须由你在 _physics_process 中把速度交给 move_and_slide() 执行。导航 App 播报“左转”,但拧车把的永远是骑手本人。
为何常见:Agent 翻译成“代理”,直觉上像“替身”,让人觉得它能替角色做所有事。
误区导航网格是静态数据,场景一改就得重新烘焙整张地图,开销巨大。
正解可以用 NavigationRegion3D 做局部更新,动态障碍用 NavigationObstacle 或 RVO 避障处理,完全不必全图重烘焙。骑手发现某段路临时施工,绕一下就够,没必要重绘整张城市地图。
为何常见:教程多演示静态封闭地图,让人以为导航只能整体烘焙、整体替换。
误区我用 if 判断敌人距离也能切换行为,何必学状态机?
正解状态机把“条件判断”和“行为执行”解耦:每个状态是独立封装的对象,转移由事件触发,逻辑清晰、可复用、易调试。if-else 像把所有菜谱挤在一张小纸片上,状态机则是分门别类的菜谱档案夹。
为何常见:小 Demo 里 if-else 确实能跑,但 NPC 一多、状态一交错就会失控,新手往往没踩过这个坑。
误区Godot 没内置行为树节点,所以必须装 LimboAI 之类的插件,否则做不了复杂 AI。
正解行为树的核心只是一套节点协议——序列、选择、装饰器、叶子动作,用 GDScript 几十行就能自实现;插件只是提供可视化编辑器与调试面板,属于“好用”而非“必需”。
为何常见:大家看到教程都用插件,误以为功能必须靠第三方完成,反而忽略了行为树本身的原理。
面试题
Q1. Godot 中用哪种数据结构来表示角色可站立的地面区域?
- A.NavigationMesh 导航网格← 正确答案
- B.Voxel 体素云数据
- C.Point 点云地图数据
- D.Height 高度图数据
答案:A。NavigationMesh(导航网格)把可行走地面切成多边形,是 Godot 导航的基础。体素云、点云、高度图是其他领域的概念,正如地图 App 用的是“路网图层”而非“卫星正射图”。
Q2. NavigationServer3D 烘焙导航网格时,以下哪项是核心输入?
- A.角色的贴图素材
- B.场景的几何体← 正确答案
- C.背景的音效文件
- D.玩家的输入数据
答案:B。烘焙把 MeshInstance3D 等场景几何体解析为可行走网格,贴图、音效、输入都与地图生成无关。就像测绘车扫描的是道路本身,而不是路灯上的广告。
Q3. NavigationAgent3D 的 target_position 属性主要用来做什么?
- A.设置角色的朝向
- B.设置目的地坐标← 正确答案
- C.设置相机视角
- D.设置动画状态
答案:B。target_position 告诉 Agent“要去哪里”,Agent 会查询路径并返回路点。朝向、相机、动画分别由其他模块负责,就像导航 App 只管目的地,不替骑手打转向灯。
Q4. 在 Godot 中让 AI 角色移动到目标点,正确的流程包含哪些步骤?
- A.获取 Agent 的路径路点← 正确答案
- B.计算期望速度的方向← 正确答案
- C.使用瞬间传送代替移动
- D.调用 move 方法驱动身体← 正确答案
答案:ABD。正确流程是“获取路点→计算速度→物理驱动”三步。瞬间传送不叫移动,还跳过了避障与动画。就像骑手要“看导航→踩油门→过路口”,而不能直接瞬移到下一单客户门口。
Q5. 有限状态机中,从一个状态切换到另一个状态需要什么?
- A.转移条件← 正确答案
- B.随机数值
- C.玩家按键
- D.网络同步
答案:A。状态切换由转移条件(事件或判断)触发,随机数、玩家按键、网络都不是必要条件。就像骑手从“送餐”切到“休息”,是因为“订单送达”这个条件成立,而不是抛硬币决定的。
Q6. 行为树中常见的组合节点类型有哪些?
- A.序列节点← 正确答案
- B.选择节点← 正确答案
- C.装饰节点← 正确答案
- D.渲染节点
答案:ABC。序列、选择、装饰器是行为树三大控制节点;渲染节点属于图形管线,与决策无关。就像骑手手册里只有三种句式:按顺序做、二选一、加条件重试。
Q7. 群体 AI 的三大经典规则不包括以下哪项?
- A.分离规则
- B.对齐规则
- C.聚拢规则
- D.隐身规则← 正确答案
答案:D。Boids 三规则是分离、对齐、聚拢;隐身属于渲染效果,与群体移动无关。骑手之间的默契是“别撞、同向、别掉队”,没有一条是“隐身”。
Q8. 在 Godot 中开启 RVO 避障后,NavigationAgent3D 的 velocity 属性返回什么?
- A.角色当前的物理速度
- B.避障后的推荐速度← 正确答案
- C.服务器生成的随机速度
- D.玩家输入的原始速度
答案:B。启用 RVO 后,velocity 返回的是融合避障修正的推荐速度,角色按它驱动即可避开其他 Agent。它既不是当前物理速度,也不是随机值。类似导航 App 播报“前方拥挤,建议减速变道”。
Q9. 下列关于 Godot 导航网格权重(cost)的说法,正确的是?
- A.权重会影响路径的选择← 正确答案
- B.权重会调度算力资源
- C.权重值越大越容易被选
- D.权重值越小越容易被选← 正确答案
答案:AD。代价(权重)越小表示“越好走”,寻路比较总代价 Σ(w × d),最小者胜出。权重不影响算力调度。就像骑手会选“路程短且好走”的路,而不是表面上近却堵车的路。
Q10. 行为树中装饰器节点的主要作用是什么?
- A.改变子节点执行规则← 正确答案
- B.存储角色世界坐标
- C.播放角色动作动画
- D.烘焙导航网格地图
答案:A。装饰器不执行具体行为,而是包装子节点、改变执行方式(取反、限时、重试、限次)。坐标存储、动画播放、网格烘焙都另有其责。它像骑手手册里的“加粗红字”:不改变任务本身,却改变任务被对待的方式。