C++ Smooth RTS Camera in Unreal Engine (Cruise Line Captain Devlog 1)

How I used C++ to create a smooth RTS camera component for Unreal Engine for my new game, Cruise Line Captain.

I recently started a new game project in Unreal Engine called Cruise Line Captain. While in it's in extremely early stages, Cruise Line Captain is ultimately going to be a fun, dynamic cruise ship building tycoon game. I'll be posting a lot more about the project here as it develops.

This game is set up with a free camera system, similar to a real-time strategy game (RTS). Having a useful in-game camera helps tremendously in my early game development allowing me to easily move around my level to different areas I may have game actors in.

TLDR; Want to just see the full code? Then checkout my Smooth Camera Unreal Engine plugin on GitHub!

In the past, I've roughed out a quick camera system that included:

  • Panning across the X and Y axis using the keyboard WASD keys or the edge of the screen
  • Zooming using the mouse scroll wheel

That camera system looks something like this:

This camera stops moving abruptly.

This time, however, I wanted to take my camera system a little bit further.

You can see that the movements feel quite choppy--there is no easing and the camera stops abruptly in both panning and zooming. Plus, we can't rotate the camera so that's our only angle we get at the level. This time around, I thought I can do more. So I set out to update my camera system to support these new features:

  • Smooth panning and zooming
  • Camera rotation around a point, a.k.a "orbiting"
  • Panning laterally, forward, and backwards based on the direction I am looking

I also want to break this off into a SceneComponent so I can reuse the RTS camera on other Pawns, by further putting that into it's own Unreal Engine plugin I can reuse this across future projects as well!

Smooth Panning and Zooming

It turns out this was an extremely simple fix! On an Unreal Engine camera boom object (USpringArmComponent), there is a flag called bEnableCameraLag which is defined as "If true, camera lags behind target position to smooth its movement." That sounds like exactly what I need!

UMWSmoothCameraComponent::UMWSmoothCameraComponent()
{
// ... other constructor code here

// Camera lag enables smooth panning
CameraSpringArm->bEnableCameraLag = true;
CameraSpringArm->CameraLagSpeed = 10.0;
}

This code tells the camera arm to lag behind the actor a small amount when moving which allows the camera to smoothly follow the actor instead of starting and stopping suddenly.

With this enabled, my panning is smooth as butter:

This camera can now pan smoothly and zoom smoothly.

One other thing I did early on was disable the motion blur on the camera object for more crisp movement that does't appear as blurry.

UMWSmoothCameraComponent::UMWSmoothCameraComponent()
{
// ... other constructor code here

// Overrides motion blur for crisp movement
Camera->PostProcessSettings.bOverride_MotionBlurAmount = true;
Camera->PostProcessSettings.MotionBlurAmount = 0;
Camera->PostProcessSettings.MotionBlurMax = 0;
}

This tells the camera we are overriding it's motion blur settings and setting them to zero.

Here's the code for enabling smooth zoom on my camera:

void UMWSmoothCameraComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) 
{
// ... other Tick code here

// Apply zoom if we are not within 0.5 units of our desired zoom
if (!FMath::IsNearlyEqual(CameraSpringArm->TargetArmLength, TargetCameraZoomDistance, 0.5f))
{
// This allows us to smoothly zoom to our desired target arm length over time
CameraSpringArm->TargetArmLength = FMath::FInterpTo(
CameraSpringArm->TargetArmLength, // the current value
TargetCameraZoomDistance, // the desired length
DeltaTime, // time passed
CameraZoomSpeed // speed at which to move
);
}
}

What this does is interpolate, or estimate, how to go from our current zoom distance to the desired zoom distance over time. This allows us to smoothly zoom in from A to B, rather than jump between the two distances.

Camera rotation around a point, a.k.a "orbiting"

Even we are using a "free form camera" that can go around our level without an accompanying character, it is still techincally attached to an invisible "pawn" actor and a camera boom. We do this because our player can control the pawn, and with minimal setup plays the game through the pawn's camera. The camera boom allows us to place the camera behind the invisible actor and follow it's movements. What this also helps us do is orbit the camera around our level by rotating the boom's pitch and yaw. We don't need to rotate the pawn itself, just the boom. Here's the code we add to support orbiting:

void UMWSmoothCameraComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) 
{

// ... other Tick code here

// If the middle mouse button is down we will be rotating the camera as the mouse moves
if (bShouldRotateCamera)
{
FVector2D MouseLocation;
PlayerController->GetMousePosition(MouseLocation.X, MouseLocation.Y);

// Get how much we have moved since the last frame/rotate start
const float XPercent = (MouseLocation.X - RotateCameraMouseStart.X) / ViewportSizeX;
const float YPercent = (RotateCameraMouseStart.Y - MouseLocation.Y) / ViewportSizeY;

// Get the current rotation within -180 to 180 degrees, instead of 0-360
const FRotator CurrentRot = CameraSpringArm->GetRelativeRotation().GetNormalized();

// Update our rotation based on 100% mouse movement across the screen equals 180 degrees
// rotation, limiting pitch to near vertical to limit issues at -90 and 90 degrees
CameraSpringArm->SetRelativeRotation(
FRotator(
FMath::Clamp<float>(CurrentRot.Pitch + (YPercent * 180), -88, 88),
CurrentRot.Yaw + (XPercent * 180),
0
));

// Update the "last frame" mouse location
RotateCameraMouseStart = MouseLocation;
}
}

What we're doing here is getting our current mouse position and comparing it to our last mouse position from the previous frame. We calculate a percentage that our mouse has moved in the X and Y direction, which we then translate into a rotation amount for our camera arm to rotate around the pawn. Because the camera is attached to the camera arm, it follows that rotation as if orbiting around the "focus point" where are pawn is.

Now we can smoothly orbit around the scene using the middle mouse button.

Panning based on direction I am looking

Now that we can rotate our camera, our camera panning logic is out of date. We are panning based on fixed directions, forward/back on the X axis and left/right on the Y axis. But now our camera can have an arbitrary rotation, we want to pan forward in the direction we are looking, and then move left and right based on what's currently to the right and left of where we are looking. Fortunately Unreal Engine has a useful method for getting the right vector_ in the direction to the right of where our camera is facing. We just have to adjust a few lines of code here to now pan based off where the camera is looking:

void UMWSmoothCameraComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) 
{
// ... other Tick code here

FVector Location = Pawn->GetActorLocation();

// Update the left/right movement based on the direction to the right of where we are facing
Location += CameraSpringArm->GetRightVector() * CameraSpeed * CameraLeftRightAxisValue * DeltaTime;

// Update the forward/backwards movement based on the yaw rotation, **ignoring pitch and roll**
// so the camera remains level as it moves (looking down would otherwise pan the
// camera forward and down)
Location += FRotationMatrix(FRotator(0, CameraSpringArm->GetRelativeRotation().Yaw, 0))
.GetScaledAxis(EAxis::X) * CameraSpeed * CameraUpDownAxisValue * DeltaTime;

// Update the pawn's location and the attached camera will follow
Pawn->SetActorLocation(Location);

This code uses the CameraLeftRightAxisValue and CameraUpDownAxisValue, which are values between -1.0 and 1.0, which are populated by the defined input axis actions from the user's keyboard. For more information on how to do this, see Unreal's documentation on input.

The "final" product

Everything comes together and we can even combine movements like panning and orbiting.

And there we have it! A much more featured camera component fit for our game, and really any RTS style game. There are of course more improvements and optimizations I could make as well as adding even more features, but this is perfect for my needs right now. I mean, all I have in the entire game right now is a sphere and a grassy cube!

You can find the full code packaged as an Unreal Engine plugin on Github with helpful in-code comments and instructions on adding this smooth RTS camera to your next game project!

My next devlog will focus on research into the AI system I want to use for guests on our ship, so follow me on Twitter to get notified about my next post!

Further Resources

As with most things on the internet, this was not created without help from other resources. The following resources were key to me making this camera component and making it its own plugin!