UE4的导航系统:基于Tile的导航

前言

UE4的导航使用的是RecastDetour组件,这是一个开源组件,主要支持3D场景的导航网格导出和寻路,或者有一个更流行的名字叫做NavMesh。不管是Unity还是UE都使用了这一套组件。

Github上有更为详细的源码、Demo和说明:https://github.com/recastnavigation/recastnavigation

这一篇会阐述UE4是如何划分Tile,并基于Tile构建导航数据的。在后续的文章中,会详细介绍每个Tile如何构建导航数据。

Tile的概念

在 Unreal Engine 4 (UE4) 中,Tile(瓦片)是构建导航网格的基本单元。每个 Tile 是一个正方形的格子,具有独立的导航网格数据,这种设计使得导航系统在性能和灵活性上都得到了显著提升。以下是对 Tile 概念及其在导航网格构建中的作用的详细分析。

Tile 的概念

  1. 正方形格子

    • Tile 是一个固定大小的正方形区域,通常用于划分游戏世界中的导航网格。每个 Tile 可以独立处理其导航数据,便于管理和更新。
  2. 独立的导航网格数据

    • 每个 Tile 拥有自己的导航网格数据,这意味着可以在不影响其他 Tile 的情况下,对某个 Tile 的导航数据进行修改或更新。

Tile 的作用

  1. 支持多线程构建导航网格数据

    • 由于每个 Tile 的导航网格数据是独立的,UE4 可以利用多线程并行构建多个 Tile 的导航数据。这种方式显著提高了导航网格的构建效率,尤其是在大型场景中。
  2. 动态更新导航网格

    • 通过标记每个 Tile 的 Dirty 属性,开发者可以在运行时动态更新特定 Tile 的导航网格。这种灵活性使得游戏能够根据环境变化(如障碍物的移动)实时调整导航路径。
  3. 以 Tile 为粒度加载导航网格

    • 在使用静态导航网格时,可以按 Tile 的粒度加载和卸载导航数据。这种方式可以有效管理内存,减少不必要的资源占用,尤其是在大型开放世界游戏中。

Tile 的导航数据构建步骤

在构建导航数据时,每个 Tile 会经历以下步骤:

  1. 体素化(Voxelization)

    • 将 Tile 中的几何体转换为体素表示。体素化过程将场景中的三维信息转化为离散的体素数据,便于后续处理。
  2. 构造连续区域

    • 根据体素中的信息,识别并构造出可行走的连续区域。这一步骤确保了导航网格能够反映出实际可行走的空间。
  3. 计算连通性

    • 计算每个 cell(单元格)与周围 cell 的连通性。这一步骤是为了确保 AI 能够在导航网格中找到有效的路径。
  4. 裁剪可行走区域

    • 根据配置的 agentRadius(代理半径),裁剪出可行走区域。这一过程确保了导航网格能够适应不同大小的角色或物体。
  5. 划分区域

    • 将可行走区域划分为不同的区域,以便于后续的路径查找和导航决策。
  6. 竖直方向分层并合并

    • 在竖直方向上对区域进行分层,并合并相邻的层。这一过程有助于处理多层建筑或复杂地形中的导航问题。
  7. 写入 rcHeightfieldLayer 类

    • 将所有构建的数据写入到 rcHeightfieldLayer 类中,以供 Detour(UE4 的寻路系统)使用。这一步骤是将构建的导航数据整合到最终的导航网格中。

总结

Tile 的设计在 UE4 的导航系统中起到了至关重要的作用。通过将导航网格划分为独立的 Tile,UE4 能够实现高效的多线程构建、动态更新和按需加载。这种灵活的导航网格管理方式不仅提高了性能,还增强了游戏的可玩性和响应性。后续的文章将深入探讨每个步骤的具体实现和优化策略,帮助开发者更好地理解和利用基于 Tile 的导航数据构建。

NavigationSystem的八叉树

在 Unreal Engine 4 (UE4) 中,导航系统使用八叉树(Octree)来高效管理与导航相关的物理数据。八叉树的结构使得空间划分更加高效,能够快速定位和更新与导航相关的对象。以下是关于八叉树如何更新的详细说明。

八叉树的基本概念

八叉树是一种空间划分的数据结构,适用于三维空间中的对象管理。在 UE4 的导航系统中,八叉树用于存储和管理与导航相关的 Actor 和 Component 的数据。通过使用 TOctree2 模板类,UE4 能够高效地进行空间查询和更新。

更新八叉树的流程

当与导航相关的 Actor 或 Component 的 Transform 发生变化时,导航系统会通过一系列的事件和委托来更新八叉树。以下是更新流程的详细步骤:

  1. Transform 变化的检测
    USceneComponent 的 Transform 发生变化时,确实会通过 InternalSetWorldLocationAndRotation 方法进行检测和处理。以下是这个过程的详细说明:

Transform 变化的检测流程

  1. Transform 变化的调用

    • 当需要更新 USceneComponent 的位置和旋转时,通常会调用 InternalSetWorldLocationAndRotation 方法。这个方法是设置组件的新位置和旋转的核心逻辑。
    bool USceneComponent::InternalSetWorldLocationAndRotation(FVector NewLocation, const FQuat& RotationQuat, bool bNoPhysics, ETeleportType Teleport)
    {
        // ... 其他逻辑
    
        // 检查组件是否注册且可以影响导航
        if (IsRegistered() && bCanEverAffectNavigation)
        {
            PostUpdateNavigationData(); // 调用更新导航数据的方法
        }
    
        // ... 其他逻辑
    }
    
  2. 条件检查

    • 在调用 PostUpdateNavigationData 之前,首先会检查两个条件:
      • IsRegistered():确保组件已经注册到世界中。这意味着组件已经被添加到场景中,并且可以参与游戏逻辑。
      • bCanEverAffectNavigation:这是一个布尔值,指示该组件是否可以影响导航系统。如果该值为 true,则表示该组件的变化可能会影响导航网格的生成和更新。
  3. 调用 PostUpdateNavigationData

    • 如果上述条件都满足,PostUpdateNavigationData 方法将被调用。这个方法的主要作用是通知导航系统该组件的 Transform 已经发生变化。
    void USceneComponent::PostUpdateNavigationData()
    {
        SCOPE_CYCLE_COUNTER(STAT_ComponentPostUpdateNavData); // 性能计数器
        FNavigationSystem::OnComponentTransformChanged(*this); // 通知导航系统
    }
    
  4. 通知导航系统

    • PostUpdateNavigationData 方法中,使用 FNavigationSystem::OnComponentTransformChanged 静态方法来通知导航系统。这个方法会触发相应的委托,进而执行导航系统的更新逻辑。

总结

通过这种机制,USceneComponent 的 Transform 变化能够被有效地检测到,并且在必要时通知导航系统进行更新。这种设计确保了导航数据的实时性和准确性,使得 AI 和其他依赖导航的系统能够在动态环境中做出正确的决策。开发者可以利用这一机制来确保游戏中的导航系统始终反映当前的场景状态。

  1. 触发导航系统更新
    USceneComponent 的 Transform 发生变化并调用 PostUpdateNavigationData 方法后,OnComponentTransformChanged 委托会被触发。这一过程是导航系统更新的关键环节。以下是这个过程的详细说明:

触发导航系统更新的流程

  1. 触发委托

    • PostUpdateNavigationData 方法中调用 FNavigationSystem::OnComponentTransformChanged(*this),这会触发 OnComponentTransformChanged 委托。这个委托是导航系统用来响应组件 Transform 变化的机制。
    void USceneComponent::PostUpdateNavigationData()
    {
        SCOPE_CYCLE_COUNTER(STAT_ComponentPostUpdateNavData); // 性能计数器
        FNavigationSystem::OnComponentTransformChanged(*this); // 通知导航系统
    }
    
  2. 调用 UpdateNavOctreeAfterMove

    • OnComponentTransformChanged 委托的实现中,会调用 UpdateNavOctreeAfterMove 方法。这个方法的主要职责是检查八叉树(Nav Octree)是否需要更新,并在必要时进行更新。
    void FNavigationSystem::OnComponentTransformChanged(USceneComponent& Component)
    {
        // 其他逻辑...
    
        UpdateNavOctreeAfterMove(Component);
    }
    
  3. 检查八叉树更新的必要性

    • UpdateNavOctreeAfterMove 方法会根据组件的新位置和旧位置来判断是否需要更新八叉树。具体来说,它会检查以下几个方面:
      • 组件是否在导航网格的有效范围内。
      • 组件的移动是否超出了当前八叉树节点的边界。
      • 其他可能影响导航的条件。
    void FNavigationSystem::UpdateNavOctreeAfterMove(USceneComponent& Component)
    {
        // 检查组件的移动是否需要更新八叉树
        if (ShouldUpdateNavOctree(Component))
        {
            // 执行八叉树更新逻辑
            UpdateNavOctree(Component);
        }
    }
    
  4. 执行八叉树更新

    • 如果判断需要更新八叉树,UpdateNavOctree 方法将被调用。这个方法会处理八叉树的更新逻辑,包括:
      • 从八叉树中移除旧的导航数据。
      • 在新的位置插入更新后的导航数据。
      • 可能还会涉及到重新计算导航网格,以确保导航系统的准确性。

总结

通过 OnComponentTransformChanged 委托的触发,导航系统能够及时响应组件 Transform 的变化,并通过 UpdateNavOctreeAfterMove 方法检查和更新八叉树。这一机制确保了导航数据的实时性和准确性,使得 AI 和其他依赖导航的系统能够在动态环境中做出正确的决策。开发者可以利用这一机制来确保游戏中的导航系统始终反映当前的场景状态,从而提升游戏的整体体验。

  1. 更新八叉树元素
    在导航系统中,更新八叉树元素的过程是确保导航数据准确性的重要环节。具体来说,当组件的 Transform 发生变化并需要更新八叉树时,UpdateNavOctreeElement 方法会被调用。以下是这个过程的详细说明:

更新八叉树元素的流程

  1. 调用 UpdateNavOctreeElement

    • 当需要更新八叉树中的元素时,UpdateNavOctreeElement 方法会被调用。这个方法负责处理八叉树中元素的注册和注销。
    void FNavigationSystem::UpdateNavOctreeElement(USceneComponent& Component)
    {
        // 检查当前元素是否已经存在于八叉树中
        if (NavOctree->IsElementRegistered(Component))
        {
            // 如果存在,注销原有的节点
            NavOctree->UnregisterElement(Component);
        }
    
        // 注册新的节点
        NavOctree->RegisterElement(Component);
    }
    
  2. 检查元素是否存在

    • UpdateNavOctreeElement 方法中,首先会调用 IsElementRegistered 方法来检查当前的组件(元素)是否已经存在于八叉树中。这是为了避免重复注册。
  3. 注销原有节点

    • 如果组件已经存在于八叉树中,调用 UnregisterElement 方法将其从八叉树中注销。这一步是必要的,因为组件的 Transform 发生了变化,原有的节点位置可能不再有效。
  4. 注册新的节点

    • 在注销原有节点后,调用 RegisterElement 方法将组件的新位置注册到八叉树中。这一步确保了导航系统能够使用组件的新位置进行路径计算和导航。

总结

通过 UpdateNavOctreeElement 方法,导航系统能够有效地管理八叉树中的元素。这个过程确保了当组件的 Transform 发生变化时,导航数据能够及时更新,从而保持导航系统的准确性和实时性。开发者可以利用这一机制来确保游戏中的导航系统始终反映当前的场景状态,提升 AI 和其他依赖导航的系统的表现。

  1. 标记脏区域
    在导航系统中,脏区域(Dirty Areas)的管理是确保导航数据准确性和效率的重要机制。脏区域是指那些由于 Actor 或 Component 的位置、形状或其他属性发生变化而需要重新计算导航数据的区域。以下是脏区域管理的详细说明:

脏区域的定义与标记

  1. 脏区域的定义

    • 脏区域是指那些由于场景中某些元素的变化而需要更新导航网格的区域。这些变化可能包括:
      • Actor 或 Component 的位置变化。
      • Actor 或 Component 的形状或大小变化。
      • 新的障碍物的添加或现有障碍物的移除。
  2. 标记脏区域

    • 当 Actor 或 Component 的 Transform 发生变化时,导航系统会通过注销原有的八叉树节点并调用 RemoveNavOctreeElementId 方法来标记受影响的区域为脏区域。这一过程确保了相关的区域在后续的更新中被重新计算。
    void FNavigationSystem::RemoveNavOctreeElementId(FNavOctreeElementId ElementId)
    {
        // 获取元素的边界
        FBox ElementBounds = GetElementBounds(ElementId);
    
        // 标记受影响的区域为脏区域
        MarkDirtyAreas(ElementBounds);
    }
    

脏区域的管理与更新

  1. 脏区域的存储

    • 脏区域通常会被存储在一个集合中,以便在后续的更新过程中进行处理。这个集合可以是一个简单的列表或更复杂的数据结构,具体取决于实现的需求。
    TArray<FBox> DirtyAreas; // 存储脏区域的数组
    
  2. 更新脏区域

    • 在导航网格生成器(如 FRecastNavMeshGenerator)中,会定期检查脏区域,并对其进行更新。更新过程通常包括:
      • 遍历所有脏区域。
      • 重新生成相应的 Tile。
      • 清理已处理的脏区域,以便为下一次更新做好准备。
    void FRecastNavMeshGenerator::UpdateDirtyAreas()
    {
        for (const FBox& DirtyArea : DirtyAreas)
        {
            // 重新生成相应的 Tile
            GenerateTile(DirtyArea);
        }
    
        // 清空已处理的脏区域
        DirtyAreas.Empty();
    }
    

优化与性能

  1. 避免不必要的计算

    • 通过标记脏区域,导航系统能够避免对整个导航网格进行不必要的重新计算。只有那些受影响的区域会被更新,从而提高了性能。
  2. 增量更新

    • 脏区域的管理允许导航系统进行增量更新,即只更新那些实际发生变化的区域。这种方法在动态场景中尤为重要,因为它可以显著减少计算开销。

总结

脏区域的管理机制在导航系统中起着至关重要的作用。通过标记和管理脏区域,导航系统能够确保导航数据的准确性,同时避免不必要的计算。这种机制不仅提高了性能,还使得动态场景中的导航系统能够更有效地响应环境变化,从而提升了游戏的整体体验。开发者可以利用这一机制来优化导航系统的表现,确保 AI 和其他依赖导航的系统能够在不断变化的环境中做出正确的决策。

标记脏区域的流程

  1. 调用 RemoveNavOctreeElementId

    • UpdateNavOctreeElement 方法中,当需要注销原有的节点时,会调用 RemoveNavOctreeElementId 方法。这个方法的主要职责是从八叉树中移除元素,并标记受影响的区域为脏区域。
    void FNavigationSystem::UnregisterElement(USceneComponent& Component)
    {
        // 获取当前元素的 ID
        FNavOctreeElementId ElementId = GetElementId(Component);
    
        // 调用方法标记脏区域
        RemoveNavOctreeElementId(ElementId);
    }
    
  2. 标记脏区域

    • RemoveNavOctreeElementId 方法中,会根据组件的旧位置和大小来确定受影响的区域,并将这些区域标记为脏区域。这意味着这些区域的导航数据需要在后续的更新中被重新计算。
    void FNavigationSystem::RemoveNavOctreeElementId(FNavOctreeElementId ElementId)
    {
        // 获取元素的边界
        FBox ElementBounds = GetElementBounds(ElementId);
    
        // 标记受影响的区域为脏区域
        MarkDirtyAreas(ElementBounds);
    }
    
  3. 更新导航网格生成器

    • 标记为脏区域后,导航网格生成器(如 FRecastNavMeshGenerator)会在后续的更新过程中检查这些脏区域,并重新生成相应的 Tile。这确保了导航网格的准确性和实时性。
    void FRecastNavMeshGenerator::UpdateDirtyAreas()
    {
        // 遍历所有脏区域并更新导航网格
        for (const FBox& DirtyArea : DirtyAreas)
        {
            // 重新生成相应的 Tile
            GenerateTile(DirtyArea);
        }
    }
    

总结

通过在注销节点时调用 RemoveNavOctreeElementId 方法,导航系统能够有效地标记受影响的区域为脏区域。这一机制确保了导航网格生成器能够在后续的更新中重新计算和生成受影响的 Tile,从而保持导航数据的准确性和实时性。开发者可以利用这一机制来确保游戏中的导航系统始终反映当前的场景状态,提升 AI 和其他依赖导航的系统的表现。

总结

通过上述流程,UE4 的导航系统能够高效地更新八叉树,以反映场景中 Actor 和 Component 的变化。这种机制确保了导航网格的实时性和准确性,使得 AI 能够在动态环境中做出正确的导航决策。八叉树的使用不仅提高了空间管理的效率,还为复杂场景的导航提供了必要的支持。以下是对八叉树更新机制的进一步总结和扩展:

进一步的细节

  1. 脏区域的管理

    • 在导航系统中,脏区域(Dirty Areas)是指那些需要重新计算导航数据的区域。当 Actor 或 Component 的位置或形状发生变化时,相关的脏区域会被标记,以便在后续的更新过程中进行处理。这种机制确保了导航网格的准确性,避免了不必要的计算。
  2. 父节点更新

    • 在更新八叉树元素时,UpdateNavOctreeParentChain 方法会被调用,以确保所有父节点也得到更新。这是因为在八叉树中,子节点的变化可能会影响到其父节点的状态,因此需要向上更新整个父链。
  3. 多线程支持

    • UE4 的导航系统设计考虑了多线程的支持。在某些情况下,导航数据的更新可能会在不同的线程中进行,这要求系统能够安全地处理并发访问和修改。这种设计使得导航系统在大型场景中仍然能够保持高效。
  4. 网络同步

    • 在网络游戏中,导航数据的更新需要考虑到客户端和服务器之间的同步。导航系统会根据网络模式(如客户端或服务器)决定是否进行更新,以确保所有玩家都能看到一致的导航信息。
  5. 性能优化

    • 为了提高性能,导航系统会在更新时使用一些优化策略。例如,只有在必要时才会进行八叉树的更新,避免频繁的注册和注销操作。此外,导航系统还会使用缓存机制来减少重复计算。

实际应用

在实际开发中,开发者可以通过以下方式与导航系统进行交互:

  • 自定义导航行为:开发者可以通过实现 INavRelevantInterface 接口来定义自定义的导航行为,使得特定的 Actor 或 Component 能够影响导航网格的生成和更新。

  • 事件驱动更新:通过监听特定的事件(如 Actor 的注册、注销、移动等),开发者可以主动触发导航系统的更新,确保导航数据的实时性。

  • 调试和优化:UE4 提供了调试工具,可以帮助开发者可视化导航网格和脏区域,便于发现和解决潜在的问题。

结论

UE4 的导航系统通过八叉树的使用,提供了一种高效的方式来管理和更新导航数据。通过事件驱动的更新机制、脏区域管理、父节点更新等策略,导航系统能够在动态环境中保持高效和准确。这种设计不仅适用于单人游戏,也能很好地支持网络游戏中的多玩家场景。开发者可以利用这些功能来创建复杂的 AI 导航行为,提升游戏的整体体验。

NavigationSystem分块构建过程

在UE4中,导航系统的分块构建过程是一个复杂而高效的机制,旨在支持动态场景中的导航数据更新。以下是对该过程的详细分析,特别是从UNavigationSystemV1::Build()开始的全量构建流程。

1. 收集导航包围盒

构建过程的第一步是收集场景中所有的ANavMeshBoundsVolume包围盒。这一过程在GatherNavigationBounds()函数中实现,主要通过遍历世界中的所有相关类来完成。

void UNavigationSystemV1::GatherNavigationBounds(TArray<FNavigationBounds>& OutBounds)
{
    // 遍历场景中的所有NavMeshBoundsVolume
    for (TActorIterator<ANavMeshBoundsVolume> It(World); It; ++It)
    {
        // 收集包围盒信息
        FNavigationBounds NewBounds;
        NewBounds.Bounds = It->GetComponentsBoundingBox();
        OutBounds.Add(NewBounds);
    }
}

2. Tile的划分

对于每个包围盒,使用配置的TileSizeCellSize来划分Tile。每个Tile的大小是TileSize * CellSize,并且每个Tile都是一个轴对齐的包围盒(AABB)。这一过程在MarkDirtyTiles()函数中实现。

void FRecastNavMeshGenerator::MarkDirtyTiles(const TArray<FNavigationDirtyArea>& DirtyAreas)
{
    TSet<FPendingTileElement> DirtyTiles;
    for (const FNavigationDirtyArea& DirtyArea : DirtyAreas)
    {
        FBox AdjustedAreaBounds = DirtyArea.Bounds;
        const FRcTileBox TileBox(AdjustedAreaBounds, RcNavMeshOrigin, TileSizeInWorldUnits);
        
        for (int32 TileY = TileBox.YMin; TileY <= TileBox.YMax; ++TileY)
        {
            for (int32 TileX = TileBox.XMin; TileX <= TileBox.XMax; ++TileX)
            {
                FPendingTileElement Element;
                Element.Coord = FIntPoint(TileX, TileY);
                DirtyTiles.Add(Element);
            }
        }
    }
    
    // 将脏区域的Tile记录到PendingDirtyTiles中
    PendingDirtyTiles.Empty(DirtyTiles.Num());
    for (const FPendingTileElement& Element : DirtyTiles)
    {
        PendingDirtyTiles.Add(Element);
    }

    // 按距离主角的远近对Tile进行排序
    if (NumTilesMarked > 0)
    {
        SortPendingBuildTiles();
    }
}

3. 多线程生成导航数据

在构建过程中,UE4使用多线程来生成导航数据。每个Tile由一个独立的任务(Task)负责,这部分逻辑在FRecastNavMeshGenerator::ProcessTileTasks()中实现。EnsureBuildCompletion()函数负责驱动这个过程。

void FRecastNavMeshGenerator::EnsureBuildCompletion()
{
    do 
    {
        const int32 NumTasksToProcess = (bDoAsyncDataGathering ? 1 : MaxTileGeneratorTasks) - RunningDirtyTiles.Num();
        ProcessTileTasks(NumTasksToProcess);
        
        // 等待所有任务完成
        for (FRunningTileElement& Element : RunningDirtyTiles)
        {
            Element.AsyncTask->EnsureCompletion();
        }
    }
    while (GetNumRemaningBuildTasks() > 0);
}

4. Tile的生成过程

每个任务负责对其对应的Tile进行完整的体素化、分层、区域划分和构建相邻三角形导航数据等过程。关键函数包括FRecastTileGenerator::GenerateCompressedLayers()FRecastTileGenerator::GenerateNavigationData()

bool FRecastTileGenerator::GenerateCompressedLayers(FNavMeshBuildContext& BuildContext)
{
    // 创建高度场,准备构建体素
    if (!CreateHeightField(BuildContext, RasterContext))
    {
        return false;
    }
    
    // 将三角形光栅化到体素上
    ComputeRasterizationMasks(BuildContext, RasterContext);
    RasterizeTriangles(BuildContext, RasterContext);
    
    // 根据边界剔除边界之外的体素
    if (TileConfig.bPerformVoxelFiltering && !bFullyEncapsulatedByInclusionBounds)
    {
        ApplyVoxelFilter(RasterContext.SolidHF, TileConfig.walkableRadius);
    }
    
    // 构建压缩高度场
	if (!BuildCompactHeightField(BuildContext, RasterContext))
	{
	    return false;
	}
	
	// 生成导航层
	if (!GenerateNavigationData(BuildContext, RasterContext))
	{
	    return false;
	}

5. 生成导航数据

GenerateNavigationData()函数负责将高度场转换为可用于AI导航的三角形网格。这个过程包括以下几个关键步骤:

  • 区域划分:将高度场划分为不同的区域,以便于AI在导航时能够识别不同的地形类型(如可行走区域、不可行走区域等)。
  • 构建相邻三角形:为每个区域生成相邻的三角形,以便于AI在路径计算时能够快速找到可行走的路径。
bool FRecastTileGenerator::GenerateNavigationData(FNavMeshBuildContext& BuildContext, FRasterContext& RasterContext)
{
    // 进行区域划分
    if (!BuildRegions(BuildContext, RasterContext))
    {
        return false;
    }

    // 构建相邻三角形
    if (!BuildAdjacency(BuildContext, RasterContext))
    {
        return false;
    }

    // 其他导航数据的生成逻辑...
    
    return true;
}

6. 完成Tile的构建

一旦所有的Tile都完成了生成过程,导航系统会将这些Tile合并到主导航网格中。此时,系统会更新导航数据,以确保AI能够使用最新的导航信息。

void FRecastNavMeshGenerator::FinalizeTileBuilds()
{
    // 合并所有生成的Tile到主导航网格
    for (const FPendingTileElement& Tile : PendingDirtyTiles)
    {
        // 将Tile的导航数据合并到主网格
        MergeTileToNavMesh(Tile);
    }

    // 清理临时数据
    ClearPendingTiles();
}

7. 整体流程总结

整个导航系统的分块构建过程可以总结为以下几个步骤:

  1. 收集包围盒:通过遍历场景中的ANavMeshBoundsVolume,收集所有需要构建导航数据的区域。
  2. 划分Tile:根据配置的TileSizeCellSize,将每个包围盒划分为多个Tile。
  3. 标记脏区域:在场景发生变化时,标记受影响的Tile为脏区域。
  4. 多线程处理:使用多线程为每个Tile生成导航数据,确保构建过程的高效性。
  5. 生成高度场和导航数据:通过体素化、区域划分和相邻三角形构建,生成最终的导航数据。
  6. 合并和更新:将生成的Tile合并到主导航网格中,并更新导航数据。

8. 性能优化

在实际应用中,开发者可以通过以下方式优化导航系统的性能:

  • 合理配置Tile和Cell大小:根据场景的复杂性和动态性,合理设置TileSizeCellSize,以平衡性能和导航精度。
  • 选择性更新:在场景变化时,仅更新受影响的Tile,避免不必要的全量构建。
  • 异步处理:利用多线程和异步处理,确保导航数据的生成不会阻塞主线程,从而提高游戏的流畅性。

结论

UE4的导航系统通过分块构建和脏区域管理,能够高效地处理动态场景中的导航数据更新。理解这一过程的细节和优化方法,对于开发者在实现复杂AI行为和动态环境中的导航至关重要。

Tile的加载和使用

在UE4中,导航系统的Tile加载和使用是一个重要的过程,涉及到如何存储、序列化和反序列化导航数据。以下是对这一过程的详细解析。

1. Tile的结构

在UE4中,FRecastTileData结构体用于承载Recast导航数据。它包含了Tile的位置信息、数据大小以及实际的导航数据。具体结构如下:

struct FRecastTileData
{
    struct FRawData
    {
        FRawData(uint8* InData);
        ~FRawData();

        uint8* RawData;
    };

    FRecastTileData();
    FRecastTileData(int32 TileDataSize, uint8* TileRawData, int32 TileCacheDataSize, uint8* TileCacheRawData);
    
    // Tile的原始位置
    int32 OriginalX;    // Tile的X坐标
    int32 OriginalY;    // Tile的Y坐标
    int32 X;            // 当前Tile的X坐标
    int32 Y;            // 当前Tile的Y坐标
    int32 Layer;        // Tile的层级
        
    // Tile数据
    int32 TileDataSize; // Tile数据的大小
    TSharedPtr<FRawData> TileRawData; // Recast使用的原始数据

    // 压缩的Tile缓存层
    int32 TileCacheDataSize; // 缓存数据的大小
    TSharedPtr<FRawData> TileCacheRawData; // 压缩的Tile缓存数据

    // Tile是否附加到NavMesh
    bool bAttached;    
};

2. Tile的加载

在UE4中,Persistent Level在加载时会自动创建一个ARecastNavMesh类的实例,导航数据会直接生成在这个对象中。而对于SubLevel,导航数据则会序列化到ULevel类的NavMeshChunk对象中,随场景加载。

2.1 序列化过程

FPImplRecastNavMesh::Serialize()函数中,导航数据的加载过程如下:

void FPImplRecastNavMesh::Serialize(FArchive& Ar, int32 NavMeshVersion)
{
    if (Ar.IsLoading())
    {
        // 分配导航网格对象
        ReleaseDetourNavMesh();
        DetourNavMesh = dtAllocNavMesh();
    }

    // 读取每个Tile的内容,添加到DetourNavMesh中
    for (int i = 0; i < NumTiles; ++i)
    {
        unsigned char* TileData = NULL;
        int32 TileDataSize = 0;

        // 读取每个TileData的内容
        SerializeRecastMeshTile(Ar, NavMeshVersion, TileData, TileDataSize);

        if (TileData != NULL)
        {
            // 将每个Tile数据添加到DetourNavMesh中
            dtMeshHeader* const TileHeader = (dtMeshHeader*)TileData;
            DetourNavMesh->addTile(TileData, TileDataSize, DT_TILE_FREE_DATA, TileRef, NULL);
            
            // 处理CompressedData序列化
        }
    }
}

3. Tile的使用

一旦Tile数据被加载到DetourNavMesh中,AI系统就可以使用这些数据进行寻路。具体来说,A*算法会利用这些预生成的导航数据来计算路径。

3.1 寻路过程

在寻路时,AI会根据当前的位置和目标位置,使用A*算法在DetourNavMesh中查找路径。这个过程通常涉及以下步骤:

  1. 获取起始和目标位置:AI获取当前的位置和目标位置。
  2. 查找可用的Tile:根据起始位置,查找相应的Tile。
  3. 执行A*算法:使用DetourNavMesh中的数据,执行A*算法来计算路径。
  4. 返回路径:将计算出的路径返回给AI进行移动。

4. 性能考虑

在使用Tile进行寻路时,UE4的设计考虑了性能优化。通过将导航数据分块存储,系统可以在需要时快速加载和使用特定的Tile,而不是每次都加载整个导航网格。这种方法不仅提高了性能,还减少了内存占用。

结论

UE4中的Tile加载和使用机制通过FRecastTileData结构体和FPImplRecastNavMesh::Serialize()函数实现了高效的导航数据管理。这种设计使得游戏开发者能够在复杂的场景中实现灵活且高效的AI寻路系统。

结语

这一篇作为系列第一篇,从整个框架层,来分析了UE4是如何划分导航的Tile,如何分Tile构建导航网格,Tile的存储方式,以及加载上来如何使用Tile,并简单介绍了UE4如何支持动态更新导航。

你可能感兴趣的:(UE4虚幻引擎,ue4)