Base Classes

Firstly we should implement our game mode class and corresponding classes we are going to be setting in our game mode. We are not going to use any of the default classes, so we need to create each one of them separately.

Before starting, if any of the following code chunks do not contain a class implementation then that class is just an empty class, but you should pay attention to make the classes derived from base or Unreal’s default derived classes(e.g.: GameMode and GameState or GameModeBase and GameStateBase etc.).

Let’s start with our game mode:

Game Mode

Game mode class is the heart of our games in Unreal Engine. It controls a lot of things like our HUD, PlayerController, main player pawn and so on, then it spawns an instance of all of these classes at the beginning of the game. For example, a spectator pawn class will be spawned and the camera component it has will be set as the main camera.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "RTSBR_GameMode.generated.h"

/**
 * 
 */
UCLASS()
class RTSBATTLEROYALE_API ARTSBR_GameMode : public AGameMode
{
	GENERATED_BODY()

public:
        // Constructor
	ARTSBR_GameMode();
	
	
};
#include "RTSBR_GameMode.h"
#include "RTSBR_GameState.h"
#include "RTSBR_PlayerController.h"
#include "RTSBR_SpectatorPawn.h"
#include "RTSBR_HUD.h"
#include "RTSBR_PlayerState.h"

ARTSBR_GameMode::ARTSBR_GameMode()
{ 
        // Let us set all class members in the constructor
	GameStateClass = ARTSBR_GameState::StaticClass();
	PlayerControllerClass = ARTSBR_PlayerController::StaticClass();
	SpectatorClass = ARTSBR_SpectatorPawn::StaticClass();
	DefaultPawnClass = ARTSBR_SpectatorPawn::StaticClass();
	PlayerStateClass = ARTSBR_PlayerState::StaticClass();
	HUDClass = ARTSBR_HUD::StaticClass();
}

Player Controller

Player controller class is the brain of the player pawn. It gets input from the player and tells pawn what to do. Basicly it provides the communication between pawn and the player. Actually we can make all of our input controls in our spectator pawn class, but we want to be away from complexity.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "RTSBR_PlayerController.generated.h"

class ARTSBR_SpectatorPawn;

/**
 * 
 */
UCLASS()
class RTSBATTLEROYALE_API ARTSBR_PlayerController : public APlayerController
{
	GENERATED_BODY()
	
public:
	ARTSBR_PlayerController();

	ARTSBR_SpectatorPawn *GetMainSpectatorPawn() const;

protected:
	virtual void SetupInputComponent() override;
	virtual void ProcessPlayerInput(const float deltaTime, const bool bGamePaused) override;

	void MovePawnForward(float value);
	void MovePawnRight(float value);
	void ZoomIn();
	void ZoomOut();

private:

};
#include "RTSBR_PlayerController.h"

#include "Engine/LocalPlayer.h"

#include "RTSBR_CameraComponent.h"
#include "RTSBR_SpectatorPawn.h"
#include "RTSBR_PlayerCameraManager.h"

ARTSBR_PlayerController::ARTSBR_PlayerController()
{
	PrimaryActorTick.bCanEverTick = true;
	bHidden = false;
	bShowMouseCursor = true;

	PlayerCameraManagerClass = ARTSBR_PlayerCameraManager::StaticClass();
}

ARTSBR_SpectatorPawn* ARTSBR_PlayerController::GetMainSpectatorPawn() const
{
	return Cast<ARTSBR_SpectatorPawn>(GetPawn());
}

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

	InputComponent->BindAction("ZoomIn", IE_Pressed, this, &ARTSBR_PlayerController::ZoomIn);
	InputComponent->BindAction("ZoomOut", IE_Pressed, this, &ARTSBR_PlayerController::ZoomOut);

	InputComponent->BindAxis("MoveForward", this, &ARTSBR_PlayerController::MovePawnForward);
	InputComponent->BindAxis("MoveRight", this, &ARTSBR_PlayerController::MovePawnRight);
}

void ARTSBR_PlayerController::ProcessPlayerInput(const float deltaTime, const bool bGamePaused)
{
	Super::ProcessPlayerInput(deltaTime, bGamePaused);

	{
		const ULocalPlayer* localPlayer = Cast<ULocalPlayer>(Player);
		ARTSBR_SpectatorPawn* spectatorPawn = GetMainSpectatorPawn();

		if (spectatorPawn && localPlayer)
		{
			if (localPlayer->ViewportClient)
			{
				spectatorPawn->GetCameraComponent()->UpdateCameraMovement(this);
			}
		}
	}
}

void ARTSBR_PlayerController::MovePawnForward(const float value)
{
	ARTSBR_SpectatorPawn *spectatorPawn;
	if (value != 0.f && (spectatorPawn = Cast<ARTSBR_SpectatorPawn>(GetPawn())) != nullptr)
	{
		spectatorPawn->MoveForward(value);
	}
}

void ARTSBR_PlayerController::MovePawnRight(const float value)
{
	ARTSBR_SpectatorPawn *spectatorPawn;
	if (value != 0.f && (spectatorPawn = Cast<ARTSBR_SpectatorPawn>(GetPawn())) != nullptr)
	{
		spectatorPawn->MoveRight(value);
	}
}

void ARTSBR_PlayerController::ZoomIn()
{
	ARTSBR_SpectatorPawn *spectatorPawn;
	if ((spectatorPawn = Cast<ARTSBR_SpectatorPawn>(GetPawn())) != nullptr)
	{
		spectatorPawn->OnMouseScrollUp();
	}
}

void ARTSBR_PlayerController::ZoomOut()
{
	ARTSBR_SpectatorPawn *spectatorPawn;
	if ((spectatorPawn = Cast<ARTSBR_SpectatorPawn>(GetPawn())) != nullptr)
	{
		spectatorPawn->OnMouseScrollDown();
	}
}

Input Settings

Spectator Pawn

Spectator pawn will be the main pawn class, and players will control this class and its children. We are going to start implementing this class by adding input controls and a camera component. Later on we are going to move the input operations to player controller.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/SpectatorPawn.h"
#include "RTSBR_SpectatorPawn.generated.h"

class URTSBR_CameraComponent;

/**
 * 
 */
UCLASS()
class RTSBATTLEROYALE_API ARTSBR_SpectatorPawn : public ASpectatorPawn
{
	GENERATED_BODY()

	friend class ARTSBR_PlayerController;

public:
	ARTSBR_SpectatorPawn(const FObjectInitializer& ObjectInitializer);
	virtual void BeginPlay() override;
	virtual void Tick(float deltaTime) override;
	
	URTSBR_CameraComponent* GetCameraComponent()
	{
		return cameraComponent_;
	}

private:
	void MoveForward(float value);
	void MoveRight(float value);
	void OnMouseScrollUp();
	void OnMouseScrollDown();

	URTSBR_CameraComponent* cameraComponent_;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "RTSBR_SpectatorPawn.h"
#include "Components/SphereComponent.h"
#include "Engine/CollisionProfile.h"
#include "Components/InputComponent.h"
#include "RTSBR_CameraComponent.h"
#include "RTSBR_SpectatorPawnMovement.h"

ARTSBR_SpectatorPawn::ARTSBR_SpectatorPawn(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<URTSBR_SpectatorPawnMovement>(Super::MovementComponentName))
{
	PrimaryActorTick.bCanEverTick = false;

	GetCollisionComponent()->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
	bAddDefaultMovementBindings = false;

	cameraComponent_ = CreateDefaultSubobject<URTSBR_CameraComponent>(TEXT("Camera Component"));
}

void ARTSBR_SpectatorPawn::BeginPlay()
{
	Super::BeginPlay();

	SetActorLocation(FVector::ZeroVector);
}

void ARTSBR_SpectatorPawn::Tick(float deltaTime)
{
	Super::Tick(deltaTime);
}

void ARTSBR_SpectatorPawn::MoveForward(const float value)
{
	if(value != 0.f)
	{
		cameraComponent_->MoveForward(value);
	}
}

void ARTSBR_SpectatorPawn::MoveRight(const float value)
{
	if (value != 0.f)
	{
		cameraComponent_->MoveRight(value);
	}
}

void ARTSBR_SpectatorPawn::OnMouseScrollUp()
{
	cameraComponent_->OnZoomIn();
}

void ARTSBR_SpectatorPawn::OnMouseScrollDown()
{
	cameraComponent_->OnZoomOut();
}

Camera Component

Camera component is the eye of a player. In camera component class, we are going to set movement and zooming functions, camera rotation, and move the camera according to the mouse position. Even if the movement parameters come from the spectator pawn, we are setting owning pawn’s location here because of the mouse operations.

#pragma once

#include "CoreMinimal.h"
#include "Camera/CameraComponent.h"
#include "RTSBR_CameraComponent.generated.h"

class ARTSBR_PlayerController;

UCLASS()
class RTSBATTLEROYALE_API URTSBR_CameraComponent : public UCameraComponent
{
	GENERATED_BODY()
	
public:
	URTSBR_CameraComponent();
	
	virtual void GetCameraView(float deltaTime, FMinimalViewInfo& desiredView) override;

	void OnZoomIn();
	void OnZoomOut();
	void UpdateCameraMovement(const APlayerController* playerController) const;
	void MoveForward(float value) const;
	void MoveRight(float value) const;
	void SetZoomLevel(float level);
	void ClampCameraLocation(const APlayerController *playerController, FVector& outCameraLocation) const;

private:
	float minCameraOffset_;
	float maxCameraOffset_;
	FRotator fixedCameraAngle_;

	float cameraScrollSpeed_;
	uint32 cameraActiveBorder_;
	float minZoomLevel_;
	float maxZoomLevel_;
	float zoomAlpha_;
	float movementSpeed_;

	bool bShouldClampCamera_ : 1;

	FBox cameraMovementBounds_;
	FVector2D cameraMovementViewportSize_;

	APawn* GetOwnerPawn() const;
	ARTSBR_PlayerController *GetPlayerController() const;
	void UpdateCameraBounds(const APlayerController* playerController) const;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "RTSBR_CameraComponent.h"

#include "Engine/LocalPlayer.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/SpectatorPawn.h"


#include "RTSBR_PlayerController.h"
#include "RTSBR_SpectatorPawn.h"
#include "RTSBR_SpectatorPawnMovement.h"
#include "Engine/Engine.h"
#include "RTSBR_GameState.h"

URTSBR_CameraComponent::URTSBR_CameraComponent()
{
	bShouldClampCamera_ = true;

	zoomAlpha_ = 1.f;
	cameraScrollSpeed_ = 3000.f;
	cameraActiveBorder_ = 100.f;
	minZoomLevel_ = 0.4f;
	maxZoomLevel_ = 1.f;
	minCameraOffset_ = 0.f;
	maxCameraOffset_ = 20000.f;
	movementSpeed_ = 100.f;

	fixedCameraAngle_ = FRotator(-60.f, 0.f, 0.f);
}

void URTSBR_CameraComponent::GetCameraView(float deltaTime, FMinimalViewInfo& desiredView)
{
	APlayerController* playerController = GetPlayerController();

	if (playerController)
	{
		desiredView.FOV = 30.f;
		const float currentOffset = minCameraOffset_ + zoomAlpha_ * (maxCameraOffset_ - minCameraOffset_);
		const FVector focalPos = playerController->GetFocalLocation();
		desiredView.Location = focalPos - fixedCameraAngle_.Vector() * currentOffset;
		desiredView.Rotation = fixedCameraAngle_;
	}
}

void URTSBR_CameraComponent::OnZoomIn()
{
	SetZoomLevel(zoomAlpha_ - 0.1f);
}

void URTSBR_CameraComponent::OnZoomOut()
{
	SetZoomLevel(zoomAlpha_ + 0.1f);
}

void URTSBR_CameraComponent::UpdateCameraMovement(const APlayerController* playerController) const
{
	ULocalPlayer* const localPlayer = Cast<ULocalPlayer>(playerController->Player);

	if(localPlayer && localPlayer->ViewportClient && localPlayer->ViewportClient->Viewport)
	{
		FVector2D mousePosition;
		if(!localPlayer->ViewportClient->GetMousePosition(mousePosition))
		{
			return;
		}

		FViewport* viewport = localPlayer->ViewportClient->Viewport;
		const float scrollSpeed = 60.f;
		const FIntPoint viewportSize = viewport->GetSizeXY();

		const uint32 viewLeft = FMath::TruncToInt(localPlayer->Origin.X * viewportSize.X);
		const uint32 viewRight = viewLeft + FMath::TruncToInt(localPlayer->Size.X * viewportSize.X);
		const uint32 viewTop = FMath::TruncToInt(localPlayer->Origin.Y * viewportSize.Y);
		const uint32 viewBottom = viewTop + FMath::TruncToInt(localPlayer->Size.Y * viewportSize.Y);

		const float maxSpeed = cameraScrollSpeed_ * FMath::Clamp(zoomAlpha_, minZoomLevel_, maxZoomLevel_);

		const uint32 mouseX = mousePosition.X;
		const uint32 mouseY = mousePosition.Y;
		float spectatorCameraSpeed = maxSpeed;
		ARTSBR_SpectatorPawn *spectatorPawn = nullptr;
		
		if(true) //!bNoScrollZone_
		{
			if (mouseX >= viewLeft && mouseX <= (viewLeft + cameraActiveBorder_))
			{
				const float delta = 1.0f - float(mouseX - viewLeft) / cameraActiveBorder_;
				spectatorCameraSpeed = delta * maxSpeed;
				MoveRight(-scrollSpeed * delta);
			}
			else if (mouseX >= (viewRight - cameraActiveBorder_) && mouseX <= viewRight)
			{
				const float delta = float(mouseX - viewRight + cameraActiveBorder_) / cameraActiveBorder_;
				spectatorCameraSpeed = delta * maxSpeed;
				MoveRight(scrollSpeed * delta);
			}

			if (mouseY >= viewTop && mouseY <= (viewTop + cameraActiveBorder_))
			{
				const float delta = 1.0f - float(mouseY - viewTop) / cameraActiveBorder_;
				spectatorCameraSpeed = delta * maxSpeed;
				MoveForward(scrollSpeed * delta);
			}
			else if (mouseY >= (viewBottom - cameraActiveBorder_) && mouseY <= viewBottom)
			{
				const float delta = float(mouseY - (viewBottom - cameraActiveBorder_)) / cameraActiveBorder_;
				spectatorCameraSpeed = delta * maxSpeed;
				MoveForward(-scrollSpeed * delta);
			}
		}
	}
}

void URTSBR_CameraComponent::MoveForward(const float value) const
{
	APawn* ownerPawn = GetOwnerPawn();

	if (ownerPawn)
	{
		APlayerController* controller = GetPlayerController();
		if (value != 0.f && controller)
		{
			// If our camera is not rotated or we want to use camera's local coordinates, we can use rotationMatrix
			//const FRotationMatrix rotationMatrix(controller->PlayerCameraManager->GetCameraRotation());
			const FVector worldSpaceAcc = /*rotationMatrix.GetScaledAxis(EAxis::X)*/ FVector(1.f, 0.f, 0.f) * movementSpeed_;
			
			ownerPawn->AddMovementInput(worldSpaceAcc, value);
		}
	}
}

void URTSBR_CameraComponent::MoveRight(const float value) const
{
	APawn* ownerPawn = GetOwnerPawn();

	if (ownerPawn)
	{
		APlayerController* controller = GetPlayerController();
		if (value != 0.f && controller)
		{
			// If our camera is not rotated or we want to use camera's local coordinates, we can use rotationMatrix
			//const FRotationMatrix rotationMatrix(controller->PlayerCameraManager->GetCameraRotation());
			const FVector worldSpaceAcc = /*rotationMatrix.GetScaledAxis(EAxis::Y)*/ FVector(0.f, 1.f, 0.f) * movementSpeed_;

			ownerPawn->AddMovementInput(worldSpaceAcc, value);
		}
	}
}

void URTSBR_CameraComponent::SetZoomLevel(float level)
{
	zoomAlpha_ = FMath::Clamp(level, minZoomLevel_, maxZoomLevel_);
}

APawn* URTSBR_CameraComponent::GetOwnerPawn() const
{
	return Cast<APawn>(GetOwner());
}

ARTSBR_PlayerController* URTSBR_CameraComponent::GetPlayerController() const
{
	ARTSBR_PlayerController* controller = nullptr;
	APawn* owner = GetOwnerPawn();
	if (owner)
	{
		controller = Cast<ARTSBR_PlayerController>(owner->GetController());
	}

	return controller;
}

Final demo:

Github link: https://github.com/emrebarisc/RTSBattleRoyale

By the way, you should check output log frequently for warnings and errors if you think your code should be working but it does not. Output log gives quite good information in such cases.

Next time I am going to go over our character implementation. Until then, take care 🙂

P.s. I cannot take all the credits for this article because I’ve taken some help from Unreal Engine’s strategy game tutorial.  This tutorial is a huge source of knowledge for me and I recommend you to check it out as well.