【技术·真相谈一谈游戏AI 真的搞懂寻路(二)

2023年 7月 14日 137.9k 0

如果你想要进步,别在意别人觉得你很愚蠢。

郑重说明:本文适合对游戏开发感兴趣的初级及中级开发和学习者,本人力图将技术用简单的语言表达清楚。鉴于水平有限,能力一般,文章如有错漏之处,还望批评指正,谢谢。

在前面一篇 谈一谈游戏AI - 真的搞懂寻路(一) 中我们谈到了一些经典寻路算法,较为常用的算法是 A*。在讲述这些算法时,我们使用的地图都是用简单的格子来表示。实际上,游戏中的地图有很多种不同的表示方式,如网格面的方式、顶点的方式等,不同的表示方式有自己的适用场景和局限性。但是不管如何表示地图,A* 等寻路算法都是可以使用的。

下面我们看看有哪些地图表示方法。

一、2D地图之格子划分(关注可行走区+阻挡)

将地图作为一个整体考虑,可行走区域和阻挡都用格子表示。格子一般有正方形、三角形和六边形。依赖于周围的N个格子来做寻路(如使用A*)。

image.png

一般来说,用正方形格子比较简单;
六边形格子的好处是到周围的格子的距离一样,格子看起来比较漂亮,用在很多策略游戏的地图设计中:

image.png

如果要用在实际可行走的地图中,六边形的计算坐标和找周边格子则比较复杂,性能没有正方形格子好。有兴趣的可以看看这篇文章:
Implementation of Hex Grids

使用 Grid 的好处是可以更精细的存储信息,例如倾斜度,不同地形,不同寻路消耗,各种标记等等meta信息。
坐标定位快。方便扩展,定制等。

image.png

但是高精度的格子和meta信息也意味着大量的内存占用,很多地图较大的 MMO 游戏中,单张地图资源的内存占用就有十几GB、几十GB之多;此外,精度高的时候,格子数量多,这样寻路算法(如A*算法)的性能也会随之下降。

二、2D地图之路点waypoint(关注可行走区)

路点 waypoint 的思路和 Grid 的不太一样,waypoint 只表示可行走的区域,对于寻路不会用到的阻挡区域不去管它。
waypoint 找出地图中的一些关键点,然后将这些关键点连接起来。寻路算法在实施时,关注的是当前关键点周围的关键点。

一张路点的经典图:

image.png

它其实非常类似于我们日常生活中的地铁线路图,地铁站就是路点:

image.png

找出起点和终点最近的地铁站,剩下的就是按照规划好的路线进行寻路了。

waypoints 的地图表示方法有很多好处很明显:

  • 使用 waypoints 可以将游戏地图划分成一些离散的区域,每个区域之间都有一个或多个连接点,这样一来,寻路算法就可以只考虑相邻的 waypoints 之间的连通关系,而不需要考虑地图上的每一个点,这样可以大大简化寻路算法的逻辑,提高寻路效率。

但它也有较大局限性:

  • 需要手动标记路点,工作量大
  • 由于路点都是一些离散的点,数量有限,规划出的路径有很僵硬的折角,走路不自然,容易走Z型
  • 线路固定,不能根据 agent(NPC)的半径做线路调整

image.png

waypoints 路点方法在某些特定场合非常好用:

  • 一些游戏中的押镖等固定线路玩法
  • 塔防类型的固定路线游戏

另外,提一点,waypoints 组成的边可以动态调整,如有个门,可以开关,对应边可以行走和不可行走,这对于可变场景是很方便的。

三、2D地图之多边形可见图(关注阻挡)

将地图场景中所有的阻挡用多边形表达出来,多边形顶点之间是直线

image.png

将阻挡多边形化,确定顶点,然后找出顶点之间的连接关系,最后将起点start和终点 end 加进来,一起绘制出可见图。找到的路径可能就像黄色路径所示。

局限性:

  • 每次寻路 start 和 end 每次都要在可见图中动态的添加和删除,可见图动态生成的消耗大;
  • 如果地图很大,阻挡特别多,这个可见图的形状会特别复杂,因此这种方式对于复杂的地图性能不高(如下图);

image.png

四、2D 地图之navmesh(关注可行走区域)

对于阻挡比较多的情况,我们反过来思考,把上图反过来,不关注阻挡,只关注可行走区域的分割。

前面我们也提到,waypoints 的方式也是只考虑可行走区域,只不过是在可行走区域里找出一些关键点,然后连线做路径。

其实还有一种方法,就是把可行走区域划分成一个个的不规则区块(网格navmesh),例如下图深色部分所示,把可行走区域划分成很多凸多边形:

image.png

如果我们用不规则凸多边形的相对中心点来表示这个区块,其实就可以将寻路算法(如A*)应用到区块间的寻路,效果如下:

image.png

网格是不重叠的凸多边形,这样任意一个多边形内的两点可以直线到达,而不会穿过别的区块:

image.png

如果我们要将前面 waypoints 中的地图用这种网格(navmesh)来重新组织,效果如下:

image.png

有时候,单个格子还是显得很大,就可以继续划分,精度提高之后,寻路的路径也就更自然。

4.1 unity中的navmesh寻路

在 Unity 引擎中,可以对一张 2D 地图提前生成好网格,这个过程称为烘焙(bake):标记待烘焙的场景(Navgation Static) -> 设置参数 -> Bake

image.png

在实际使用网格 navmesh 来寻路时,设置寻路agent的目的地即可,agent 会自动根据 bake 好的 navmesh来寻路到目的地:

public class navctl : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        NavMeshAgent agent = GetComponent();
        agent.SetDestination(new Vector3(-31.41f, -0.42f, 0.04f));
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

此外,unity 的 navgation 还支持设置地形的 AreaType,可以配置不同的寻路消耗

image.png

4.2 开源 recastnagivation 库

如果想在自己的引擎中编写 navmesh 寻路相关的代码,可以使用经典开源 navmesh 实现:recastnagivation(c++)库,跨平台;作者 Mikko Mononen 曾经是 CryTek 工作室的 AI 工程师,大名鼎鼎的 CryEngine 就都是 Crytek 工作室开发的。众多游戏引擎和 AI 项目的3D寻路都是基于这个库(Unity、Unreal 等)

github.com/recastnavig…

这个开源实现包含了:

  • Recast:烘焙(bake)生成navmesh数据

例如,可以将unity中的场景obj文件导出,利用 recast 库API来生成可以供服务器使用的 mesh 二进制数据(可以用官方提供的RecastDemo.exe GUI工具直接从obj文件导出二进制,build->save)

image.png

  • Detour:用生成的mesh来导航,使用 A* 算法在mesh中寻路
//加载网格数据
m_navMesh = loadAll("navmesh.bin");
m_navQuery->init(m_navMesh, 2048);

//使用MeshQuery对象来寻路
m_navQuery->findNearestPoly(m_spos, m_polyPickExt, &m_filter, &m_startRef, 0);
m_navQuery->findNearestPoly(m_epos, m_polyPickExt, &m_filter, &m_endRef, 0);
m_navQuery->findPath(m_startRef, m_endRef, m_spos, m_epos, &m_filter, m_polys, &m_npolys, MAX_POLYS);

五、3D地图之格子+多层(grid+layer)

我们之前在介绍 2D 地图的格子表示时谈到可以用 Grid 来表示整张地图。Grid 在表示 2D 地图时是没有问题的,但是如果涉及 3D 地形,由于地形往往存在重叠,Grid 就有很大的问题。如:

image.png

一种比较自然的思路是采用多层 Grid。示意图如下:

image.png

  • 2D的格子寻路+多层,行走会判断连通性,同一层可以认为是连通的,只需要判断2D阻挡;
  • 如果路径点是不同层,连通性需要在切层处额外增加判断;

通过普通二维 grid(大地)加上多层layer补丁(地上建筑、桥梁等)的形式,是可以表示这些地图结构的。

下面是一个人上桥的行走判断:

image.png

但是,经典难题来了:螺旋楼梯怎么处理?楼梯本身就是有重叠的,在不同高度按照我们的约定,应该属于不同的层级,但是切层点应该在哪里?

image.png

上图中,从垂直方向看,红色部分和临近的路面所在的网格是相邻的,因此此时根据层级来判断连通性有很大的局限性,在复杂地形中不能得到广泛应用。

六、3D 地图之 navmesh (可行走区域)

前面我们提到,navmesh 在 2D 地图中可以很方便的使用,其实 navmesh 在 3D 地形中也是适用的。
去掉场景内的阻挡物,只建模可行走区域。下面是unity中的navmesh示例

image.png

在前面我们讲过的会重叠的 3D 桥用 navmesh 表示如下:

image.png

navmesh寻路时,在通过A*算法找到一系列连通的polygon之后,可以选择polygon的顶点或者中心点来找到路径点,连接起来后就是最终的路径。此外,也会做一些优化,比如两两相邻线段是否可以拉直合并成一条直线。

navmesh寻路看起来非常有效,但是也有很大的局限性:

  • 地图建模复杂:Navmesh 寻路需要将地图划分成多网格,因此需要进行复杂的地图建模工作。如果地图结构复杂,建模工作量大,会影响开发效率。
  • 因为涉及几何体,可能会有浮点数的计划,消耗大;
  • 非贴地寻路(轻功)没法支持,只能沿着生成好的mesh来贴地寻路;
  • 地形的定制性差

七、3D地图之体素voxel(可行走区)

voxel最开始由天涯明月刀项目在2014年引入,用于场景的碰撞计算。

7.1 体素表达

类似2D地图中用格子来做像素化(pixel),3D世界中模型可以体素化。

image.png

实际在生成体素的时候,从每个格子的最低层向上做投影,就会产生多层的分割。举个例子,一间房子的投影:

image.png

上层为屋顶,人可以通过门从室外走入室内。在垂直方向,最下面为第0层,代表地表,往上依次为1、2层...

每一层会记录上下沿的高度值,行走时 agent 就在各个layer的上沿。

类似的,下面是一个桥的体素剖面图:

image.png

相邻的两个grid,其层id可能不同,这和之前 grid + layer 的表达方式不同,同一id层的grid不一定连通(比如高度相差很大),不同层的grid却可能连通。

下面是一个实际场景的体素化视图(场景很大,需要分割,只显示其中一块的体素效果)

image.png

7.2 碰撞/连通检测算法

从voxel A (x + y + layer) 是否能走到 voxel B(x + y + layer),会进行如下判断:

  • voxel B 所在grid的meta信息,是否支持行走,是否有阻挡(是可行走区),包括静态阻挡和动态阻挡
  • layer上沿高度差是否合理,假如agent身高1.8米,超过1.5米的可以认为走不上去(往下可以)
  • 是否能容纳 agent(如果 agent 身高固定,对于不能容纳agent的其实可以在体素化的时候把不能容纳的层之间连接起来)
  • 对于轻功飞行,是不在各个layer的上沿的,会在solid层之间的空间移动,从 voxel A(x + y + z + layer)飞行到 voxel B(x + y + z + layer) 进行如下判断:

  • voxel B 所在grid的meta信息,是否支持飞行,是否有阻挡
  • 是否能容纳 agent
  • 如果是垂直飞行,要判断往上的路径有没有碰到其他层的下沿
  • 检测示例:

    image.png

    体素的特点有:

    • 对CPU计算友好,没有浮点运算
    • 资源描述与2D的格子接近,可以用规格化的数组表述,每个grid 需要存储垂直投影的多个 layer
    • 资源格式自定义,可以存储很多的meta信息,例如是否是水,寻路消耗等,可以自由扩展以实现不同的需求
    • 内存占用较高,如果是非常大的地图,一张地图内存占用会很大。

    八、3D地图之八叉树

    在一些可飞行的星际类型的游戏中,往往有很多飞行的路径。此时可以考虑使用八叉树来表示 3D 空间。

    image.png

    在考虑飞行时,八叉树可以用来检测碰撞:

    image.png

    具体的,笔者研究不多,这里只做抛砖引玉,留待读者自己继续深入学习。

    小结

    在本文中,我们谈到了地图中常用的表示方式,涉及:

    • 2D 和 3D 游戏中地图表达,如 grid、waypoints、navmesh、体素等;
    • 不同地图表示方法的优缺点和适用场景;

    参考文献:

    Map representations

    现代游戏引擎 - 基础AI

    希望本文对大家有所帮助!

    作者:我是码财小子,会点编程代码,懂些投资理财;期待你的关注,不要错过我后续的文章更新。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论