虚幻引擎4 实现一个基础雷达系统

2017年04月14日 11:15 0 点赞 0 评论 更新于 2020-01-11 22:03

在这篇文章中,我们将为角色创建一个简单的雷达 HUD(Head Up Display,平视显示信息)。在开始之前,先让我们看看最终结果。

在我们的 HUD 中画出雷达

首先,创建一个第一人称 C++ 模板项目并打开生成的 HUD 类。在绘制雷达之前,我们需要确定它在玩家屏幕中的位置。

由于玩家的设备可能具有不同的分辨率,我们需要告知 UE4 将雷达绘制在相对位置,而非直接输入宽和高的数值。幸运的是,在 HUD 类中,UE4 包含了一个画布(Canvas),其中存储着玩家屏幕的实际宽度和高度。为了使用该功能,我们暴露一个名为 RadarStartLocation 的 2D 向量。我们将其作为乘数,使用相对数值来确定雷达的位置,而非直接输入雷达的位置。

假设屏幕分辨率为 1920x1080,如果将 1920 和 1080 都乘以 0,我们将得到屏幕的左上角。以下是一张解释图:X 和 Y 的值对应着该乘数(即 RadarStartLocation)的值。所以,如果将 RadarStartLocation 设置为 0.9 和 0.2,那么 UE4 会将雷达放置在靠近右上角的位置。在绘制雷达后,建议稍微调整这些数值,使其处于最佳位置。

解释完这些后,让我们开始绘制雷达。打开第一人称 C++ 模板项目中的 HUD 类,添加以下属性:

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;

然后创建以下私有函数:

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

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

切换到源文件,为上面声明的函数实现以下逻辑:

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 is 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 函数的参数解释:

  • 线段起点的 X 坐标
  • 线段起点的 Y 坐标
  • 线段终点的 X 坐标
  • 线段终点的 Y 坐标
  • 线段的颜色
  • 线段的厚度

假设你已经实现了上述逻辑,切换到 DrawHUD 函数中,并在默认已实现的代码之前加入 DrawRadar() 函数的调用。

保存并编译代码后,切换到编辑器,然后进行以下操作:

  • 创建一个基于默认游戏模式的游戏模式蓝图
  • 创建一个基于我们 C++ HUD 类的 HUD 蓝图
  • 在全局设置中分配游戏模式和蓝图 HUD

你还可以为 HUD 类分配 C++ 类,但由于我们在后面将暴露更多的属性,建议你使用上述方法,这样每次调整暴露属性时,不需要再编译代码。

目前为止,你应该能够看到右上角的灰色雷达了。

在雷达中画出玩家的位置

因为玩家永远处于雷达的中心,我们创建一个名为 DrawPlayerInRadar 的函数,并实现以下逻辑:

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

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

然后切换到 DrawHUD 函数,紧接着 DrawRadar() 函数调用 DrawPlayerInRadar()

为我们的雷达定位周围的 Actor

在这个项目中,我们决定对玩家周围的多个 Actor 进行射线跟踪(raycast),并将所有含有“Radar”标签的 Actor 的引用放在一个数组中,以便将它们显示在我们的雷达中。不过,根据你的游戏需求,情况可能会略有不同。建议你按照以下步骤操作,当有了一个完整的可正常工作的雷达时,再实现你自己的逻辑。

达成共识后,让我们在 HUD 类的头文件中创建以下保护属性:

/** 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<AActor*> RadarActors;

这里不会详细解释该射线跟踪的逻辑,因为已经在 这里 写了一篇完整的教程。

然后创建以下私有函数:

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

if (Player)
{
TArray<FHitResult> 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);
}
}
}

添加完成后,紧接着 DrawHUD 函数中 DrawPlayerInRadar() 函数的调用,调用 PerformRadarRaycast() 函数。

画出射线追踪到的 Actor

为了画出被射线追踪到的 Actor,我们将创建两个函数:

  • 一个会根据玩家的位置将它们的位置从全局位置转换到本地位置
  • 一个会在雷达中画出射线追踪到的 Actor

在 HUD 类的头文件中声明以下属性和函数:

/** 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();

以下是转换函数的逻辑:

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 函数添加以下逻辑:

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);
}
}

添加完成后,紧接着 DrawHUD 函数中的 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();

以下是 DrawHUD 函数的一个完整实现:

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();
}

保存并编译你的代码。然后切换到你的编辑器,别忘了在一些 Actor 中指定 Radar 标签。完成后,你就可以测试你的雷达了!

作者信息

孟子菇凉

孟子菇凉

共发布了 1189 篇文章