Краткая информация

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

Скриншот из Unity
Скриншот из 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 модели.

Навигационный меш
Навигационный меш

Подробнее про Navigation Mesh

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

Навигационный сектор
Навигационный сектор

Подробнее про Navigation Sector

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

Препятствие в виде куба
Препятствие в виде куба

Подробнее про Obstacle

Подготовка мира для поиска пути

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

Обустроенный мир в Unigine
Обустроенный мир в Unigine

Создаем навигационной сектор 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.

Теперь можно добавить этот скрипт к зеленому кубу и синей капсуле.

Пример Navigation Agent
Пример Navigation Agent

Создадим скрипт 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

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

Скриншот из Unigine
Скриншот из Unigine
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 );
	}
}

Официальный сайт Unigine

Документация Unigine

Комментарии (0)