338 lines
16 KiB
Markdown
338 lines
16 KiB
Markdown
# TASK-0023 Runtime NavMesh Implementation Plan
|
||
|
||
## Goal
|
||
|
||
Реализовать 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`
|
||
- в проекте уже есть `ApplicationLifetimeScope` с `MessagePipe` registration
|
||
- первая итерация scheduler может начинать приоритизацию от одного player actor
|
||
- внешний interest-контракт сразу остается actor-level interest set: `players + active NPC`
|
||
- один тип агента
|
||
- динамические изменения мира пока не реализуются, но контракты под них должны быть предусмотрены
|
||
- WebGL-host остается целевой платформой, поэтому базовый pipeline не зависит от потоков
|
||
|
||
## Chosen Technical Direction
|
||
|
||
### 1. NavMesh не внедряется в `VoxelWorldGenerator` как internal partial-логика
|
||
|
||
Решение:
|
||
- `VoxelWorldGenerator` остается producer world state и chunk geometry
|
||
- runtime NavMesh живет в отдельном модуле и подписывается на world contracts
|
||
|
||
Почему:
|
||
- это соответствует целевой модели feature-модулей как подключаемых подсистем
|
||
- модуль можно будет реально отключить
|
||
- world feature не будет знать детали nav scheduling и `NavMeshData` lifecycle
|
||
|
||
### 2. Модуль использует `MessagePipe` для событий, но не опирается только на сообщения
|
||
|
||
Решение:
|
||
- `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
|
||
|
||
Почему:
|
||
- per-chunk ведет к слишком большому числу мелких build operations
|
||
- один большой moving volume слишком дорог и плохо контролируется по бюджету
|
||
- region-based rebuild дает лучший компромисс между стоимостью и связностью
|
||
|
||
### 5. Целью считается эквивалентная walkable topology, а не bit-identical NavMesh artifact
|
||
|
||
Почему:
|
||
- runtime NavMesh остается derived cache, а не canonical network state
|
||
- gameplay correctness не должен опираться на клиентский NavMesh как на источник authority
|
||
- это не создает ложного требования к protocol-grade детерминизму там, где он не нужен для MVP
|
||
|
||
### 6. Источники build sources публикуются world feature как узкие source descriptors
|
||
|
||
Выбор:
|
||
- world feature отдает snapshot nav sources чанка через стабильный reader contract
|
||
- текущая реализация `VoxelWorld` может внутри строить эти sources из `GroundCollider` и `MountainCollider`, но эта деталь не протекает в API sidecar-модуля
|
||
|
||
Почему:
|
||
- не нужен scene-wide scanning
|
||
- не требуется отдельная nav-only геометрия на первом этапе
|
||
- sidecar-модуль не цементируется на конкретных `Collider`-компонентах и scene hierarchy мира
|
||
|
||
## Target Module Boundaries
|
||
|
||
### Assembly Layout
|
||
|
||
- `Assets/Features/VoxelWorld/Contracts/VoxelWorld.Contracts.asmdef`
|
||
- `Assets/Features/VoxelWorld/Runtime/VoxelWorld.Runtime.asmdef`
|
||
- `Assets/Features/VoxelWorldNavMesh/Runtime/VoxelWorld.NavMesh.Runtime.asmdef`
|
||
|
||
Почему так:
|
||
- `Contracts` фиксируют стабильную внешнюю границу
|
||
- `Runtime` реализует мир и публикует contracts
|
||
- `VoxelWorld.NavMesh.Runtime` остается optional consumer-модулем
|
||
|
||
## Contracts To Add
|
||
|
||
### Reader Interfaces
|
||
|
||
- `IChunkNavSourceReader`
|
||
- `IWorldInterestReader`
|
||
|
||
Минимальная ответственность:
|
||
- `IChunkNavSourceReader` умеет вернуть текущие nav build sources чанка и список уже загруженных чанков
|
||
- `IWorldInterestReader` умеет вернуть текущий actor-level interest set
|
||
|
||
### DTO / Contracts
|
||
|
||
- `ChunkNavSourceSnapshot`
|
||
- `ChunkNavBuildSourceDescriptor`
|
||
- `WorldInterestPoint`
|
||
|
||
Состав DTO:
|
||
- `ChunkNavSourceSnapshot`
|
||
- `Vector2Int Coord`
|
||
- `int Version`
|
||
- `ChunkNavBuildSourceDescriptor[] Sources`
|
||
|
||
- `ChunkNavBuildSourceDescriptor`
|
||
- `NavMeshBuildSourceShape Shape`
|
||
- `Matrix4x4 Transform`
|
||
- `Vector3 Size` для box-source
|
||
- `Mesh Mesh` для mesh-source
|
||
- `int Area`
|
||
|
||
- `WorldInterestPoint`
|
||
- `Vector3 Position`
|
||
- `float Priority`
|
||
- `WorldInterestKind Kind`
|
||
|
||
Примечание:
|
||
- DTO должен содержать только то, что реально нужно для NavMesh source collection и build prioritization
|
||
- private nested types `VoxelWorldGenerator` не должны утекать наружу
|
||
- `Transform`, `MeshCollider` и `BoxCollider` не должны становиться каноническим внешним состоянием NavMesh integration, если достаточно source descriptors
|
||
|
||
### 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
|
||
|
||
- `IChunkNavSourceReader`
|
||
- `IWorldInterestReader`
|
||
|
||
### 3. Публиковать сообщения после world lifecycle changes
|
||
|
||
Нужно публиковать:
|
||
- `ChunkNavGeometryReadyMessage` после фактического применения collider mesh
|
||
- `ChunkNavGeometryRemovedMessage` перед уничтожением чанка
|
||
- `WorldInterestChangedMessage` при изменении actor-level interest set
|
||
|
||
### 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 уже загруженных чанков через `IChunkNavSourceReader`
|
||
- построить initial set dirty regions
|
||
- поддерживать `NavMeshData` по регионам
|
||
- собирать `NavMeshBuildSource` из source descriptors, а не через прямой доступ к world colliders
|
||
- запускать throttled `UpdateNavMeshDataAsync`
|
||
- переоценивать build priority относительно current interest set
|
||
- удалять region data, когда она выходит из активного диапазона и становится пустой
|
||
|
||
## Region Runtime Data
|
||
|
||
- `Dictionary<Vector2Int, NavRegionRuntime> navRegions`
|
||
- `Queue<Vector2Int> dirtyNavRegions`
|
||
- `HashSet<Vector2Int> queuedNavRegions`
|
||
|
||
`NavRegionRuntime` должен хранить:
|
||
- `NavMeshData NavMeshData`
|
||
- `NavMeshDataInstance Instance`
|
||
- `AsyncOperation ActiveBuild`
|
||
- `int Version`
|
||
- `bool BuildRequestedWhileRunning`
|
||
- `Bounds BuildBounds`
|
||
|
||
## New Config Settings
|
||
|
||
Добавить в NavMesh module config:
|
||
- `int navRegionSizeInChunks = 2`
|
||
- `int maxConcurrentNavMeshBuilds = 1`
|
||
- `int maxNavMeshBuildsPerFrame = 1`
|
||
- `float navBoundsHorizontalPadding`
|
||
- `float navBoundsVerticalPadding`
|
||
- `int navWarmupRadiusInRegions`
|
||
|
||
Примечание:
|
||
- для первой итерации `maxConcurrentNavMeshBuilds` должен оставаться `1`
|
||
|
||
## Build Flow
|
||
|
||
### Initial Sync
|
||
|
||
1. Сервис стартует.
|
||
2. Через `IChunkNavSourceReader` получает список уже загруженных чанков.
|
||
3. Помечает соответствующие nav regions dirty.
|
||
4. Через `IWorldInterestReader` получает текущий interest set.
|
||
5. Запускает scheduler.
|
||
|
||
### Incremental Update
|
||
|
||
1. Приходит `ChunkNavGeometryReadyMessage`.
|
||
2. Сервис читает актуальные sources через `IChunkNavSourceReader`.
|
||
3. Помечает nav region dirty.
|
||
4. Если chunk расположен на границе region, дополнительно маркирует соседний region.
|
||
|
||
### Removal
|
||
|
||
1. Приходит `ChunkNavGeometryRemovedMessage`.
|
||
2. Сервис помечает соответствующий region dirty.
|
||
3. Если region опустел и вышел из активной зоны, удаляет `NavMeshDataInstance`.
|
||
|
||
### Interest Update
|
||
|
||
1. Приходит `WorldInterestChangedMessage`.
|
||
2. Сервис обновляет current interest set.
|
||
3. Scheduler пересчитывает порядок rebuild и warmup regions.
|
||
|
||
## Source Collection Rules
|
||
|
||
Для каждого затронутого region:
|
||
- собрать build sources только из чанков региона и соседнего margin
|
||
- использовать только известные source snapshots из reader interface
|
||
- не сканировать произвольные объекты сцены
|
||
|
||
Для каждого chunk snapshot:
|
||
- добавить sources из `ChunkNavBuildSourceDescriptor`
|
||
- если текущий `VoxelWorld` строит эти descriptors из `GroundCollider` и `MountainCollider`, это остается его внутренней деталью и не становится contract-level зависимостью NavMesh-модуля
|
||
|
||
## Performance Rules
|
||
|
||
- не делать full-scene bake
|
||
- не пересобирать NavMesh синхронно через `BuildNavMesh()` на gameplay path
|
||
- не строить систему в расчете на обязательный background threading
|
||
- держать максимум один активный region build
|
||
- rebuild запускать только после фактического применения collider mesh
|
||
- события из `MessagePipe` не должны тащить heavy geometry payload, только invalidation keys
|
||
- scheduler должен быть bounded и deterministic по budget
|
||
|
||
## Verification Plan
|
||
|
||
### Functional
|
||
|
||
1. Запустить `VoxelWorldTestScene`.
|
||
2. Убедиться, что NavMesh module можно отключить и world generation продолжает работать.
|
||
3. Подключить NavMesh module и проверить появление walkable NavMesh на уже загруженных чанках.
|
||
4. Проверить, что agent из AI Navigation samples строит путь по поверхности.
|
||
5. Проверить unload чанков: старый NavMesh не должен оставлять висячие walkable islands.
|
||
|
||
### Integration
|
||
|
||
1. Проверить старт сервиса после world generator: missed events не должны ломать initial sync.
|
||
2. Проверить, что модуль работает только через DI-injected `MessagePipe` и reader interfaces.
|
||
3. Проверить, что отключение регистрации `VoxelWorldNavMesh` не ломает world feature.
|
||
4. Проверить, что внешний interest contract допускает один или несколько interest points, даже если первая scene wiring пока подает только одного player actor.
|
||
|
||
### 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
|
||
- multi-agent bake
|
||
- networked NavMesh replication
|
||
- ownership migration для чанков или NPC
|
||
- финальная multiplayer interest model вокруг всех actors
|
||
- прямые зависимости NavMesh feature от `VoxelWorldGenerator` internals
|
||
|
||
## Execution Order
|
||
|
||
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.
|