Готовый проект можно найти здесь: 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, которая и отделит кость от меша. При отделении кости от меша будем также придавать импульс данной кости, чтобы дрон "разлетался" на части:

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

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


  1. VBDUnit
    28.10.2025 10:47

    Интересно. Правильно ли я понимаю, что если я захочу запустить рой (например, 1000) таких дронов, то они должны воспринимать друг друга как препятствие, и сложность вычислений пути взлетит по экспоненте? И тут надо прикручивать что‑то типа растровой 3D сетки (ну или октодерева для навигации, но это перебор имхо)?


    1. 123skipper123 Автор
      28.10.2025 10:47

      Должны ли дроны воспринимать друг друга как препятствие зависит от настроек объекта DonNavigationManagerUnbound (какие каналы коллизий он воспринимает) и от настроек коллизии самих дронов. То есть даже стены при желании можно убрать из вычисления пути.


    1. Jijiki
      28.10.2025 10:47

      не знаю как в Анриале, но вся суть укладывается в

      • террейн текущий(с эвристикой проходов - там по разному делают у меня градиенты покачто,а в подсистеме штраф за угол) (с него берем граф)(!1 граф)(типо нав меш ну вобщем граф - сетка)

      • подсистемой будет движение по Route (для обьекта, который на текущей сетке - графе)

      • маршрут персональный вернется из подсистемы на обьект или А* или Дейкстра

        на таком сурсе я пока тестил 2 кубика, на подходе 400 анимационных скелетных моделек )

      можно физикой(если физика нелинейная на столкновения то поидее на 1000 не много, надо смотреть) наверно корректировать при проводимости обьекта на подсистеме, тоесть просто упорядочивать относительно друг друга наверно, чтоб роут не улетел в небо при поиске пути, тоесть искать только по статике, а в момент роута упорядочивать относительно статики и обьектов(по флагу проходят сквозь динамичные обьекты или нет типо)

      но вообще уникальных моделек из таблицы предположим 1000 в 1 месте много, соотв нагруз на столкновения будет добавлять, там общая кривая около 200 взависимости от железа в одном месте


  1. PatakinVVV
    28.10.2025 10:47

    Без полноценного контроллера движения такие дроны часто дергаются, особенно при поворотах на высокой скорости


  1. nukler
    28.10.2025 10:47

    Мне кажется идея с таймером перезарядки надумана. Зачем он? Что решает? На авиационных пулемётах это лента, на автопушках были кассеты, но после выстрела автопушки, ваш дрон унесёт куда нить за горизонт, да и сколько снарядов будет? Если уж на нормальных самолётах их было ну штук 30, ну прям вот 40 это же предел.

    А вот что не надумано, это отдача от выстрела. В некоторых автоматах есть "отложенная" отдача (смещенный импульс отдачи) АН-94/АЕК-971, можно ещё вспомнить пулемёт Шоша́, когда после выстрела смещается и затвор и ствол, но там всё сложно да и надо ли, так как сам по себе он не скорострельный и смысла перезарядки вообще не будет.

    То есть два/три выстрела фактически мгновенно, далее дрон получив кинетическую энергию от выстрела отлетает от игрока. Затем он возвращается на расстояние прицельной стрельцы и цикл повторяется.