Camera Navigation for Strategy Games in Unreal Engine 4

Lately, I’ve been fiddling around with a game prototype for a strategy game in Unreal Engine 4. Although it’s possible to create any kind of game with the engine, it’s main focus rests on the action and RPG genres. Thus, things like a camera navigation for strategy games have been a little tricky. I couldn’t find much help in the forums and the strategy game sample didn’t help.

Strategy Game Prototype
Prototype of a stragety game I’ve been working on.

However, I’ve come up with a solution that I wanted to share with you. So maybe you are also working on a strategy game in UE4 and need some help.

By implementing the camera navigation of this tutorial, you can navigate around as shown in the following video. This is also the intended result of following the tutorial.

It doesn’t matter what kind of project template you choose, but I’m using the “Top Down” C++ template. The engine will create a Visual Studio project for you with the game mode, player controller and player character. Let’s start by editing the latter one.

UE4 Project Creation
Create a new project with the top down template.

The Player Character class

Actually you don’t want to move a character across the level. In a strategy game you want to be able to pan, rotate and zoom the camera. Therefore, you have to change the base class of the player character from ACharacter to ASpectatorPawn. You also need to add some functions that will help you with the navigation later on.

#pragma once
#include "GameFramework/Character.h"
#include "GameFramework/SpectatorPawn.h"
#include "StrategySampleCharacter.generated.h"

UCLASS(Blueprintable)
class AStrategySampleCharacter : public ASpectatorPawn
{
	GENERATED_BODY()

	/** Top down camera */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class UCameraComponent* TopDownCameraComponent;

	/** Camera boom positioning the camera above the character */
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true"))
	class USpringArmComponent* CameraBoom;
public:
	AStrategySampleCharacter(const FObjectInitializer& ObjectInitializer);

	/** Returns TopDownCameraComponent subobject **/
	FORCEINLINE class UCameraComponent* GetTopDownCameraComponent() const { return TopDownCameraComponent; }
	/** Returns CameraBoom subobject **/
	FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; }

	void ChangeCameraArmLength(float changeValue);
	void RotateCameraArm(FRotator rotation);
	void MoveCharacterForward(float changeValue);
	void MoveCharacterRight(float changeValue);
};

If you look at the definition of the class, you will notice in the constructor that Intellisense tells you that our character class can’t find GetCapsuleComponent() and GetCharacterMovement(). That’s because the inherited ASpectatorPawn class doesn’t need them. You can either remove the appropriate lines or delete them. Additionally, you need to change some of the properties, so that the camera moves like you want it to and follows the “spectator”. In the following code I’ve also added camera lag because I personally like it for such movement.

AStrategySampleCharacter::AStrategySampleCharacter(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	// Set size for player capsule
	//GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);

	// Don't rotate character to camera direction, except for the yaw axis
	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = true;
	bUseControllerRotationRoll = false;

	// Disable Jump and Crouch actions
	bAddDefaultMovementBindings = false;

	// Configure character movement
	/*GetCharacterMovement()->bOrientRotationToMovement = true; // Rotate character to moving direction
	GetCharacterMovement()->RotationRate = FRotator(0.f, 640.f, 0.f);
	GetCharacterMovement()->bConstrainToPlane = true;
	GetCharacterMovement()->bSnapToPlaneAtStart = true;*/

	// Create a camera boom...
	CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
	CameraBoom->AttachTo(RootComponent);
	CameraBoom->bAbsoluteRotation = false; // Rotate arm relative to character
	CameraBoom->TargetArmLength = 800.f;
	CameraBoom->RelativeRotation = FRotator(-60.f, 0.f, 0.f);
	CameraBoom->bDoCollisionTest = false; // Don't want to pull camera in when it collides with level

	// Move camera boom with character only on yaw rotation
	CameraBoom->bUsePawnControlRotation = false;
	CameraBoom->bInheritPitch = false;
	CameraBoom->bInheritRoll = false;
	CameraBoom->bInheritYaw = true;

	// Enables camera lag - matter of taste
	CameraBoom->bEnableCameraLag = true;
	CameraBoom->bEnableCameraRotationLag = true;

	// Disable collisions
	GetCollisionComponent()->bGenerateOverlapEvents = false;
	GetCollisionComponent()->SetCollisionProfileName("NoCollision");

	// Create a camera...
	TopDownCameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("TopDownCamera"));
	TopDownCameraComponent->AttachTo(CameraBoom, USpringArmComponent::SocketName);
	TopDownCameraComponent->bUsePawnControlRotation = false; // Camera does not rotate relative to arm
}

All that’s left is to implement the functions that you’ve previously declared in the header file. They will help you to navigate the camera later on and provide a consistent interface for the corresponding operations.

void AStrategySampleCharacter::ChangeCameraArmLength(float changeValue)
{
	CameraBoom->TargetArmLength += changeValue * 100.0f; // Change 100.0f with zoom speed property
}

void AStrategySampleCharacter::RotateCameraArm(FRotator rotation)
{
	CameraBoom->AddRelativeRotation(rotation);
}

void AStrategySampleCharacter::MoveCharacterForward(float changeValue)
{
	AddMovementInput(GetActorForwardVector(), changeValue);
}

void AStrategySampleCharacter::MoveCharacterRight(float changeValue)
{
	AddMovementInput(GetActorRightVector(), changeValue);
}

The Player Controller class

Let’s move on to the player controller class. Its main purpose is to manage the input for our navigation. First of all you need to get rid of all the unnecessary top down movement stuff. So either comment out or delete the corresponing lines. Basically your files should look like this:

#pragma once
#include "GameFramework/PlayerController.h"
#include "StrategySamplePlayerController.generated.h"

UCLASS()
class AStrategySamplePlayerController : public APlayerController
{
	GENERATED_BODY()
public:
	AStrategySamplePlayerController(const FObjectInitializer& ObjectInitializer);
protected:
	// Begin PlayerController interface
	virtual void PlayerTick(float DeltaTime) override;
	virtual void SetupInputComponent() override;
	// End PlayerController interface
};
#include "StrategySample.h"
#include "StrategySamplePlayerController.h"

AStrategySamplePlayerController::AStrategySamplePlayerController(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer)
{
	bShowMouseCursor = true;
}

void AStrategySamplePlayerController::PlayerTick(float DeltaTime)
{
	Super::PlayerTick(DeltaTime);
}

void AStrategySamplePlayerController::SetupInputComponent()
{
	// set up gameplay key bindings
	Super::SetupInputComponent();
}

Make sure everything works and compiles before you continue. Let’s look again at the header file of the AStrategySamplePlayerController class. There you need to add some methods for handling the player input and private member variables for managing the mouse cursor.

UCLASS()
class AStrategySamplePlayerController : public APlayerController
{
	GENERATED_BODY()
public:
	AStrategySamplePlayerController(const FObjectInitializer& ObjectInitializer);
protected:
	// Begin PlayerController interface
	virtual void PlayerTick(float DeltaTime) override;
	virtual void SetupInputComponent() override;
	// End PlayerController interface

	// Input handlers
	void OnMoveForwardAxis(float axisValue);
	void OnMoveRightAxis(float axisValue);
	void OnMouseHorizontal(float axisValue);
	void OnMouseVertical(float axisValue);
	void OnZoomInAction();
	void OnZoomOutAction();
	void OnLookAroundStart();
	void OnLookAroundStop();
private:
	bool lookAroundEnabled = false;
	int32 mouseLockPositionX;
	int32 mouseLockPositionY;
};

Now you need to bind the declared input handlers to the appropriate axes and actions in the SetupInputComponent() method.

void AStrategySamplePlayerController::SetupInputComponent()
{
	// Set up gameplay key bindings
	Super::SetupInputComponent();

	InputComponent->BindAxis("MoveForward", this, &AStrategySamplePlayerController::OnMoveForwardAxis);
	InputComponent->BindAxis("MoveRight", this, &AStrategySamplePlayerController::OnMoveRightAxis);
	InputComponent->BindAxis("MouseHorizontal", this, &AStrategySamplePlayerController::OnMouseHorizontal);
	InputComponent->BindAxis("MouseVertical", this, &AStrategySamplePlayerController::OnMouseVertical);
	InputComponent->BindAction("ZoomIn", EInputEvent::IE_Pressed, this, &AStrategySamplePlayerController::OnZoomInAction);
	InputComponent->BindAction("ZoomOut", EInputEvent::IE_Pressed, this, &AStrategySamplePlayerController::OnZoomOutAction);
	InputComponent->BindAction("LookAround", EInputEvent::IE_Pressed, this, &AStrategySamplePlayerController::OnLookAroundStart);
	InputComponent->BindAction("LookAround", EInputEvent::IE_Released, this, &AStrategySamplePlayerController::OnLookAroundStop);
}

All that’s left for you to do in the code project is to implement the input handles.

void AStrategySamplePlayerController::OnMoveForwardAxis(float axisValue)
{
	APawn* const Pawn = GetPawn();
	AStrategySampleCharacter* character = Cast<AStrategySampleCharacter>(Pawn);
	if (character)
	{
		character->MoveCharacterForward(axisValue);
	}
}

void AStrategySamplePlayerController::OnMoveRightAxis(float axisValue)
{
	APawn* const Pawn = GetPawn();
	AStrategySampleCharacter* character = Cast<AStrategySampleCharacter>(Pawn);
	if (character)
	{
		character->MoveCharacterRight(axisValue);
	}
}

void AStrategySamplePlayerController::OnMouseHorizontal(float axisValue)
{
	if (lookAroundEnabled)
	{
		APawn* const Pawn = GetPawn();
		Pawn->AddControllerYawInput(axisValue);
		Cast<ULocalPlayer>(Player)->ViewportClient->Viewport->SetMouse(mouseLockPositionX, mouseLockPositionY);
	}
}
void AStrategySamplePlayerController::OnMouseVertical(float axisValue)
{
	if (lookAroundEnabled)
	{
		APawn* const Pawn = GetPawn();
		AStrategySampleCharacter* character = Cast<AStrategySampleCharacter>(Pawn);
		if (character)
		{
			character->RotateCameraArm(FRotator(axisValue, 0.0f, 0.0f));
		}
		Cast<ULocalPlayer>(Player)->ViewportClient->Viewport->SetMouse(mouseLockPositionX, mouseLockPositionY);
	}
}

void AStrategySamplePlayerController::OnZoomInAction()
{
	APawn* const Pawn = GetPawn();
	AStrategySampleCharacter* character = Cast<AStrategySampleCharacter>(Pawn);
	if (character)
	{
		character->ChangeCameraArmLength(-1.0f);
	}
}

void AStrategySamplePlayerController::OnZoomOutAction()
{
	APawn* const Pawn = GetPawn();
	AStrategySampleCharacter* character = Cast<AStrategySampleCharacter>(Pawn);
	if (character)
	{
		character->ChangeCameraArmLength(1.0f);
	}
}

void AStrategySamplePlayerController::OnLookAroundStart()
{
	//Lock mouse cursor
	lookAroundEnabled = true;
	bShowMouseCursor = false;
	mouseLockPositionX = Cast<ULocalPlayer>(Player)->ViewportClient->Viewport->GetMouseX();
	mouseLockPositionY = Cast<ULocalPlayer>(Player)->ViewportClient->Viewport->GetMouseY();
}

void AStrategySamplePlayerController::OnLookAroundStop()
{
	//Unlock mouse cursor
	lookAroundEnabled = false;
	bShowMouseCursor = true;
}

It’s important to note that the mouse cursor is locked when pressing the “LookAround” button. The code saves the current mouse position from the viewport and restores it everytime you move the mouse cursor. This solves the problem of the cursor being locked to the viewport and not moving when reaching the border. It also means that the player doesn’t have to look for the cursor cursor everytime he rotates the view.

Of course, you also have to set the correct input bindings in the project settings.

Camera Navigation Input Bindings
Input bindings for the camera navigation.

Maybe you are asking yourself, why I didn’t use an axis mapping for the zooming and instead went with the action mappings. Simply put, the mouse wheel doesn’t respond to the axes. The Unreal Engine interprets the mouse wheel scrolling as press and release operations.

Please note that this is a very basic implementation of the strategy game camera navigation. You are now free to add minimum and maximum zoom leves, customizable panning and rotations speeds and so on. If you have any further questions, please use the comment section below.

7 thoughts on “Camera Navigation for Strategy Games in Unreal Engine 4”

  1. How can I avoid an error which says <>

    And <>

    When I’m sure the constructor DO exists! And this occurs only when I change:

    class AIA_ProjectCharacter : public ACharacter
    for
    class AIA_ProjectCharacter : public ASpectatorPawn

    I’m pretty new in programming on UE4 but I have a little expierence in C++ if U could help me, it would be awesome… I’m breaking my head with this… almost literally

  2. Nice article, and the code too. But i doesn’t understand it too well but that’s because i’am not well with the ‘gameplay framwork’ of ue4. They simply just explain it well.

  3. Hi, I’m using Unreal Engine 4.10.4 and when I try to compile I get this error “pointer to incomplete class type is not allowed” when I use the cast on ULocalPlayer. I’ve done some research and it basically means that the class ULocalPlayer is not implemented.
    I’m kind of new to UE4 and C++ so i’m a little bit confused here, do I need to implement it manually or did I simply fuck up in some way?

    1. Hi Aleksi,

      it seems like you are missing an “#include” line. Thus, the linker can’t find the declaration for the ULocalPlayer class.
      Please check that you have the correct header files included.

      My project has no problem finding the correct header files, but I guess you could try to explicitly include the ULocalPlayer class via
      #include "Engine/LocalPlayer.h"
      at the beginning of your .cpp file.

      Cheers,
      Simon

  4. Awesome tutorial! Was just what I was looking for when getting started with the UE4 engine. A lot of the videos on how to control cameras focus on blueprints, but I am planning on hooking in via C++ for my current task.

    If anyone is interested, you can find the github code including all these sets checked in as separate commits here:

    https://github.com/rockhowse/UE4_TopDown_ZoomPan

    And a quick youtube video showing the before/after here:

    https://youtu.be/rHeml4Ju76k

    One thing that doesn’t seem to be working while using your code as it exists here is the “mouse look”. I can zoom/pan but holding the middle mouse button doesn’t seem to allow me to “look around”.

    Feel free to fork/clone my repo and see if there is something I am missing. Maybe it is something that has changed with the current version of UE4 (v4.14.3 as of this writing)

    Thanks again for the great write up!

    1. Hi Nate, thank you very much for your comment!

      Regarding the “look around” problem: Please change the name of the axes in your “Engine – Input” settings from
      MoveHorizontal” and “MoveVertical”
      to
      MouseHorizontal” and “MouseVertical”.

      That should do the trick. Now you should be able to look around as much as you want :)

      Happy developing!
      Cheers,
      Simon

Leave a Reply

Your email address will not be published. Required fields are marked *