从 NavMesh 网格寻路回归到 Grid 网格寻路。

  上一个项目的寻路方案是客户端和服务器都采用了 NavMesh 作为解决方案,当时的那几篇文章()是很多网友留言和后台发消息询问最多的,看来这个方案有着广泛的需求。但因为是商业项目,我无法贴出代码,只能说明下我的大致思路,况且也有些悬而未决的不完美的地方,比如客户端和服务器数据准确度和精度的问题,但是考虑到项目类型和性价比,我们忽略了这个点。

  从今年5月份开始为期一个月,我的主要工作是为新项目寻找一个新的寻路方案。新项目是一个 RTS 实时竞技游戏,寻路要求是:每个寻路单位之间的碰撞精确,不能出现不正确的拥挤和穿插,并且单位大小和所在的任何位置,都会影响到其他活动单位的通路性选择,看起来就是一个典型的 RTS 游戏寻路,和红警,星际等这些游戏非常像。

  项目最初的阶段为了先快速迭代功能,使用了 Unity 内建的 NavMesh 系统,当单位停下使用 NavMeshObstacle 在地上挖个坑来影响通路性,但是经常会出现两个建筑型的单位中间会有个小缝,虽然缝很细,但是也是可以走的,而很可能一个半径巨大的单位直接就试图传过去,结果是被卡在这里。如果给单位的半径很小就可以穿过去,但是感觉很怪,而且单位之间的穿插也会很明显,十分影响游戏的观感,更重要的是会影响游戏性。终于有一天,策划的同学们再也不能忍了,必须改掉!

  在我看来我首先要解决的是能够将现有的或者寻找到一个能支持单位半径大小的寻路方案。但是这从一开始就排除掉了 Unity 的 NavMesh 的寻路方案,因为没有任何 api 提供给用户可供做类似的修改,通过修改 RecastNavigation 项目然后编译 Native 插件的方式我也否掉了,不划算,其实本质上是因为 NavMesh 系统无法提供我们需要的高精度要求,所以我把精力集中到寻找传统的 Grid 网格寻路上了。

  过程中找到了一篇专门讲解不同单位通路性的文章:《Clearance-based Pathfinding and Hierarchical Annotated A* Search》,讲解深入详细,并且还配有源码。

  

  这看起来似乎正好是满足我要求的东西,但是它有个致命的缺点:所有一切都是预先烘焙和计算的,如果有任何物体或者情况影响了原有的通路性,那么整个烘焙过程必须重新进行,按照作者的算法和流程,对于我们这种时刻都在改变整个地图通路性的情况来讲,完全不可能进行不断地实时计算,所以该方案最终还是放弃了。

  后来朋友介绍了个很有意思的项目:Stratagus - GitHub - Wargus/stratagus: The Stratagus strategy game engine。这个项目很有意思,安装到手机后,直接把星际争霸1的资源导入,然后就可以在手机上玩星际争霸了,狂拽酷炫吊炸天。不过我安装并且导入星际资源到安卓手机后,一进入战斗场景就崩溃,试了很多次都不行,其实就是想看看它寻路的表现。好了不浪费时间,直接下载代码开始阅读,各种查找翻阅,最后看到了它是如何处理单位的大小和通路性的关键判断:位于 src/pathfinder/astar.cpp:CostMoveToCallBack_Default @line:506

 1 /* build-in costmoveto code */
 2 static int CostMoveToCallBack_Default(unsigned int index, const CUnit &unit)
 3 {
 4 #ifdef DEBUG
 5     {
 6         Vec2i pos;
 7         pos.y = index / Map.Info.MapWidth;
 8         pos.x = index - pos.y * Map.Info.MapWidth;
 9         Assert(Map.Info.IsPointOnMap(pos));
10     }
11 #endif
12     int cost = 0;
13     const int mask = unit.Type->MovementMask;
14     const CUnitTypeFinder unit_finder((UnitTypeType)unit.Type->UnitType);
15
16     // verify each tile of the unit.
17     int h = unit.Type->TileHeight;
18     const int w = unit.Type->TileWidth;
19     do {
20         const CMapField *mf = Map.Field(index);
21         int i = w;
22         do {
23             const int flag = mf->Flags & mask;
24             if (flag && (AStarKnowUnseenTerrain || mf->playerInfo.IsExplored(*unit.Player))) {
25                 if (flag & ~(MapFieldLandUnit | MapFieldAirUnit | MapFieldSeaUnit)) {
26                     // we can‘t cross fixed units and other unpassable things
27                     return -1;
28                 }
29                 CUnit *goal = mf->UnitCache.find(unit_finder);
30                 if (!goal) {
31                     // Shouldn‘t happen, mask says there is something on this tile
32                     Assert(0);
33                     return -1;
34                 }
35                 if (goal->Moving)  {
36                     // moving unit are crossable
37                     cost += AStarMovingUnitCrossingCost;
38                 } else {
39                     // for non moving unit Always Fail unless goal is unit, or unit can attack the target
40                     if (&unit != goal) {
41                         if (goal->Player->IsEnemy(unit) && unit.IsAgressive() && CanTarget(*unit.Type, *goal->Type)
42                             && goal->Variable[UNHOLYARMOR_INDEX].Value == 0 && goal->IsVisibleAsGoal(*unit.Player)) {
43                                 cost += 2 * AStarMovingUnitCrossingCost;
44                         } else {
45                         // FIXME: Need support for moving a fixed unit to add cost
46                             return -1;
47                         }
48                         //cost += AStarFixedUnitCrossingCost;
49                     }
50                 }
51             }
52             // Add cost of crossing unknown tiles if required
53             if (!AStarKnowUnseenTerrain && !mf->playerInfo.IsExplored(*unit.Player)) {
54                 // Tend against unknown tiles.
55                 cost += AStarUnknownTerrainCost;
56             }
57             // Add tile movement cost
58             cost += mf->getCost();
59             ++mf;
60         } while (--i);
61         index += AStarMapWidth;
62     } while (--h);
63     return cost;
64 }

  每次进行 AStar 寻路,计算寻路 Cost 的时候,都会走到这个回调函数,请注意以上代码中每个 Unit(建筑和可活动单位)都有一个 TileWidth,这个TileWidth 就是寻路单位在地图上所占的一个正方形的宽度(如果使用长方形会极大的增加计算复杂度,因为要考虑旋转后占格的问题。)一次遍历这个正方形,看看每一个格子是否被任何单位设置了使用 Mask,如果没有就说明可通过,最终如果一个正方形内所有格子都没有被设置任何 Mask 说明该区域可走,对于移动中的物体,认为它所占的区域属于可走区域,但是会给予比普通可走区域更高的 Cost。所以看来原理很简单,就是在寻找 AStar 节点的时候要遍历该节点所占的每个格子看是否可以通过,条件变得更加严格。

  这个方式非常适合我的需求,我决定采取这个方式来进行后一步工作。考虑到时间紧迫且对稳定性要求高,我不打算自己重新编写整个寻路算法和框架了,寻找一个成熟的合适的插件来进行后一步工作,经过试验考察,我选择了使用 Unity 上一个非常强大和成熟的插件 A* Pathfinding Project,来进行扩展和升级以便达到我的要求。

  我们在 Unity Asset Store 中购买了此插件(很贵:100刀),然后我针对 GridGraph 类型进行了深度的修改,增加单位的 TraverseSize 作为寻路的参数传入,以便影响寻路结果,这样不同单位的大小,在穿过缝隙时,就可能会有不同的路径,如下图,每一个寻路的 Node 设置为了0.5,大的单位半径0.5,小的单位半径0.25,(寻路计算最终是直径),前往同一个地点,有如下结果:

  小的单位可以通过宽度为0.5的缝隙而大的不可以,只能通过最小为1.0大小的缝隙,于是我的最基本的需求得到满足。

  接下来,我需要处理碰撞的问题,精细碰撞需要单位无论大小,速度,位置,都要准确的处理,他们只能在边缘接触,不能过于穿插。期初我试用了这个插件自带的 RVO 系统,但是精度真的不够,后来我决定采用一个比较诡异但十分凑效的方案:使用 Unity 的 NavMesh 系统的 NavMeshAgent 的 Detour 系统来实现碰撞,当初据 RecastNavigation 的作者说,Unity 深度改写了该系统的 Detour 系统,可能这就是直接的体现吧。也就是说在地图上生成一个没有任何阻挡的完整的 NavMesh 可走区域,但是不用来走寻路目的,只是为了使用 NavMeshAgent 的碰撞计算而已,真是的寻路结果是 A* Pathfinding Project 提供的。

  以上所有需要的基础系统都已经完成,但这只是另一个开始,和项目的结合和调试过程也是一个不断地改进的过程,需要和策划的同学们不断地沟通和交流进行调校,经过一段时间的磨合,终于开始稳定的按照预期目标进行工作了。结项!

时间: 2024-08-06 16:06:37

从 NavMesh 网格寻路回归到 Grid 网格寻路。的相关文章

python之tkinter使用-Grid(网格)布局管理器

1 # 使用tkinter编写登录窗口 2 # Grid(网格)布局管理器会将控件放置到一个二维的表格里,主控件被分割为一系列的行和列 3 # stricky设置对齐方式,参数N/S/W/E分别表示上.下.左.右 4 # columnspan:指定控件跨越多列显示 5 # rowspan:指定控件跨越多行显示 6 # padx.pady分别设置横向和纵向间隔大小 7 8 import tkinter as tk 9 10 root = tk.Tk() 11 root.title("请登录&quo

CSS Grid 网格布局教程

一.概述 网格布局(Grid)是最强大的 CSS 布局方案. 它将网页划分成一个个网格,可以任意组合不同的网格,做出各种各样的布局.以前,只能通过复杂的 CSS 框架达到的效果,现在浏览器内置了. 上图这样的布局,就是 Grid 布局的拿手好戏. Grid 布局与 Flex 布局有一定的相似性,都可以指定容器内部多个项目的位置.但是,它们也存在重大区别. Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局.Grid 布局则是将容器划分成"行"

CSS3 网格布局(grid-layout)基础知识 - 网格模板属性(grid-template)使用说明

CSS3引入了新的网格布局(grid layout),以适应显示和设计技术的发展(尤其是移动设备优先的响应式设计). 主要目标是建立一个稳定可预料且语义正确的网页布局模式,用来替代过往表现不稳定且繁琐的table.flow以及JS脚本混合技术来实现的网页动态布局. 本文将简单而准确的介绍网格布局属性的基本概念和使用方法(摘自踏得网在线HTML5教程). 1. 概述 网格模板区域(grid-template-areas).网格模板行(grid-template-rows)和网格模板列(grid-t

一个简单的响应式横向网格布局框架Responsive Grid System

网页设计中有一个怎么也绕不开的问题,那就是布局问题.一般来说,一个div可能会分割成很多部分,同样是横向分割成两部分,但是在不同的项目中,我们可能会用不同的css代码,网格系统其实就是为这一类似的问题提供一个统一的解决办法. 网格布局有很多,但是有很多非常复杂,往往无法二次开发.Responsive Grid System是国外的一个网站,他们提供了一种非常简单的实现.其实每一个前端开发人员都可以写出这样的框架,但是,从头开始也许会让很多人动力不足.这个框架代码很简单,对我而言,采用它的原因仅仅

Grid网格布局

<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>网格布局</title> <style> .container { display: grid; grid-template-columns: 100px 100px 100px 100px 100px: grid-template-rows: 100px 100px 100px 10

grid - 网格项目层级

网格项目可以具有层级和堆栈,必要时可能通过z-index属性来指定. 1.在这个例子中,item1和item2的开始行都是1,item1列的开始是1,item2列的开始是2,并且它们都跨越两列.两个网格项目都是由网格线数字定位,结果这两个网格项目重叠了. 默认情况下,item2在item1上面,但是,我们在item1中设置了z-index:1;,导致item1在item2之上. 1 <view class="grid"> 2 <view class='item1'&g

web前端入门到实战:CSS自定义属性+CSS Grid网格实现超级的布局能力

最近我还注意到的一件事就是CSS自定义属性.CSS自定义属性的工作方式有点像SASS和其他预处理器中的变量,主要的区别在于其它方法都是在浏览器中编译后生成,还是原本的CSS写法.CSS自定义属性是真正的动态变量,可以在样式表中或使用javascript即时更新,这使得它们具有更多的可能性.如果你熟悉JavaScript,我喜欢把预处理器变量和CSS自定义属性之间的区别想象成与const和let之间的区别相似--它们都有不同的用途. CSS自定义属性可以方便的实现很多功能(例如主题变化).最近我一

css3 grid 网格布局

1.grid-template设置网格模板,实现四列两行布局:grid-gap设置网格间隙,包括行和列 2.grid布局,使用fr单位实现等比例分配空间.fr是分数(fraction)的缩写 .con{ display: grid; grid-template-columns: 1fr 3fr 2fr 6fr; grid-template-rows: 50px 50px 50px ; grid-gap: 10px;}.con div{ background: orange;}.con div:n

寻路专题一之WayPoint寻路

寻路在游戏开发中的重要性,不用多说了.从大型ARPG游戏到小规模体验游戏,都会不同程度的用到寻路功能. 塔防类游戏,战棋类游戏经常用到waypoint(路径)寻路. 下面我们来介绍一下waypoint寻路. 1.Path(路径)的设计. 图中每一个蓝色的Cube表示一个路径点(WayPoint),由7个waypoint构成path;         Path.cs代码如下: using UnityEngine; using System.Collections; public class Pat