Files
TheDeclineOfWarriors/docs/plans/TASK-0023-runtime-navmesh-implementation-plan.md
T
Alexander Borisov 36c67558dd document runtime navmesh architecture decisions
Fix the authority and world-state assumptions before implementation so runtime NavMesh work can stay consistent with the project's future DI and peer-host multiplayer model.
2026-04-08 01:47:31 +03:00

12 KiB
Raw Blame History

TASK-0023 Runtime NavMesh Implementation Plan

Goal

Реализовать runtime NavMesh для procedural voxel world без фризов и без camera-driven assumptions, с архитектурой, совместимой с будущей peer-host multiplayer моделью.

Inputs And Assumptions

  • текущая test scene: Assets/Features/VoxelWorld/Scenes/VoxelWorldTestScene.unity
  • основной runtime: Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs
  • текущий world config asset:
    • chunkSize = 16
    • generationRadius = 3
    • maxMountainHeight = 6
    • renderRegionSizeInChunks = 4
    • maxAsyncChunkJobs = 2
    • maxChunkBuildsPerFrame = 1
    • maxChunkMeshBuildsPerFrame = 1
    • maxColliderAppliesPerFrame = 1
  • первая итерация учитывает область вокруг player actor
  • долгосрочный контракт остается players + active NPC
  • один тип агента
  • динамические изменения мира пока не реализуются, но точки расширения под них должны быть предусмотрены

Chosen Technical Direction

1. Не использовать NavMeshSurfaceVolumeUpdater как основу решения

Причина:

  • пример из samples двигает один build volume за tracked agent и не подходит как production-модель для chunk streaming
  • он делает слишком coarse-grained rebuild и оставляет мало контроля над budget, dirty queue и multi-region state

2. Использовать ручной runtime pipeline через NavMeshBuilder.UpdateNavMeshDataAsync

Причина:

  • дает прямой контроль над build sources, bounds, lifecycle NavMeshData и количеством одновременных rebuild
  • позволяет отказаться от scene-wide source collection и собирать только известные chunk sources
  • лучше подходит для throttling под WebGL-host

3. Строить NavMesh не per-chunk, а по небольшим nav regions

Выбор:

  • отдельный NavMeshData на nav region
  • стартовый размер region рекомендуется сделать 2x2 чанка, configurable отдельно от render regions

Почему выбран region-based подход:

  • per-chunk rebuild создает слишком много мелких операций и лишние seam-риски на границах
  • один большой sliding volume вокруг interest target слишком дорог для WebGL-host
  • небольшой region дает контролируемый компромисс между стоимостью rebuild и связностью навигации

4. Источник build sources брать из runtime collider-геометрии чанков

Выбор:

  • GroundCollider каждого чанка дает box source
  • MountainCollider.sharedMesh дает mesh source

Почему так:

  • не нужно сканировать всю сцену
  • не нужно строить отдельную nav-only геометрию на первом этапе
  • collider topology уже является ближайшим к gameplay физическим представлением поверхности

5. Rebuild делать через dirty queue и budgeted scheduler

Выбор:

  • region помечается dirty при ApplyColliderMesh и при unload чанка
  • scheduler сортирует dirty regions по расстоянию до interest actor
  • одновременно идет максимум один nav rebuild
  • если region снова стал dirty во время build, версия region увеличивается и после завершения запускается новый rebuild только для актуальной версии

Почему так:

  • это bounded и предсказуемо для WebGL-host
  • исключает лавинообразные rebuild при быстром перемещении игрока

Proposed Runtime Structure

File Placement

  • расширить VoxelWorldGenerator новыми partial-файлами, а не вводить отдельный service layer на этой стадии
  • рекомендуемые файлы:
    • Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.NavMesh.cs
    • Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.NavMesh.Types.cs

Причина:

  • nav lifecycle напрямую зависит от chunk lifecycle, которым уже владеет VoxelWorldGenerator
  • это минимальное изменение без раннего DI/refactor

New Runtime Data

  • NavRegionRuntime
    • NavMeshData NavMeshData
    • NavMeshDataInstance Instance
    • AsyncOperation ActiveBuild
    • int Version
    • bool IsDirty
    • bool BuildRequestedWhileRunning
    • Bounds BuildBounds
    • List<NavMeshBuildSource> reusable sources buffer
  • Queue<Vector2Int> dirtyNavRegions
  • HashSet<Vector2Int> queuedNavRegions
  • Dictionary<Vector2Int, NavRegionRuntime> navRegions

New Config Settings

Добавить в VoxelWorldConfig отдельную секцию NavMesh:

  • int navRegionSizeInChunks = 2
  • int maxNavMeshBuildsPerFrame = 1
  • int maxConcurrentNavMeshBuilds = 1
  • float navBoundsVerticalPadding
  • float navBoundsHorizontalPadding
  • int navWarmupRadiusInRegions

Примечание:

  • maxConcurrentNavMeshBuilds для первой итерации должен остаться 1
  • horizontal padding нужен для корректной стыковки границ region data

Integration With World Lifecycle

Chunk load / update flow

  1. GenerateChunkData завершает данные чанка.
  2. RenderChunk собирает render snapshot и collider mesh.
  3. После фактического применения collider mesh chunk помечает свой nav region dirty.
  4. Если чанк лежит у границы nav region, дополнительно dirty-mark соседний region, который делит с ним границу.
  5. Scheduler позже запускает rebuild региона по budget.

Chunk unload flow

  1. Перед runtime.Dispose() определить nav region чанка.
  2. Пометить соответствующий region dirty.
  3. Если region стал пустым и вышел из active nav range, удалить его NavMeshDataInstance.

Interest target flow

  1. Убрать каноническую зависимость от Camera.main как источника стриминга/nav interest.
  2. Ввести actor-level target semantics.
  3. Для сохранения сцены использовать rename с FormerlySerializedAs, если будет меняться имя поля.
  4. Для первой итерации target задается явно со сцены или от будущего player actor bootstrap.

Region Build Flow

  1. Определить regionCoord по координате чанка.
  2. Вычислить Bounds региона с padding по XZ и по высоте.
  3. Собрать build sources только из чанков, попадающих в region и в соседний margin вокруг него.
  4. Для каждого активного чанка добавить:
    • NavMeshBuildSourceShape.Box из GroundCollider
    • NavMeshBuildSourceShape.Mesh из MountainCollider.sharedMesh, если mesh не пустой
  5. Запустить NavMeshBuilder.UpdateNavMeshDataAsync для region-local NavMeshData.
  6. При завершении проверить актуальность версии и либо оставить data, либо сразу перезапустить rebuild актуальной версии.

Region Granularity And Boundary Rules

Start choice

  • navRegionSizeInChunks = 2

Почему не 1:

  • слишком много мелких NavMeshData
  • больше seam pressure на стыках
  • выше scheduler overhead

Почему не 4:

  • rebuild слишком дорогой для частого runtime update на WebGL-host
  • это уже заметный кусок от всего active world при generationRadius = 3

Boundary handling

  • build bounds должны быть больше чистого region rectangle
  • source collection должна захватывать соседние чанки на один region-margin
  • region dirty-mark должен учитывать chunk changes на границах

Причина:

  • без overlap на границах легко получить cracks и непредсказуемую связность между соседними NavMeshData

Multiplayer And Authority Contract For This Task

  • базовый voxel world генерируется локально у каждого peer из одинакового deterministic input
  • NavMesh строится локально у каждого peer и не реплицируется по сети
  • authoritative gameplay использует host-side NPC simulation
  • текущая итерация NavMesh coverage вокруг player actor считается временным MVP simplification
  • при переходе к реальной multiplayer-сцене host должен строить priority coverage вокруг players + active NPC
  • будущие world changes должны приходить как authoritative deltas и маркировать nav regions dirty локально на каждом peer

Performance Rules

  • не делать full-scene bake
  • не пересобирать NavMesh синхронно через BuildNavMesh() на gameplay path
  • не сканировать произвольные scene objects через generic collection APIs, если можно собрать sources из известных chunk runtimes
  • держать максимум один активный build
  • переиспользовать buffers, где это возможно
  • rebuild запускать только после фактического применения collider mesh
  • unload и load чанков должны только маркировать region dirty, а не запускать немедленный build вне scheduler

Verification Plan

Manual verification

  1. Запустить VoxelWorldTestScene.
  2. Использовать debug NavMeshAgent из AI Navigation samples.
  3. Проверить, что агент строит путь по поверхности уже загруженных чанков.
  4. Быстро перемещать actor target по миру и отслеживать отсутствие заметных фризов.
  5. Проверить unload чанков: после ухода области старый NavMesh не должен оставлять висячие walkable islands в уже удаленных регионах.

Debug instrumentation

  • gizmos для region bounds и состояния region build
  • лог счетчиков:
    • active nav regions
    • dirty nav regions
    • builds started/completed/cancelled as stale

Explicit Non-Goals For This Iteration

  • NavMeshObstacle carving
  • multi-agent bake
  • DI integration через VContainer
  • Addressables integration
  • ownership migration для чанков или NPC
  • финальная multiplayer interest model вокруг всех actors

Execution Order

  1. Добавить nav settings в VoxelWorldConfig и resolved settings.
  2. Добавить runtime структуры nav regions и dirty scheduler в VoxelWorldGenerator.
  3. Привязать dirty-marking к chunk collider apply и unload.
  4. Реализовать source collection из chunk colliders.
  5. Реализовать region-local NavMeshData lifecycle и async rebuild.
  6. Убрать camera-driven fallback из world/nav interest path.
  7. Добавить debug visualization и ручную проверку через sample agent.
  8. Задокументировать фактические perf observations после первой проверки гипотезы.