禁止选择、避免选择和优先选择 NavTag

如果使用 NavTag 标记包含自定义数据的地形区域,则可以确定哪些 NavTag 被认为是可供每个角色行走的有效地形,即使这些角色使用相同的数据库(因此使用同一组 NavData)时也是如此。您也可以自定义不同的人物优先选择或避免选择穿越不同 NavTag 的范围。

例如,下图显示了穿过街道的三种可能的路径:

  1. 在路径 1 中,禁止穿过紫色的 NavTag。这将强制路径在人行横道横穿马路。
  2. 在路径 2 中,紫色 NavTag 被视为可导航,其成本与通过未标记区域的成本相同。这允许路径直接从起点到目标位置。
  3. 在路径 3 中,紫色 NavTag 被视为可导航,但是穿过它的成本是未标记的蓝色区域的两倍。这就使通过该区域的最终路径尽可能缩短。该路径不选择穿过最宽的街道部分,而是选择了更短的横穿路线,即使相对于使用人行横道也仍然属于捷径。

步骤 1. 标记地形

有关如何在数据生成阶段或游戏的动态过程中将 NavTag 应用到地形的详细信息,请参见使用自定义数据进行标记

步骤 2. 编写 TraverseLogic

若要在您自己的路径中禁止、首选或避免 NavTag,您需要编写自定义 TraverseLogic 类。此类用于下列情况:

当您针对 NavMesh 运行其他种类的查询时,也可以使用同样的 TraverseLogic 类测试 NavTag。例如,如果运行 RayCastQuery 来检测沿两点之间的直线路径上是否存在任何障碍,可以使用该类实例设置查询,它将使用此实例确定哪些不同的 NavTag 可以穿越。

以下为三个 TraverseLogic 接口。它们的默认实现具有三个不同的复杂性级别,您可以选择符合您要求的级别。

如果 TraverseLogic 之一作为模板参数传递给 NavigationProfile 或查询,则会自动生成相应的代码。请参见步骤 3. 设置 TraverseLogic

计算并存储三角形的成本倍增

TraverseLogicWithCostPerTriangle 中,在单个 PathFinderQuery 期间多次请求三角形的成本倍增。若要避免因实现欠缺而出现性能问题,TriangleCostMap 类用于计算并存储查询可以访问的一些三角形的成本倍增。您需要编写从 ITriangleCostMap 派生的 TriangleCostMap 类。它仅具有一个纯虚拟的 Recompute() 函数。

使用之前,必须通过 BindToDatabase() 函数将 ITriangleCostMap 绑定到数据库。如果数据的三角形化中存在某些更改,如动态 NavMesh,则数据库可内部调用 Recompute() 函数。客户可调用它来更新它的成本倍增,但这不能频繁执行。您可调用以下辅助函数:Recompute() 中的 ITriangleCostMap::InitAndBrowseAllTrianglesInBox3f()ITriangleCostMap::InitAndPropagateInTrianglesFromPosInVolume(),以自动初始化并设置三角形的成本倍增。有关详细信息,请参见 itrianglecostmap.h

您可以以自己的方式编写三角形成本的设置,但必须确保调用 InitCostMapForBox3f() 函数之后,再针对三角形调用 SetCostMultiplier() 函数,以更改成本倍增(默认成本倍增为 1.f)。

当某些人物针对三角形进行寻径时,您不能更改三角形的成本。建议取消使用 TriangleCostMap 的人物的异步路径计算。您不能频繁更新 TriangleCostMap,否则人物可能无法在两次更新之间有足够的时间来查找路径。以下代码示例显示在 Recompute() 函数中可以执行以取消路径计算的操作:

for (KyUInt32 i = 0; i < gameWorld->GetBots().GetCount(); ++i)
    {
        GameBot* gamebot = gameWorld->GetBots()[i];
        Kaim::Bot* navBot = gamebot->GetBot();
        if (navBot->IsComputingNewPath())
            navBot->CancelAsyncPathComputation();
    }

您可确定已跟随路径的人物是否需要重新启动路径计算以保持跟随与更新的 TriangleCostMap 一致的路径。

另请参见 DefaultTraverseLogic 类,在您没有设置自己的 TraverseLogic 时默认使用该类。如果直接从 DefaultTraverseLogicSimpleTraverseLogic 派生自定义类,只需要实现用于完成所需行为的替代方法。

控制 NavTag 变换

默认情况下,DefaultTraverseLogicSimpleTraverseLogic 类可防止所有移动穿过禁止的 NavTag。

但是,在某些特殊情况下,您可能要仅在一个方向上禁止导航穿过某一 NavTag,或仅从某些其他 NavTag 穿过。在这种方法中,您允许在 NavTag 所覆盖的区域中移动,但禁止跨越 NavTag 边界的移动。

在这种情况下,您必须:

  • 直接从 SimpleTraverseLogic 派生类,并提供 Kaim::LogicDoUseCanEnterNavTag 作为模板参数。例如:
    class MyTraverseLogicClass : public Kaim::SimpleTraverseLogic<Kaim::LogicDoUseCanEnterNavTag>
    {
        ...
    };

    或者,如果您从头开始编写自己的 TraverseLogic 类,而不是从一个现有的类进行派生,请在您的类中包含以下行:

    typedef Kaim::LogicDoUseCanEnterNavTag CanEnterNavTagMode;
  • 设置 CanTraverse() 方法的实现以允许移动穿越 NavTag。
  • 设置 CanEnterNavTag() 方法的实现以确定是否应允许从上一 NavTag 变换到此 NavTag。

为了获得最佳性能,应该仅在有特定的游戏玩法用例时设置 TraverseLogic 以检查 NavTag 变换。

用户数据

TraverseLogic 接口中的所有方法都是静态的;因此,它们不与任何特定对象或状态绑定。这样不需要在运行时为要使用的人物和查询创建任何类实例,从而节省运行时内存。

但是,为了使 TraverseLogic 做出良好决定,以确定是否需要禁止 NavTag,或在不同环境中应该指定给不同 NavTag 的成本倍增,您可能需要使用绑定到对象的特定实例的某些种类的数据。出于此原因,所有 TraverseLogic 方法都接受无效指针。设置了模板以使用 TraverseLogic 的每个类(最明显的是 Bot 类,以及诸如 AStarQueryRayCastQuery 等查询类)提供取值函数,可以用于设置和检索 BotQuery 在需要调用 TraverseLogic 接口中的方法时传递的用户数据。例如:

Bot::SetBotTraverseLogicUserData()
Bot::GetBotTraverseLogicUserData()

IQuery::SetTraverseLogicUserData()
IQuery::GetTraverseLogicUserData()

步骤 3. 设置 TraverseLogic

设置人物:

  1. 编写从 NavigationProfile 类派生的您自己的类,并将 TraverseLogic 类的名称作为 NavigationProfile 的模板参数进行传递。您不必替代 NavigationProfile 类中的任何虚拟方法(如果不需要替代);例如:
    class MyNavigationProfile : public Kaim::NavigationProfile<MyTraverseLogicClass>
    {
    };
  2. 设置 WorldBots 以使用新的 NavigationProfile。有关详细信息,请参见 自定义路径跟随

设置您将自己处理的查询:

限制

如何计算成本

每条线路分段的成本将由 RayCanGoQuery(由 AStarQuery 或路径跟随系统在内部运行)单独计算,以测试路径是否存在障碍物。每次由 RayCanGoQuery 测试的线路分段穿过 NavTag 边界,查询将边界点垂直投影到该线路分段。然后将沿其线路分段的每个子分段的距离乘以指定给相应的 NavTag 的成本倍增。

例如,在以下情景中,RayCanGoQuery 测试的线路分段在通过 NavMesh 三角形传播时跨越了多个不同的 NavTag:

总成本的计算方法如下:

                        ( Distance(a) * CostMultiplier(blue) )
                    +   ( Distance(b) * CostMultiplier(grey) )
                    +   ( Distance(c) * CostMultiplier(blue) )
                    + ( Distance(d) * CostMultiplier(purple) )
                    +   ( Distance(e) * CostMultiplier(blue) )
                    +   ( Distance(f) * CostMultiplier(grey) )
                    +   ( Distance(g) * CostMultiplier(blue) ) 

在确定完整路径的成本时,以这种方式对每个路径分段进行测试,总成本是所有分段的费用之和。

请注意,出于性能方面的原因,这一测量成本的方法不使用路径通过 NavMesh 的三角形所实际覆盖的精确距离,也不是角色沿该路径移动的精确距离。但是,这种近似值应足够接近,以允许您找到一组成本倍增,从而能够产生您的游戏玩法所需要的效果。

如果直接在自己的代码中运行 RayCanGoQuery,您可以对其进行配置,以计算测试的线路的成本。调用其 RayCanGoQuery::SetComputeCostMode() 方法,并传递 QUERY_COMPUTE_COST_ALONG_3DAXIS,确保使用应该用于确定 NavTag 的适航性和成本倍增的 TraverseLogic 来设置查询。运行查询之后,您可以通过调用 RayCanGoQuery::GetComputedCost() 检索成本。

教程示例

有关演示自定义 TraverseLogic 类的使用以影响由 A* 查询生成的路径以及用作谓词以在路径跟随期间禁止特定 NavTag 的工作代码示例,请参见 Tutorial_NavTag.cpp 文件。