在这篇文章中我们将为我们的角色创建一个简单的雷达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 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函数的参数解释:
- 线段起点的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;</aactor*>
我不会详细解释该射线跟踪的逻辑,因为我已经在这里写了一篇完整的教程。
然后创建以下私有函数:
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标签:
完成后你就可以测试你的雷达了!