Краткая информация
Представлена сцена в Unity, по которой передвигается зеленый куб, управляемый игроком мышкой, и синяя капсула, которая всегда следует за кубом. Они перемещаются по белому плейну вокруг красных препятствий.

Была сгенерирована навигационная сетка. На зеленом кубе содержатся компоненты 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 модели.

Навигационный сектор — область в виде куба. Он поддерживает возможность построения пути между несколькими секторами и построение 2D и 3D маршрутов. Чтобы маршрут был составлен между несколькими секторами, нужно выставить радиус и высоту маршрута достаточными, чтобы точка маршрута могла поместиться в области пересечения секторов. Подробнее будет рассмотрено ниже.

Подробнее про Navigation Sector
Obstacle (препятствия) заставляют огибать область в форме куба, сферы или капсулы маршрут во время поиска пути.

Подготовка мира для поиска пути
Создаем мир и обустраиваем его по аналогии со сценой Unity.

Создаем навигационной сектор Create -> Navigation -> Navigation Sector. Располагаем его выше плейна и изменяя Size в окне Parameters, устанавливаем размер сектора в форме уровня.

Создаем красные кубы, внутри них создаем препятствия Create -> Navigation -> Obstacle Box.

Чтобы постоянно отображать зоны сектора и препятствий, можно включить хелперы.


Класс PathRoute
PathRoute позволяет найти точки пути маршрута между A и B с помощью двух методов: Create2D(vec3 A, vec3 B) и Create3D(vec3 A, vec3 B).
Задав маски для сектора (навигационного меша) и препятствия, можно фильтровать поиск маршрута.

Маршруту можно задать радиус и высоту. Если сектор или пересечение секторов меньше, то они будут исключены из поиска пути.


Навигационный агент
Теперь создадим навигационного агента. Зададим базовые поля для нашего агента: скорость, поворот, радиус, высоту, расстояние до остановки и маски. Важно отметить, что данный скрипт будет работать только для навигационного меша или в пределах одного сектора!
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.
Теперь можно добавить этот скрипт к зеленому кубу и синей капсуле.

Создадим скрипт 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, чтобы лучу ничего не мешало и он работал только на плейне. Теперь можно запустить.

Финальный результат проделанной работы.

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 ); } }
ссылка на оригинал статьи https://habr.com/ru/articles/716722/
Добавить комментарий