To display a dialog box, we need a custom (heads-up display) HUD. In the UE4 editor, go to File | Add Code To Project... and choose the HUD
class from which the subclass is created. Name your subclass as you wish; I've named mine MyHUD
.
After you have created the MyHUD
class, let Visual Studio reload. We will make some code edits.
Inside the AMyHUD
class, we need to implement the DrawHUD()
function in order to draw our messages to the HUD and to initialize a font to draw to the HUD with, as shown in the following code:
UCLASS() class GOLDENEGG_API AMyHUD : public AHUD { GENERATED_UCLASS_BODY() // The font used to render the text in the HUD. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUDFont) UFont* hudFont; // Add this function to be able to draw to the HUD! virtual void DrawHUD() override; };
The HUD font will be set in a blueprinted version of the AMyHUD
class. The DrawHUD()
function runs once per frame. In order to draw within the frame, add a function to the AMyHUD.cpp
file:
void AMyHUD::DrawHUD() { // call superclass DrawHUD() function first Super::DrawHUD(); // then proceed to draw your stuff. // we can draw lines.. DrawLine( 200, 300, 400, 500, FLinearColor::Blue ); // and we can draw text! DrawText( "Greetings from Unreal!", FVector2D( 0, 0 ), hudFont, FVector2D( 1, 1 ), FColor::White ); }
Wait! We haven't initialized our font yet. To do this, we need to set it up in blueprints. Compile and run your Visual Studio project. Once you are in the editor, go to the Blueprints menu at the top and navigate to GameMode | HUD | + Create | MyHUD.
I called mine BP_MyHUD
. Edit BP_MyHUD
and select a font from the drop-down menu under HUDFont:
Next, edit your Game Mode blueprint (BP_GameModeGoldenEgg) and select your new BP_MyHUD
(not MyHUD
class) for the HUD Class panel:
Test your program by running it! You should see text printed on the screen.
Each message we want to display for the player will have a few properties:
FString
variable for the messagefloat
variable for the time to display itFColor
variable for the color of the messageSo it makes sense for us to write a little struct
function to contain all this information.
At the top of MyHUD.h
, insert the following struct
declaration:
struct Message { FString message; float time; FColor color; Message() { // Set the default time. time = 5.f; color = FColor::White; } Message( FString iMessage, float iTime, FColor iColor ) { message = iMessage; time = iTime; color = iColor; } };
Now, inside the AMyHUD
class, we want to add a TArray
of these messages. A TArray
is a UE4-defined special type of dynamically growable C++ array. We will cover the detailed use of TArray
in the next chapter, but this simple usage of TArray
should be a nice introduction to garner your interest in the usefulness of arrays in games. This will be declared as TArray<Message>
:
UCLASS() class GOLDENEGG_API AMyHUD : public AHUD { GENERATED_UCLASS_BODY() // The font used to render the text in the HUD. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = HUDFont) UFont* hudFont; // New! An array of messages for display TArray<Message> messages; virtual void DrawHUD() override; // New! A function to be able to add a message to display void addMessage( Message msg ); };
Now, whenever the NPC has a message to display, we're just need to call AMyHud::addMessage()
with our message. The message will be added to TArray
of the messages to be displayed. When a message expires (after a certain amount of time), it will be removed from the HUD.
Inside the AMyHUD.cpp
file, add the following code:
void AMyHUD::DrawHUD() { Super::DrawHUD(); // iterate from back to front thru the list, so if we remove // an item while iterating, there won't be any problems for( int c = messages.Num() - 1; c >= 0; c-- ) { // draw the background box the right size // for the message float outputWidth, outputHeight, pad=10.f; GetTextSize( messages[c].message, outputWidth, outputHeight, hudFont, 1.f ); float messageH = outputHeight + 2.f*pad; float x = 0.f, y = c*messageH; // black backing DrawRect( FLinearColor::Black, x, y, Canvas->SizeX, messageH ); // draw our message using the hudFont DrawText( messages[c].message, messages[c].color, x + pad, y + pad, hudFont ); // reduce lifetime by the time that passed since last // frame. messages[c].time -= GetWorld()->GetDeltaSeconds(); // if the message's time is up, remove it if( messages[c].time < 0 ) { messages.RemoveAt( c ); } } } void AMyHUD::addMessage( Message msg ) { messages.Add( msg ); }
The AMyHUD::DrawHUD()
function now draws all the messages in the messages
array, and arranges each message in the messages
array by the amount of time that passed since the last frame. Expired messages are removed from the messages
collection once their time
value drops below 0.
Refactor the DrawHUD()
function so that the code that draws the messages to the screen is in a separate function called DrawMessages()
.
The Canvas
variable is only available in DrawHUD()
, so you will have to save Canvas->SizeX
and Canvas->SizeY
in class-level variables.
Refactoring means to change the way code works internally so that it is more organized or easier to read but still has the same apparent result to the user running the program. Refactoring often is a good practice. The reason why refactoring occurs is because nobody knows exactly what the final code should look like once they start writing it.
To trigger an event near the NPC, we need to set an additional collision detection volume that is a bit wider than the default capsule shape. The additional collision detection volume will be a sphere around each NPC. When the player steps into the NPC sphere, the NPC reacts and displays a message.
Inside your NPC.h
class file, add the following code in order to declare ProxSphere
and UFUNCTION
called Prox
:
UCLASS() class GOLDENEGG_API ANPC : public ACharacter { GENERATED_UCLASS_BODY() // This is the NPC's message that he has to tell us. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = NPCMessage) FString NpcMessage; // The sphere that the player can collide with to get item UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Collision) TSubobjectPtr<class USphereComponent> ProxSphere; // The corresponding body of this function is // ANPC::Prox_Implementation, __not__ ANPC::Prox()! // This is a bit weird and not what you'd expect, // but it happens because this is a BlueprintNativeEvent UFUNCTION(BlueprintNativeEvent, Category = "Collision") void Prox( AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult ); };
This looks a bit messy, but it is actually not that complicated. Here, we declare an extra bounding sphere volume called ProxSphere
, which detects when the player is near the NPC.
In the NPC.cpp
file, we need to add the following code in order to complete the proximity detection:
ANPC::ANPC(const class FPostConstructInitializeProperties& PCIP) : Super(PCIP) { ProxSphere = PCIP.CreateDefaultSubobject<USphereComponent>(this, TEXT("Proximity Sphere")); ProxSphere->AttachTo( RootComponent ); ProxSphere->SetSphereRadius( 32.f ); // Code to make ANPC::Prox() run when this proximity sphere // overlaps another actor. ProxSphere->OnComponentBeginOverlap.AddDynamic( this, &ANPC::Prox ); NpcMessage = "Hi, I'm Owen";//default message, can be edited // in blueprints } // Note! Although this was declared ANPC::Prox() in the header, // it is now ANPC::Prox_Implementation here. void ANPC::Prox_Implementation( AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult ) { // This is where our code will go for what happens // when there is an intersection }
When the player is near the NPC sphere collision volume, display a message to the HUD that alerts the player about what the NPC is saying.
This is the complete implementation of ANPC::Prox_Implementation
:
void ANPC::Prox_Implementation( AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult & SweepResult ) { // if the overlapped actor is not the player, // you should just simply return from the function if( Cast<AAvatar>( OtherActor ) == nullptr ) { return; } APlayerController* PController = GetWorld()- >GetFirstPlayerController(); if( PController ) { AMyHUD * hud = Cast<AMyHUD>( PController->GetHUD() ); hud->addMessage( Message( NpcMessage, 5.f, FColor::White ) ); } }
The first thing we do in this function is to cast OtherActor
(the thing that came near the NPC) to AAvatar
. The cast succeeds (and is not nullptr
) when OtherActor
is an AAvatar
object. We get the HUD object (which happens to be attached to the player controller) and pass a message from the NPC to the HUD. The message is displayed whenever the player is within the red bounding sphere surrounding the NPC.
UPROPERTY
function name for the NPC's name so that the name of the NPC is editable in blueprints, similar to the message that the NPC has for the player. Show the NPC's name in the output.UPROPERTY
function (type UTexture2D*
) for the NPC's face texture. Draw the NPC's face beside its message in the output.Add this property to the ANPC
class:
// This is the NPC's name UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = NPCMessage) FString name;
Then, in ANPC::Prox_Implementation
, change the string passed to the HUD to:
name + FString(": ") + message
This way, the NPC's name will be attached to the message.
Add this
property to the ANPC
class:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = NPCMessage) UTexture2D* Face;
Then you can select face icons to be attached to the NPC's face in blueprints.
Attach a texture to your struct Message
:
UTexture2D* tex;
To render these icons, you need to add a call to DrawTexture()
with the right texture passed in to it:
DrawTexture( messages[c].tex, x, y, messageH, messageH, 0, 0, 1, 1 );
Be sure to check whether the texture is valid before you render it. The icons should look similar to what is shown here, at the top of the screen:
This is how a function to draw the player's remaining health in a bar will look:
void AMyHUD::DrawHealthbar() { // Draw the healthbar. AAvatar *avatar = Cast<AAvatar>( UGameplayStatics::GetPlayerPawn(GetWorld(), 0) ); float barWidth=200, barHeight=50, barPad=12, barMargin=50; float percHp = avatar->Hp / avatar->MaxHp; DrawRect( FLinearColor( 0, 0, 0, 1 ), Canvas->SizeX - barWidth - barPad - barMargin, Canvas->SizeY - barHeight - barPad - barMargin, barWidth + 2*barPad, barHeight + 2*barPad ); DrawRect( FLinearColor( 1-percHp, percHp, 0, 1 ), Canvas->SizeX - barWidth - barMargin, Canvas->SizeY - barHeight - barMargin, barWidth*percHp, barHeight ); }
18.191.68.18