想象你住在幸福里社区。每天傍晚七点,六十位阿姨准时出现在中心广场,没有导演、没有对讲机,领舞李阿姨往台前一站,音乐响起,大家自动散开成方阵跳起来。神奇的是:没人喊“向左三步、向右两步”,队伍却始终整整齐齐;有人踩到水坑会绕开,有人掉队会小跑补位,队形变换丝毫不乱。她们靠的不是中央指挥,而是三条刻在肌肉记忆里的默契:别踩到旁边人的脚、跟着身边人的方向、别离人群太远。这种“让大量个体用极简规则自发形成有序集体”的思路,就是游戏开发中大名鼎鼎的群体AI(Swarm AI)。在《星际争霸》里框选两百个士兵一键推进,在塔防游戏里看着上千只怪物沿着多条道路涌向基地,背后正是这套“广场舞学问”:Boids 群集算法管“怎么聚成一群”,编队阵型管“站成什么形状”,流场寻路管“跟着地面的箭头走”,再配上空间哈希这类优化手段,让几百个单位在手机上也能跑出 60 帧。

岗位关联

对 AI 从业者而言,掌握群体AI与编队技术,意味着你能为 RTS、塔防、开放世界等大规模 NPC 场景设计出既高效又生动的集体行为,这是游戏 AI 工程师的核心竞争力之一。

目录
  1. 可视化演示
  2. 群体AI概述:从“指挥千人”到“唤醒默契”
  3. Boids群集:广场舞的三条默契
  4. 编队阵型:给每个位置发一张“站位卡”
  5. 阵型保持:站准位置,又不撞车
  6. 流场寻路:地面画好箭头,跟着走
  7. 行为融合:什么时候听谁的
  8. 性能优化:百人团的分片管理
  9. 实战案例:RTS 与塔防中的落地
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

Boids三规则:分离(避免拥挤) + 对齐(速度同步) + 聚合(趋向中心) | 编队:槽位跟随+弹簧阻尼 | 流场:共享方向场+O(n)复杂度

群体AI概述:从“指挥千人”到“唤醒默契”

老式节日游行需要总导演拿着对讲机,逐个方阵下达指令,一旦台上的人喊错拍子,全场就乱套。幸福里舞团却完全不同——领舞李阿姨从不逐个人指挥,她只负责把动作跳标准,其余阿姨各自看着身边三五个人调整脚步。这就是群体AI(Swarm AI)的核心思想:放弃中央大脑,把智能下沉到每一个个体。

在游戏里,“对讲机总导演”对应的是传统的单智能体控制:每个 NPC 由行为树(Behavior Tree)或状态机精细驱动。当单位少,这很有效;可当单位多到几百上千,逐个指挥的成本会爆炸式增长,而且“整齐划一的集体感”极难用中央控制写出来——没人能给三百个士兵逐帧写脚本。群体AI 换了个思路:每个单位只感知邻居,执行几条简单规则,整体的行军、分散、包抄等复杂行为便自然涌现(Emergence)出来。

从数学上看,单位 i 最终受到的“合力”是它对邻域内所有邻居的交互函数之和:

就像阿姨们从不听总指挥、只观察身边几米的人,广场舞的整齐不是“管出来”的,而是“长出来”的。群体AI 的工程价值正在于此:用接近线性的算力,换来千军万马的生动秩序。

Fi=∑j∈N(i)ϕ(pi,pj,vi,vj)\displaystyle \mathbf{F}_i = \sum_{j \in \mathcal{N}(i)} \phi(\mathbf{p}_i, \mathbf{p}_j, \mathbf{v}_i, \mathbf{v}_j)
这个公式说明:群体行为不是某个统帅设计出来的,而是“每个人只回应身边人”这一朴素事实的数学总和。
  1. 个体合力 $\mathbf{F}_i$单位 i 最终受到的推动力,是所有局部交互的总和,相当于阿姨感受到周围所有舞伴施加的“牵绊力”叠加。
    Fi\displaystyle \mathbf{F}_i
  2. 邻域集合 $\mathcal{N}(i)$只统计感知半径内的邻居,远处的舞伴不影响你,这是“局部感知”的数学表达。
    N(i)\displaystyle \mathcal{N}(i)
  3. 交互函数 $\phi$描述一对个体如何互相影响——靠太近要分开、方向偏离要修正等。
    ϕ(pi,pj,vi,vj)\displaystyle \phi(\mathbf{p}_i, \mathbf{p}_j, \mathbf{v}_i, \mathbf{v}_j)
  4. 位置与速度 $\mathbf{p}, \mathbf{v}$交互只看对方“在哪里、往哪走”,信息完全局域,没有全局透视线。
    pi,pj,vi,vj\displaystyle \mathbf{p}_i, \mathbf{p}_j, \mathbf{v}_i, \mathbf{v}_j

Boids群集:广场舞的三条默契

1986 年,计算机图形学家 Craig Reynolds 发明了 Boids 算法,用来模拟鸟群、鱼群。他把复杂的群体行为拆成了三条极其简单的规则,恰好就是幸福里舞团阿姨们心照不宣的三条默契。Boids 这个单词念作“博伊兹”,取 Bird-oid(像鸟的东西)之意,如今是所有群体AI 教材的开篇第一课。

第一条默契叫分离(Separation):别踩到旁边人的脚。每个单位都会检查邻居,一旦有人靠得太近,就施加一个“推开”的力,让彼此保持最小距离。第二条叫对齐(Alignment):跟着身边人的方向。每个人把邻居们速度的平均方向当作参考,悄悄修正自己的朝向,于是整群人会朝同一个方向移动。第三条叫凝聚(Cohesion):别掉队。每个单位被拉向邻居们的“质心”——也就是人群的中心,谁落单了就会被群体“吸”回来。

三条力按权重叠加,就得到了每个单位的最终转向力:

Boids 的效果就像舞团阿姨们:没人发号施令,音乐一响,一群人自然而然地聚成一个流动的整体,忽而散开、忽而聚拢,却始终不散架。不过要注意,Boids 产生的是“液态”的松散集群,离“站成方阵”还差一步——那就要靠编队阵型了。

Fboid=ws⋅Fsep+wa⋅Falign+wc⋅Fcoh\displaystyle \mathbf{F}_{boid} = w_s \cdot \mathbf{F}_{sep} + w_a \cdot \mathbf{F}_{align} + w_c \cdot \mathbf{F}_{coh}
类比:分离力是“躲开”,对齐力是“跟上”,凝聚力是“归队”。权重决定了这个团体是松散的散步团还是紧凑的阅兵队。
  1. 分离力 $\mathbf{F}_{sep}$方向由最近邻居指向自己,距离越近力越强,像阿姨慌忙往旁边挪半步。
    Fsep\displaystyle \mathbf{F}_{sep}
  2. 对齐力 $\mathbf{F}_{align}$取邻域内所有舞伴速度的平均方向,帮助自己把朝向修正到集体方向。
    Falign\displaystyle \mathbf{F}_{align}
  3. 凝聚力 $\mathbf{F}_{coh}$指向邻居质心的吸引力,把掉队的人拉回人群。
    Fcoh\displaystyle \mathbf{F}_{coh}
  4. 权重 $w_s, w_a, w_c$三条力的“话语权”,调大某一项就相当于改变整个团体的性格——分离权重高则松散,凝聚权重高则扎堆。
    ws:wa:wc\displaystyle w_s : w_a : w_c

编队阵型:给每个位置发一张“站位卡”

舞团要在社区文艺汇演上表演,光靠三条默契只能跳成“一窝蜂”。于是李阿姨拿出一叠“站位卡”:每个人领到一张卡片,上面写着“第二排左边第三个”“第三排中间”——这就是编队阵型(Formation)的雏形。在游戏里,“站位卡”就是槽位偏移(Slot Offset),“李阿姨的位置”就是锚点(Anchor)。

阵型模板是一组槽位偏移的集合:方阵、圆阵、箭头阵、楔形阵,本质都是“相对锚点的坐标表”。每个单位每帧做两件事:先拿到锚点的世界坐标,再把自己的槽位偏移加上去,得到本帧应该站到的目标点。队伍转弯时,偏移坐标还要跟着锚点的朝向旋转,否则队伍就会“横着走”。

有了站位卡,舞团想换队形时,只需重新发卡——游戏里就是切换阵型模板,方阵瞬间变圆阵。但站位卡只回答了“该站哪”,还没回答“怎么站过去又不撞人”,这就轮到下一节出马了。

Pslot=Panchor+R(θ)⋅Oslot\displaystyle \mathbf{P}_{slot} = \mathbf{P}_{anchor} + \mathbf{R}(\theta) \cdot \mathbf{O}_{slot}
这句话翻译成人话就是:你的站位 = 领舞站的位置 + 你那张站位卡上的相对位置(并随队伍转向而转动)。
  1. 槽位目标点 $\mathbf{P}_{slot}$单位这一帧应该站到的世界坐标,即“站位卡”落地后的世界位置。
    Pslot\displaystyle \mathbf{P}_{slot}
  2. 锚点位置 $\mathbf{P}_{anchor}$阵型的基准点,通常是领队单位或队伍质心的实时位置,全队都跟着它走。
    Panchor\displaystyle \mathbf{P}_{anchor}
  3. 旋转矩阵 $\mathbf{R}(\theta)$队伍整体转向角度 θ 时,偏移随之旋转,保证队形朝向与行进方向一致,就像阿姨转身时整排人跟着转向。
    R(θ)\displaystyle \mathbf{R}(\theta)
  4. 槽位偏移 $\mathbf{O}_{slot}$单位在队伍坐标系里的相对位置,阵型模板就是一张这样的偏移表。
    Oslot\displaystyle \mathbf{O}_{slot}

阵型保持:站准位置,又不撞车

阿姨们拿到站位卡,目标点清清楚楚。但广场上不是真空的:前面有人挡路,旁边有人也在往自己的点走。如果每个人都“直线硬冲”自己的槽位,队伍还没站好就撞成一团。解决办法是把“归位”想象成一根看不见的弹簧:你偏离目标越远,弹簧拉你的力越大;同时弹簧还带着阻尼,防止你在目标点附近来回晃悠。

阵型保持的经典做法是 PD 控制(比例-微分控制),它产生一个“回拉力”:

弹簧模型让每个单位既能“站准”,又能“顺滑”。但现实总有不讲理的时候——正前方突然出现一个水坑,或者一辆电动车横穿广场。这时候“归队”得给“保命”让路,于是我们需要一套规则来决定:什么时候听谁的。

Fform=kp⋅(Ptarget−Punit)+kd⋅(Vtarget−Vunit)\displaystyle \mathbf{F}_{form} = k_p \cdot \big( \mathbf{P}_{target} - \mathbf{P}_{unit} \big) + k_d \cdot \big( \mathbf{V}_{target} - \mathbf{V}_{unit} \big)
可以理解为:第一个括号是“你站偏了多少”,第二个括号是“你冲得多快”。前者负责拽回来,后者负责别冲过头。
  1. 位置误差 $\mathbf{P}_{target} - \mathbf{P}_{unit}$当前站位与目标点的偏差,偏差越大,回拉力越大,就像弹簧拉得越长越用力。
    Ptarget−Punit\displaystyle \mathbf{P}_{target} - \mathbf{P}_{unit}
  2. 位置增益 $k_p$弹簧的刚度,调大队伍更“整”但也更僵硬,调小则拖泥带水。
    kp\displaystyle k_p
  3. 速度误差 $\mathbf{V}_{target} - \mathbf{V}_{unit}$目标点移动速度与单位速度的差,提供阻尼,避免单位带着惯性冲过目标。
    Vtarget−Vunit\displaystyle \mathbf{V}_{target} - \mathbf{V}_{unit}
  4. 阻尼系数 $k_d$控制“刹车力度”,防止阿姨在点周围前后抖动,参数过大过小都会让队伍看起来“神经质”。
    kd\displaystyle k_d

流场寻路:地面画好箭头,跟着走

地铁站里,地面贴着花花绿绿的导流箭头:进站的、出站的、换乘的,人流浩荡却极少相撞,因为每个人只需低头看脚下的箭头,不必掏出手机重新规划路线。流场寻路(Flow Field Pathfinding)正是把这种智慧搬进了游戏:与其让几百个单位各自做 A* 寻路(等于人手一个导航仪),不如预先在整个地图上画好一张“箭头图”。

制作这张箭头图分两步。第一步,从目标点出发,像水波一样向外扩散,计算每个格子到达目标的代价场(Cost Field)——绕障碍多走一步,代价就高一点。第二步,站在每个格子上,环顾周围邻居,指向“代价最低的那个格子”的方向,就是这个格子的流场箭头。此后,任何单位走到某个格子,只需查表取箭头方向,跟着走即可。

流场最大的好处是共享:一千只怪物共用同一张箭头图,每帧只做一次查表,成本几乎可以忽略。如果某个目标点变化(比如塔防里老家换了位置),只需重新扩散一次代价场,所有怪物自动改道——相当于地铁站连夜重贴了一遍地面箭头。

D(x)=x∗−x∥x∗−x∥,x∗=arg⁡min⁡n∈N(x)C(n)\displaystyle \mathbf{D}(\mathbf{x}) = \frac{ \mathbf{x}^{*} - \mathbf{x} }{ \lVert \mathbf{x}^{*} - \mathbf{x} \rVert }, \qquad \mathbf{x}^{*} = \arg\min_{\mathbf{n} \in \mathcal{N}(\mathbf{x})} C(\mathbf{n})
类比:每个格子的箭头 = 指向“周围代价最低的格子”的那个方向。单位是乘客,流场是地铁站的地面箭头。
  1. 代价场 $C(\mathbf{n})$每个格子存一个数,表示“从这里到目标要付出多大代价”,障碍越多代价越高,从目标点向外扩散计算。
    C(n)\displaystyle C(\mathbf{n})
  2. 邻居集合 $\mathcal{N}(\mathbf{x})$当前格子周围的 8 个相邻格子,查表只比较这几个。
    N(x)\displaystyle \mathcal{N}(\mathbf{x})
  3. 最小代价邻居 $\mathbf{x}^{*}$在邻居里选出代价最低的那个,它是“下一步最划算”的方向。
    x∗=arg⁡min⁡n∈N(x)C(n)\displaystyle \mathbf{x}^{*} = \arg\min_{\mathbf{n} \in \mathcal{N}(\mathbf{x})} C(\mathbf{n})
  4. 归一化方向 $\mathbf{D}(\mathbf{x})$把“指向最佳邻居”的向量缩成单位长度,作为流场箭头存进格子。
    D(x)=x∗−x∥x∗−x∥\displaystyle \mathbf{D}(\mathbf{x}) = \frac{ \mathbf{x}^{*} - \mathbf{x} }{ \lVert \mathbf{x}^{*} - \mathbf{x} \rVert }

行为融合:什么时候听谁的

阿姨们跳舞时,同时要“跟上队形”“避开洒水车”“踩准音乐节拍”,这些指令经常互相打架。如果她把三个方向的力平均一下,结果就是原地打转、寸步难行。游戏里的 NPC 更甚:避障力、队形力、流场力、攻击意图同时压过来,谁说了算?答案是优先级仲裁(Priority Arbitration)——危急时刻,低优先级行为直接让位。

最常见的策略是“安全距离门槛”:先看单位与最近障碍物的距离 d。如果 d 小于安全距离,说明马上要撞上了,此时只有避障力生效,其他一切靠后——先保命要紧。如果距离安全,再把队形力、流场力、墙体排斥力按权重混合相加。这种“分支 + 混合”的结构写出来就是:

优先级仲裁让群体在复杂环境中既灵活又整齐:平时队列一丝不苟,遇到突发状况立刻“化整为零”各自规避,危机解除又自动“化零为整”归队。这正是广场舞团让围观群众啧啧称奇的原因——她们从不硬挤,却也从未散场。

Vfinal={Vavoid,d<dsafeVform+Vflow+Vwall,d≥dsafe\displaystyle \mathbf{V}_{final} = \begin{cases} \mathbf{V}_{avoid}, & d < d_{safe} \\[2pt] \mathbf{V}_{form} + \mathbf{V}_{flow} + \mathbf{V}_{wall}, & d \ge d_{safe} \end{cases}
类比:阿姨看到电动车冲过来时绝不纠结“舞步完不完整”,直接闪开;安全了才重新找站位、跟队形。
  1. 安全距离 $d_{safe}$触发避障的阈值,相当于“电动车距离我几米就该紧张”的警戒线。
    dsafe\displaystyle d_{safe}
  2. 危险分支 $\mathbf{V}_{avoid}$当 d 小于阈值,最终速度只由避障力决定,其余行为全部让位。
    Vavoid\displaystyle \mathbf{V}_{avoid}
  3. 安全分支 $\mathbf{V}_{form} + \mathbf{V}_{flow} + \mathbf{V}_{wall}$距离安全时,队形力、流场力、墙体排斥力按权重线性混合,各司其职。
    Vform+Vflow+Vwall\displaystyle \mathbf{V}_{form} + \mathbf{V}_{flow} + \mathbf{V}_{wall}
  4. 状态切换分支条件本身就是一个隐式的状态机,NPC 在“避障态”与“正常态”之间平滑切换。
    {⋯⋯\displaystyle \begin{cases} \cdots \\ \cdots \end{cases}

性能优化:百人团的分片管理

如果舞团的默契规则要求“每个人每时每刻都要知道其他所有人的位置”,60 个阿姨就得互相喊话几千次,广场瞬间变成菜市场。程序里的朴素写法正是如此:N 个单位两两计算距离,复杂度是 O(N²)。300 个单位就要算 9 万次距离,再乘以每帧,性能直接崩溃。

程序员的对策叫空间哈希(Spatial Hashing):把地图切成一个个小格子,每个单位只登记进自己所在的格子;查询邻居时,只检查自己格子和周围相邻的 8 个格子,远处的人根本不用管。这就像广场被网格员分成片区,每个阿姨只需认识本片区的舞伴,不同片区互不干扰。

分片管理让 500 个单位的 Boids 模拟跑得动,远近分级让上千只怪物也不拖垮渲染管线。如果说 Boids 是“阿姨的个人修养”,空间哈希就是“网格员的分管制度”——群体AI 能走向工业化,靠的正是这种工程智慧。

Tnaive=O(N2),Thash=O(N⋅kˉ)\displaystyle T_{naive} = O(N^2), \qquad T_{hash} = O\big( N \cdot \bar{k} \big)
类比:朴素法是全班同学两两互相认识,空间哈希是只认识同桌和前后桌——人际关系瞬间清爽。
  1. 朴素复杂度 $O(N^2)$每单位与其余 N-1 个单位都要算一次距离,总计算量随个体数平方增长。
    Tnaive=O(N2)\displaystyle T_{naive} = O(N^2)
  2. 空间哈希地图划分为网格,单位登记入格,查询只扫本格与相邻格,远处个体完全不参与计算。
    Thash=O(N⋅kˉ)\displaystyle T_{hash} = O(N \cdot \bar{k})
  3. 平均邻居数 $\bar{k}$每个格子平均容纳的单位数,通常远小于 N,这是性能提升的关键变量。
    kˉ\displaystyle \bar{k}
  4. 细节分级(LOD)与批渲染远处怪群降低 AI 更新频率、用简模合批绘制,进一步把帧时间压到 60 帧以内。
    LOD+Batching\displaystyle \text{LOD} + \text{Batching}

实战案例:RTS 与塔防中的落地

舞团学成,终于要上台演出。但演出有两种风格:一种是国庆阅兵式的RTS 编队,追求排面整齐;另一种是春运车站式的塔防怪物潮,追求量大管饱。同样是群体AI,两套需求直接把参数调向了两个极端。

RTS 游戏(如《星际争霸》)里,玩家框选一两百个士兵,命令他们推进。玩家对“队形整齐度”极其敏感,所以这里用锚点跟随 + 队形保持:一个领队算全局路径,其余单位追各自的动态槽位,权重里队形力占大头,视觉上就是“一队人齐刷刷碾过去”。塔防游戏则相反,怪物上千只,没人关心它们站得整不整齐,只关心它们别卡在墙角。于是这里放弃精确编队,改用流场寻路:全场共享一张方向图,怪物各自查表前进,单位之间的避碰用很轻的分离力即可搞定。

两种需求的折中可以用一个混合式表达:

阅兵式要的是“整”,春运要的是“通”。同一条广场舞学问,换个舞台就换套参数——这正是群体AI 在真实项目中魅力所在:规则极简,调参极活,规模极大。

Vunit=Vflow+λ⋅Vform+μ⋅Vavoid\displaystyle \mathbf{V}_{unit} = \mathbf{V}_{flow} + \lambda \cdot \mathbf{V}_{form} + \mu \cdot \mathbf{V}_{avoid}
类比:λ 是“队列纪律”的权重,μ 是“个人避让”的权重——阅兵式调高 λ,春运人流调高 μ。
  1. 流场主导 $\mathbf{V}_{flow}$全局方向由流场提供,保证单位不迷路,这是所有方案的地基。
    Vflow\displaystyle \mathbf{V}_{flow}
  2. 编队系数 $\lambda$RTS 中调大,队形紧凑像阅兵;塔防中调小甚至归零,队伍松散像潮水。
    λ⋅Vform\displaystyle \lambda \cdot \mathbf{V}_{form}
  3. 避障系数 $\mu$控制单位避让邻居与障碍的力度,调大则单位均匀铺开,调小则密集堆叠。
    μ⋅Vavoid\displaystyle \mu \cdot \mathbf{V}_{avoid}
  4. 调参验证先跑通、再压性能、最后微调权重,观察“是否有抖动、是否卡位、是否散架”三个体检指标。
    λ,μ∈[0,1]\displaystyle \lambda, \mu \in [0, 1]

AI 应用场景

场景一:RTS 框选编队推进

阶段:单位决策与路径规划

解决的问题:传统做法让 200 个士兵各自独立寻路,不仅路径交叉拥堵,而且队形散乱、毫无“军队感”。AI 方案用“一个领队算路径 + 全员追槽位”:框选后领队做一次全局寻路(A* 或流场),其余单位通过队形保持力跟随各自的相对槽位,形成整齐方阵。

具体案例:《星际争霸 2》中框选一队机枪兵,命令它们穿过狭窄通道。领队选择绕行路线,其余单位通过阵型保持 + 避障融合,鱼贯通过通道后自动恢复矩形阵,全程无卡死。

场景二:塔防怪物洪流

阶段:大规模寻路与行为融合

解决的问题:上千只怪物如果各自 A* 寻路,CPU 直接爆掉。流场寻路把“逐单位计算”变成“全场预计算一次、所有单位查表”,再叠加轻量分离力防止怪物叠成一座山。

具体案例:《王国保卫战》式塔防中,怪物从多个入口涌向基地。地图预生成多张流场图,怪物按兵种选择不同流场,工程上只需每帧读取当前格子方向 + 施加分离力,即可支撑千级单位同时移动。

场景三:开放世界人群与动物群

阶段:群体行为模拟

解决的问题:开放世界的街道行人、草原兽群需要“活的”集体行为,又不能为每个个体写复杂逻辑。Boids 三规则 + 行为优先级融合,让 NPC 自然分流、结伴、避让,并配合 LOD 分级控制成本。

具体案例:某开放世界游戏中,城市广场刷出数百 NPC:他们自动沿着流场箭头逛街,遇到玩家坐骑会触发避障优先,坐骑离开后重新回归群体行为,观感自然且 CPU 占用极低。

Python 演示

python113 行
import random import numpy as np  class Boid:    """一个广场舞单元:位置、速度、是否领舞。"""     def __init__(self, pos, vel, is_leader=False):        self.pos = np.array(pos, dtype=float)        self.vel = np.array(vel, dtype=float)        self.is_leader = is_leader  def separation(boid, boids, radius=25.0):    force = np.zeros(2)    for other in boids:        if other is boid:            continue        diff = boid.pos - other.pos        dist = np.linalg.norm(diff)        if 0 < dist < radius:            force += diff / dist    return force  def alignment(boid, boids, radius=50.0):    avg = np.zeros(2)    count = 0    for other in boids:        if other is boid:            continue        if np.linalg.norm(other.pos - boid.pos) < radius:            avg += other.vel            count += 1    if count > 0:        avg /= count        return avg - boid.vel    return avg  def cohesion(boid, boids, radius=60.0):    center = np.zeros(2)    count = 0    for other in boids:        if other is boid:            continue        if np.linalg.norm(other.pos - boid.pos) < radius:            center += other.pos            count += 1    if count > 0:        center /= count        return center - boid.pos    return center  def seek(boid, target, speed=2.5):    desired = target - boid.pos    dist = np.linalg.norm(desired)    if dist < 1e-6:        return np.zeros(2)    return desired / dist * speed - boid.vel  def make_slots(rows, cols, spacing):    slots = []    for r in range(rows):        for c in range(cols):            slots.append(np.array([                (c - cols / 2.0 + 0.5) * spacing,                (r - rows / 2.0 + 0.5) * spacing,            ]))    return slots  def update(boids, dt=0.1):    for b in boids:        force = 1.0 * separation(b, boids)        force += 0.8 * alignment(b, boids)        force += 0.4 * cohesion(b, boids)        b.vel += force * dt        speed = np.linalg.norm(b.vel)        if speed > 4.0:            b.vel *= 4.0 / speed        b.pos += b.vel * dt        b.pos %= 500.0  def main():    random.seed(7)    boids = [        Boid([random.uniform(200, 300), random.uniform(200, 300)],             [random.uniform(-1, 1), random.uniform(-1, 1)])        for _ in range(45)    ]    boids[0].is_leader = True    slots = make_slots(rows=3, cols=14, spacing=16.0)    anchor = np.array([400.0, 250.0])    for frame in range(120):        update(boids)        boids[0].vel += seek(boids[0], anchor) * 0.1        for unit, slot in zip(boids[1:], slots):            target = anchor + slot            unit.vel += seek(unit, target) * 0.08        if frame % 20 == 0:            leader = boids[0]            dists = [np.linalg.norm(u.pos - leader.pos) for u in boids]            print("frame", frame, "leader", leader.pos.round(1),                  "avg_dist", round(sum(dists) / len(dists), 1))  if __name__ == "__main__":    main()
目的:用 45 个 Boid 演示“先靠三规则聚群,再靠锚点+槽位排成矩形编队”的完整流程,模拟 RTS 框选编队。
  1. force = 1.0 * separation(b, boids)分离力权重 1.0,保证单位不会互相重叠。
  2. force += 0.8 * alignment(b, boids)对齐力权重 0.8,让群体朝一致方向移动。
  3. force += 0.4 * cohesion(b, boids)凝聚力权重 0.4,防止个体掉队。
  4. if speed > 4.0: b.vel *= 4.0 / speed限制最大速度,模拟单位移动上限。
  5. b.pos %= 500.0环形世界:走出边界就从对面回来,省去边界碰撞处理。
  6. boids[0].vel += seek(boids[0], anchor) * 0.1领舞被引向锚点,作为全队的移动基准。
  7. slots = make_slots(rows=3, cols=14, spacing=16.0)生成 3×14 的矩形阵型槽位表。
  8. target = anchor + slot每个单位的目标点 = 锚点 + 自己的槽位偏移(站位卡)。
  9. unit.vel += seek(unit, target) * 0.08跟随单位向自己的槽位点靠近,形成编队。
  10. dists = [np.linalg.norm(u.pos - leader.pos) for u in boids]统计全体与领舞的平均距离,观察编队收敛程度。

常见误区

误区单位越多,行为树就越丰富。把行为树、状态机复制几百份,不就是一个“群体”了吗?每个 NPC 都需要详细的逻辑才能显得智能。

正解行为树适合“小而精”的个体智能,单个单位可以写几百行逻辑;当单位达到数百上千,每帧遍历所有行为树状态机的开销会爆炸,而且用中央脚本很难写出“同一方向齐步走”的集体感。群体AI 把智能下沉到 3~5 条局部规则,用交互涌现队形,算力近似线性。类比:行为树是教练对每个运动员单独指导,群体AI 是阿姨们自己看身边人——效率天差地别。

为何常见:多数教程先教行为树,容易形成思维定势,认为“更智能 = 更多逻辑”,忽略了“局部规则 + 涌现”这条完全不同的路径。

误区把分离、对齐、凝聚的权重调一调,Boids 就能走出方阵、圆阵、箭头阵,毕竟演示视频里它们看起来那么整齐。

正解Boids 产生的是“液态”的松散集群(像鸟群、鱼群),不是“固态”的严格编队。要形成方阵、圆阵、箭头,必须显式定义槽位偏移和锚点,让每个单位“追自己的站位卡”。Boids 管“聚得自然”,编队管“站得整齐”,两者是叠加的两层系统。类比:三条默契只保证大家不撞车、不掉队,但谁站哪个点,还得靠站位卡。

为何常见:Boids 演示里单位确实“成群结队”,外观看上去就是队形,容易误以为松散集群等于严格编队。

误区流场寻路无非是 A* 的加速版,本质还是每个单位独立规划一条路径,只是跑得快一点。

正解恰恰相反。流场寻路把“算路径”从单位身上整体剥离:预先(或低频)从目标点向全地图扩散生成代价场,再为每格存一个“指向低代价邻居”的方向;单位每帧只做一次查表,成本是 O(1) 级别的。逐单位 A* 是“每个人开车时现开导航”,流场是“地铁站提前贴好地面箭头”。

为何常见:习惯了逐单位寻路的实现思路,难以理解“先算一张全场图,再让所有单位共享”这种逆向设计。

误区把避障、队形、寻路、攻击意图全部加权叠加,行为越多越丰富,NPC 自然更聪明。

正解多行为线性叠加会互相“拔河”:队形力拉左,避障力拉右,寻路力拉前,最终单位原地抖动或绕圈。工程上普遍用优先级仲裁——先用安全距离判断是否触发避障,高优先级行为独占,低优先级让位;即使混合,也要保证某一项在局部占主导。类比:阿姨遇到洒水车时,绝不会同时执行“跟上队形”和“避开洒水车”。

为何常见:“叠加”在直觉上很自然,但忽略了不同力的方向冲突,以及“同时服从所有指令=原地不动”的现实。

误区空间哈希提速全靠格子小,格子切得越细,每格单位越少,查询越快,索性把格子切成像素级。

正解格子尺寸必须和感知半径匹配:查 3×3 邻域时,格子边长至少是感知半径的 2 倍,才能保证所有真邻居都落在相邻格里。格子过小会导致真正邻居被漏掉,单位“看不见”身边人,群体瞬间散架;格子过大则每格单位太多,退化回 O(N²)。哈希只是把“全局扫描”变成“局部扫描”,正确率与格尺寸强绑定。

为何常见:直觉认为“粒度越细越精确”,忽略了邻域覆盖对格子尺寸的下限约束,属于典型的过度优化。

面试题

Q1. Boids 算法的三条基本规则是哪三条?

单选题 · easy

  • A.攻击、防守、巡逻规则
  • B.搜索、追逐、撤退规则
  • C.分离、对齐与凝聚规则← 正确答案
  • D.加速、减速与转向规则

答案:C。Boids 的三规则是分离(别踩脚)、对齐(跟方向)、凝聚(别掉队),对应答案 C。A 和 B 是战斗行为,D 是运动学控制,都不属于群体规则层面。

Q2. 群体AI 相比“每个单位一套行为树”的核心优势是什么?

单选题 · easy

  • A.大规模单位算力更低← 正确答案
  • B.每个单位行为更复杂
  • C.完全无需导航网格支持
  • D.完全不需要任何参数调优

答案:A。群体AI 用少量局部规则替代海量个体逻辑,计算量与个体数近似线性增长,适合大规模 NPC。B 恰恰相反,行为树个体逻辑更重;C、D 说法绝对化,群体AI 仍需导航与调参。

Q3. 编队阵型中“槽位(Slot)”指的是什么?

单选题 · mid

  • A.地图上刷怪的固定出生点
  • B.相对锚点的队形相对坐标← 正确答案
  • C.单位自带的朝向角度数值
  • D.领队单位自身血量上限值

答案:B。槽位是单位在队伍坐标系中的相对位置,即“站位卡”,世界坐标 = 锚点 + 槽位偏移。A 是刷怪点概念,C 是朝向,D 是属性,都与编队坐标无关。

Q4. 流场寻路中,每个格子通常存储什么数据?

单选题 · mid

  • A.一条完整的最优路径信息
  • B.地图格子的地形高度值
  • C.指向低代价格子的方向← 正确答案
  • D.单位当前位置的坐标值

答案:C。流场每个格子存的是“推荐移动方向”,来自代价场的梯度。A 是逐单位路径,B 是地形数据,D 是单位属性,都不是流场内容。

Q5. 空间哈希查询邻居时,需要检查哪些格子?

单选题 · mid

  • A.遍历整个地图的所有格子
  • B.本格与周围相邻的格子内← 正确答案
  • C.只检查距离最近的单个格子
  • D.只检查同阵营单位的格子

答案:B。空间哈希把单位登记进所在格,查询邻居只需扫描本格和相邻的 8 个格子,前提是格子尺寸匹配感知半径。A 退化为暴力遍历,C、D 都会漏掉真实邻居。

Q6. 单位同时收到避障力与队形力且方向冲突时,常见仲裁策略是?

单选题 · hard

  • A.两力直接取算术平均
  • B.安全距离内避障有限优先← 正确答案
  • C.队形力永远大于避障力
  • D.每帧随机选择一种力执行

答案:B。工程上普遍采用优先级仲裁:距离小于安全阈值时避障独占,否则才混合队形力等。A 会导致原地抖动,C 会撞上障碍,D 随机性不可控。

Q7. 以下哪些属于流场寻路的特点?

多选题 · mid

  • A.支持上千单位同时寻路← 正确答案
  • B.全场单位共享一张方向图← 正确答案
  • C.每个单位独立计算最短 A*
  • D.代价场可融入地形权重← 正确答案

答案:ABD。流场的核心是“全场共享一张方向图”,单位查表即可,天然支持海量单位;代价场可以加权地形、威胁区。C 是逐单位 A* 的思路,与流场相反。

Q8. 提升大规模 NPC 群体AI 性能的常用手段有哪些?

多选题 · hard

  • A.空间哈希加速邻居查询← 正确答案
  • B.远处单位降低更新频率← 正确答案
  • C.全员改用更高精细模型
  • D.合批绘制减少调用次数← 正确答案

答案:ABD。空间哈希把邻居查询从 O(N²) 降到近似线性;LOD 降频和渲染合批都是标准优化手段。C 高精模型反而增加渲染负担,方向完全错误。

Q9. 群体AI 中所说的“涌现”指的是什么?

单选题 · easy

  • A.预设写死每个个体的全部动作
  • B.局部规则交互产生宏观集体行为← 正确答案
  • C.在线学习自动生成角色动画
  • D.游戏引擎随机生成地图地形

答案:B。涌现指大量个体按简单局部规则交互,整体出现无人预设的复杂集体行为,如鸟群转向、鱼群散开。A 是中央控制,C 是机器学习,D 是程序化生成。

Q10. 关于编队保持的 PD 控制,以下说法正确的有哪些?

多选题 · hard

  • A.位置误差越大回拉力度越强← 正确答案
  • B.速度项提供阻尼防止抖动← 正确答案
  • C.增益系数越大效果必然越好
  • D.可再叠加避障力做局部修正← 正确答案

答案:ABD。PD 控制中位置误差产生回拉力,速度差提供阻尼防振荡,并可叠加避障力做优先级混合。C 错误:增益过大会导致振荡甚至发散,并非越大越好。