使用碰撞系统

Gameware Navigation 包含一个复杂性较低的优化系统,可用于针对层几何体在空间中的两个点之间执行高性能碰撞测试(光线投射)。

例如,您可以使用该系统执行以下操作:

通常,这些测试由物理子系统执行。然而,对于许多游戏(通常为 MMO),网络通信延迟、技术复杂性和/或运行时性能开销可能使一个完善的物理系统的集成出现问题或受到禁止。如果您的项目出现这种情况,但仍然需要针对游戏关卡中的几何体进行这些种类的碰撞测试,那么您或许可以利用 Gameware Navigation 碰撞系统。

仅静态对象

内置的碰撞系统依赖于预生成的数据,该数据独立于您的地形的 NavData。因此,它只能检测与您在数据生成过程中提供的地形的静态元素的碰撞。它不能处理您在运行时添加的动态对象,即使您将这些对象集成到您的地形的 NavData 中也是如此。

先决条件

为了生成碰撞数据,使用 C++ NavData 生成 API。如果尚未将 NavData 生成 API 集成到您的游戏编辑器、关卡设计工具或构建过程,请参见集成阶段 6:使用 NavData 生成 API

步骤 1. 生成碰撞数据

在 NavData 生成代码中,需要为地形中每个所需的地块激活碰撞数据生成。

为地块激活碰撞数据生成:

当您为该地块生成 NavData 时,其碰撞数据也将从传递给该地块的 NavData 生成系统的三角形自动生成。每个地块的数据将写入到磁盘上的 .ColData 文件中,该文件的保存位置和文件名与为该地块生成的 .NavData 文件的保存位置和文件名相同。

例如:

// Create a sector...
Kaim::GeneratorSectorConfig sectorConfig(Kaim::KyGuid::GetDefaultGuid(), generationName);
Kaim::GeneratorSector* sector = env.m_generatorInputOutput.AddSector(sectorConfig);
...
// Activate collision data generation
sector->SetColDataBuildMode(Kaim::GenFlags::SECTOR_COLDATA_BUILD_ENABLED);
...
// Generate NavData and collision data
KyResult result = m_generator->Generate(m_generatorInputOutput);

选项:使用 heightfield 以获得更好的性能

您可以为 NavData 生成系统提供 heightfield 和三角形网格,而不是为其提供构成地形每个地块的单个三角形。

heightfield 是一种优化的方式,用于表达简单单层地形的海拔高度。它由单个原点以及一组围绕原点在常规栅格中采样的海拔高度值组成,而不是包含构成地形网格的所有三角形。由于与完整三角形网格相比,数据更加精简、复杂性更低,因此其在碰撞测试中占用的运行时内存量更少,并且性能更高。

使用 heightfield 的负面影响是,它们通常对于在栅格中的采样间距内发生的高度局部变化也会不太精确。这会导致从 heightfield 生成的 NavMesh 以及针对它们执行的碰撞测试不太精确。为了抵消此缺乏精确性问题,您也可以为地形的较复杂区域(这些区域需要针对地形细小特征提供较接近的逼真度)提供三角形网格。

为了获得效率和精确度之间的最佳折衷结果,请使用 heightfield 表示完全开放的区域(例如田地),使用三角形网格来表示需要更高精度的区域(例如城市街区)。例如:

若要为地块提供 heightfield 和索引网格,必须更新 GeneratorInputProducer 中的 GeneratorInputProducer::Produce() 实现(在 NavData 生成管线中使用以将地形几何体传递给 NavData 生成系统)。

为地块提供 heightfield:

  • 对于要为地块提供的每个 heightfield,GeneratorInputProducer 应创建 Kaim::Heightfield 类的一个实例,使用当前地块的数据设置它,然后在 ClientInputConsumer::ConsumeHeightField() 调用中传递指针。

为地块提供三角形网格:

  • 对于要为地块提供的每个网格,GeneratorInputProducer 应创建 Kaim::IndexedMesh 类的一个实例,使用当前地块的数据设置它,然后在 ClientInputConsumer::ConsumeIndexedMesh() 调用中传递指针。

请注意,如果使用此方法,将在 heightfield 和三角形网格上生成 NavData。因此,无需直接向 ClientInputConsumer 提供单个三角形。

步骤 2. 设置运行时 World

每个 World 都在其 World::m_collisionWorld 类成员中维护 CollisionWorld 类的一个实例。该类负责聚合您加载到内存中的碰撞数据,以及管理针对该数据执行的碰撞测试。它通过调用实现 ICollisionInterface 的类实例的方法(您需要在初始化 World 之后提供)来执行此操作。

Gameware Navigation 包含依赖于开源 Bullet 物理和碰撞测试系统的 ICollisionInterface 实现。有关详细信息,请参见 www.bulletphysics.org

使用默认的 ICollisionInterface 实现:

  1. integration\gwnavruntimeglue\bulletcollisioninterface 目录中的所有头文件和源文件添加到您的项目,并将您的编译器配置为在该目录中查找包含文件。
  2. 在运行时初始化代码中,创建 CollisionInterface 类的一个新实例。
  3. 创建 World 后,将其 World::m_collisionWorld 设置为使用您的接口对象(通过在 CollisionWorld::SetCollisionInterface() 方法调用中传递)。

例如:

m_navigationWorld = *KY_NEW Kaim::World(databaseCount);
Kaim::Ptr<Kaim::ICollisionInterface> visInterface = *KY_NEW CollisionInterface(m_navigationWorld);
m_navigationWorld->m_collisionWorld->SetCollisionInterface(visInterface);

步骤 3. 在运行时加载碰撞数据

如果需要针对您为地形创建的碰撞数据执行碰撞测试,您需要将碰撞数据加载到内存中,并将其添加到碰撞世界。通常,您应该在将地形每一地块所需的 NavData 和其他种类的数据以流式加载方式载入和清除出内存时,为相应地块执行此操作。该过程非常类似于将 NavData 以流式加载方式载入和清除出 Database

在运行时将碰撞数据加载到碰撞世界中:

  1. 创建新的 CollisionData 对象。
  2. 将碰撞数据加载到该对象中。您可以利用使用预生成的 NavData 填充 NavData 对象时所用的相同方法:

    • 调用 CollisionData::Load() 以直接从磁盘上的 .ColData 文件或从文件流加载数据。
    • 将碰撞数据加载到内存中,并调用 CollisionData::LoadFromMemory() 以从您提供的缓冲区中初始化 CollisionData 对象。您可以通过以下方式获取此内存缓冲区:从文件自行读取,或者如集成阶段 6a:将 NavData 集成到资源管线中所述,将其与其他地块相关资源打包在一起。

    这两种方法结果是相同的;您可以使用更适合您的资源管线的方法。

  3. 检索在 World::m_collisionWorld 中维护的 CollisionWorld 对象,调用其 CollisionWorld::AddCollisionData() 方法,并将指针传递给 CollisionData 对象。

例如:

Kaim::Ptr<Kaim::CollisionData> colData = *KY_NEW Kaim::CollisionData;
if (Kaim::Result::Fail(colData->Load(GetAbsoluteInputFileName(fileName).c_str(), &m_fileOpener)))
		return KY_NULL;

GetWorld()->m_collisionWorld->AddCollisionData(colData);

从碰撞世界中删除碰撞数据:

例如:

GetWorld()->m_collisionWorld->RemoveCollisionData(colData);

步骤 4. 执行碰撞查询

若要针对您加载到 World 中的碰撞数据运行碰撞测试,请设置并启动 CollisionRayCastQuery 类的一个实例。

例如,以下代码测试沿点 A 和点 B 之间的直线路径的碰撞:

Kaim::CollisionRayCastQuery query;
query.BindToWorld(m_navigationWorld);
query.Initialize(pointA, pointB);
query.PerformQuery();

if (query.GetResult() == Kaim::RayHit)
    ... // A collision was found
if (query.GetResult() == Kaim::RayDidNotHit)
    ... // No collision was detected

您还可以利用查询系统提供的机制在 World 维护的队列中或在使用每帧最大时间预算创建和设置的自定义队列中异步运行 CollisionRayCastQuery。如果您的游戏可能需要在同一帧中请求多次碰撞测试,则这些方法可帮助避免 CPU 峰值。

有关详细信息,请参见使用查询系统