Player-Controlled Cameras

Learn to manipulate a Camera and a Pawn at the same time, using player input.

Choose your operating system:

Windows

macOS

Linux

Attaching a spring arm and a camera to a Pawn provides the capability to adjust how our camera follows a Pawn in the world.

1. Attach a Camera to a Pawn

  1. Begin by launching Unreal Engine and creating a new blank Template.

    TemplateSelect.png

  2. Select a new Games Project. In the Project Settings window, we will change the default code from Blueprint to C++, with starter content enabled. Choose the directory where you want your unreal project to save in, then name your project HowTo_PlayerCamera and click Create Project.

    ProjectSettings.png

  3. From the sources panel, navigate to your C++ Classes folder. You should notice a single class containing your Game mode base class. Right click on the adjacent grey space, and from the drop down window, select New C++ class

    NewClass.png

  4. Select Pawn as your Parent Class and click Next. Name your new Pawn PawnWithCamera.

    NamePawn.png

  5. in Visual Studio, Navigate to the PawnWithCamera.h file and add the following code to the protected namespace of your class definition:

    protected:
        UPROPERTY(EditAnywhere)
        class USpringArmComponent* SpringArmComp;
    
        UPROPERTY(EditAnywhere)
        class UCameraComponent* CameraComp;
    
        UPROPERTY(EditAnywhere)
        UStaticMeshComponent* StaticMeshComp;

    These variables are used to create a SpringArmComponent with a CameraComponent attached to the end. Spring arms provide a simple way to attach cameras (or other Components) so that it's motion is smooth and fluid as they move with the object they observe.

  6. Navigate to PawnWithCamera.cpp and declare the following includes:

        #include "GameFramework/SpringArmComponent.h"
        #include "Camera/Component.h"
  7. Now, you will need to create your static mesh, spring arm and camera components. Declare the following code to PawnWithCamera.cpp from inside the class constructor APawnWithCamera::APawnWithCamera: Add the following code:

    //Create our components
    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootSceneComponent"));
    StaticMeshComp = CreateDefaultSubobject <UStaticMeshComponent>(TEXT("StaticMeshComponent"));
    SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComponent"));
    CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
    
    //Attach our components
    StaticMeshComp->SetupAttachment(RootComponent);
    SpringArmComp->SetupAttachment(StaticMeshComp);
    CameraComp->SetupAttachment(SpringArmComp,USpringArmComponent::SocketName);
    
    //Assign SpringArm class variables.
    SpringArmComp->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, 50.0f), FRotator(-60.0f, 0.0f, 0.0f));
    SpringArmComp->TargetArmLength = 400.f;
    SpringArmComp->bEnableCameraLag = true;
    SpringArmComp->CameraLagSpeed = 3.0f;

    This creates a SceneComponent as the root of our Component List Hierarchy, then creates and attaches a StaticMeshComponent to it. Afterwards, both a SpringArmComponent and a Camera Component is created and the Camera Component is attached to the end of the Spring ArmComponent's socket length. The Spring Arm is set to a default pitch of -60 degrees and a position of 50 units above the root. We also establish a few values specific to the SpringArmComponent class that will determine its length and the smoothness of its motion.

  8. Finally, we will set the default Player Controller to possess our Pawn from the constructor.

    //Take control of the default Player
    AutoPossessPlayer = EAutoReceiveInput::Player0;

We now have a simple Pawn that will allow us to control our camera comfortably. Next, we'll configure our input in the Unreal Engine editor and create code that reacts to it.

Work-In-Progress Code

PawnWithCamera.h

// Copyright 1998-2021 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "PawnWithCamera.generated.h"

UCLASS()
class HOWTO_PLAYERCAMERA_API APawnWithCamera : public APawn
{
    GENERATED_BODY()

public:
    // Sets default values for this pawn's properties
    APawnWithCamera();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    // Called every frame
    virtual void Tick( float DeltaSeconds ) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;

protected:
    UPROPERTY(EditAnywhere)
    class USpringArmComponent* SpringArmComp;

    UPROPERTY(EditAnywhere)
    class UCameraComponent* CameraComp;

    UPROPERTY(EditAnywhere)
    UStaticMeshComponent* StaticMeshComp;

};

PawnWithCamera.cpp

// Copyright 1998-2021 Epic Games, Inc. All Rights Reserved.

#include "HowTo_PlayerCamera.h"
#include "PawnWithCamera.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"

// Sets default values
APawnWithCamera::APawnWithCamera()
{
    // Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    //Create our components
    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootSceneComponent"));
    StaticMeshComp = CreateDefaultSubobject <UStaticMeshComponent>(TEXT("StaticMeshComponent"));
    SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComponent"));
    CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));

    //Attach our components
    StaticMeshComp->SetupAttachment(RootComponent);
    SpringArmComp->SetupAttachment(StaticMeshComp);
    CameraComp->SetupAttachment(SpringArmComp,USpringArmComponent::SocketName);

    //Assign SpringArm class variables.
    SpringArmComp->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, 50.0f), FRotator(-60.0f, 0.0f, 0.0f));
    SpringArmComp->TargetArmLength = 400.f;
    SpringArmComp->bEnableCameraLag = true;
    SpringArmComp->CameraLagSpeed = 3.0f;

    //Take control of the default Player
    AutoPossessPlayer = EAutoReceiveInput::Player0;
}

// Called when the game starts or when spawned
void APawnWithCamera::BeginPlay()
{
    Super::BeginPlay();

}

// Called every frame
void APawnWithCamera::Tick( float DeltaTime )
{
    Super::Tick( DeltaTime );

}

// Called to bind functionality to input
void APawnWithCamera::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
    Super::SetupPlayerInputComponent(InputComponent);

}

2. Configure Input to Control the Camera

You will need to decide the input keys that control your camera and pawn, then set up the inputs accordingly. For this project sample, let us allow our follow distance to shorten and field of view to zoom in when the right mouse button is held down.

  1. Let's also control our viewing angle with the mouse, and our Pawn 's movement with the WASD keys. To do this, we'll open the Project Settings from the Edit dropdown menu in the Unreal Engine editor.

    EditProjectSettings.png

  2. From the Project Settings window, navigate to Engine>Input>Bindings . From here, click the + signs next to Action and Axis mappings to add a new key map.

    InputNavigation.png

  3. You will want to define the following input Action Mapping and Axis Mapping , which will be used to control our input logic.

    Action Mapping:

    ZoomIn

    Right Mouse Button

    Axis Mapping:

    MoveForward

    W

    1.0

    S

    -1.0

    MoveRight

    A

    -1.0

    D

    1.0

    CameraPitch

    MouseY

    1.0

    CameraYaw

    MouseX

    1.0

    InputConfig.png

    Player Input and Pawns

Now that you've defined your inputs, return to Visual Studio to proceed onward to the next steps.

3. Write C++ Code to React to Input

You will write the code necessary to utilize your input mappings by setting up member variables to store the input data you will receive. During the Event Tick Update, you will need to know the values of your movement and mouse-looking axes, each of which are two-dimensional vectors.

You will also want to know whether you should be moving toward a zoomed-in or zoomed-out camera view, and how far between those two states your camera currently is.

  1. Begin by adding the following code to your protected namespace in the class definition of PawnWithCamera.h:

    //Input variables
    FVector2D MovementInput;
    FVector2D CameraInput;
    float ZoomFactor;
    bool bZoomingIn;
  2. You will need to create functions to track your input, begin by adding the following to your class definition in PawnWithCamera.h:

    //Input functions
    void MoveForward(float AxisValue);
    void MoveRight(float AxisValue);
    void PitchCamera(float AxisValue);
    void YawCamera(float AxisValue);
    void ZoomIn();
    void ZoomOut();
  3. Now you will want to navigate to your PawnWithCamera.Cpp file, where you will begin to implement your function logic by adding the following code:

    //Input functions
    void APawnWithCamera::MoveForward(float AxisValue)
    {
        MovementInput.X = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
    }
    
    void APawnWithCamera::MoveRight(float AxisValue)
    {
        MovementInput.Y = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
    }
    
    void APawnWithCamera::PitchCamera(float AxisValue)
    {
        CameraInput.Y = AxisValue;
    }
    
    void APawnWithCamera::YawCamera(float AxisValue)
    {
        CameraInput.X = AxisValue;
    }
    
    void APawnWithCamera::ZoomIn()
    {
        bZoomingIn = true;
    }
    
    void APawnWithCamera::ZoomOut()
    {
        bZoomingIn = false;
    }

    Pawn 's Tick

  4. Now that you have created the code necessary to store your input data, the next step is to tell the engine when to call your code. Binding functions to input events for Pawns can be accomplished by adding code to APawnWithCamera::SetupPlayerInputComponent method, as follows:

    //Hook up events for "ZoomIn"
    InputComponent->BindAction("ZoomIn", IE_Pressed, this, &APawnWithCamera::ZoomIn);
    InputComponent->BindAction("ZoomIn", IE_Released, this, &APawnWithCamera::ZoomOut);
    
    //Hook up every-frame handling for our four axes
    InputComponent->BindAxis("MoveForward", this, &APawnWithCamera::MoveForward);
    InputComponent->BindAxis("MoveRight", this, &APawnWithCamera::MoveRight);
    InputComponent->BindAxis("CameraPitch", this, &APawnWithCamera::PitchCamera);
    InputComponent->BindAxis("CameraYaw", this, &APawnWithCamera::YawCamera);
  5. Finally, you can use those values in your Tick function to update your Pawn and Camera each frame. The following code blocks should all be added to APawnWithCamera::Tick in PawnWithCamera.cpp:

    //Zoom in if ZoomIn button is down, zoom back out if it's not
    {
        if (bZoomingIn)
        {
            ZoomFactor += DeltaTime / 0.5f;         //Zoom in over half a second
        }
        else
        {
            ZoomFactor -= DeltaTime / 0.25f;        //Zoom out over a quarter of a second
        }
        ZoomFactor = FMath::Clamp<float>(ZoomFactor, 0.0f, 1.0f);
    
        //Blend our camera's FOV and our SpringArm's length based on ZoomFactor
        CameraComp->FieldOfView = FMath::Lerp<float>(90.0f, 60.0f, ZoomFactor);
        SpringArmComp->TargetArmLength = FMath::Lerp<float>(400.0f, 300.0f, ZoomFactor);
    }

    UPROPERTY(EditAnywhere)

    //Rotate our actor's yaw, which will turn our camera because we're attached to it
    {
        FRotator NewRotation = GetActorRotation();
        NewRotation.Yaw += CameraInput.X;
        SetActorRotation(NewRotation);
    }
    
    //Rotate our camera's pitch, but limit it so we're always looking downward
    {
        FRotator NewRotation = OurCameraSpringArm->GetComponentRotation();
        NewRotation.Pitch = FMath::Clamp(NewRotation.Pitch + CameraInput.Y, -80.0f, -15.0f);
        SpringArmComp->SetWorldRotation(NewRotation);
    }

    This block of code rotates our Pawn's yaw directly with the mouse's X axis, but only the camera system responds to the pitch changes from the mouse's Y axis. Rotating any Actor or Actor subclass actually rotates the root-level Component , which indirectly affects everything attached to it.

    //Handle movement based on our "MoveX" and "MoveY" axes
    {
        if (!MovementInput.IsZero())
        {
            //Scale our movement input axis values by 100 units per second
            MovementInput = MovementInput.SafeNormal() * 100.0f;
            FVector NewLocation = GetActorLocation();
            NewLocation += GetActorForwardVector() * MovementInput.X * DeltaTime;
            NewLocation += GetActorRightVector() * MovementInput.Y * DeltaTime;
            SetActorLocation(NewLocation);
        }
    }

    Using GetActorForwardVector and GetActorRightVector allows us to move relative to the direction the actor is facing. Since the camera faces the same way as the actor, this ensures that our forward key is always forward relative to what the player sees.

  6. Congratulations! We have finished coding. You can now compile your code and drag an instance of your new class from the Content Browser into the Level Editor window inside the Unreal Engine editor.

    ClassInContentBrowser.png

    Feel free to add a Static Mesh or other visual Component, or play without one. You should feel a smooth acceleration and deceleration to your camera's movement as it follows you around the level, but your rotation should feel tight and instantaneous. Try changing some of the properties on the SpringArmComponent to see how they affect the feel of your controls, such as adding "Camera Rotation Lag" or increasing or decreasing Camera Lag.

    SpringArmValues.png

  7. The Completed functionality of the code provided should look as it does below:

    Camera Pitch/Yaw

    Camera.gif

    Move Forward/Right

    Move.gif

    Zoom In/Out

    Zoom.gif

Finished Code

PawnWithCamera.h

// Copyright 1998-2021 Epic Games, Inc. All Rights Reserved.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Pawn.h"
#include "PawnWithCamera.generated.h"

UCLASS()
class HOWTO_PLAYERCAMERA_API APawnWithCamera : public APawn
{
    GENERATED_BODY()

public:
    // Sets default values for this pawn's properties
    APawnWithCamera();

protected:
    // Called when the game starts or when spawned
    virtual void BeginPlay() override;

public:
    // Called every frame
    virtual void Tick( float DeltaSeconds ) override;

    // Called to bind functionality to input
    virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override;

protected:

    UPROPERTY(EditAnywhere)
    class USpringArmComponent* OurCameraSpringArm;

    UPROPERTY(EditAnywhere)
    class UCameraComponent* OurCamera;

    UPROPERTY(EditAnywhere)
    UStaticMeshComponent* StaticMeshComp;

    //Input variables
    FVector2D MovementInput;
    FVector2D CameraInput;
    float ZoomFactor;
    bool bZoomingIn;

    //Input functions
    void MoveForward(float AxisValue);
    void MoveRight(float AxisValue);
    void PitchCamera(float AxisValue);
    void YawCamera(float AxisValue);
    void ZoomIn();
    void ZoomOut();
};

PawnWithCamera.cpp

// Copyright 1998-2021 Epic Games, Inc. All Rights Reserved.

#include "PawnWithCamera.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"

// Sets default values
APawnWithCamera::APawnWithCamera()
{
    // Set this pawn to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;

    //Create our components
    RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
    StaticMeshComp = CreateDefaultSubobject <UStaticMeshComponent>(TEXT("StaticMeshComponent"));
    SpringArmComp = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComponent"));
    CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));

    StaticMeshComp->SetupAttachment(RootComponent);
    SpringArmComp->SetupAttachment(StaticMeshComp);
    CameraComp->SetupAttachment(SpringArmComp,USpringArmComponent::SocketName);

    //Setting Default Variables and Behavior of the SpringArmComponent
    SpringArmComp->SetRelativeLocationAndRotation(FVector(0.0f, 0.0f, 50.0f), FRotator(-60.0f, 0.0f, 0.0f));
    SpringArmComp->TargetArmLength = 400.f;
    SpringArmComp->bEnableCameraLag = true;
    SpringArmComp->CameraLagSpeed = 3.0f;
    //Take control of the default Player
    AutoPossessPlayer = EAutoReceiveInput::Player0;
}

// Called when the game starts or when spawned
void APawnWithCamera::BeginPlay()
{
    Super::BeginPlay();

}

// Called every frame
void APawnWithCamera::Tick( float DeltaTime )
{
    Super::Tick(DeltaTime);

    //Zoom in if ZoomIn button is down, zoom back out if it's not
    {
        if (bZoomingIn)
        {
            ZoomFactor += DeltaTime / 0.5f;         //Zoom in over half a second
        }
        else
        {
            ZoomFactor -= DeltaTime / 0.25f;        //Zoom out over a quarter of a second
        }
        ZoomFactor = FMath::Clamp<float>(ZoomFactor, 0.0f, 1.0f);
        //Blend our camera's FOV and our SpringArm's length based on ZoomFactor
        CameraComp->FieldOfView = FMath::Lerp<float>(90.0f, 60.0f, ZoomFactor);
        SpringArmComp->TargetArmLength = FMath::Lerp<float>(400.0f, 300.0f, ZoomFactor);
    }

    //Rotate our actor's yaw, which will turn our camera because we're attached to it
    {
        FRotator NewRotation = GetActorRotation();
        NewRotation.Yaw += CameraInput.X;
        SetActorRotation(NewRotation);
    }

    //Rotate our camera's pitch, but limit it so we're always looking downward
    {
        FRotator NewRotation = SpringArmComp->GetComponentRotation();
        NewRotation.Pitch = FMath::Clamp(NewRotation.Pitch + CameraInput.Y, -80.0f, -15.0f);
        SpringArmComp->SetWorldRotation(NewRotation);
    }

    //Handle movement based on our "MoveX" and "MoveY" axes
    {
        if (!MovementInput.IsZero())
        {
            //Scale our movement input axis values by 100 units per second
            MovementInput = MovementInput.SafeNormal() * 100.0f;
            FVector NewLocation = GetActorLocation();
            NewLocation += GetActorForwardVector() * MovementInput.X * DeltaTime;
            NewLocation += GetActorRightVector() * MovementInput.Y * DeltaTime;
            SetActorLocation(NewLocation);
        }
    }
}

// Called to bind functionality to input
void APawnWithCamera::SetupPlayerInputComponent(class UInputComponent* InputComponent)
{
    Super::SetupPlayerInputComponent(InputComponent);

    //Hook up events for "ZoomIn"
    InputComponent->BindAction("ZoomIn", IE_Pressed, this, &APawnWithCamera::ZoomIn);
    InputComponent->BindAction("ZoomIn", IE_Released, this, &APawnWithCamera::ZoomOut);

    //Hook up every-frame handling for our four axes
    InputComponent->BindAxis("MoveForward", this, &APawnWithCamera::MoveForward);
    InputComponent->BindAxis("MoveRight", this, &APawnWithCamera::MoveRight);
    InputComponent->BindAxis("CameraPitch", this, &APawnWithCamera::PitchCamera);
    InputComponent->BindAxis("CameraYaw", this, &APawnWithCamera::YawCamera);
}

//Input functions
void APawnWithCamera::MoveForward(float AxisValue)
{
    MovementInput.X = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
}

void APawnWithCamera::MoveRight(float AxisValue)
{
    MovementInput.Y = FMath::Clamp<float>(AxisValue, -1.0f, 1.0f);
}

void APawnWithCamera::PitchCamera(float AxisValue)
{
    CameraInput.Y = AxisValue;
}

void APawnWithCamera::YawCamera(float AxisValue)
{
    CameraInput.X = AxisValue;
}

void APawnWithCamera::ZoomIn()
{
    bZoomingIn = true;
}

void APawnWithCamera::ZoomOut()
{
    bZoomingIn = false;
}

4. On Your Own!

Using what you have learned, try to do the following:

  • Give the player a run key to hold down that will increase the Pawn's movement speed.

  • Experiment with different ways to mix automatic and input-driven camera movement. This is a very deep area of game development with a lot of room to explore!

  • Increase, decrease, or remove the lag from your Spring Component to get a better understanding of how much lag can affect your camera's overall feel.

  • Implement a small amount of periodic motion, possibly slightly randomized or using a Curve asset, to create a handheld feel to your camera.

  • Give your Camera some amount of automatic turning, so that the camera gradually gets behind the moving player object and faces the direction the player is moving.

As for the specifics covered in this tutorial:

Help shape the future of Unreal Engine documentation! Tell us how we're doing so we can serve you better.
Take our survey
Dismiss