Краткая информация
Представлена сцена в 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 );
}
}