虚幻引擎4 实现一个基础雷达系统
在这篇文章中,我们将为角色创建一个简单的雷达 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
标签。完成后,你就可以测试你的雷达了!