从 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
/* build-in costmoveto code */
static int CostMoveToCallBack_Default(unsigned int index, const CUnit &unit)
{
#ifdef DEBUG
{
Vec2i pos;
pos.y = index / Map.Info.MapWidth;
pos.x = index - pos.y * Map.Info.MapWidth;
Assert(Map.Info.IsPointOnMap(pos));
}
#endif
int cost = ;
const int mask = unit.Type->MovementMask;
const CUnitTypeFinder unit_finder((UnitTypeType)unit.Type->UnitType); // verify each tile of the unit.
int h = unit.Type->TileHeight;
const int w = unit.Type->TileWidth;
do {
const CMapField *mf = Map.Field(index);
int i = w;
do {
const int flag = mf->Flags & mask;
if (flag && (AStarKnowUnseenTerrain || mf->playerInfo.IsExplored(*unit.Player))) {
if (flag & ~(MapFieldLandUnit | MapFieldAirUnit | MapFieldSeaUnit)) {
// we can't cross fixed units and other unpassable things
return -;
}
CUnit *goal = mf->UnitCache.find(unit_finder);
if (!goal) {
// Shouldn't happen, mask says there is something on this tile
Assert();
return -;
}
if (goal->Moving) {
// moving unit are crossable
cost += AStarMovingUnitCrossingCost;
} else {
// for non moving unit Always Fail unless goal is unit, or unit can attack the target
if (&unit != goal) {
if (goal->Player->IsEnemy(unit) && unit.IsAgressive() && CanTarget(*unit.Type, *goal->Type)
&& goal->Variable[UNHOLYARMOR_INDEX].Value == && goal->IsVisibleAsGoal(*unit.Player)) {
cost += * AStarMovingUnitCrossingCost;
} else {
// FIXME: Need support for moving a fixed unit to add cost
return -;
}
//cost += AStarFixedUnitCrossingCost;
}
}
}
// Add cost of crossing unknown tiles if required
if (!AStarKnowUnseenTerrain && !mf->playerInfo.IsExplored(*unit.Player)) {
// Tend against unknown tiles.
cost += AStarUnknownTerrainCost;
}
// Add tile movement cost
cost += mf->getCost();
++mf;
} while (--i);
index += AStarMapWidth;
} while (--h);
return cost;
}
每次进行 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 提供的。
以上所有需要的基础系统都已经完成,但这只是另一个开始,和项目的结合和调试过程也是一个不断地改进的过程,需要和策划的同学们不断地沟通和交流进行调校,经过一段时间的磨合,终于开始稳定的按照预期目标进行工作了。结项!
从 NavMesh 网格寻路回归到 Grid 网格寻路。的更多相关文章
- CSS Grid 网格布局全解析
介绍 CSS Grid(网格) 布局使我们能够比以往任何时候都可以更灵活构建和控制自定义网格. Grid(网格) 布局使我们能够将网页分成具有简单属性的行和列.它还能使我们在不改变任何HTML的情况下 ...
- grid - 网格项目对齐方式(Box Alignment)
CSS的Box Alignment Module补充了网格项目沿着网格行或列轴对齐方式. <view class="grid"> <view class='ite ...
- grid - 通过网格区域命名和定位网格项目
1.像网格线名称一样,网格区域的名称也可以使用grid-template-areas属性来命名.引用网格区域名称也可以设置网格项目位置. 设置网格区域的名称应该放置在单引号或双引号内,每个名称由一个空 ...
- python之tkinter使用-Grid(网格)布局管理器
# 使用tkinter编写登录窗口 # Grid(网格)布局管理器会将控件放置到一个二维的表格里,主控件被分割为一系列的行和列 # stricky设置对齐方式,参数N/S/W/E分别表示上.下.左.右 ...
- Grid 网格布局
CSS 网格布局(Grid Layout) 是CSS中最强大的布局系统. 这是一个二维系统,这意味着它可以同时处理列和行,不像 flexbox 那样主要是一维系统. 你可以通过将CSS规则应用于父元素 ...
- Android BottomSheet:List列表或Grid网格展示(3)
Android BottomSheet:List列表或Grid网格展示(3) BottomSheet可以显示多种样式的底部弹出面板风格,比如常见的List列表样式或者Grid网格样式,以一个例子 ...
- CSS Grid网格布局全攻略
CSS Grid网格布局全攻略 所有奇技淫巧都只在方寸之间. 几乎从我们踏入前端开发这个领域开始,就不停地接触不同的布局技术.从常见的浮动到表格布局,再到如今大行其道的flex布局,css布局技术一直 ...
- CSS 之Grid网格大致知识梳理1
CSS所提供的关于网格Grid属性让我们可以更方便编写页面以及布局,而它的一些主要应用属性如下: 1.将父容器的display属性值设置为grid 即可将其转换为网格容器: 2.在网格容器中添加列的属 ...
- CSS 之Grid 网格知识梳理2
继上篇的CSS 之Grid下半部分 14.将单元格划分到一个区域,使用grid-template-areas属性: ag: grid-template-areas: "header h ...
随机推荐
- 安装JDK设置环境变量
PS:之前在CSDN上写的文章,现在转到博客园~ 在安装过程中第一次让选择jdk的安装路径,第二次让选择jre的安装路径.两者不可以在同一个文件夹下,否则在cmd中运行javac时会报:摘不到或无法加 ...
- Linux下GPIO驱动(二) ----s3c_gpio_cfgpin();gpio_set_value();
首先来看s3c_gpio_cfgpin(); int s3c_gpio_cfgpin(unsigned int pin, unsigned int config) { struct s3c_gpio_ ...
- informix 查看数据库空间名
查看bhrs库的空间名 onstat -d 导出一个表 的结构 dbschema -d bhrs -t infotrans > xxx.sql 微网点 报表已经上线 cbs.sql 提交,生产 ...
- 扩展pl0编译器设计——总述
所谓编译器,实际上就是我们编程时将输入的高级语言代码转换成相应的目标代码,从而实现将目标代码转换成汇编码的一种过渡工具. 这种工具根据具体情况不同,可以将不同的高级语言代码转换成不同的目标代码,例如将 ...
- BZOJ 4005 [JLOI 2015] 骗我呢
首先,我们可以得到:每一行的数都是互不相同的,所以每一行都会有且仅有一个在 $[0, m]$ 的数没有出现. 我们可以考虑设 $Dp[i][j]$ 为处理完倒数 $i$ 行,倒数第 $i$ 行缺的数字 ...
- 服务器监控之 ping 监控
在运维人员的日常工作中,对物理服务器的监控十分重要.物理机的 CPU.内存.磁盘使用率,网卡流量,磁盘 IO 等都需要进行监控.通过 ICMP 协议的 ping 监控,可以判断物理服务器运行是否正常或 ...
- Nagios 邮箱告警的方式太OUT了!
一般来讲,在安装完 Nagios 后,我们做的第一件最正确的事,就是设置它的邮件通知,对吧.因为如果没有这一步骤的话,你怎么能够知道什么时候会出现问题呢? 伴随着成功的初始安装,你即将是你司唯一一个能 ...
- Ubuntu zookeeper-3.5.0-alpha启动错误 zkEnv.sh: Syntax error: "(" unexpected (expecting "fi")(转)
昨天小猿我把Ubuntu Server64位上的 zookeeper换成了最新版本的,结果启动的时候出错:之前zookeeper-3.3.6是没有任何问题的,换成了zookeeper3.5出现了下面的 ...
- Android include的使用
如果在程序中多次用到一部分相同的布局,可以先将这部分布局定义为一个单独的XML,然后在需要的地方通过<include>引入,如下: main.xml <?xml version=&q ...
- 【HDOJ】2828 Lamp
DLX简单题目. /* */ #include <iostream> #include <sstream> #include <string> #include & ...