大规模对象的寻路与性能优化 Pathfinding at Scale & Performance Optimization
当一千个顾客同时想抄近路,你的 CPU 就哭了——用空间分区、分层寻路和 LOD 降级保住帧率与经营逻辑。
为什么路径搜索密度是第一块基石
做经营游戏做到后期,你一定会撞上这堵墙:城市人口过了三千,每个小人都在实时寻路,CPU 使用率像坐火箭一样往上蹿。更难受的是,你明明只是想让面包师从厨房走到柜台,结果全城的人都卡在原地发抖——你以为你在做游戏,其实你在做交通调度中心。
这事我翻过车。做早期原型的时候,我用最朴素的寻路逻辑,每个角色每帧都重新计算路径。测试时一切正常,因为地图上只有十几个小人。等我把人口上限调到两百人,帧率直接掉到个位数。那天我盯着性能分析器,心里只有一个念头:这游戏不是被玩家玩死的,是被自己家里那两百个 NPC 活活卡死的。
所以这就是为什么这篇文章要聊寻路性能。你的经济系统设计得再精妙,你的员工 AI 再智能,如果一百个顾客同时想买面包时你的游戏变成了幻灯片,一切都白搭。性能不是后期优化的脏活,它决定了你能不能在游戏里兑现你的设计野心——你想让城市活起来,你首先得让它动得起来。
第一层:空间分区——先别急着寻路,先把地图切块
想象一下你要在一座城市里找一家咖啡店。你不会一条街一条街地扫过去,你会直接确认它在哪个区,再在那个区里找。空间分区就是把这个常识翻译给计算机。
网格划分是最老派也最可靠的做法:把地图切成等大的格子,每个格子记录自己区域里有哪些障碍物、哪些角色、哪些服务设施。角色需要寻路的时候,先确认自己所在格子,再确认目标所在格子,如果距离远,就直接在格子层面做粗粒度路径规划——这就是第一层过滤,把庞大的搜索空间砍掉一个数量级。
更高级一点是四叉树或者八叉树分区:地图大但人稀疏的地方用大格子,市中心人多物杂的地方自动切成小格子。代价是实现复杂度高一点,但收益很直接——你可以让 CPU 花在真正需要精细计算的地方。
实际落地的时候,有几个参数值得你反复调:
| 参数项 | 建议区间 | 调太高会怎样 | 调太低会怎样 |
|---|---|---|---|
| 基础网格尺寸 | 地图边长的 1/50 到 1/80 | 分区太粗,粗粒度寻路失真 | 格子太多,维护成本高 |
| 动态对象分桶密度 | 每格 8-15 个角色 | 格子内检索变慢 | 格子数量膨胀,更新开销大 |
| 粗粒度寻路触发距离 | 格子边长的 3-5 倍 | 近距离寻路也走粗粒度,路线不自然 | 粗粒度层形同虚设 |
说个真实案例。玩过《城市:天际线》的人都知道,这个游戏最著名的玩家吐槽就是“堵车”。但实际上天际线的寻路机制相当务实——它用分层道路网络做路径规划,主干道、次干道、支路分成不同层级,长距离出行优先走高层级道路,短距离才钻小巷。这个设计让几十万人口的城市场景还能保持可玩帧率。你可能会问,那为什么还会堵车?因为玩家自己把城市规划成了一条主干道通到底,所有车都挤上去——
你看,这里有个顶层设计反直觉的地方:寻路框架越高效,玩家越容易用极端的城市规划来榨干你的系统。所以性能优化不只是技术活,它也在跟玩家的创造力赛跑。
第二层:分层寻路——让远见的角色走大路,让眼前的任务走小路
分层寻路的思路很简单:不要把寻路想象成一张地图上的一次搜索,而是把它拆成两张图——一张是宏观的(城市级别的道路网络),一张是微观的(社区级别的步行小径)。角色要跨越大半个地图去干一件事,先在大图上找一条粗路线,然后逐段地在小图上细化每一条路段该怎么走。
这样做有两个直接好处。第一,计算量大幅下降:大图上搜索的节点数少,小图上每段搜索的范围也小。第二,路径质量反而更高:你可以在宏观层做交通流量预测,让角色避开拥堵的主干道,这在单层寻路里得靠额外的启发式算法才能实现。
有一个细节特别容易被忽略:分层切换的时机。角色走到路段的尽头,发现下一条路段堵死了,这时候应该重新规划全局吗?如果你每次都全局重算,那分层就没有意义了。但如果完全不管,角色就会傻乎乎地站在原地转圈。实用做法是设置一个“重规划冷却时间”:角色撞上拥堵后等待几秒,然后再重新算一次。这个等待时间让拥堵自然消散,又不会让玩家觉得角色是傻子。
关于分层寻路,可以做一张权衡表:
| 分层深度 | 典型场景 | 性能开销 | 路径自然度 |
|---|---|---|---|
| 两层(宏观+微观) | 小型城镇规模地图 | 低 | 中等,可能出现“到了路口才想起要转弯” |
| 三层(区域+道路+步行) | 中大型城市或殖民地图 | 中 | 高,长距离路线合理,短距离路径自然 |
| 单层全量搜索 | 极小地图(如两三层建筑内) | 高(但地图小,可接受) | 最高,因为充分考虑了所有细节 |
《缺氧》就是分层寻路的好样本。它的地图本身是网格化的(一层层地基建成的网格),所以天然适合做分层:高优先级的“主干梯道”连接各个区域,次要梯道在区域内部分支,管道和输送带又有自己的寻路逻辑。最妙的是,缺氧的角色会在路径选择时综合考虑氧气管、水管的布局——你刚建好一条新走廊,小人就会自发地开始使用它,因为寻路器在宏观层面检测到了路径阻力下降。
这就是设计意图和性能框架握手的好例子:分层寻路不只是为了省 CPU,它本身就催生了玩家可以观察和利用的系统行为——你修好一条路,你就真的改变了城市的流动。如果系统只做单层全量寻路,你修路的反馈会微弱得多。
第三层:对象 LOD 降级——别让三百米外的老奶奶在精确计算怎么跨过一颗石子
你有没有注意过,现实生活里你不会每秒钟都决定下一步踩在哪块地砖上——你走着走着,就开启了自动驾驶,只有在碰到障碍物的时候才回过神来。游戏里的 LOD(细节层级)降级,就是模拟这种“自动驾驶”。
核心逻辑:角色的寻路计算量应该跟屏幕上的可见度挂钩。你不需要对每个角色都做每帧精确寻路。离镜头近的、正在交互的角色,走一步算一步,帧级响应;离镜头远的、在屏幕边缘溜达的角色,也许一两秒才更新一次路径就完全够用。
这听起来很合理,真正的工程难点在于判断“谁需要高精度”。我的经验是分几档:
| LOD 层级 | 更新频率 | 路径类型 | 适用条件 |
|---|---|---|---|
| LOD-0(高精度) | 每帧更新 | 全量动态避障 | 玩家镜头附近、正在交互的角色 |
| LOD-1(中精度) | 每 0.3-0.5 秒 | 主路径 + 近似避障 | 屏幕内但非焦点区域角色 |
| LOD-2(低精度) | 每 1-2 秒 | 粗粒度路径 | 屏幕外或极小比例出现在屏幕上 |
| LOD-3(估算) | 事件驱动 | 仅目标点移动时更新 | 地图边缘、资源节点附近的角色 |
有一个很猥琐但很有效的细节:你可以让角色在走长直道的时候切换到低 LOD,只在转角、路口这些“决策点”才把精度拉起来。你不需要让角色一路都像刚学会走路一样小心。
另外一个几乎是免费的优化是“路径平滑后处理”的降级——高精度角色用完整的多段路径平滑,让行走轨迹圆润自然;低精度角色直接用直线连接路点,视觉上可能有点生硬,但在远处根本没人看得见。这个差别可以用“视觉误差预算”来管理:允许低精度角色偏离理想路径最多半个身位,误差控制在这个范围内,玩家就不会发现。
《双点医院》是个很典型的案例,这个游戏里几十个病人在一张不大的地图上同时寻路找科室。双点医院的做法非常务实:大多数病人的路径不是每帧重算的,而是“走到某个路点后重新确认下一步”。于是你经常看到一群病人站在走廊交叉口短暂停顿,然后再呼啦啦地朝同一个方向走——这个停顿在玩家眼里是“医院里的日常”,但在性能分析器里,那是寻路系统在拼命刹车给它自己喘口气。它用微小的视觉瑕疵换来了平滑的帧率,值。
第四层:多线程模拟——把寻路交给别的大脑
如果你把寻路系统按好了分区、做好了分层、也上了 LOD,但你的城市仍然有三千个角色在午后高峰期同时涌向餐厅——你需要的最后一个手段,是把寻路丢给别的线程。
多线程的核心思路:把寻路从主循环里拆出来,交给工作线程异步处理。角色的行为逻辑照旧在主线程里跑,但“帮我算一条从面包店到家的路径”这个请求会排队给到专门的寻路线程池。几毫秒之后,线程把路径结果送回来,角色再沿着结果走。
这里最容易踩的坑是“数据竞争”。如果你让两个线程同时读写了同一块地图数据,轻则路径结果诡异,重则直接闪退崩溃。所以应遵循一个铁律:地图数据(障碍物、通行 cost 表)是只读的,任何想修改地图数据的请求(比如玩家放了一堵墙)都必须先把角色寻路线程挂起,改完再恢复。这个“暂停-修改-恢复”的逻辑,如果你处理不好,就会出现“角色穿墙”这种阴阳BUG。
多线程的收益分配很有讲究。一个较稳的拆分方式:
| 线程职责 | 优先级 | 忠告 |
|---|---|---|
| 主线程:角色行为、状态机、经济逻辑 | 最高 | 不能因为寻路线程忙碌而卡顿 |
| 寻路线程池(4-8 线程) | 高 | 按区域分配任务,避免多线程同时搜索同一区域造成数据热点 |
| 动态网格更新线程 | 中 | 玩家的建筑操作触发地图变更,由这个线程负责合并改动 |
| 后台分析线程(流量统计等) | 低 | 不阻碍游戏运行,慢慢算,算完展示 |
给你看一个真实的坑:《监狱建筑师》早期版本里,数百名囚犯同时在一条走廊寻路去食堂吃饭,寻路线程被排队的请求塞满了,导致所有囚犯都僵在原地,游戏看起来像冻结了。后来开发组用了“批处理”策略:每一帧只处理最多 64 个寻路请求,其余排队等到下一帧。这个“帧预算”的思路极其关键——不是为了把请求全算完,而是保证每一帧主线程都能稳定推进,哪怕有些角色延迟一两帧才出发。玩家感知不到这一两帧的延迟,但游戏的稳定性就保住了。
多线程的事,最后说一句真话:它不能拯救一个烂设计。如果你的寻路系统本身每次请求都算得很慢,丢给再多线程也只是推迟爆炸。先把前三层做好,多线程是锦上添花,不是雪中送炭。
调参工具箱——一张能直接抄的配置表示意图
理论讲了这么多,直接给一套能用的参数起点。以下是在我的项目里跑过的配置,你拿去当基线再根据自己的游戏调整。
| 系统 | 参数 | 基线值 | 说明 |
|---|---|---|---|
| 空间分区 | 基础格边长与地图宽度比 | 1/60 | 太粗则粗粒度寻路失真,太细则维护开销大 |
| 空间分区 | 每格最大动态对象数 | 12 | 超过则触发集群迁移/重新划分逻辑 |
| 分层寻路 | 粗粒度路径更新间隔 | 0.8 秒 | 长距离路径保持稳定,短距离则每帧微调 |
| 分层寻路 | 拥堵重新规划冷却时间 | 2.5 秒 | 避免全员因同一点拥堵而集体重算 |
| 对象 LOD | LOD-0 的检测半径(以屏幕中央为基准) | 屏幕宽度的 1/3 | 以肉眼可观察为主,避免浪费计算 |
| 对象 LOD | 视觉误差预算 | 半个角色身位 | 超出则强制提升 LOD,防止“穿模”感知 |
| 多线程 | 每帧寻路请求上限 | 64 | 超出排队,每帧最多处理 64 个,保持主线程稳定 |
| 多线程 | 寻路线程数 | 硬件逻辑核心数 × 0.5 | 留一半核心给渲染和其他模拟 |
再给三个压箱底的调参经验:
一,所有参数都要有运行时可视化。把寻路请求队列长度、单次寻路平均耗时、LOD 分布比例打成 on-screen 调试面板,一边看游戏一边调,比事后看日志高效十倍。
二,别让玩家的“小动作”触发全局寻路风暴。比如玩家拖动城墙画一道线——如果每一段墙都触发一次全城寻路重算,你的 CPU 直接冒烟。正确做法:把修改操作合并成一次“地图变更事件”,延迟 0.5 秒统一更新,让所有角色在下一轮寻路请求时自然感知到新地图。
三,设置“寻路压力测试”的内置作弊码。开发时一键生成 1000 个随机角色冲向地图中心,看看帧率跌成什么样。如果没有这个测试脚本,你永远只能在玩家论坛上知道自己游戏的性能上限。
初级用户速览 vs 进阶深入
初级用户:先跑起来,别管那么多
如果你的游戏还没达到几千个角色的规模,别折腾多线程,别搞四叉树。你只需要做三件事:一,把地图分成均匀网格,每个角色只在自己的网格以及相邻网格内检测障碍物;二,角色只在碰到障碍物或者到达路点时才重新寻路,不要每帧都算;三,离镜头远的角色每半秒更新一次路径。这三招能让你撑到几千人规模,而代码量不超过一天的工作量。
把基础跑起来之后,再去研究 LOD 降级和分层寻路。你会发现你已经有了一个能玩的版本,性能虽然有个上限,但足够你验证游戏核心乐趣。别一开始就上复杂架构,否则你会在性能优化上花掉两倍于做游戏玩法的时间——你没那么多时间。
进阶用户:用数据驱动优化
如果你的地图真的很大、角色密度真的很高(比如上万),那就需要更系统的思路。核心建议是:做一个“寻路成本热力图”的调试工具,把每个格子在最近一秒内的寻路请求数量渲染成颜色叠加层。你会发现拥堵不是均匀分布的——永远有几个热点格点(比如市中心广场、唯一一座桥的桥头)吸收了大部分请求。
这时候你就有两个选择:要么在热点周围增加“虚拟成本”(让寻路器绕开那些格点),要么修改地图设计(加桥、加路)。我的建议是:先调虚拟成本,观察玩家的行为有没有变得“反常”。如果玩家觉得“我的市民为什么绕着家门口的广场走”,那你可能改过头了,回退到地图改造,加一条侧路。用户反馈是检验性能优化的终极标尺——你再怎么自认为很懂寻路,也不如玩家的一句“我的人好像变蠢了”来得警醒。
别碰这些雷区:寻路优化的真实代价
踩过坑的人都知道,寻路优化有时候是“负优化”。我总结几个最经典的困境:
困境一:路径很少“恰好最优”。你做了分层寻路,宏观找了一条大路径,微观在局部修正,但合并起来的路线可能比最优路线长 20%。玩家通过观察,可能会发现市民绕远路了,然后质疑 AI 的智力。解决思路是:把“微距修正”的触发条件放宽——只有当成本差距超过 15% 时才修正。这样既能保证大部分路径看起来自然,又给了系统一点容错空间。
困境二:LOD 降级让角色显得“呆”。低精度角色可能站在路口无所事事好几秒才重新寻路,玩家如果恰好把镜头拉近,就会觉得这傻子卡住了。这里没有完美的解法,但我发现一个比较有效的缓解:让低精度角色在等待时播放“思考”或“整理衣服”之类的待机动画,把技术上的等待转化成有生活气息的行为。玩家会以为这人在发呆,而不会觉得游戏坏了。
困境三:多线程的隐性成本。你可能以为加了多线程就万事大吉。但线程间同步也要花时间。如果你的地图特别小、角色只有几百个,多线程的开销可能比单线程还慢——数据要复制到线程池,算完再拷贝回来,一来一回比直接算更耗时。所以请记住:多线程适用于“大计算量”,而不是“中等计算量”。
还有那些战斗民族般的挑战:有玩家会在你的沙盒里用两千头牛把市中心填满,然后截图发到社区说你寻路系统 bug 了。这时候除了苦笑,你可选的兜底方案只有一个:给“极端角色密度”设个物理上限——比如某个区域内角色数超过阈值时,自动把远处角色的 AI 切换成“离线模式”(直接瞬移到目标点,不做实时寻路)。虽然这看起来作弊,但你想清楚:玩家要的不是精准寻路,而是不要卡成 PPT。有时候放弃一部分物理正确性,换来的是流畅的体验。
FAQ
Q1:我的城市一到 2000 人口就卡成 PPT,应该先优化哪个系统?
先做性能分析器,看看瓶颈在寻路、渲染还是游戏逻辑(比如经济模拟)。如果是寻路,大概率是因为你全地图的角色都是每帧全量寻路。按文章里的三层优化跑一遍:空间分区(把搜索范围切小)+ LOD 降级(远处角色降低更新频率)+ 帧预算(每帧最多处理 64 个请求)。2000 人口的规模,这三板斧够你撑到发布。
Q2:我的角色走到了目的地门口,但门被另一角色堵住了,怎么处理?
给每个门/通道加“占用状态”,寻路器在计算最后一段路径时检查占用状态。如果门被堵了超过两秒,先让角色在门口小范围随机徘徊(视觉上像在等待),同时排队的角色自动走另一条路。这就是用“排队系统”替代“实时路径重算”的轻量方案。
Q3:LOD 降级会不会让角色看起来很蠢?
会有风险。但你把镜头拉近到某个低精度角色时,触发一次即时升级(强制它立刻重新寻路),这样可以保证近距离观察时都是高精度模式。玩家镜头是性能优化最好的触发器。
Q4:有没有必要为了寻路性能给玩家限制人口上限?
这是个产品决策,不是纯技术决策。如果你把寻路优化做好了,你可以给更高的上限,让玩家自由建造。但如果你给玩家 5000 人口上限却让游戏在 3000 人口时卡爆,那才是真灾难。技术作为底裤——上限可以高,但你要保证在接近上限时也能跑得动。开发者版做压力测试,比玩家替你找性能上限好一万倍。
Q5:如果我想做超大地图(比如 1024×1024 的格子),第一层的粗粒度寻路还有意义吗?
意义更大。超大地图最怕的是把每个格子都算一遍。空间分区 + 分层寻路是唯一能让超大地图跑起来的方式。唯一要注意的是:当你做大尺度规划时,粗粒度路径的“层数”要相应增加(比如从两层加到三层甚至四层),否则你会看到角色绕远路绕得离谱。