Files
TheDeclineOfWarriors/docs/plans/TASK-0023-runtime-navmesh-implementation-plan.md
T
2026-04-08 02:39:17 +03:00

338 lines
16 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 как подключаемый 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.