document modular navmesh and agent prompts

Update the runtime NavMesh architecture to a DI and MessagePipe sidecar model, and add reusable agent prompt templates that capture the project's current multiplayer, WebGL, and modularity constraints.
This commit is contained in:
Alexander Borisov
2026-04-08 02:19:03 +03:00
parent 36c67558dd
commit 6227542d2d
6 changed files with 707 additions and 164 deletions
@@ -2,228 +2,314 @@
## Goal
Реализовать runtime NavMesh для procedural voxel world без фризов и без camera-driven assumptions, с архитектурой, совместимой с будущей peer-host multiplayer моделью.
Реализовать runtime NavMesh для procedural voxel world как подключаемый sidecar-модуль в отдельной assembly, без camera-driven assumptions, с совместимостью с будущей peer-host multiplayer моделью и уже внедренными `VContainer` + `MessagePipe`.
## 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
- основной runtime генерации мира: `Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.cs`
- в проекте уже есть `ApplicationLifetimeScope` с `MessagePipe` registration
- первая итерация NavMesh coverage строится вокруг player actor
- долгосрочный контракт остается `players + active NPC`
- один тип агента
- динамические изменения мира пока не реализуются, но точки расширения под них должны быть предусмотрены
- динамические изменения мира пока не реализуются, но контракты под них должны быть предусмотрены
- WebGL-host остается целевой платформой, поэтому базовый pipeline не зависит от потоков
## Chosen Technical Direction
### 1. Не использовать `NavMeshSurfaceVolumeUpdater` как основу решения
### 1. NavMesh не внедряется в `VoxelWorldGenerator` как internal partial-логика
Причина:
- пример из samples двигает один build volume за tracked agent и не подходит как production-модель для chunk streaming
- он делает слишком coarse-grained rebuild и оставляет мало контроля над budget, dirty queue и multi-region state
Решение:
- `VoxelWorldGenerator` остается producer world state и chunk geometry
- runtime NavMesh живет в отдельном модуле и подписывается на world contracts
### 2. Использовать ручной runtime pipeline через `NavMeshBuilder.UpdateNavMeshDataAsync`
Почему:
- это соответствует целевой модели feature-модулей как подключаемых подсистем
- модуль можно будет реально отключить
- world feature не будет знать детали nav scheduling и `NavMeshData` lifecycle
Причина:
- дает прямой контроль над build sources, bounds, lifecycle `NavMeshData` и количеством одновременных rebuild
- позволяет отказаться от scene-wide source collection и собирать только известные chunk sources
- лучше подходит для throttling под WebGL-host
### 2. Модуль использует `MessagePipe` для событий, но не опирается только на сообщения
### 3. Строить NavMesh не per-chunk, а по небольшим nav regions
Решение:
- `MessagePipe` используется для lifecycle/invalidation notifications
- reader-интерфейсы используются для чтения текущего состояния world geometry и interest points
Почему:
- message-only подход ломается на late subscription и startup ordering
- NavMesh service должен уметь стартовать позже publisher-а и восстановить актуальное состояние
### 3. Runtime pipeline строится через `NavMeshBuilder.UpdateNavMeshDataAsync`
Почему:
- это дает контроль над `NavMeshData`, `Bounds`, build sources и budget
- лучше подходит для region-based rebuild под WebGL-host, чем sample-подход с одним sliding volume
### 4. NavMesh строится по nav regions, а не per-chunk и не full-volume вокруг target
Выбор:
- отдельный `NavMeshData` на nav region
- стартовый размер region рекомендуется сделать `2x2` чанка, configurable отдельно от render regions
- стартовый размер region: `2x2` чанка, configurable
Почему выбран region-based подход:
- per-chunk rebuild создает слишком много мелких операций и лишние seam-риски на границах
- один большой sliding volume вокруг interest target слишком дорог для WebGL-host
- небольшой region дает контролируемый компромисс между стоимостью rebuild и связностью навигации
Почему:
- per-chunk ведет к слишком большому числу мелких build operations
- один большой moving volume слишком дорог и плохо контролируется по бюджету
- region-based rebuild дает лучший компромисс между стоимостью и связностью
### 4. Источник build sources брать из runtime collider-геометрии чанков
### 5. Источники build sources берутся из chunk colliders, публикуемых world feature
Выбор:
- `GroundCollider` каждого чанка дает box source
- `GroundCollider` дает box source
- `MountainCollider.sharedMesh` дает mesh source
Почему так:
- не нужно сканировать всю сцену
- не нужно строить отдельную nav-only геометрию на первом этапе
- collider topology уже является ближайшим к gameplay физическим представлением поверхности
Почему:
- не нужен scene-wide scanning
- не требуется отдельная nav-only геометрия на первом этапе
- это наиболее близкое к gameplay представление walkable/non-walkable world geometry
### 5. Rebuild делать через dirty queue и budgeted scheduler
## Target Module Boundaries
Выбор:
- region помечается dirty при `ApplyColliderMesh` и при unload чанка
- scheduler сортирует dirty regions по расстоянию до interest actor
- одновременно идет максимум один nav rebuild
- если region снова стал dirty во время build, версия region увеличивается и после завершения запускается новый rebuild только для актуальной версии
### Assembly Layout
- `Assets/Features/VoxelWorld/Contracts/VoxelWorld.Contracts.asmdef`
- `Assets/Features/VoxelWorld/Runtime/VoxelWorld.Runtime.asmdef`
- `Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorld.NavMesh.Runtime.asmdef`
Почему так:
- это bounded и предсказуемо для WebGL-host
- исключает лавинообразные rebuild при быстром перемещении игрока
- `Contracts` фиксируют стабильную внешнюю границу
- `Runtime` реализует мир и публикует contracts
- `VoxelWorld.NavMesh.Runtime` остается optional consumer-модулем
## Proposed Runtime Structure
## Contracts To Add
### File Placement
### Reader Interfaces
- расширить `VoxelWorldGenerator` новыми partial-файлами, а не вводить отдельный service layer на этой стадии
- рекомендуемые файлы:
- `Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.NavMesh.cs`
- `Assets/Features/VoxelWorld/Runtime/VoxelWorldGenerator.NavMesh.Types.cs`
- `IChunkNavGeometryReader`
- `IWorldInterestReader`
Причина:
- nav lifecycle напрямую зависит от chunk lifecycle, которым уже владеет `VoxelWorldGenerator`
- это минимальное изменение без раннего DI/refactor
Минимальная ответственность:
- `IChunkNavGeometryReader` умеет вернуть текущую nav-геометрию чанка и список уже загруженных чанков
- `IWorldInterestReader` умеет вернуть текущую primary interest point
### New Runtime Data
### DTO / Contracts
- `NavRegionRuntime`
- `NavMeshData NavMeshData`
- `NavMeshDataInstance Instance`
- `AsyncOperation ActiveBuild`
- `int Version`
- `bool IsDirty`
- `bool BuildRequestedWhileRunning`
- `Bounds BuildBounds`
- `List<NavMeshBuildSource>` reusable sources buffer
- `ChunkNavGeometry`
Состав DTO:
- `Vector2Int Coord`
- `Transform Root`
- `BoxCollider GroundCollider`
- `MeshCollider MountainCollider`
- `int Version`
Примечание:
- DTO должен содержать только то, что реально нужно для NavMesh source collection
- private nested types `VoxelWorldGenerator` не должны утекать наружу
### Message Types
- `ChunkNavGeometryReadyMessage`
- `ChunkNavGeometryRemovedMessage`
- `WorldInterestChangedMessage`
- позже: `ChunkWalkabilityChangedMessage` или аналогичный delta-invalidating message
Правило:
- сообщения несут ключ и минимальные данные invalidation
- тяжелое актуальное состояние читается через reader interfaces
## Required Changes In `VoxelWorldGenerator`
### 1. Перестать быть единственной точкой nav logic
`VoxelWorldGenerator` должен только:
- генерировать и стримить чанки
- создавать/apply collider mesh
- публиковать world contracts
Он не должен:
- владеть dirty nav region queue
- владеть `NavMeshData`
- напрямую запускать `NavMeshBuilder`
### 2. Реализовать reader interfaces
- `IChunkNavGeometryReader`
- `IWorldInterestReader`
### 3. Публиковать сообщения после world lifecycle changes
Нужно публиковать:
- `ChunkNavGeometryReadyMessage` после фактического применения collider mesh
- `ChunkNavGeometryRemovedMessage` перед уничтожением чанка
- `WorldInterestChangedMessage` при смене actor-level interest point
### 4. Убрать каноническую зависимость от `Camera.main`
Для первой итерации допускается scene wiring через explicit target reference, но не через runtime fallback на `Camera.main` как на архитектурную норму.
## NavMesh Module Structure
Рекомендуемые файлы:
- `Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshService.cs`
- `Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshTypes.cs`
- `Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshConfig.cs`
- `Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshModule.cs`
Дополнительно, если нужен bridge для scene binding на переходном этапе:
- `Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorldNavMeshEntry.cs`
## DI Composition
### Registration Model
`VoxelWorld` регистрирует:
- `VoxelWorldGenerator` как реализацию reader interfaces
- publishers соответствующих world messages
`VoxelWorldNavMesh` регистрирует:
- `VoxelWorldNavMeshService`
- его subscribers
- config instance
### Important Rule
- feature-код не должен использовать `GlobalMessagePipe` как основную integration point
- `IPublisher<T>` и `ISubscriber<T>` должны приходить через DI
Почему:
- это сохраняет тестируемость
- не создает скрытых runtime-зависимостей
- лучше соответствует модульному подключению через `LifetimeScope`
## NavMesh Service Responsibilities
- подписаться на world lifecycle messages
- на старте получить snapshot уже загруженных чанков через `IChunkNavGeometryReader`
- построить initial set dirty regions
- поддерживать `NavMeshData` по регионам
- собирать `NavMeshBuildSource` из chunk colliders
- запускать throttled `UpdateNavMeshDataAsync`
- переоценивать build priority относительно current interest point
- удалять region data, когда она выходит из активного диапазона и становится пустой
## Region Runtime Data
- `Dictionary<Vector2Int, NavRegionRuntime> navRegions`
- `Queue<Vector2Int> dirtyNavRegions`
- `HashSet<Vector2Int> queuedNavRegions`
- `Dictionary<Vector2Int, NavRegionRuntime> navRegions`
### New Config Settings
`NavRegionRuntime` должен хранить:
- `NavMeshData NavMeshData`
- `NavMeshDataInstance Instance`
- `AsyncOperation ActiveBuild`
- `int Version`
- `bool BuildRequestedWhileRunning`
- `Bounds BuildBounds`
Добавить в `VoxelWorldConfig` отдельную секцию `NavMesh`:
## New Config Settings
Добавить в NavMesh module config:
- `int navRegionSizeInChunks = 2`
- `int maxNavMeshBuildsPerFrame = 1`
- `int maxConcurrentNavMeshBuilds = 1`
- `float navBoundsVerticalPadding`
- `int maxNavMeshBuildsPerFrame = 1`
- `float navBoundsHorizontalPadding`
- `float navBoundsVerticalPadding`
- `int navWarmupRadiusInRegions`
Примечание:
- `maxConcurrentNavMeshBuilds` для первой итерации должен остаться `1`
- horizontal padding нужен для корректной стыковки границ region data
- для первой итерации `maxConcurrentNavMeshBuilds` должен оставаться `1`
## Integration With World Lifecycle
## Build Flow
### Chunk load / update flow
### Initial Sync
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.
1. Сервис стартует.
2. Через `IChunkNavGeometryReader` получает список уже загруженных чанков.
3. Помечает соответствующие nav regions dirty.
4. Через `IWorldInterestReader` получает текущую точку интереса.
5. Запускает scheduler.
### Chunk unload flow
### Incremental Update
1. Перед `runtime.Dispose()` определить nav region чанка.
2. Пометить соответствующий region dirty.
3. Если region стал пустым и вышел из active nav range, удалить его `NavMeshDataInstance`.
1. Приходит `ChunkNavGeometryReadyMessage`.
2. Сервис читает актуальную geometry через `IChunkNavGeometryReader`.
3. Помечает nav region dirty.
4. Если chunk расположен на границе region, дополнительно маркирует соседний region.
### Interest target flow
### Removal
1. Убрать каноническую зависимость от `Camera.main` как источника стриминга/nav interest.
2. Ввести actor-level target semantics.
3. Для сохранения сцены использовать rename с `FormerlySerializedAs`, если будет меняться имя поля.
4. Для первой итерации target задается явно со сцены или от будущего player actor bootstrap.
1. Приходит `ChunkNavGeometryRemovedMessage`.
2. Сервис помечает соответствующий region dirty.
3. Если region опустел и вышел из активной зоны, удаляет `NavMeshDataInstance`.
## Region Build Flow
### Interest Update
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 актуальной версии.
1. Приходит `WorldInterestChangedMessage`.
2. Сервис обновляет current interest point.
3. Scheduler пересчитывает порядок rebuild и warmup regions.
## Region Granularity And Boundary Rules
## Source Collection Rules
### Start choice
Для каждого затронутого region:
- собрать build sources только из чанков региона и соседнего margin
- использовать только известную geometry из reader interface
- не сканировать произвольные объекты сцены
- `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
Для каждого chunk geometry:
- добавить `NavMeshBuildSourceShape.Box` из `GroundCollider`
- добавить `NavMeshBuildSourceShape.Mesh` из `MountainCollider.sharedMesh`, если mesh не пустой
## Performance Rules
- не делать full-scene bake
- не пересобирать NavMesh синхронно через `BuildNavMesh()` на gameplay path
- не сканировать произвольные scene objects через generic collection APIs, если можно собрать sources из известных chunk runtimes
- держать максимум один активный build
- переиспользовать buffers, где это возможно
- не строить систему в расчете на обязательный background threading
- держать максимум один активный region build
- rebuild запускать только после фактического применения collider mesh
- unload и load чанков должны только маркировать region dirty, а не запускать немедленный build вне scheduler
- события из `MessagePipe` не должны тащить heavy geometry payload, только invalidation keys
- scheduler должен быть bounded и deterministic по budget
## Verification Plan
### Manual verification
### Functional
1. Запустить `VoxelWorldTestScene`.
2. Использовать debug `NavMeshAgent` из AI Navigation samples.
3. Проверить, что агент строит путь по поверхности уже загруженных чанков.
4. Быстро перемещать actor target по миру и отслеживать отсутствие заметных фризов.
5. Проверить unload чанков: после ухода области старый NavMesh не должен оставлять висячие walkable islands в уже удаленных регионах.
2. Убедиться, что NavMesh module можно отключить и world generation продолжает работать.
3. Подключить NavMesh module и проверить появление walkable NavMesh на уже загруженных чанках.
4. Проверить, что agent из AI Navigation samples строит путь по поверхности.
5. Проверить unload чанков: старый NavMesh не должен оставлять висячие walkable islands.
### Debug instrumentation
### Integration
- gizmos для region bounds и состояния region build
- лог счетчиков:
- active nav regions
- dirty nav regions
- builds started/completed/cancelled as stale
1. Проверить старт сервиса после world generator: missed events не должны ломать initial sync.
2. Проверить, что модуль работает только через DI-injected `MessagePipe` и reader interfaces.
3. Проверить, что отключение регистрации `VoxelWorldNavMesh` не ломает world feature.
### Performance
1. Быстро перемещать actor target по миру.
2. Снять показатели:
- active nav regions
- queued dirty regions
- builds started
- stale rebuilds dropped
- worst-frame rebuild spikes
## Explicit Non-Goals For This Iteration
- NavMeshObstacle carving
- `NavMeshObstacle` carving
- multi-agent bake
- DI integration через VContainer
- Addressables integration
- networked NavMesh replication
- ownership migration для чанков или NPC
- финальная multiplayer interest model вокруг всех actors
- прямые зависимости NavMesh feature от `VoxelWorldGenerator` internals
## 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 после первой проверки гипотезы.
1. Добавить contracts assembly для world-to-navmesh integration.
2. Добавить message types и reader interfaces.
3. Адаптировать `VoxelWorldGenerator` под публикацию world contracts и messages.
4. Создать отдельную assembly и runtime module `VoxelWorld.NavMesh.Runtime`.
5. Реализовать `VoxelWorldNavMeshService` с initial sync через readers и incremental updates через `MessagePipe`.
6. Реализовать region scheduler и `NavMeshBuilder.UpdateNavMeshDataAsync`.
7. Подключить модуль через DI registration.
8. Провести ручную проверку и зафиксировать фактические perf observations.