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

230 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 после первой проверки гипотезы.