Система м'якої прив'язки — це така система, в якій камера не прив'язана до напрямку, в якому персонаж буде завдавати удару. Замість цього персонаж обертатиметься до найбільш відповідної цілі, на основі напрямку, в якому гравець хоче атакувати. Перевага цього підходу полягає в тому, що гравець не повинен "націлювати" свої удари, що може бути незручним, особливо при використанні клавіатури та миші. Копіюючи код нижче, не забудьте оновити свій заголовочний файл і замінити ім'я класу на ваше.
Основні кроки в цьому посібнику виглядають наступним чином:
1. Знаходження цілі для фокусування атак
-
Перевірка, чи знаходиться ціль в межах досяжності
-
Поворот персонажа до цілі перед атакою
Налаштування проекту
Для цього посібника я почну з створення проекту з прикладом третьої особи в Unreal Engine 5 та вибору опції C++.
Якщо ви намагаєтеся додати це програмне забезпечення до існуючого проекту, і не починали з C++, ви можете додати його, перейшовши в меню Tools -> generate visual studio/xcode project.
Створення проекту
Далі запустіть проект Visual Studio / Xcode, вибравши Tools -> open visual studio/xcode project.
Перейдіть до класу персонажа, це буде файл, що має таку ж назву, як і ваш проект. У моєму випадку це SoftLockTutCharacter.cpp і SoftLockTutCharacter.h. Файли C++ зазвичай знаходяться в папці private, а заголовочні файли — в папці public на Windows.
На Mac вони всі знаходяться в одній директорії.
Запуск Xcode
Шляхи до папок
Налаштування каналу зіткнень
В Unreal є різні методи зіткнень, які підтримуються двигуном, а також різні канали. У нашому випадку ми додамо трасу зіткнення в налаштуваннях проекту, щоб уникнути попадання в об’єкти, які нас не цікавлять.
Для цього в Unreal перейдіть у Edit -> Project Settings
Налаштування проекту
Перейдіть у меню "collision" в розділі Engine:
Налаштування зіткнень проекту
Додайте новий канал трасування, назвіть його "enemy" і встановіть дію за замовчуванням на "ігнорувати цей слід", після чого закрийте меню налаштувань проекту.
Давайте також додамо кілька цілей, на які можна буде націлюватися, для цього просто перетягніть манекен Unreal 5 у проект.
Додавання манекена
Виберіть манекен і перейдіть до панелі деталей для манекена, прокрутіть вниз до меню зіткнень і змініть налаштування зіткнень на "Custom". Також змініть трасу "enemy" на блокування зіткнень.
Це дозволяє нам блокувати сфери трасування, які ми налаштували раніше на каналі зіткнень.
Пошук найкращої цілі
Щоб знайти найкращу ціль, користувач може здійснити сферичне трасування в напрямку останнього вектору введення.
Це буде проводити трасування в напрямку, в якому користувач хоче атакувати, і знаходити наступного ворога, якого користувач може вразити.
Повна функція виглядає наступним чином, а розбір різних частин можна побачити нижче.
void ASoftLockTutCharacter::FindBestTarget(ACharacter* PlayerCharacter, float softLockDistance)
{
UWorld* World = PlayerCharacter->GetWorld();
if (!PlayerCharacter || !PlayerCharacter->GetController() || !World)
{
UE_LOG(LogTemp, Warning, TEXT("PlayerCharacter is null"));
return;
}
FVector characterDirection = PlayerCharacter->GetLastMovementInputVector();
FVector playerLocation = PlayerCharacter->GetActorLocation();
float sphereDiameter = 300.0f;
FVector StartLocation = PlayerCharacter->GetActorLocation();
FVector EndLocation = PlayerCharacter->GetActorLocation() + characterDirection * 1000;
// Налаштування параметрів запиту зіткнень
FCollisionQueryParams TraceParams(FName(TEXT("sphereTrace")), true);
TraceParams.AddIgnoredActor(PlayerCharacter);
// Створюємо масив для збереження результатів зіткнень
TArray HitResults;
DrawDebugSphere(World, StartLocation, sphereDiameter, 16, FColor::Red, false, 2.0f, 0, 5.0f);
DrawDebugSphere(World, EndLocation, sphereDiameter, 16, FColor::Green, false, 2.0f, 0, 5.0f);
DrawDebugCylinder(World, StartLocation, EndLocation, sphereDiameter, 16, FColor::Green, 0, 5.0);
bool bHit = World->SweepMultiByChannel(
HitResults,
StartLocation,
EndLocation,
FQuat::Identity,
ECC_GameTraceChannel1, // Канал трасування
FCollisionShape::MakeSphere(sphereDiameter),
TraceParams
);
// Обробка результатів зіткнень
if (bHit)
{
UE_LOG(LogTemp, Warning, TEXT("Found a target"));
for (FHitResult Hit : HitResults)
{
AActor* PotentialTarget = Hit.GetActor();
if (!PotentialTarget && PotentialTarget != softLockTarget)
{
UE_LOG(LogTemp, Warning, TEXT("target is %s"), *PotentialTarget->GetActorLabel());
continue;
}
UE_LOG(LogTemp, Warning, TEXT("Found an enemy"));
float DistanceToTarget = FVector::Dist(playerLocation, PotentialTarget->GetActorLocation());
// Оновлення найближчої цілі
if (DistanceToTarget < softLockDistance)
{
softLockTarget = PotentialTarget;
softLockTargetAquired = true;
DrawDebugLine(World, StartLocation, PotentialTarget->GetActorLocation(), FColor::Red, false, 2.0f, 0, 5.0f);
}
}
}
}
Почніть з додавання імені функції у файл заголовка під ключовим словом public:.
UFUNCTION(BlueprintCallable) дозволяє викликати цю функцію з всередині blueprint, що полегшує інтеграцію з рештою гри.
UFUNCTION(BlueprintCallable)
void FindBestTarget(ACharacter* PlayerCharacter, float softLockDistance);
Ми також додамо змінну для цілі softlock, щоб зберігати поточну заблоковану ціль, а також прапорець, щоб перевіряти, чи є ціль все ще дійсною.
Це дозволяє нам мати доступ до цілі в інших функціях.
Додайте наступне в файл заголовка під тегом public:.
Тег UPROPERTY дозволяє редагувати змінну в панелі деталей blueprint або в редакторі blueprint.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="softlock")
AActor * softLockTarget;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="softlock")
bool softLockTargetAquired = false;
Наступним кроком давайте визначимо цю функцію в C++ файлі, відкрийте C++ файл класу персонажа. Зверніть увагу, що ім’я класу має відповідати імені класу в визначенні. В моєму випадку це “ASoftLockTutCharacter”
void ASoftLockTutCharacter::FindBestTarget(ACharacter* PlayerCharacter, float softLockDistance)
{
}
Тепер ми проведемо швидку перевірку, щоб переконатися, що жоден з вказаних вказівників не є нульовим.
Це дуже важлива перевірка в C++, оскільки нульовий вказівник спричинить аварійне завершення гри під час виконання.
if (!PlayerCharacter || !PlayerCharacter->GetController() || !World)
{
UE_LOG(LogTemp, Warning, TEXT("PlayerCharacter is null"));
return;
}
Наступним кроком налаштуємо кілька змінних для використання в функції:
- Напрямок персонажа — це напрямок, у якому користувач намагається рухати персонажа, ми будемо використовувати його для вибору напрямку для пошуку цілі.
- Місце розташування гравця — це місце знаходження нашого персонажа у світі.
- Місце кінцевої точки — це місце розташування актора та напрямок руху персонажа, помножений на 1000. 1000 — це відстань, на яку повинна йти прив’язка до цілі (можна налаштувати це значення відповідно до дальності атаки).
- Параметри трасування — це специфічні параметри для запиту колізії, які будуть використовуватися в сферичному трасуванні. Тут поточний актор, що надсилає трасування, ігнорує себе в трасуванні.
HitResults — це масив-контейнер, який буде використовуватися для зберігання цілей, яких торкнеться трасування.
FVector characterDirection = PlayerCharacter->GetLastMovementInputVector();
FVector playerLocation = PlayerCharacter->GetActorLocation();
float sphereDiameter = 300.0f;
FVector StartLocation = PlayerCharacter->GetActorLocation();
FVector EndLocation = PlayerCharacter->GetActorLocation() + characterDirection * 1000;
// Налаштування параметрів запиту колізії
FCollisionQueryParams TraceParams(FName(TEXT("sphereTrace")), true);
TraceParams.AddIgnoredActor(PlayerCharacter);
// Створення масиву для зберігання результатів зіткнень
TArray HitResults;
Функції для малювання відладкових сфер і циліндрів використовуються лише для того, щоб показати візуалізацію того, як виглядає трасування.
DrawDebugSphere(World, StartLocation, sphereDiameter, 16, FColor::Red, false, 2.0f, 0, 5.0f);
DrawDebugSphere(World, EndLocation, sphereDiameter, 16, FColor::Green, false, 2.0f, 0, 5.0f);
DrawDebugCylinder(World, StartLocation, EndLocation, sphereDiameter, 16, FColor::Green, 0, 5.0);
Функція sweep multi by channel виконує трасування колізії від початкового напрямку до кінцевого та зберігає результати в наш масив HitResults.
Канал трасування 1 — це користувацький "ворожий" канал, який ми створили в Unreal Engine (якщо у вас уже був інший канал до того, який ми додали, це буде канал 2). Форма колізії вказує движку, яку форму використовувати для трасування, можливі варіанти: сфера, капсула чи коробка. Параметри трасування — це змінні, які ми налаштували раніше. Обертання FQuat не має значення, оскільки ми здійснюємо трасування колом.
bool bHit = World->SweepMultiByChannel(
HitResults,
StartLocation,
EndLocation,
FQuat::Identity,
ECC_GameTraceChannel1, // Канал трасування
FCollisionShape::MakeSphere(sphereDiameter),
TraceParams
);
Якщо функція повертає успішне зіткнення, нам потрібно переглянути всі результати зіткнень і перевірити, чи є серед них дійсна ціль в межах відстані, на яку ми хочемо зафіксувати.
Якщо знайдена дійсна ціль, вона зберігається в класі і використовується для націлювання на об'єкт.
if (bHit)
{
UE_LOG(LogTemp, Warning, TEXT("Знайдена ціль"));
for (FHitResult Hit : HitResults)
{
AActor* PotentialTarget = Hit.GetActor();
if (!PotentialTarget && PotentialTarget != softLockTarget)
{
UE_LOG(LogTemp, Warning, TEXT("ціль є %s"), *PotentialTarget->GetActorLabel());
continue;
}
UE_LOG(LogTemp, Warning, TEXT("Знайдений ворог"));
float DistanceToTarget = FVector::Dist(playerLocation, PotentialTarget->GetActorLocation());
// Оновлюємо найближчу ціль
if (DistanceToTarget < softLockDistance)
{
softLockTarget = PotentialTarget;
softLockTargetAquired = true;
DrawDebugLine(World, StartLocation, PotentialTarget->GetActorLocation(), FColor::Red, false, 2.0f, 0, 5.0f);
}
}
}
Перевірка, чи ціль в межах досяжності
Щоб перевірити, чи має сенс для персонажа продовжувати націлюватися на ціль, яку ми "м'яко зафіксували", необхідно перевірити, чи знаходиться ця ціль все ще в межах нашої зони атаки.
Відстань між нашим персонажем і персонажем, на якого ми хочемо здійснити м'яке блокування (softlock), можна порівняти. Якщо відстань більша, ціль повинна бути стерта, і персонаж повинен атакувати просто вперед.
Оновлення заголовного файлу:
Додайте наступний прототип функції в заголовний файл під тегом public:
UFUNCTION(BlueprintCallable)
void TargetRangeCheck(float softLockDistance);
Ця функція використовує логіку порівняння відстані з функції пошуку цілі і дозволяє переконатися, що ціль, на яку ми націлювалися, все ще знаходиться в межах досяжності.
Цю функцію можна використовувати після функції придбання цілі, щоб переконатися, що ціль є дійсною.
void ASoftLockTutCharacter::TargetRangeCheck(ACharacter* PlayerCharacter, float softLockDistance)
{
if (!softLockTargetAquired || !PlayerCharacter)
return;
FVector playerLocation = PlayerCharacter->GetActorLocation();
float DistanceToTarget = FVector::Dist(playerLocation, softLockTarget->GetActorLocation());
if (DistanceToTarget > softLockDistance)
{
softLockTarget = nullptr;
softLockTargetAquired = false;
}
}
Поворот персонажа до цілі перед атакою
Оновлення заголовного файлу:
UFUNCTION(BlueprintCallable)
void AimAtTarget(ACharacter* PlayerCharacter, float AimSpeed);
Для того, щоб переконатися, що наш персонаж вразить потрібну ціль, ми повинні повернути героя так, щоб він дивився в напрямку ворога.
Ця функція обчислює необхідний кут для повороту персонажа до ворога і потім виконує цей поворот, змінюючи обертання актора.
void ASoftLockTutCharacter::AimAtTarget(ACharacter* PlayerCharacter, float AimSpeed)
{
if (!softLockTarget || !PlayerCharacter) return;
// Отримати напрямок до заблокованої цілі
FVector DirectionToTarget = softLockTarget->GetActorLocation() - PlayerCharacter->GetActorLocation();
DirectionToTarget.Z = 0.0f; // Тримати прицілювання в 2D (опціонально)
// Обчислити бажане обертання
FRotator TargetRotation = DirectionToTarget.Rotation();
// Інтерполювати до цільового обертання для плавного прицілювання
FRotator NewRotation = FMath::RInterpTo(PlayerCharacter->GetActorRotation(), TargetRotation, GetWorld()->GetDeltaSeconds(), AimSpeed);
PlayerCharacter->SetActorRotation(NewRotation);
}
Використання
Приклад використання цих функцій
При реальній реалізації цієї системи в повноцінній грі функції пошуку найкращої цілі та перевірки діапазону цілі повинні виконуватись під час анімації атаки. Так персонажі завжди будуть рухатись в правильному напрямку при атаці.
Перекладено з: Unreal engine C++: Soft lock target system