HUD, Canvas, Code Sample of 800+ Lines, Create Buttons & Draw Materials
Contents
Overview
Dear Community,
In this tutorial I am giving you a fully functional HUD class that is 800+ lines of UE4 C++ code.
Features of this sample HUD class include:
Multiple convenience functions for
- drawing textures
- drawing materials
- drawing lines
- drawing rectangles
- drawing text
The functions use the new UE4 method of using CanvasItems.
Transparency
Every HUD element I show in my sample has at least some transparency!
The main HUD menu graphic is actually mostly transparent, except for the edges!
Centering
- how to draw stuff centered in the screen (used by confirm dialog)
Rest of Game Code Integration
I have code in the tutorial to show you how to get a reference to your custom PC or Character class from within the HUD.
From your PC reference you can get access to your custom Game Mode and Game State classes (using the Cast<> code I show in tutorial PostInit)
Player Keyboard,Mouse,Controller Input
How to capture player key presses for ANY key,mouse,controller that you want from within the HUD
Key Controls used in this tutorial code:
- ESC: clears the screen lock so camera will move with cursor.
- F: Toggle screen lock so only the cursor moves as player moves the mouse.
- H: Toggles hiding the HUD
- LMB: Clicks on buttons
- Y: Confirm yes if confirm dialog open
- N: Cancel if confirm dialog is open
Text Size
How to measure text, accounting for scaling and font size, to size backgrounds,buttons,tooltips appropriately
Buttons
A button system and corresponding USTRUCT
Cursor
How to draw cursor that player can move in-game
How to change cursor when hovering over button
Tooltips
Button tooltips
Drawing Images/Textures/Materials
Drawing custom button and menu backgrounds that come from textures/materials
Video
Here is a video of this exact code in action!
Customizing the Appearance
Note that I made my own fonts and cursors and materials as seen in video, you can make your own appearance using same code :)
<youtube>https://www.youtube.com/watch?v=85Rp4Xb2Ja0</youtube>
Credit
Feel free to use this as a template/foundation for making a c++ HUD-based GUI for your game!
Please do give me credit in some appropriate way for this base-line code contribution.
All of this code is of my own creation, and I am using functions made only by Epic.
Why Did I Do All This?
I have implemented a very featured GUI for my game using only UE4 C++ and the HUD class!
I was able to create from scratch everything I wanted, including scrollable panels and an in-game file system.
I wanted to demo for you how this can be done, so that you can do this in your game too!
Constructing this tutorial for you took me about 5 hours, 2 hours of which was just documation / video
Adding buttons and such to this code should only take you minutes.
Integrating this code with your game engine will be easy, you can get a reference to the owning Character or the player controller from within the HUD class itself.
Pre-requisites
Make at least one font of your own, right click and make a new font asset.
I recommend a large default size like 36, as explained in the code below.
C++ Code
The code below assumes you have created a now Class named "JoyHUD". If you "add code to project" from the Editor, then you will need to replace the default files with the code below.
JoyHUD.h
// Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
//VictoryHUD extension by Rama
#pragma once
#include "JoyHUD.generated.h"
USTRUCT()
struct FJoyButtonStruct
{
GENERATED_USTRUCT_BODY()
//Vars
int32 type;
FString toolTip;
float minX;
float maxX;
float minY;
float maxY;
//~
//default properties
FJoyButtonStruct()
{
type = -1;
toolTip = "";
minX = 0;
maxX = 0;
minY = 0;
maxY = 0;
}
};
UCLASS()
class AJoyHUD : public AHUD
{
GENERATED_UCLASS_BODY()
// Font
// I recommend creating the font at a high resolution / size like 36
// then you can scale down the font as needed to any size of your choice
/** Verdana */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=JoyHUD)
UFont* VerdanaFont;
/** Put Roboto Here */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=JoyHUD)
UFont* UE4Font;
/** Font Scaling Used By Your HUD Code */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=JoyHUD)
float DefaultFontScale;
/** HUD Scaling */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=JoyHUD)
float GlobalHUDMult;
// T2D
/** Cursor */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=T2D)
UTexture2D* CursorMain;
/** Hovering */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=T2D)
UTexture2D* CursorHoveringButton;
/** Button */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=T2D)
UTexture2D* ButtonBackground;
// Materials
/** Events */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Materials)
UMaterialInterface* MaterialBackground;
//
/* Draw Hud? */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Options)
bool DontDrawHUD;
//Cursor
public:
FVector2D MouseLocation;
void DrawHUD_DrawCursor();
//Buttons
public:
TArray<FJoyButtonStruct> ButtonsMain;
TArray<FJoyButtonStruct> ButtonsConfirm;
//Cursor In buttons
void DrawHUD_CheckCursorInButtons();
void CheckCursorInButtonsMain();
void CheckCursorInButtonsConfirm();
const FJoyButtonStruct* CurCheckButton;
int32 CheckCursorInButton(const TArray<FJoyButtonStruct>& ButtonArray);
int32 ClickedButtonType;
//States
bool ConfirmDialogOpen;
bool InMainMenu;
int32 ActiveButton_Type;
FString ActiveButton_Tip;
bool CursorHoveringInButton;
//Colors
public:
const FLinearColor * ColorPtr;
//Colors
static const FColor FColorBlack;
static const FColor FColorRed;
static const FColor FColorYellow;
static const FColor FColorBlue;
static const FColor FColor_White;
static const FLinearColor LC_Black;
static const FLinearColor LC_Pink;
static const FLinearColor LC_Red;
static const FLinearColor LC_Yellow;
//FString
public:
//`Titles
static const FString S_Title_Main;
static const FString S_Title_Confirm;
//`Button Text
static const FString S_Button_Restart;
static const FString S_Button_Exit;
// Utility
//Stop Camera From Moving With Mouse
FORCEINLINE void SetCursorMoveOnly(bool CursorOnly)
{
if(!ThePC) return;
//
ThePC->SetIgnoreLookInput(CursorOnly);
}
//DrawLine
FORCEINLINE void DrawJoyLine
(
const FVector2D& Start,
const FVector2D& End,
const FLinearColor& TheColor,
const float& Thick
)
{
if (!Canvas) return;
//
FCanvasLineItem NewLine(Start,End);
NewLine.SetColor(TheColor);
NewLine.LineThickness = Thick;
Canvas->DrawItem(NewLine);
}
//~
FORCEINLINE void DrawJoyRect(
float X, float Y,
float Width, float Height,
const FLinearColor& Color
)
{
if(!Canvas) return;
//
FCanvasTileItem RectItem(
FVector2D(X, Y),
FVector2D( Width, Height ),
Color
);
RectItem.BlendMode = SE_BLEND_Translucent;
Canvas->DrawItem(RectItem);
}
//~
//DrawText
FORCEINLINE void DrawJoyText(
UFont* TheFont,
const FString& TheStr,
const float& X, const float& Y,
const FLinearColor& TheColor,
const float& TheScale,
bool DrawOutline=false,
const FLinearColor OutlineColor=FLinearColor(0,0,0,1)
) {
if(!Canvas) return;
//
//Text and Font
FCanvasTextItem NewText(
FVector2D(X,Y),
FText::FromString(TheStr),
TheFont,
TheColor
);
//Text Scale
NewText.Scale.Set(TheScale,TheScale);
//Outline gets its alpha from the main color
NewText.bOutlined = true;
NewText.OutlineColor = OutlineColor;
NewText.OutlineColor.A = TheColor.A * 2;
//Draw
Canvas->DrawItem(NewText);
}
//~
//Draw Full Size Tile
FORCEINLINE void DrawFullSizeTile(UTexture2D* tex, float x, float y, const FColor& Color)
{
if (!Canvas) return;
if (!tex) return;
//~~
Canvas->SetDrawColor(Color);
//Draw
Canvas->DrawTile(
tex, x, y, 0, //z pos
tex->GetSurfaceWidth(), //screen width
tex->GetSurfaceHeight(), //screen height
0, //texture start width
0, //texture start height
tex->GetSurfaceWidth(), //texture width from start
tex->GetSurfaceHeight(), //texture height from start
BLEND_Translucent
);
}
//~
FORCEINLINE void VDrawTile(UTexture2D* tex, float x, float y, float screenX, float screenY, const FColor& TheColor)
{
if (!Canvas) return;
if (!tex) return;
//~
Canvas->SetDrawColor(TheColor);
//Draw
Canvas->DrawTile(
tex, x, y, 0, //z pos
screenX, //screen width
screenY, //screen height
0, //texture start width
0, //texture start height
tex->GetSurfaceWidth(), //texture width from start
tex->GetSurfaceHeight(), //texture height from start
BLEND_Translucent
);
}
//~
//Draw
public:
void DrawHUD_DrawDialogs();
//Menus
void DrawMainMenu();
void DrawConfirm();
//Buttons
void DrawMainMenuButtons();
void DrawConfirmButtons();
public:
void DrawToolTip();
//Core
public:
APlayerController* ThePC;
void PlayerInputChecks();
protected:
//Draw HUD
void DrawHUD_Reset();
virtual void DrawHUD() OVERRIDE;
/** after all game elements are created */
virtual void PostInitializeComponents() OVERRIDE;
};
JoyHUD.cpp
// Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
//JoyHUD extension by Rama
#include "VictoryGame.h" //Replace with a reference to the header file of your own project
#define BUTTONTYPE_MAIN_RESTART 1
#define BUTTONTYPE_MAIN_EXIT 2
#define BUTTONTYPE_CONFIRM_YES 1
#define BUTTONTYPE_CONFIRM_NO 2
#define CANVAS_WHITE if(Canvas) Canvas->SetDrawColor(FColor_White);
//Cursor Draw Offset
// use this to position texture over the point of your cursor,
// if the point is not at exactly 0,0
#define CURSOR_DRAW_OFFSET 3
//
//Static Consts
//
const FString AJoyHUD::S_Title_Main = FString("Joy!");
const FString AJoyHUD::S_Title_Confirm = FString("Exit Game?");
const FString AJoyHUD::S_Button_Restart = FString("Restart");
const FString AJoyHUD::S_Button_Exit = FString("Exit");
// Colors
const FColor AJoyHUD::FColorBlack = FColor(0,0,0,255);
const FColor AJoyHUD::FColorRed = FColor(255,0,0,255);
const FColor AJoyHUD::FColorYellow = FColor(255,255,0,255);
const FColor AJoyHUD::FColorBlue = FColor(0,0,255,255);
const FColor AJoyHUD::FColor_White = FColor(255,255,255,255);
// Backgrounds
const FLinearColor AJoyHUD::LC_Black = FLinearColor(0, 0, 0, 1);
const FLinearColor AJoyHUD::LC_Pink = FLinearColor(1, 0, 1, 1);
const FLinearColor AJoyHUD::LC_Red = FLinearColor(1, 0, 0, 1);
const FLinearColor AJoyHUD::LC_Yellow = FLinearColor(1, 1, 0, 1);
AJoyHUD::AJoyHUD(const class FPostConstructInitializeProperties& PCIP) : Super(PCIP)
{
//Draw HUD?
DontDrawHUD = false;
//States
ConfirmDialogOpen = false;
InMainMenu = true;
//Scale
GlobalHUDMult = 1;
DefaultFontScale = 0.7; //scaling down a size 36 font
// I recommend creating fonts at a high resolution / size like 36
// then you can scale down the font as needed to any size of your choice
// this avoids needing to make multiple fonts for different sizes, but have a high
// resolution when you use larger font sizes
}
//Core
void AJoyHUD::PostInitializeComponents()
{
Super::PostInitializeComponents();
//Establish the PC
ThePC = GetOwningPlayerController();
//How to get a ref to your custom PC
//AYourPlayerController* YourChar = Cast<AYourPlayerController>(ThePC);
//How to Get The Character
//AYourCharacterClass* YourChar = Cast<AYourCharacterClass>(GetOwningPawn());
}
//===============
// Draw Dialogs
//===============
void AJoyHUD::DrawHUD_DrawDialogs()
{
DrawMainMenu();
if(ConfirmDialogOpen) DrawConfirm();
}
//Menus
void AJoyHUD::DrawMainMenu()
{
//Background
DrawMaterialSimple(
MaterialBackground,
10, 10,
256,
512,
1.3
);
//Menu Title
//Draw buttons
DrawMainMenuButtons();
}
void AJoyHUD::DrawConfirm()
{
//Blue rect with alpha 50%
DrawJoyRect(Canvas->SizeX/2 - 100, Canvas->SizeY/2 - 50,200,100,FLinearColor(0,0,1,0.2333));
//Confirm Title
//Draw buttons
DrawConfirmButtons();
}
//Buttons
void AJoyHUD::DrawMainMenuButtons()
{
//Start Point
float xStart = 100;
float yStart = 110;
//Background
VDrawTile(ButtonBackground,xStart,yStart,150,80,FColor(255,255,255,120)); //alpha 120/255
//Text
DrawJoyText(
VerdanaFont,"Restart",xStart+30,yStart+20,
LC_Black, DefaultFontScale,
true,LC_Red
);
//Struct
//Add Button If Necessary
// could be cleared and need refreshing if using a different menu
// clear buttons with ButtonsMain.Empty()
if (ButtonsMain.Num() < 1 )
{
FJoyButtonStruct newButton = FJoyButtonStruct();
newButton.type = BUTTONTYPE_MAIN_RESTART;
newButton.toolTip = "Restart the Game!";
newButton.minX = xStart;
newButton.maxX = xStart + 150;
newButton.minY = yStart;
newButton.maxY = yStart + 80;
//Add to correct array
ButtonsMain.Add(newButton);
}
xStart = 100;
yStart = 410;
VDrawTile(ButtonBackground,xStart,yStart,150,80,FColor(255,255,255,120)); //alpha 120/255
//Text
DrawJoyText(
VerdanaFont,"Exit",xStart+55,yStart+20,
LC_Black, DefaultFontScale,
true,LC_Red
);
if (ButtonsMain.Num() < 2 )
{
FJoyButtonStruct newButton = FJoyButtonStruct();
newButton.type = BUTTONTYPE_MAIN_EXIT;
newButton.toolTip = "Exit the Game!";
newButton.minX = xStart;
newButton.maxX = xStart + 150;
newButton.minY = yStart;
newButton.maxY = yStart + 80;
//Add to correct array
ButtonsMain.Add(newButton);
}
}
void AJoyHUD::DrawConfirmButtons()
{
float xStart = Canvas->SizeX/2 - 100;
float yStart = Canvas->SizeY/2 - 40;
//Highlighted?
if(ActiveButton_Type == BUTTONTYPE_CONFIRM_YES ) ColorPtr = &LC_Pink;
else ColorPtr = &LC_Yellow;
//Text
DrawJoyText(
VerdanaFont,"Yes",xStart+30,yStart+20,
*ColorPtr, DefaultFontScale,
true
);
if (ButtonsConfirm.Num() < 1 )
{
FJoyButtonStruct newButton = FJoyButtonStruct();
newButton.type = BUTTONTYPE_CONFIRM_YES ;
newButton.toolTip = "";
newButton.minX = xStart;
newButton.maxX = xStart + 75;
newButton.minY = yStart + 20;
newButton.maxY = yStart + 60;
//could use GetTextSize to streamline this
//Add to correct array
ButtonsConfirm.Add(newButton);
}
xStart = Canvas->SizeX/2 + 20;
yStart = Canvas->SizeY/2 - 40;
//Highlighted?
if(ActiveButton_Type == BUTTONTYPE_CONFIRM_NO) ColorPtr = &LC_Black;
else ColorPtr = &LC_Yellow;
//Text
DrawJoyText(
VerdanaFont,"No",xStart+30,yStart+20,
*ColorPtr, DefaultFontScale,
true
);
if (ButtonsConfirm.Num() < 2 )
{
FJoyButtonStruct newButton = FJoyButtonStruct();
newButton.type = BUTTONTYPE_CONFIRM_NO;
newButton.toolTip = "";
newButton.minX = xStart;
newButton.maxX = xStart + 75;
newButton.minY = yStart + 20;
newButton.maxY = yStart + 60;
//could use GetTextSize to streamline this
//Add to correct array
ButtonsConfirm.Add(newButton);
}
}
//===============
// Cursor In Buttons
//===============
int32 AJoyHUD::CheckCursorInButton(const TArray<FJoyButtonStruct>& ButtonArray)
{
for(int32 b = 0; b < ButtonArray.Num(); b++)
{
CurCheckButton = &ButtonArray[b];
//check cursor in bounds
if (CurCheckButton->minX <= MouseLocation.X && MouseLocation.X <= CurCheckButton->maxX &&
CurCheckButton->minY <= MouseLocation.Y && MouseLocation.Y <= CurCheckButton->maxY )
{
//Active Button Type
ActiveButton_Type = CurCheckButton->type;
//Tool Tip
ActiveButton_Tip = CurCheckButton->toolTip;
//Change Cursor
CursorHoveringInButton = true;
//Mouse Clicked?
if (ThePC->WasInputKeyJustPressed(EKeys::LeftMouseButton))
{
return ActiveButton_Type;
//~~
//no need to check rest of buttons
}
}
}
//No Click Occurred This Tick
return -1;
}
//Check Confirm
void AJoyHUD::CheckCursorInButtonsConfirm()
{
//Check Confirm Buttons
ClickedButtonType = CheckCursorInButton(ButtonsConfirm); //fills global ActiveButton_Type
if(ClickedButtonType == BUTTONTYPE_CONFIRM_YES )
{
ThePC->ConsoleCommand("Exit");
return;
}
if(ClickedButtonType == BUTTONTYPE_CONFIRM_NO)
{
ConfirmDialogOpen = false;
ButtonsConfirm.Empty(); //Buttons not needed anymore
return;
}
}
//Check Buttons
void AJoyHUD::CheckCursorInButtonsMain()
{
//Check Confirm Buttons
ClickedButtonType = CheckCursorInButton(ButtonsMain);
if(ClickedButtonType == BUTTONTYPE_MAIN_RESTART )
{
ThePC->ConsoleCommand("RestartLevel");
return;
}
if(ClickedButtonType == BUTTONTYPE_MAIN_EXIT)
{
ConfirmDialogOpen = true;
return;
}
}
void AJoyHUD::DrawHUD_CheckCursorInButtons()
{
if(ConfirmDialogOpen)
{
CheckCursorInButtonsConfirm();
//Take Focus Away From All Other buttons
return;
//~
}
//Main
CheckCursorInButtonsMain();
}
void AJoyHUD::DrawToolTip()
{
//if mouse is too far to right, draw from left instead
float xStart = MouseLocation.X + 150;
float yStart = MouseLocation.Y + 5;
//out vars
float RV_xLength;
float RV_yLength;
//Text Size
GetTextSize(
ActiveButton_Tip,
RV_xLength,
RV_yLength,
UE4Font,
DefaultFontScale * 2
);
// Decide Draw to Left or to the Right
//Draw to the Left
if (xStart + RV_xLength >= Canvas->SizeX - 40)
{
xStart -= 150 + 140 + 64 + RV_xLength;
//If Text is too long, bring it closer to the cursor
if(xStart < 33 ) xStart = 33;
}
//Background
DrawJoyRect(
xStart, yStart,
RV_xLength + 70,
80,
FLinearColor(0, 0, 1, 0.7) //alpha 0.7
);
//Tool Tip
DrawText(
ActiveButton_Tip,
LC_Pink,
xStart + 32, yStart + 20,
UE4Font,
DefaultFontScale * 2,
false //scale position of message with HUD scale
);
}
void AJoyHUD::DrawHUD_DrawCursor()
{
//Tool Tip
if(ActiveButton_Tip != "") DrawToolTip();
//Cursor Hovering in a Button?
if (CursorHoveringInButton)
{
//pointer tex found?
if (!CursorHoveringButton) return;
DrawFullSizeTile(CursorHoveringButton, MouseLocation.X - CURSOR_DRAW_OFFSET, MouseLocation.Y - CURSOR_DRAW_OFFSET, FColor_White );
}
else
{
//cursor tex found?
if(!CursorMain) return;
DrawFullSizeTile(CursorMain, MouseLocation.X - CURSOR_DRAW_OFFSET, MouseLocation.Y - CURSOR_DRAW_OFFSET, FColor_White );
}
}
void AJoyHUD::PlayerInputChecks()
{
//check out this tutorial of mine for a list of all EKeys::
//http://forums.epicgames.com/threads/972861-Tutorials-C-for-UE4-Code-Samples-gt-gt-New-Video-Freeze-Render-When-Tabbed-Out?p=31660286&viewfull=1#post31660286
if(ThePC->WasInputKeyJustPressed(EKeys::Escape))
{
SetCursorMoveOnly(false);
return;
}
if(ThePC->WasInputKeyJustPressed(EKeys::F))
{
SetCursorMoveOnly(!ThePC->IsLookInputIgnored());
return;
}
if(ThePC->WasInputKeyJustPressed(EKeys::H))
{
DontDrawHUD = !DontDrawHUD;
return;
}
//Confirm
if(ConfirmDialogOpen)
{
if(ThePC->WasInputKeyJustPressed(EKeys::Y))
{
ThePC->ConsoleCommand("Exit");
//could replace with function based on confirm context
return;
}
if(ThePC->WasInputKeyJustPressed(EKeys::N))
{
ConfirmDialogOpen = false;
ButtonsConfirm.Empty(); //Buttons not needed anymore
//Cancel Confirm
return;
}
}
}
void AJoyHUD::DrawHUD_Reset()
{
ActiveButton_Type = -1;
ActiveButton_Tip = "";
CursorHoveringInButton = false;
}
void AJoyHUD::DrawHUD()
{
//==============================
//==============================
//==============================
//Have PC for Input Checks and Mouse Cursor?
if(!ThePC)
{
//Attempt to Reacquire PC
ThePC = GetOwningPlayerController();
//Could Not Obtain PC
if(!ThePC) return;
//~~
}
//Multiplayer Safety Check
if(!ThePC->PlayerInput) return; //not valid for first seconds of a multiplayer client
//~~
//==============================
//==============================
//==============================
//Player Input
PlayerInputChecks();
//Draw HUD?
if(DontDrawHUD) return;
//~~
//Super
Super::DrawHUD();
//No Canvas?
if(!Canvas) return;
//
//Reset States
DrawHUD_Reset();
//================
//Get New Mouse Position
//================
ThePC->GetMousePosition(MouseLocation.X,MouseLocation.Y);
//Cursor In Buttons
DrawHUD_CheckCursorInButtons();
//Draw Dialogs
DrawHUD_DrawDialogs();
//### Do Last ###
//Draw Cursor
DrawHUD_DrawCursor();
//Debugging Info
//ThePC->ClientMessage("HUD Loop Completed!");
}
Compile and Make BP
After you get the above to compile, you need to go into the editor and make a blueprint of this JoyHUD
Then you need to go to your GameMode.cpp file and add this line:
AYourGameMode::AYourGameMode(const class FPostConstructInitializeProperties& PCIP)
: Super(PCIP)
{
// You can obtain the asset path of your HUD blueprint through the editor
// by right-clicking the Blueprint asset and choosing "Copy Reference".
// You should then add the "_C" suffix so that the class finder properly
// points to the actual class used by the game, as opposed to its Blueprint
// which is an editor-only concept).
//
// For instance, given a blueprint named BP_JoyHUD, the class path would be
// "/Game/Blueprints/BP_JoyHUD_C"
static ConstructorHelpers::FClassFinder<AHUD> TheHUDOb(TEXT("/Game/Blueprints/BP_JoyHUD_C"));
if (TheHUDOb.Class != NULL)
{
HUDClass = TheHUDOb.Class;
}
}
Adding The Graphics
Open the defaults of your new HUD BP and set the various assets!
Conclusion
Now you have the code to make your own button system, with tooltips, and draw textures and materials of any size and shape to the screen during game time!
Using the DrawMaterial function you can make fancy effects in your game's GUI as you see in my video!
You also have a solid foundation for a mouse/pointer driven GUI that responds to hover as well as click events!
Enjoy!