Implementing a basic Radar System

Переводил: Александр Ретюнский
Original Page

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

Video
[свернуть]
Все исходники доступны в Репозитории автора

Рисование радара в нашем HUD

Создайте проект C++ First Person Template и откройте сгенерированный HUD класс. Перед рисованием радара, мы должны определить его положение на экране игрока.

Не забывайте, что игроки могут иметь разные разрешения экранов, так что нам нужен способ сообщить UE4, чтобы он рисовал наш радар относительно, а не прописывал в коде значения ширины и высоты. К счастью, внутри HUD класса UE4 добавил Canvas, который содержит фактическую ширину и высоту экрана игрока. Для того, чтобы использовать этот функционал, мы сделаем 2D вектор с названием RadarStartLocation, который будет множителем для выбора позиции нашего радара, что будет осуществляться передачей относительных значений вместо жестко фиксированных.

Так, давайте предположим, что моё разрешение экрана 1920×1080. Если я умножу ширину и высоту на ноль, я получу точку в левом верхнем углу экрана. Лучше посмотреть на граф для более точного понимания работы:

Image

[свернуть]

Значения X и Y соответствуют значениям нашего множителя (в этом случаем RadarStartLocation). Поэтому, если я введу значения 0.9 и 0.2 (читай 90% и 20% – прим. пер.) в RadarStartLocation, UE4 поставит наш радар где-то около верхнего левого угла. Как только мы нарисуем наш радар, я предлагаю откалибровать значения, изменяя эти два коэффицента.

Со всей этой теорией давайте-таки нарисуем наш радар. Откройте HUD класс, представленный в проекте First Person C++ Template, который вы создали, и введите следующие свойства:

Code


protected:

/*The start location of our radar*/
UPROPERTY(EditAnywhere, Category = Radar)
FVector2D RadarStartLocation = FVector2D(0.9f,0.2f);

/*The radius of our radar*/
UPROPERTY(EditAnywhere, Category = Radar)
float RadarRadius = 100.f;

UPROPERTY(EditAnywhere, Category = Radar)
float DegreeStep = 0.25f;

/*The pixel size of the drawable radar actors*/
UPROPERTY(EditAnywhere, Category = Radar)
float DrawPixelSize = 5.f;

[свернуть]

Далее, создайте приватные функции:

Code


/*Returns the center of the radar as a 2d vector*/
FVector2D GetRadarCenterPosition();

/*Draws the radar*/
void DrawRadar();

[свернуть]

Переключитесь на исходный файл и включите следующую логику для функций, объявленных выше:

Code


darCenterPosition and DrawRadar implementationC++

FVector2D AMinimapHUD::GetRadarCenterPosition()
{
//If the canvas is valid, return the center as a 2d vector
return (Canvas) ? FVector2D(Canvas->SizeX*RadarStartLocation.X, Canvas->SizeY*RadarStartLocation.Y) : FVector2D(0, 0);
}

void AMinimapHUD::DrawRadar()
{
FVector2D RadarCenter = GetRadarCenterPosition();

for (float i = 0; i < 360; i+=DegreeStep) { //We want to draw a circle in order to represent our radar //In order to do so, we calculate the sin and cos of almost every degree //It it impossible to calculate each and every possible degree because they are infinite //Lower the degree step in case you need a more accurate circle representation //We multiply our coordinates by radar size //in order to draw a circle with radius equal to the one we will input through the editor float fixedX = FMath::Cos(i) * RadarRadius; float fixedY = FMath::Sin(i) * RadarRadius; //Actual draw DrawLine(RadarCenter.X, RadarCenter.Y, RadarCenter.X + fixedX, RadarCenter.Y + fixedY, FLinearColor::Gray, 1.f); } } FVector2D AMinimapHUD::GetRadarCenterPosition() { //If the canvas is valid, return the center as a 2d vector return (Canvas) ? FVector2D(Canvas->SizeX*RadarStartLocation.X, Canvas->SizeY*RadarStartLocation.Y) : FVector2D(0, 0);
}

void AMinimapHUD::DrawRadar()
{
FVector2D RadarCenter = GetRadarCenterPosition();

for (float i = 0; i < 360; i+=DegreeStep) { //We want to draw a circle in order to represent our radar //In order to do so, we calculate the sin and cos of almost every degree //It it impossible to calculate each and every possible degree because they are infinite //Lower the degree step in case you need a more accurate circle representation //We multiply our coordinates by radar size //in order to draw a circle with radius equal to the one we will input through the editor float fixedX = FMath::Cos(i) * RadarRadius; float fixedY = FMath::Sin(i) * RadarRadius; //Actual draw DrawLine(RadarCenter.X, RadarCenter.Y, RadarCenter.X + fixedX, RadarCenter.Y + fixedY, FLinearColor::Gray, 1.f); } }

[свернуть]

Объяснение параметров DrawLine:
1. Координата X – начало линии
2. Координата Y – начало линии
3. Координата X – конец линии
4. Координата Y – конец линии
5. Цвет линии
6. Толщина линии
Предполагая, что вы реализовали логику выше, перейдите в функцию DrawHUD и после предварительно реализованного кода по-умолчанию, введите вызов функции DrawRadar().
- Как только вы сохранили и скомпилировали ваш код, переключитесь в ваш редактор и:
- Создайте game mode Blueprint, основанный на дефолтном game mode Blueprint
- Создайте HUD Bluepriint, основанный на нашем C++ классе
Назначьте game mode и HUD Blueprint в world settings:
Вы можете присвоить C++ класс в HUD класс так же, однако с того момента, как мы будем выставлять дополнительные свойства, я предлагаю использовать этот подход для того, чтобы избежать компилирования кода при каждом изменении выставленных свойств.
Сейчас вы должны быть в состоянии видеть серый радар в правом верхнем углу.

Image

[свернуть]

Рисование позиции игрока на радаре
Поскольку игрок всегда будет находиться в центре радара, создайте функцию DrawPlayerInRadar и реализуйте следующую логику:

Code


void AMinimapHUD::DrawPlayerInRadar()
{
FVector2D RadarCenter = GetRadarCenterPosition();

DrawRect(FLinearColor::Blue, RadarCenter.X, RadarCenter.Y, DrawPixelSize, DrawPixelSize);
}

[свернуть]

Далее перейдите к функции DrawHUD, и сразу после функции DrawRadar() вызовите функцию DrawPlayerInRadar()

Поиск ближайший Actors для нашего радара

В этом случае я решил использовать метод “бросания лучей” (raycast) для нахождения ближайших actors к игроку и записывать всех actors, содержащих метку “радар” в массив для последующего вывода в радаре. Однако, в зависимости от вашей игры ваша оптимальная реализация может отличаться! Я предлагаю следовать моему подходу, и, как только у вас есть полностью работающий радар, реализовать собственную логику.
Оговорив все детали, создайте следующие защищённые (protected) свойства в header файле нашего HUD класса:

Code


/*Sphere height and radius for our raycast*/
UPROPERTY(EditAnywhere, Category = Radar)
float SphereHeight = 200.f;

UPROPERTY(EditAnywhere, Category = Radar)
float SphereRadius = 2750.f;

/*Holds a reference to every actor we are currently drawing in our radar*/
TArray RadarActors;

[свернуть]

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

Code


void AMinimapHUD::PerformRadarRaycast()
{
APawn* Player = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);

if (Player)
{
TArray HitResults;
FVector EndLocation = Player->GetActorLocation();
EndLocation.Z += SphereHeight;

FCollisionShape CollisionShape;
CollisionShape.ShapeType = ECollisionShape::Sphere;
CollisionShape.SetSphere(SphereRadius);

//Perform a the necessary sweep for actors.
//In case you're wondering how this works, read my raycast tutorial here: http://wp.me/p6hvtS-5F
GetWorld()->SweepMultiByChannel(HitResults, Player->GetActorLocation(), EndLocation, FQuat::Identity, ECollisionChannel::ECC_WorldDynamic, CollisionShape);

for (auto It : HitResults)
{
AActor* CurrentActor = It.GetActor();
//In case the actor contains the word "Radar" as a tag, add it to our array
if (CurrentActor && CurrentActor->ActorHasTag("Radar")) RadarActors.Add(CurrentActor);
}
}
}

[свернуть]

Как только вы закончили с этим, сразу после функции DrawPlayerInRadar() вызовите внутри функцию DrawHUD и потом функцию PerformRadarRaycast().

Рисование Raycasted Actors

Для того, чтобы рисовать raycasted actors, мы создадим две функции:
1. Одну, которая будет конвертировать их положение из абсолютного в относительное, основанное на нашем игроке
2. И другую, которая будет рисовать raycasted actors внутри радара
Объявите следующие свойства и функции в header файле нашего HUD класса:

Code


/*The distance scale of the radar actors*/
UPROPERTY(EditAnywhere, Category = Radar)
float RadarDistanceScale = 25.f;

/*Converts the given actors' location to local (based on our character)*/
FVector2D ConvertWorldLocationToLocal(AActor* ActorToPlace);

/*Draws the raycasted actors in our radar*/
void DrawRaycastedActors();

[свернуть]

Логика для функции конвертации:

Code


FVector2D AMinimapHUD::ConvertWorldLocationToLocal(AActor* ActorToPlace)
{
APawn* Player = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);

if (Player && ActorToPlace)
{
//Convert the world location to local, based on the transform of the player
FVector ActorsLocal3dVector = Player->GetTransform().InverseTransformPosition(ActorToPlace->GetActorLocation());

//Rotate the vector by 90 degrees counter-clockwise in order to have a valid rotation in our radar
ActorsLocal3dVector = FRotator(0.f, -90.f, 0.f).RotateVector(ActorsLocal3dVector);

//Apply the given distance scale
ActorsLocal3dVector /= RadarDistanceScale;

//Return a 2d vector based on the 3d vector we've created above
return FVector2D(ActorsLocal3dVector);
}
return FVector2D(0,0);
}

[свернуть]

Затем используйте нижеприведённую логику для функции DrawRaycastedActors:

Code


void AMinimapHUD::DrawRaycastedActors()
{
FVector2D RadarCenter = GetRadarCenterPosition();

for (auto It : RadarActors)
{
FVector2D convertedLocation = ConvertWorldLocationToLocal(It);

//We want to clamp the location of our actors in order to make sure
//that we display them inside our radar

//To do so, I've created the following temporary vector in order to access
//the GetClampedToMaxSize2d function. This functions returns a clamped vector (if needed)
//to match our max length
FVector tempVector = FVector(convertedLocation.X, convertedLocation.Y, 0.f);

//Subtract the pixel size in order to make the radar display more accurate
tempVector = tempVector.GetClampedToMaxSize2D(RadarRadius - DrawPixelSize);

//Assign the converted X and Y values to the vector we want to display
convertedLocation.X = tempVector.X;
convertedLocation.Y = tempVector.Y;

DrawRect(FLinearColor::Red, RadarCenter.X + convertedLocation.X, RadarCenter.Y + convertedLocation.Y, DrawPixelSize, DrawPixelSize);
}
}

[свернуть]

Как только вы закончили с этим, сразу после PerfromRadarRaycast внутри функции DrawHUD введите следующий код:

Code


DrawRaycastedActors();

//Empty the radar actors in case the player moves out of range,
//by doing so, we have always a valid display in our radar
RadarActors.Empty();

[свернуть]

Полная реализация функции DrawHUD:

Code


void AMinimapHUD::DrawHUD()
{
//Default template code

Super::DrawHUD();

// Draw very simple crosshair

// find center of the Canvas
const FVector2D Center(Canvas->ClipX * 0.5f, Canvas->ClipY * 0.5f);

// offset by half the texture's dimensions so that the center of the texture aligns with the center of the Canvas
const FVector2D CrosshairDrawPosition((Center.X),
(Center.Y));

// draw the crosshair
FCanvasTileItem TileItem(CrosshairDrawPosition, CrosshairTex->Resource, FLinearColor::White);
TileItem.BlendMode = SE_BLEND_Translucent;
Canvas->DrawItem(TileItem);

//----------------Radar logic----------------

DrawRadar();

DrawPlayerInRadar();

PerformRadarRaycast();

DrawRaycastedActors();

//Empty the radar actors in case the player moves out of range,
//by doing so, we have always a valid display in our radar
RadarActors.Empty();
}

[свернуть]

Сохраните и скомпилируйте ваш код.
Далее откройте ваш редактор и не забудьте назначить метку “радар” для некоторых actors:

Image

[свернуть]

Читайте также: