Готовый проект можно найти здесь: https://github.com/JohnMega/Flying-Drone-Sample-Project

В этой статье я объясню как устроен и из чего состоит указанный выше проект. По желанию все действия ниже можно проделать самому.

1. DoN AI Navigation Plugin

Данный плагин понадобится нам для вычисления кратчайшего пути в пространстве с учетом различных препятствий (для этого он использует алгоритм A*). Для его использования, нам нужно разместить на уровне объект под названием DonNavigationManagerUnbound, который разобьет пространство на воксели указанного размера (эти воксели будут вершинами графа для алгоритма A*). После этого можно использовать ноду Fly To в нашем Behavior Tree, для того чтобы дрон перемещался в пространстве.

Демонстрация поиска пути по вокселям
Демонстрация поиска пути по вокселям

2. Подготовка сцены

  1. Выставим на сцену актора DonNavigationManagerUnbound для работы плагина DoN AI Navigation Plugin.

  2. Выставим на сцену Nav Mesh Bounds Volume для корректной работы генератора точек через EQS (точки будут генерироваться в зависимости от положения игрока. Уже эти сгенерированные точки будут передаваться в ноду Fly To).

3. Настройка генерации точек

Создадим два ассета: EQS_FindPlayer (сам генератор), EQC_TargetEnemy (EnvQueryContext для указания актора, относительно которого будут генерироваться точки. В данном случае этим актором будет сам игрок).

  • Настройка EQS:

    • Будем генерировать точки в виде круга. Поэтому создадим генератор OnCircle.

    Настройки генератора можно вставить и свои, но для этих точек вокруг игрока, нужно в параметре Circle Center выставить наш ассет EQC_TargetEnemy.

  • Настройка EQC:

    В блюпринте EQS_TargetEnemy перегрузим функцию Provide Single Actor, и в ней вернем нашего павна (игрока).

4. Создание Behavior Tree дрона

Создадим два ассета BT_FlightDrone (сам Behavior Tree) и BB_FlightDrone (Blackboard, в котором будет храниться сгенерированная точка).

  • Настройка Blackboard:

    В нашем BB_FlightDrone нужно будет создать новую переменную EQSLocation, в которой будет храниться координата одной из сгенерированных точек нашим генератором:

  • Настройка Behavior Tree:

    В BT_FlightDrone укажем наш Blackboard:

    Итоговое дерево поведения в итоге будет выглядеть следующим образом:

    Где Нода Run EQS Query отвеч��ет за запуск ассета EQS_FindPlayer (то есть за генерацию точек вокруг игрока) и передачу одной из сгенерированных точек в переменную EQSLocation из нашего Blackboard:

    Настройки ноды Run EQS Query
    Настройки ноды Run EQS Query

    Нода BTT_SetZOffsetsToEQS является кастомной таской, и отвечает за модификацию переменной EQSLocation (так как по умолчанию EQS генерирует точки на земле, то нам в данном случае их нужно поднять вверх на несколько юнитов от земли):

    Код таски BTT_SetZOffsetsToEQS
    Код таски BTT_SetZOffsetsToEQS

    Нода Fly To отвечает за поиск кратчайшего пути к EQSLocation и движение дрона к этой точке.

    Настройки Fly To
    Настройки Fly To

    В данном случае нам нужно выставить большое значение для параметра Minimum Proximity Required (минимальное расстояние до точки на котором дрон перестает получать новое ускорение), иначе, если значение данного параметра будет маленьким, дрон будет пролетать выбранную точку из-за набранного ускорения.

5. Тестирование Behavior Tree

Перед тестированием Behavior Tree создадим обычного павна AFlightDrone со скелетал мешем и AI контроллера AFlightDroneController, в бегин плее которого будем запускать выбранный Behavior Tree через функцию RunBehaviorTree().

Промежуточный результат
Промежуточный результат

6. Повороты дрона

Все повороты будут происходить в тике павна дрона, и зависеть от его Velocity:

void AFlightDrone::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (!UKismetMathLibrary::Vector_IsNearlyZero(GetVelocity(), 0.001f))
	{
		RotateToPlayer(DeltaTime);
		SetVelocityRotation(DeltaTime);
	}
}

Как можно видеть, итоговый поворот состоит из поворота к игроку, и поворота в зависимости от Velocity (наклона дрона).

  • Поворот к игроку:

    Для того чтобы дрон был постоянно нацелен на игрока, нужно найти поворот по вектору (через функцию FindLookAtRotation) направления от дрона к игроку, и уже текущий поворот интерполировать к полученному.

void AFlightDrone::RotateToPlayer(float DeltaTime)
{
	FRotator TargetRotation = UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), CachedPlayerPawn->GetActorLocation());
	SetActorRotation(UKismetMathLibrary::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, RTPInterpSpeed));
}
  • Реализация наклона в зависимости от Velocity:

    Идея этого поворота заключается в том, чтобы проецировать Velocity вектор на направляющие Forward и Left вектора дрона, и в зависимости от получившейся длины проекции наклонять дрона на некоторый угол вперед или по сторонам.

    После полученных углов наклона, конструируем FRotator, состоящий из данных углов, и интерполируем текущий поворот к вычисленному.

void AFlightDrone::SetVelocityRotation(float DeltaTime)
{
	FRotator TargetRotation(ProjectVelocityOnVector(GetActorForwardVector(), VRProjectOnForwardMaxAngle), GetActorRotation().Yaw, ProjectVelocityOnVector(GetActorRightVector() * -1.0f, VRProjectOnLeftMaxAngle));
	SetActorRotation(UKismetMathLibrary::RInterpTo(GetActorRotation(), TargetRotation, DeltaTime, VRInterpSpeed));
}

float AFlightDrone::ProjectVelocityOnVector(FVector TargetVector, float MaxAngle)
{
	FVector ProjectedVelocity = UKismetMathLibrary::ProjectVectorOnToVector(GetVelocity(), TargetVector);
	float UnsignedAngle = FMath::GetMappedRangeValueClamped<float>(TRange<float>(0, GetMovementComponent()->GetMaxSpeed()), TRange<float>(0, MaxAngle), ProjectedVelocity.Length());
	return UnsignedAngle * FMath::Sign(FVector::DotProduct(TargetVector, ProjectedVelocity)); // Скалярное произведение для определения направления Velocity вектора относительно направляющего вектора
}
Получившиеся повороты
Получившиеся повороты

7. Оружие для дрона

Оружие будем делать в качестве актора, которого потом сделаем Child актором нашего дрона. Оружие будет стрелять очередями (по 3 выстрела).

На Begin Play мы будем запускать основной таймер перезарядки, который уже в свою очередь будет запускать серию из таймеров для задержки между выстрелами из очереди:

void ADroneWeapon::BeginPlay()
{
	Super::BeginPlay();
	
	CachedPlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
	if (!CachedPlayerPawn.IsValid())
	{
		UE_LOG(LogDroneWeapon, Error, TEXT("%s::%hs: Failed to get player pawn"), *GetName(), __FUNCTION__);
		return;
	}

	GetWorld()->GetTimerManager().SetTimer(WeaponReloadTimer, this, &ADroneWeapon::WeaponReloadTimerProcess, WeaponReloadTime, false);
}

Код обработки таймеров:

void ADroneWeapon::WeaponReloadTimerProcess()
{
	GetWorld()->GetTimerManager().SetTimer(WeaponQueueTimer, this, &ADroneWeapon::WeaponQueueTimerProcess, WeaponQueueTime, false);
}

void ADroneWeapon::WeaponQueueTimerProcess()
{
	SpawnProjectile();
	if (++WeaponReleasedProjectilesNum >= WeaponProjectilesNum)
	{
		WeaponReleasedProjectilesNum = 0;
		GetWorld()->GetTimerManager().SetTimer(WeaponReloadTimer, this, &ADroneWeapon::WeaponReloadTimerProcess, WeaponReloadTime, false);
	}
	else
	{
		GetWorld()->GetTimerManager().SetTimer(WeaponQueueTimer, this, &ADroneWeapon::WeaponQueueTimerProcess, WeaponQueueTime, false);
	}
}

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

При этом в тике нам нужно держать прицел оружия на игрока (то есть поворачивать оружие на игрока):

void ADroneWeapon::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (!CachedPlayerPawn.IsValid())
	{
		return;
	}

	SetActorRotation(UKismetMathLibrary::FindLookAtRotation(GetActorLocation(), CachedPlayerPawn->GetActorLocation()));
}

Получившийся результат:

8. Уничтожение дрона

Для того чтобы дрон разлетался на части при смерти, нужно настроить PhysicsAsset модели этого дрона.

Для начала нужно для каждой кости нашего дрона сгенерировать физическую поверхность. Для этого отобразим все кости, а затем для каждой кости добавим тело через команду Add/Replace Bodies:

После этого при смерти дрона нужно для каждой значимой кости вызвать ноду Break Constraint, которая и отделит кость от меша. При отделении кости от меша будем также придавать импульс данной кости, чтобы дрон "разлетался" на части:

Код отделения костей от меша дрона и придания им импульса
Код отделения костей от меша дрона и придания им импульса
Уничтожение дрона
Уничтожение дрона