Introduction

In this article, we will go over character selection and making it move around the map. Firstly we will use box collider to select the characters, than we will implement decal component to highlight the selected characters. Finally, we will create a class derived from AIController and use its functions to move the characters on the map.

Selection

I did character selection with a rectangular box collider sized with respect to the player’s input. The box collider’s extend and center position are calculated according to the positions where the player presses and releases his/her mouse left click.

I also used a decal component in order to highlight the selected characters. Decal component is a component that is used to paint surfaces that has a colliding area with it. Decal components are used in several purposes for example in a shooter game when player shoots a wall, the bullet track on the wall is generally made with a decal component, or spraying a surface like in counter strike is made with the same technique and so on. Here, we use decal component to draw a circular highlighter on the surface of the selected characters.

Decal component has the following decal material:

class RTSBATTLEROYALE_API ARTSBR_Character : public ACharacter
{
	GENERATED_BODY()

public:
	...
	// Will be used to set the decal component visible or invisible for now
	void SetIsSelected(bool isSelected) const;
private:
	UDecalComponent* selectionDecalComp_;
};
ARTSBR_Character::ARTSBR_Character() : 
	damage_(BaseStats::PHYSICAL_DAMAGE, BaseStats::MAGIC_DAMAGE), 
	health_(BaseStats::HEALTH), armor_(BaseStats::ARMOR), magicResistance_(BaseStats::MAGIC_RESISTANCE), 
	attackSpeed_(BaseStats::ATTACK_SPEED), isAttacking_(false)
{
	... 


	// Set the character's AI controller class to our AI controller
	AIControllerClass = ARTSBR_AIController::StaticClass();

	// Create default decal component and setup to the RootComponent
	selectionDecalComp_ = CreateDefaultSubobject<UDecalComponent>(TEXT("Selection Decal"));
	selectionDecalComp_->SetupAttachment(RootComponent);

	// Find the decal material
	static ConstructorHelpers::FObjectFinder<UMaterial> decalMaterial(TEXT("Material'/Game/Assets/Materials/M_SelectionDecal.M_SelectionDecal'"));

	// If we can reach the decal material
	if(decalMaterial.Succeeded())
	{
		selectionDecalComp_->SetDecalMaterial(decalMaterial.Object);

		// Calculated properties of decal component
		selectionDecalComp_->RelativeLocation = FVector(0.f, 0.f, -80.f);
		selectionDecalComp_->DecalSize = FVector(32.f, 64.f, 64.f);
		selectionDecalComp_->SetRelativeRotation(FRotator(90.0f, 0.0f, 0.0f).Quaternion());
		selectionDecalComp_->SetVisibility(false);
	}
}

void ARTSBR_Character::SetIsSelected(const bool isSelected) const
{
	selectionDecalComp_->SetVisibility(isSelected);
}
class RTSBATTLEROYALE_API ARTSBR_PlayerController : public APlayerController
{
....
private:
....
	void StartSelection();
	void EndSelection();
	void UnitSelection() const;

	void Command();

	FVector selectionStartPosition_;
	FVector selectionEndPosition_;
	UBoxComponent* multipleSelectionBox_;
....
};
ARTSBR_PlayerController::ARTSBR_PlayerController()
{
	...
	multipleSelectionBox_ = CreateDefaultSubobject<UBoxComponent>(TEXT("MultipleSelectionBox"));
}

void ARTSBR_PlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

....
	InputComponent->BindAction("Selection", IE_Pressed, this, &ARTSBR_PlayerController::StartSelection);
	InputComponent->BindAction("Selection", IE_Released, this, &ARTSBR_PlayerController::EndSelection);

	InputComponent->BindAction("Command", IE_Released, this, &ARTSBR_PlayerController::Command);
....
}


void ARTSBR_PlayerController::StartSelection()
{
	// When player left clicks get the hit location and set selectionStartPosition_
	FHitResult hit;
	GetHitResultUnderCursor(ECC_Visibility, false, hit);
	selectionStartPosition_ = hit.Location;
}

void ARTSBR_PlayerController::EndSelection()
{
	// When player releases the mouse left click get the hit location and set selectionEndPosition_
	FHitResult hit;
	GetHitResultUnderCursor(ECC_Visibility, false, hit);
	selectionEndPosition_ = hit.Location;

	// Call unit selection which does all the calculations of the box collider and sets spectator pawn's selected character array.
	UnitSelection();
}

 ARTSBR_PlayerController::UnitSelection() const
{
	// No need to make any calculations if an error is encountered and we cannot find a RTSBR_SpectatorPawn.
	ARTSBR_SpectatorPawn *spectatorPawn;
	if ((spectatorPawn = Cast<ARTSBR_SpectatorPawn>(GetPawn())) == nullptr)
	{
		return;
	}

	// Calculate the extent by taking the difference of the selection start and end positions.
	FVector extent = FVector((selectionEndPosition_.X - selectionStartPosition_.X) * 0.5f, (selectionEndPosition_.Y - selectionStartPosition_.Y) * 0.5f, 3000.f);
	FVector midPoint = FVector(selectionStartPosition_.X + extent.X, selectionStartPosition_.Y + extent.Y, selectionStartPosition_.Z - extent.Z * 0.5f);
	
	// In order to make our box collider work with every mouse movement i.e top right to left bottom or left top to right bottom and vice versa,
	// we need to take the absolute value of the dimensions.
	extent = FVector(abs(extent.X), abs(extent.Y), extent.Z);

	multipleSelectionBox_->SetWorldLocation(midPoint);
	multipleSelectionBox_->SetBoxExtent(extent);

	// Firstly we need to set every selected character not selected and make the selected characters array empty.
	for (auto character : spectatorPawn->selectedCharacters_)
	{
		character->SetIsSelected(false);
	}
	spectatorPawn->selectedCharacters_.Empty();

	// Get the overlapping actors with the box collider
	TArray<AActor*> overlappingActors;
	multipleSelectionBox_->GetOverlappingActors(overlappingActors);

	for (auto actor : overlappingActors)
	{
		ARTSBR_Character* const character = Cast<ARTSBR_Character>(actor);

		if (character)
		{
			character->SetIsSelected(true);
			spectatorPawn->selectedCharacters_.Add(character);
		}
	}
}

GetHitResultUnderCursor function sends a ray from the mouse position and gives the corresponding hit results. Here, it is only used to get the hit position, but it can be used to get the hit actor etc., and is a very usefull function. More information about FHitResult can be found in the API: http://api.unrealengine.com/INT/API/Runtime/Engine/Engine/FHitResult/

Commanding

Let’s create a class derived from Unreal’s AIController class. Actually we do not need to create it for now, but  we will be needing this class in the future.

Since we make characters move around the map we need to create a nav mesh bounds volume:

void ARTSBR_PlayerController::Command()
{
	ARTSBR_SpectatorPawn *spectatorPawn;
	if ((spectatorPawn = Cast<ARTSBR_SpectatorPawn>(GetPawn())) != nullptr)
	{
		FHitResult hit;
		if(GetHitResultUnderCursor(ECC_Visibility, false, hit))
		{
			for (auto actor : spectatorPawn->selectedCharacters_)
			{
				ARTSBR_Character* const character = Cast<ARTSBR_Character>(actor);

				if (character)
				{
					// Call MoveToDestionation function of AIController class
					character->MoveToDestination(hit.Location);
				}
			}
		}
	}
}
#include "CoreMinimal.h"
#include "AIController.h"
#include "RTSBR_AIController.generated.h"

/**
 * 
 */
UCLASS()
class RTSBATTLEROYALE_API ARTSBR_AIController : public AAIController
{
	GENERATED_BODY()
	
};

Result


Next article will be about HUD. We will be seeing our selection box and character health and so on. Until then take care.

Github repository: github.com/emrebarisc/RTSBattleRoyale