Краткая информация
Представлена сцена в Unity, по которой передвигается зеленый куб, управляемый игроком мышкой, и синяя капсула, которая всегда следует за кубом. Они перемещаются по белому плейну вокруг красных препятствий.
![Скриншот из Unity Скриншот из Unity](https://habrastorage.org/getpro/habr/upload_files/fdf/4f1/07e/fdf4f107e6110954634a9add04754f82.png)
Была сгенерирована навигационная сетка. На зеленом кубе содержатся компоненты NavMeshAgent и PlayerNavigation. Синей капсуле NavMeshAgent и TargetNavigation. На красных кубак компонент NavMeshObstacle.
public class PlayerNavigation : MonoBehaviour
{
private NavMeshAgent _agent = null;
private Camera _camera = null;
private void Start()
{
_agent = GetComponent<NavMeshAgent>();
_camera = Camera.main;
}
private void Update()
{
if ( Input.GetMouseButtonDown( 0 ) )
{
Ray ray = _camera.ScreenPointToRay( Input.mousePosition );
if ( Physics.Raycast( ray, out RaycastHit hit ) )
_agent.SetDestination( hit.point );
}
}
}
public class TargetNavigation : MonoBehaviour
{
[SerializeField] Transform _target = null;
private NavMeshAgent _agent = null;
private void Start()
{
_agent = GetComponent<NavMeshAgent>();
}
private void Update()
{
_agent.SetDestination( _target.position );
}
}
public class VisualPath : MonoBehaviour
{
private LineRenderer _lineRenderer = null;
private NavMeshAgent _agent = null;
private void Start()
{
_agent = GetComponent<NavMeshAgent>();
_lineRenderer = GetComponent<LineRenderer>();
}
private void LateUpdate()
{
if ( _agent.hasPath )
{
_lineRenderer.positionCount = _agent.path.corners.Length;
_lineRenderer.SetPositions( _agent.path.corners );
}
}
}
Навигационная система в Unigine
Немного теории.
Область навигации в Unigine можно задать двумя способами: навигационным мешем (Navigation Mesh) и навигационным сектором (Navigation Sector), с возможностью построения маршрута в 2D и 3D пространстве (при поиске 2D маршрута координата Z не учитывается).
Навигационный меш - область в виде загружаемого полигона меша. Важно учитывать, что поиск пути происходит только в пределах одного навигационного меша. Возможность перехода на другой навигационный меш или сектор при поиске пути не поддерживается, так же доступно построение только 2D маршрута. Сам меш не генерируется автоматически и его нужно загружать в виде 3D модели.
![Навигационный меш Навигационный меш](https://habrastorage.org/getpro/habr/upload_files/6c3/27a/ad5/6c327aad542f46011fa0c47e65d20b28.png)
Навигационный сектор - область в виде куба. Он поддерживает возможность построения пути между несколькими секторами и построение 2D и 3D маршрутов. Чтобы маршрут был составлен между несколькими секторами, нужно выставить радиус и высоту маршрута достаточными, чтобы точка маршрута могла поместиться в области пересечения секторов. Подробнее будет рассмотрено ниже.
![Навигационный сектор Навигационный сектор](https://habrastorage.org/getpro/habr/upload_files/4f5/d1c/da4/4f5d1cda427342b171ac6d5f2b9cf50a.png)
Подробнее про Navigation Sector
Obstacle (препятствия) заставляют огибать область в форме куба, сферы или капсулы маршрут во время поиска пути.
![Препятствие в виде куба Препятствие в виде куба](https://habrastorage.org/getpro/habr/upload_files/941/12f/526/94112f526454b688f87f1f610f4ef9a4.png)
Подготовка мира для поиска пути
Создаем мир и обустраиваем его по аналогии со сценой Unity.
![Обустроенный мир в Unigine Обустроенный мир в Unigine](https://habrastorage.org/getpro/habr/upload_files/d36/872/bb4/d36872bb4a76fd9b48a645678a035de8.png)
Создаем навигационной сектор Create -> Navigation -> Navigation Sector. Располагаем его выше плейна и изменяя Size в окне Parameters, устанавливаем размер сектора в форме уровня.
![Навигационный сектор Навигационный сектор](https://habrastorage.org/getpro/habr/upload_files/963/323/08f/96332308fcf155f8cf7bc016d0cf0b92.png)
Создаем красные кубы, внутри них создаем препятствия Create -> Navigation -> Obstacle Box.
![Препятствия Препятствия](https://habrastorage.org/getpro/habr/upload_files/77a/375/ff2/77a375ff2d116ede1ab283c263403b86.png)
Чтобы постоянно отображать зоны сектора и препятствий, можно включить хелперы.
![Включение хелперов для навигации и препятствий Включение хелперов для навигации и препятствий](https://habrastorage.org/getpro/habr/upload_files/590/600/07d/59060007d2378e19dd8cbd6e2ec5910e.jpg)
![Готовый мир Готовый мир](https://habrastorage.org/getpro/habr/upload_files/93d/a2c/d17/93da2cd1757285e4ec042b10b4aeaff0.png)
Класс PathRoute
PathRoute позволяет найти точки пути маршрута между A и B с помощью двух методов: Create2D(vec3 A, vec3 B) и Create3D(vec3 A, vec3 B).
Задав маски для сектора (навигационного меша) и препятствия, можно фильтровать поиск маршрута.
![Маска для Навигационного сектора Маска для Навигационного сектора](https://habrastorage.org/getpro/habr/upload_files/ca9/fa9/b76/ca9fa9b76813e30737dc5f3ed7d62896.png)
Маршруту можно задать радиус и высоту. Если сектор или пересечение секторов меньше, то они будут исключены из поиска пути.
![Неправильное пересечение Навигационных секторов Неправильное пересечение Навигационных секторов](https://habrastorage.org/getpro/habr/upload_files/1ad/301/257/1ad3012579eb29b643e850b9a70a2e84.png)
![Правильное пересечение Навигационных секторов Правильное пересечение Навигационных секторов](https://habrastorage.org/getpro/habr/upload_files/bd2/0c3/479/bd20c3479e9e99ff47a0cfa17288c9cb.png)
Навигационный агент
Теперь создадим навигационного агента. Зададим базовые поля для нашего агента: скорость, поворот, радиус, высоту, расстояние до остановки и маски. Важно отметить, что данный скрипт будет работать только для навигационного меша или в пределах одного сектора!
public class NavigationAgent : Component
{
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float Speed = 4; //Скорость движения ноды по пути
[ParameterSlider( Max = 60, Min = 0, Group = "Agent" )]
public float RotationSpeed = 25; //Скорость поворота ноды
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float Radius = 0.4f; //Радиус маршрута
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float Height = 0.5f; //Высота маршрута
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float StopDistance = 0.3f; //Расстояния до прекращения поиска маршрута
[ParameterMask( Group = "Parameter Mask" )]
public int NavigationMask = 1; //Маска сектора или меша
[ParameterMask( Group = "Parameter Mask" )]
public int ObstacleMask = 1; //Маска препятствия обхода пути маршрута
}
Зададим три приватных поля для внутренней работы скрипта.
private bool _isRecalculate; //Самостоятельный пересчет маршрута
private vec3 _pointDirection; //Точка следования маршрута
private PathRoute _route; //Класс работы с маршрутом
Первый метод InitRoute будет задавать значение полям класса PathRoute при изменении извне.
private void InitRoute()
{
_route.NavigationMask = NavigationMask;
_route.ObstacleMask = ObstacleMask;
_route.Radius = Radius;
_route.Height = Height;
}
И вызываем его в методе Init и Update.
private void Init()
{
_route = new PathRoute();
InitRoute();
}
private void Update()
{
InitRoute();
}
Дабы иметь возможность в самом приложении отображать маршрут, создадим метод RenderVisualizer и вызовем его в методе Update. Само отображение можно включить, прописав в методе Init “Visualizer.Enabled = true;” или введя консольную команду “show_visualizer 1”.
private void RenderVisualizer()
{
//Рисуем цилиндр на основе высоты и радиуса маршрута
Visualizer.RenderCylinder( Radius, Height, node.WorldTransform, vec4.RED );
//Проверяем, что маршрут построен и отображаем его путь
if ( _route.IsReached )
_route.RenderVisualizer( vec4.RED );
}
Создадим метод построения маршрута SetDirection. Первый аргумент будет принимать конечную точку маршрута, второй аргумент автоматический пересчет маршрута.
public void SetDirection( in vec3 point, in bool recalculate = true )
{
_pointDirection = point;
_isRecalculate = recalculate;
_route.Create2D( node.WorldPosition, point );
}
Последний метод агента будет двигаться по пути следования маршрута. Создадим метод MoveDirection. Вначале будем проверять, что маршрут построен и расстояние до конечной точки удовлетворяет условию.
private void MoveDirection()
{
if ( _route.IsReached )
{
if ( _route.Distance <= StopDistance )
return;
//Продолжение
}
}
Далее определим вектор направления движения между двумя актуальными точками маршрута и проверим, что он еще актуален.
vec3 direction = _route.GetPoint( 1 ) - _route.GetPoint( 0 );
if ( direction.Length2 > MathLib.EPSILON )
{
//Продолжение
}
Теперь сделаем поворот в направлении движения. Методом MathLib.SetTo получим матрицу и из нее quat направления движения. Затем методом MathLib.Slerp будем плавно изменять поворот ноды агента.
//Поворот ноды в направлении движения
quat directionRotation = new quat( MathLib.SetTo( vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y ) );
quat newRotation = MathLib.Slerp( node.GetWorldRotation(), directionRotation, Game.IFps * RotationSpeed );
node.SetWorldRotation( newRotation );
Заставим ноду переместиться вперед относительно себя.
//Перемещение ноды
node.Translate( vec3.FORWARD * Game.IFps * Speed );
В конце проверим, нужен ли перерасчет поиска пути.
//Проверка на перерасчет поиска пути
if ( _isRecalculate )
_route.Create2D( node.WorldPosition, _pointDirection );
Вызовем этот метод в методе Update между InitRoute и RenderVisualizer.
Теперь можно добавить этот скрипт к зеленому кубу и синей капсуле.
![Пример Navigation Agent Пример Navigation Agent](https://habrastorage.org/getpro/habr/upload_files/599/9bc/0d1/5999bc0d1248b4b918b6e2a8cdac8b4a.png)
Создадим скрипт TargetNavigation, который позволит синей капсуле следовать за зеленым кубом. Автоматический перерасчет в NavigationAgent нам не нужен, так как наша точка следования постоянно изменяется, поэтому вызываем метод SetDirection в PostUpdate.
public class TargetNavigation : Component
{
[ShowInEditor] Node _target = null;
private NavigationAgent _agent = null;
private void Init()
{
_agent = GetComponent<NavigationAgent>( node );
}
private void PostUpdate()
{
_agent.SetDirection( _target.WorldPosition, false );
}
}
Создаем последний скрипт PlayerNavigation, который будет указывать точку следования с помощью мыши.
Необходимы три поля: навигационный агент, пересечение и камера.
public class PlayerNavigation : Component
{
private NavigationAgent _agent = null;
private WorldIntersection _intersection = new WorldIntersection();
private Player _playerCamera = null;
}
В методе Init указываем агента и камеру. Также делаем курсор мыши постоянно видимым.
private void Init()
{
_agent = GetComponent<NavigationAgent>( node );
_playerCamera = Game.Player;
Input.MouseHandle = Input.MOUSE_HANDLE.USER;
}
Создаем метод, который будет пускать луч, в точке соприкосновения которого будет указывать движение зеленому кубу.
private void MoveNavigation()
{
ivec2 mousePosition = Input.MousePosition;
vec3 start = _playerCamera.WorldPosition;
vec3 end = start + _playerCamera.GetDirectionFromMainWindow( mousePosition.x, mousePosition.y ) * 100f;
Object intersectionObject = World.GetIntersection( start, end, 1, _intersection );
if ( intersectionObject )
_agent.SetDirection( _intersection.Point );
}
В методе Update вызываем метод MoveNavigation при нажатии ЛКМ.
private void Update()
{
if ( Input.IsMouseButtonDown( Input.MOUSE_BUTTON.LEFT ) )
MoveNavigation();
}
Теперь добавляем скрипт TargetNavigation синей капсуле и PlayerNavigation зеленому кубу. Так же рекомендуется красным кубам, синей капсуле и зеленому кубу выключить Intersection в окне Parameters, чтобы лучу ничего не мешало и он работал только на плейне. Теперь можно запустить.
![Отключение Intersection Отключение Intersection](https://habrastorage.org/getpro/habr/upload_files/9ca/cc0/ba5/9cacc0ba5f2d57582edd182792d9422e.jpg)
Финальный результат проделанной работы.
![Скриншот из Unigine Скриншот из Unigine](https://habrastorage.org/getpro/habr/upload_files/c6e/2c4/5f0/c6e2c45f0b9d567b358a422bd3a3d410.png)
public class NavigationAgent : Component
{
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float Speed = 4; //Скорость движения ноды по пути
[ParameterSlider( Max = 60, Min = 0, Group = "Agent" )]
public float RotationSpeed = 25; //Скорость поворота ноды
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float Radius = 0.4f; //Радиус маршрута
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float Height = 0.5f; //Высота маршрута
[ParameterSlider( Max = 10, Min = 0, Group = "Agent" )]
public float StopDistance = 0.3f; //Расстояния до прекращения поиска маршрута
[ParameterMask( Group = "Parameter Mask" )]
public int NavigationMask = 1; //Маска сектора или меша, на которых будет
[ParameterMask( Group = "Parameter Mask" )]
public int ObstacleMask = 1; //Маска препятствия обхода пути маршрута
private bool _isRecalculate; //Самостоятельный пересчет маршрута
private vec3 _pointDirection; //Точка следования маршрута
private PathRoute _route; //Класс работы с маршрутом
private void Init()
{
_route = new PathRoute();
InitRoute();
}
private void Update()
{
InitRoute();
MoveDirection();
RenderVisualizer();
}
private void InitRoute()
{
_route.NavigationMask = NavigationMask;
_route.ObstacleMask = ObstacleMask;
_route.Radius = Radius;
_route.Height = Height;
}
private void RenderVisualizer()
{
//Рисуем цилиндр на основе высоты и радиуса маршрута
Visualizer.RenderCylinder( Radius, Height, node.WorldTransform, vec4.RED );
//Проверяем, что маршрут построен и отображаем его путь
if ( _route.IsReached )
_route.RenderVisualizer( vec4.RED );
}
private void MoveDirection()
{
if ( _route.IsReached )
{
if ( _route.Distance <= StopDistance )
return;
vec3 direction = _route.GetPoint( 1 ) - _route.GetPoint( 0 );
if ( direction.Length2 > MathLib.EPSILON )
{
//Поворот ноды в направлении движения
quat directionRotation = new quat( MathLib.SetTo( vec3.ZERO, direction.Normalized, vec3.UP, MathLib.AXIS.Y ) );
quat newRotation = MathLib.Slerp( node.GetWorldRotation(), directionRotation, Game.IFps * RotationSpeed );
node.SetWorldRotation( newRotation );
//Перемещение ноды
node.Translate( vec3.FORWARD * Game.IFps * Speed );
}
//Проверка на перерасчет поиска пути
if ( _isRecalculate )
_route.Create2D( node.WorldPosition, _pointDirection );
}
}
public void SetDirection( in vec3 point, in bool recalculate = true )
{
_pointDirection = point;
_isRecalculate = recalculate;
_route.Create2D( node.WorldPosition, point );
}
}
public class TargetNavigation : Component
{
[ShowInEditor] Node _target = null;
private NavigationAgent _agent = null;
private void Init()
{
_agent = GetComponent<NavigationAgent>( node );
}
private void PostUpdate()
{
_agent.SetDirection( _target.WorldPosition, false );
}
}
public class PlayerNavigation : Component
{
private NavigationAgent _agent = null;
private WorldIntersection _intersection = new WorldIntersection();
private Player _playerCamera = null;
private void Init()
{
_agent = GetComponent<NavigationAgent>( node );
_playerCamera = Game.Player;
Input.MouseHandle = Input.MOUSE_HANDLE.USER;
}
private void Update()
{
if ( Input.IsMouseButtonDown( Input.MOUSE_BUTTON.LEFT ) )
MoveNavigation();
}
private void MoveNavigation()
{
ivec2 mousePosition = Input.MousePosition;
vec3 start = _playerCamera.WorldPosition;
vec3 end = start + _playerCamera.GetDirectionFromMainWindow( mousePosition.x, mousePosition.y ) * 100f;
Object intersectionObject = World.GetIntersection( start, end, 1, _intersection );
if ( intersectionObject )
_agent.SetDirection( _intersection.Point );
}
}