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.
This commit is contained in:
Alexander Borisov
2026-04-08 01:45:43 +03:00
parent 46aec7ef92
commit 36c67558dd
3 changed files with 452 additions and 2 deletions
@@ -0,0 +1,219 @@
# MVP World, Authority And Runtime NavMesh
## Status
Этот документ считается каноническим для решений по детерминированному миру, authority model и runtime NavMesh, пока его явно не заменят более новым архитектурным решением.
## Purpose
Зафиксировать долгосрочные решения для MVP, чтобы downstream-задачи по FishNet, worldgen, AI и persistence не уехали в разные стороны.
## Scope
- deterministic voxel world generation
- authority model для session gameplay
- runtime NavMesh в procedural world
- риски WebGL-host режима
## Fixed Decisions
### 1. Базовый мир генерируется детерминированно и локально на каждом peer
Решение:
- базовая геометрия мира не стримится от хоста по сети
- каждый peer генерирует чанк локально из одинакового `seed`, одинакового `VoxelWorldConfig` и одинаковой версии world rules
Почему выбрано:
- для WebGL и peer-host модели это минимизирует сетевой трафик
- убирает постоянную сетевую репликацию геометрии чанков
- снимает с хоста роль единственной точки генерации базового мира
- хорошо сочетается с уже существующим `VoxelWorldGenerator`, который строит чанк из deterministic inputs
Почему не выбран host-generated world streaming:
- хост получал бы лишнюю CPU-нагрузку на генерацию и лишнюю сетевую нагрузку на раздачу чанков
- late join и догрузка дальних областей становились бы тяжелее по сети
- это хуже укладывается в бюджет WebGL-host
Последствия:
- `seed`, world config и их версия становятся частью session handshake
- любое расхождение по config/version между peers недопустимо и должно считаться protocol drift
### 2. Host остается authoritative для NPC, AI и другого gameplay state
Решение:
- NPC симулируются на хосте
- pathfinding NPC, агро, боевые решения и каноническое положение NPC принадлежат хосту
- клиенты получают состояние NPC по сети и могут делать только визуальное сглаживание
Почему выбрано:
- NPC влияют на бой, урон, столкновения и progression, значит их нельзя отдавать в authority случайному клиенту
- это радикально снижает риск читов и эксплуатационных багов
- упрощает late join, reconnect и дебаг сетевой симуляции
Почему не выбран client-owned NPC:
- ownership у первого встретившего игрока нестабилен при совместной игре
- миграция owner во время боя ломает воспроизводимость path state, aggro state и hit timing
- возрастает риск desync и эксплойтов
- резко усложняется отладка и сопровождение
Последствия:
- `client-authority` допустим только для ввода игрока и только при отдельной валидации на сервере
- для NPC authority migration в MVP не используется
### 3. У чанков нет owner и нет chunk ownership migration
Решение:
- чанк не закрепляется за конкретным игроком как за владельцем
- базовый чанк является общей детерминированной сущностью мира, а не network-owned объектом
Почему выбрано:
- при deterministic world generation ownership чанка не дает полезного выигрыша
- chunk ownership добавляет coordination cost, миграцию ответственности и новые классы сетевых гонок без пользы для MVP
- это плохо совместимо с late join и с будущими world deltas
Почему не выбран owner-per-chunk:
- первый увидевший чанк игрок не является надежным authority source
- потребуется сложная логика передачи владения при сближении игроков и при disconnect
- любые расхождения по владельцу чанка приводят к hidden state drift
Последствия:
- изменения чанка в будущем пойдут не через owner migration, а через authoritative world deltas от хоста
### 4. Runtime NavMesh строится локально на каждом peer по фактической локальной геометрии чанка
Решение:
- NavMesh не реплицируется по сети как data blob
- каждый peer строит NavMesh у себя локально из актуальной локальной геометрии мира
- NavMesh всегда считается производным кэшем от world state, а не каноническим состоянием сессии
Почему выбрано:
- NavMesh data тяжелая и плохо подходит для сетевой репликации в peer-host модели
- при deterministic base world и одинаковых world deltas peers могут независимо прийти к одинаковой walkable topology
- это сохраняет сеть для gameplay state, а не для производных навигационных артефактов
Почему не выбран network-streamed NavMesh:
- лишний трафик и высокая сложность синхронизации
- плохая масштабируемость для догрузки чанков и late join
- NavMesh все равно пришлось бы пересобирать при локальных изменениях геометрии
Последствия:
- каноничность gameplay не должна зависеть от клиентского NavMesh
- client NavMesh используется для локальных потребностей, но authoritative decisions по NPC остаются у хоста
### 5. Будущие изменения проходимости мира передаются как authoritative world deltas
Решение:
- базовый мир идет из deterministic generation
- любые будущие баррикады, спеллы, разрушаемость, carve и другие изменения мира передаются как authoritative deltas от хоста
- после применения delta каждый peer локально перестраивает затронутые nav regions
Почему выбрано:
- это отделяет immutable base generation от mutable session state
- обеспечивает late join: новому игроку можно отдать base seed/config и журнал world deltas
- не требует вводить ownership migration для чанков
Почему не выбран fully local mutable world:
- local-first изменения мира не могут быть каноническими в кооперативной сетевой игре
- конфликтуют с античитом, late join и persistence
Последствия:
- NavMesh pipeline обязан уметь маркировать локальные nav regions как dirty после world delta
### 6. NavMesh pipeline должен работать в single-thread budget; многопоточность в WebGL считается только опциональным ускорением
Решение:
- архитектура runtime NavMesh не должна зависеть от наличия потоков
- базовый режим должен укладываться в бюджет кадра на одном потоке
- если deployment позже подтвердит поддержку `SharedArrayBuffer` и `COOP/COEP`, можно добавить threaded optimization, но не делать ее обязательной
Почему выбрано:
- WebGL-host остается одной из целевых платформ
- WebGL multithreading требует специальных заголовков и эксплуатационной дисциплины на стороне хостинга
- завязка critical gameplay pipeline на эту инфраструктуру слишком рискованна для MVP
Почему не выбран threaded-only pipeline:
- он может работать в editor/desktop и развалиться в реальном WebGL deployment
- создаст ложное ощущение приемлемого бюджета, которого не будет на production-hosting
Последствия:
- rebuild должен быть incremental, throttled и bounded
- полносценовый bake вокруг камеры не подходит как каноническая модель
### 7. Первая итерация NavMesh покрывает область вокруг player actor, но долгосрочный контракт расширяется до players + active NPC
Решение:
- для первой проверки гипотезы build priority привязывается к player actor
- целевой контракт для multiplayer host: nav coverage должна учитывать игроков и активных NPC
Почему выбрано:
- это минимальный объем для MVP-проверки без ранней переплаты за сложную interest model
- при этом заранее фиксируется, что player-only coverage не является конечной архитектурой
Почему не выбран camera-driven center:
- камера не является каноническим gameplay actor
- в multiplayer и especially on host камера может не совпадать с зоной активной симуляции
- привязка к `Camera.main` ломает переносимость решения из test scene в сетевую сессию
Последствия:
- в коде нельзя оставлять `Camera.main` как канонический источник world/nav interest
- target должен представлять actor-level interest, а не presentation-level camera
### 8. Для MVP поддерживается один тип NavMesh agent
Решение:
- сейчас поддерживается только один `agentTypeID`
Почему выбрано:
- проект на стадии hypothesis/MVP
- это уменьшает стоимость runtime bake и настройки AI Navigation
- не раздувает матрицу тестирования до появления реальной необходимости
Почему не выбран multi-agent bake сразу:
- рост CPU и memory costs
- усложнение отладки при почти нулевой текущей пользе
Последствия:
- при появлении разных классов существ нужно отдельно пересмотреть agent taxonomy
### 9. В этой фазе решение остается scene-local и не привязывается к VContainer или Addressables
Решение:
- runtime NavMesh реализуется как часть текущего scene-local world runtime
- VContainer и Addressables в этой задаче не вводятся
Почему выбрано:
- в проекте пока нет production-ready composition root поверх gameplay world
- принудительное добавление DI boundary сейчас даст больше шума, чем пользы
- Addressables не подключены и не требуются для гипотезы NavMesh generation
Почему не выбран ранний DI/bootstrap refactor:
- это отвлекает от основной гипотезы по производительности и корректности NavMesh
- возникает преждевременная архитектурная сложность при еще нестабильных правилах мира
Последствия:
- код должен оставаться достаточно изолированным, чтобы позже его можно было вынести в runtime service
## Long-Term Risks
### Critical
- WebGL-host может не выдержать одновременно world streaming, runtime NavMesh rebuild и server-authoritative NPC AI.
- Любой drift по `seed`, `VoxelWorldConfig` или world rules между peers приведет к расхождению геометрии и локального NavMesh.
### High
- Цель в `50` активных NPC может упереться не в один subsystem, а в суммарный CPU budget хоста.
- Будущие изменения геометрии потребуют точной invalidation strategy по nav regions; без нее rebuild cost быстро выйдет из-под контроля.
- Если client movement в будущем начнет опираться на локальный NavMesh как на authority source, появятся расхождения с host simulation.
### Medium
- Late join требует не только `seed/config`, но и корректного воспроизведения authoritative world deltas.
- Если region size выбрать слишком крупным, rebuild будет дорогим; если слишком мелким, возрастет число build operations и seam-risk на границах.
## Downstream Implications
- `TASK-0001`: этот документ закрывает часть канонических MVP-решений по world/authority/navmesh.
- `TASK-0002`: session handshake должен включать world seed, config/version и protocol compatibility checks.
- `TASK-0012`: enemy AI проектируется только как host-authoritative.
- `TASK-0023`: runtime NavMesh обязан быть local-build, throttled и без camera-driven assumptions.
@@ -0,0 +1,229 @@
# 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 после первой проверки гипотезы.
+4 -2
View File
@@ -6,12 +6,14 @@ priority: Highest
area: ai area: ai
owner: abysscion owner: abysscion
created: 2026-03-31 created: 2026-03-31
updated: 2026-04-07 updated: 2026-04-08
execution_time: 2d execution_time: 2d
depends_on: depends_on:
- TASK-0003 - TASK-0003
canonical_docs: canonical_docs:
- docs/tasks/Index.md - docs/tasks/Index.md
- docs/architecture/mvp-world-authority-navmesh.md
- docs/plans/TASK-0023-runtime-navmesh-implementation-plan.md
related_files: related_files:
- Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs - Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs
--- ---
@@ -94,4 +96,4 @@ AI врагов (`TASK-0012`) опирается на NavMesh. Воксельн
## Handoff Notes ## Handoff Notes
Если в проекте нет пакета NavMeshComponents, возможно придется добавить его или реализовать минимальный runtime builder. Если в проекте нет пакета NavMeshComponents, возможно придется добавить его или реализовать минимальный runtime builder.