Introduction

In this chapter I have gone over the UI elements such as health bars of the character’s, selection grid and gave you some useful information about other things such as texture, and text drawings, and adding font to your project. Firstly we will create a new class ARTSBR_HUD from Unreal’s AHUD class.

Before starting I recommend you to look at the following changes of our codes:

UCLASS()
class RTSBATTLEROYALE_API ARTSBR_Character : public ACharacter
{
	GENERATED_BODY()

public:
	...
	float GetHealth() const
	{
		return health_;
	}

	float GetMaxHealth() const
	{
		return maxHealth_;
	}
	...

protected:
	...
	// Max health is held individually in order to increment it for different characters. 
	float maxHealth_;
	...
};
ARTSBR_Character::ARTSBR_Character() : 
	..., maxHealth_(BaseStats::HEALTH), ...
{
	...
}
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "RTSBR_HUD.generated.h"

class ARTSBR_PlayerController;
class ARTSBR_SpectatorPawn;

/**
 * 
 */
UCLASS()
class RTSBATTLEROYALE_API ARTSBR_HUD : public AHUD
{
	GENERATED_BODY()
public:
	ARTSBR_HUD();

	void BeginPlay() override;
	void Tick(float deltaSeconds) override;

private:
	// Main drawing function
	virtual void DrawHUD() override;
	ARTSBR_PlayerController* GetPlayerController() const;
	ARTSBR_SpectatorPawn* GetSpectatorPawn() const;

	void DrawHealthBars() const;
	void DrawSelectionGrid(FVector2D gridStartPos);

	float uiScale_;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "RTSBR_HUD.h"

#include "Components/CapsuleComponent.h"
#include "Engine/Canvas.h"
#include "Engine/Engine.h"
#include "Engine/GameViewportClient.h"

#include "RTSBR_Character.h"
#include "RTSBR_SpectatorPawn.h"
#include "RTSBR_PlayerController.h"

ARTSBR_HUD::ARTSBR_HUD()
{
	PrimaryActorTick.bCanEverTick = false;
}

void ARTSBR_HUD::BeginPlay()
{
	Super::BeginPlay();
}

void ARTSBR_HUD::Tick(float deltaSeconds)
{
	Super::Tick(deltaSeconds);
}

void ARTSBR_HUD::DrawHUD()
{
	Super::DrawHUD();

	if (GEngine && GEngine->GameViewport)
	{
		FVector2D viewportSize;
		GEngine->GameViewport->GetViewportSize(viewportSize);
		uiScale_ = viewportSize.X / 2048.f;
	}
}

ARTSBR_PlayerController * ARTSBR_HUD::GetPlayerController() const
{
	return Cast<ARTSBR_PlayerController>(PlayerOwner);
}

ARTSBR_SpectatorPawn *ARTSBR_HUD::GetSpectatorPawn() const
{
	ARTSBR_PlayerController *pc = GetPlayerController();
	if (pc)
	{
		return pc->GetMainSpectatorPawn();
	}

	return nullptr;
}
UCLASS()
class RTSBATTLEROYALE_API ARTSBR_PlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	...

	FVector const& GetSelectionStartPosition() const
	{
		return selectionStartPosition_;
	}

	bool GetIsSelectionActive() const
	{
		return isSelectionActive_;
	}

private:
	...
	bool isSelectionActive_;
};
ARTSBR_PlayerController::ARTSBR_PlayerController() : isSelectionActive_(false)
{
	...
}

void ARTSBR_PlayerController::StartSelection()
{
	FHitResult hit;
	GetHitResultUnderCursor(ECC_Visibility, false, hit);
	selectionStartPosition_ = hit.Location;

	isSelectionActive_ = true;
}

void ARTSBR_PlayerController::EndSelection()
{
	FHitResult hit;
	GetHitResultUnderCursor(ECC_Visibility, false, hit);
	selectionEndPosition_ = hit.Location;

	isSelectionActive_ = false;

	UnitSelection();
}

Health Bars

Let’s start doing our HUD with the health bars. The bars can be drawn as actors in the world space facing to the camera, but we will do it in much simpler way. The health bars will be drawn in the UI space and the actual world has nothing to do with them.

We will show all the characters on the map for now, but it can be changed in the future with a line change in the code. I am certainly not sure if we should show all the characters including enemies, only our characters, or only the selected characters. However, as I said we can change it in the future with only a single line change. Let’s decide it when we will have different teams in the game! 🙂

void ARTSBR_HUD::DrawHUD()
{
	...
	DrawHealthBars();
}

void ARTSBR_HUD::DrawHealthBars() const
{
	// Get all pawns in the game
	for (FConstPawnIterator pawn = GetWorld()->GetPawnIterator(); pawn; ++pawn)
	{
		// If the pawn is a ARTSBR_Character and is not death
		ARTSBR_Character* character = Cast<ARTSBR_Character>(*pawn);
		if (character && character->GetHealth() > 0)
		{
			// Select the center point of the bar as the character's location
			FVector center = character->GetActorLocation();
			// Offsets of the bar
			FVector extent = FVector(60.f, 34.f, 131.75f);

			// Project function of Canvas translates a world position to the screen position
			FVector2D center2D = FVector2D(Canvas->Project(FVector(center.X, center.Y, center.Z + extent.Z)));

			float actorExtent = 50.f;
			float healthPercentage = 0.5f;
			float yOffset = 10.f;

			healthPercentage = character->GetHealth() / character->GetMaxHealth();
			actorExtent = character->GetCapsuleComponent()->GetScaledCapsuleRadius();

			FVector pos1 = Canvas->Project(FVector(center.X, center.Y - actorExtent * 2, center.Z + extent.Z));
			FVector pos2 = Canvas->Project(FVector(center.X, center.Y + actorExtent * 2, center.Z + extent.Z));

			float barWidth = (pos2 - pos1).Size2D();
			float barHeight = barWidth * 0.2f;

			// Draw a background color first
			/* Background tile */
			barWidth += 2.f;
			barHeight += 2.f;

			float x = center2D.X - barWidth * 0.5f;
			float y = center2D.Y;

			FCanvasTileItem tileItem(FVector2D(x, y), FVector2D(barWidth, barHeight), FLinearColor(0.0f, 0.0f, 0.0f, 0.5f));
			tileItem.BlendMode = SE_BLEND_Translucent;
			Canvas->DrawItem(tileItem);
			/* Background tile */

			// Draw the health indicator
			/* Health tile */
			barWidth -= 2.f;
			barHeight -= 2.f;  

			x = center2D.X - barWidth * 0.5f;
			y = center2D.Y + 1.f;

			tileItem.Position = FVector2D(x, y);
			tileItem.SetColor(FLinearColor(1.0f, 1.0f, 0.3f, 1.f));
			tileItem.Size = FVector2D(barWidth * healthPercentage, barHeight);
			Canvas->DrawItem(tileItem);
		}
	}
}

Selection Grid

If the selection operation is started we will draw the selection grid in our UI. For the selection grid we will draw a rectangular and a line surrounding it.

 

void ARTSBR_HUD::DrawHUD()
{
	...
	if(GetPlayerController()->GetIsSelectionActive())
	{
		const FVector selectionStartPosition = Canvas->Project(GetPlayerController()->GetSelectionStartPosition());

		DrawSelectionGrid(FVector2D(selectionStartPosition));
	}
}
void ARTSBR_HUD::DrawSelectionGrid(FVector2D gridStartPos)
{
	FVector2D mousePosition;
	GetPlayerController()->GetMousePosition(mousePosition.X, mousePosition.Y);

	float gridWidth = mousePosition.X - gridStartPos.X;
	float gridHeight = mousePosition.Y - gridStartPos.Y;

	FCanvasTileItem fillTileItem(gridStartPos, FVector2D(gridWidth, gridHeight), FLinearColor(1.0f, 1.0f, 1.0f, 0.2f));
	fillTileItem.BlendMode = SE_BLEND_Translucent;
	Canvas->DrawItem(fillTileItem);

	FCanvasTileItem tileItem(gridStartPos, FVector2D(gridWidth, 1.f), FLinearColor(1.0f, 1.0f, 1.0f, 1.0f));
	tileItem.BlendMode = SE_BLEND_Translucent;
	tileItem.UV1 = FVector2D(gridWidth * 0.1f, 1.f);
	Canvas->DrawItem(tileItem);

	tileItem.Position = gridStartPos + FVector2D(0.f, gridHeight);
	Canvas->DrawItem(tileItem);

	tileItem.Position = gridStartPos;
	tileItem.Size = FVector2D(1.f, gridHeight);
	tileItem.UV1 = FVector2D(1.f, gridHeight * 0.1f);
	Canvas->DrawItem(tileItem);

	tileItem.Position = gridStartPos + FVector2D(gridWidth, 0.f);
	Canvas->DrawItem(tileItem);
}

Top Information Bar

Actually this part is not essential at the moment, but I wanted to show how to draw textures and texts with C++. We will also import a font and use it.  You can download the font here.

UCLASS()
class RTSBATTLEROYALE_API ARTSBR_HUD : public AHUD
{
	GENERATED_BODY()
public:
	ARTSBR_HUD();

private:
	...
	void DrawTopInfoBar();
	UTexture2D *topInfoBarTexture_;
	UFont *uiFont_;
	...
};
ARTSBR_HUD::ARTSBR_HUD()
{
	...

	const ConstructorHelpers::FObjectFinder<UTexture2D> topInfoBarTextureAsset(TEXT("Texture2D'/Game/Assets/Textures/TopUIBar.TopUIBar'"));
	if (topInfoBarTextureAsset.Object) topInfoBarTexture_ = topInfoBarTextureAsset.Object;

	const ConstructorHelpers::FObjectFinder<UFont> uiFontAsset(TEXT("Font'/Game/Assets/Fonts/SpindleRefined-Regular_Font.SpindleRefined-Regular_Font'"));
	if (uiFontAsset.Object) uiFont_ = uiFontAsset.Object;
}

void ARTSBR_HUD::DrawTopInfoBar()
{
	// Get window width to draw the bar to make it fill the entire top side
	const float gridWidth = GEngine->GameViewport->Viewport->GetSizeXY().X;
	
	// Set grid height as the texture's height
	const float gridHeight = 30.f;

	// Set the tile item. Color parameter paints the texture
	FCanvasTileItem tileItem(FVector2D::ZeroVector, topInfoBarTexture_->Resource, FVector2D(gridWidth, gridHeight), FLinearColor(1.f, 1.f, 1.f, 1.f));
	tileItem.BlendMode = SE_BLEND_Translucent;
	tileItem.UV1 = FVector2D(0.5f, 0.5f);
	Canvas->DrawItem(tileItem);

	const FString gameText("REAL TIME STRATEGY BATTLE ROYALE");
	float textWidth = 0;
	float textHeight = 0;
	GetTextSize(gameText, textWidth, textHeight, uiFont_);
	DrawText(gameText, FLinearColor::White, (gridWidth - textWidth) * 0.5f, (gridHeight - textHeight) * 0.5f, uiFont_);
}

Before importing the font

When you want to import the font into Unreal Engine, it gives you the following warning. Click yes to make it create its asset. Without the asset we cannot really use it in our project since the engine does not support regular fonts as they are, it must create its font asset itself.

After importing the font

Result