Part 1 Part 2

Creating our Pawn

We open the “New C++ Class…” dialog window again (right-click inside the “Content Browser”). This time we will inherit from the “DefaultPawn”.

After the solution has been compiled we now have the empty class declaration for our pawn. For now we will set our pawn as the default pawn in our game mode. To see that our pawn is used we log an on-screen message when our pawn is spawned.

GameModeBase.h

// we include our DefaultPawn
#include "TestDefaultPawn.h"

ATestProjectGameModeBase::ATestProjectGameModeBase()
{
    // use our custom PlayerController class
    PlayerControllerClass = ATestPlayerController::StaticClass();

    // use our DefaultPawn class
    DefaultPawnClass = ATestDefaultPawn::StaticClass();
}

DefaultPawn.h

/**
 * The DefaultPawn for the Test Project
 */
UCLASS()
class TESTPROJECT_API ATestDefaultPawn : public ADefaultPawn
{
    GENERATED_BODY()

        virtual void BeginPlay() override;
};

DefaultPawn.cpp

void ATestDefaultPawn::BeginPlay()
{
    GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Test Pawn was spawned"));
}

GEngine gives us a global pointer to the engine and to add an on screen debug message we use the handy AddOnScreenDebugMessage function. If we hit “Compile” and “Play” now we get our message in the top left corner. Everything else is the same since we have only added the log message. The next part is to handle our own input.

Adding and Receiving Input Events

To see the set input events we have to go to “Edit” -> “Project Settings…” and then to “Engine” / “Input”. There we have our input bindings. There are Action and Axis mappings. An Action is like a button and an axis has a positive and negative input. Click the plus sign to create a new mapping. We create a “ToggleRotation” action on space and the bottom Face Button for gamepads (A on an XBox gamepad and X on a Playstation one). As you can see we can use these bindings to add gamepad support and alternative keys. We also add Axes for forward and sideways movement. There you can use the scale parameter to use two keyboard keys as the positive and negative. That is not necessary for gamepads since we can map the axes directly. We do the same for our mouse look controls.

The next step is to receive the input events in our code. We can receive these in our player controller and our pawn. The events in the player controller are received as long as it exists whereas the events in the pawn are only received as long as it is possessed by the player controller. That way you can express different behaviors for the same input depending on the possessed pawn. For example if your regular pawn is a person but you want to implement vehicles that can be driven. A vehicle is another pawn that is possessed when the vehicle is entered and now it can receive the input events instead of the person.

Here we use our “ToggleRotation” action to toggle the furniture rotation from part 2 in our player controller. The axes for the pawn movement are handled in our pawn.

PlayerController.h

UCLASS()
class TESTPROJECT_API ATestPlayerController : public APlayerController
{
    GENERATED_BODY()

public:

    ATestPlayerController();

    // we override the BeginPlay function which is called once the PlayerController has been spawned in the scene and is ready
    virtual void BeginPlay() override;

    // we override the Tick function which is called every frame
    virtual void Tick(float deltaTime) override;

    // we override the override the function to add our action binding
    virtual void SetupInputComponent() override;

private:

    // toggle the rotation bool
    void OnToggleRotaion();

    // we use an array( the Unreal version of a dynamic array, like std::vector) to hold the pointers to the static meshes in the world
    UPROPERTY()
    TArray<class AActor*> m_actors;

    FName m_rotationTag;
    float m_rotationSpeed;

    bool m_shouldRotate;
};

We override the APlayerController::SetupInputComponent function to bind our action.

PlayerController.cpp

ATestPlayerController::ATestPlayerController()
    : m_rotationSpeed(100.f)
    , m_rotationTag(TEXT("Rotate"))
    , m_shouldRotate(true)
{
    // we want the Tuck function to be called for our PlayerController
    PrimaryActorTick.bCanEverTick = true;

    // we reserve some memory in our array
    m_actors.Reserve(10);
}

void ATestPlayerController::Tick(float deltaTime)
{
    // we also call the tick for the parent class
    Super::Tick(deltaTime);

    // check whether we should rotate the actors
    if (!m_shouldRotate)
    {
        return;
    }

    // we prepare our delta rotator
    const FRotator rotation(0.0f, m_rotationSpeed * deltaTime, 0.0f);

    // now we iterate through the actors and add a rotation
    const int32 numActors = m_actors.Num();
    for (int32 i = 0; i < numActors; ++i)
    {
        // we add a rotation in world space
        m_actors[i]->AddActorWorldRotation(rotation);
    }
}

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

    InputComponent->BindAction(TEXT("ToggleRotation"), IE_Pressed, this, &ATestPlayerController::OnToggleRotaion);
}

void ATestPlayerController::OnToggleRotaion()
{
    m_shouldRotate = !m_shouldRotate;
}

We add our rotation bool and bind the action to our function. The IE_Pressed is used to decide when the function should be called. There a a couple different options available.

We can now toggle the rotating furniture with space and the bottom face button, if a gamepad is connected.

Pawn Movement

We will mostly replicate the default behavior to get a feeling for the camera and movement controls.

DefaultPawn.h

UCLASS()
class TESTPROJECT_API ATestDefaultPawn : public ADefaultPawn
{
    GENERATED_BODY()

public:

        ATestDefaultPawn();

        virtual void BeginPlay() override;

        // we override the SetupPlayerInputComponent which is called when the pawn is possessed 
        virtual void SetupPlayerInputComponent(UInputComponent* input) override;

private:

    // move the pawn forward
    void OnMoveForward(float value);

    // move the pawn right
    void OnMoveRight(float value);

    // look up
    void OnLookUp(float value);

    // look right
    void OnLookRight(float value);

    float m_horizontalCamSpeed;
    float m_verticalCamSpeed;
};

We overload the SetupPlayerInputComponent function to add our input bindings. This function is called after the pawn has been possessed by a player controller. A UInputComponent is created every time the pawn is possessed with CreatePlayerInputComponent/. SetupPlayerInputComponent is called afterward. APawn::DestroyPlayerInputComponent is called when the pawn is un-possessed. Each of these functions can be overridden.

DefaultPawn.cpp

ATestDefaultPawn::ATestDefaultPawn()
    : m_horizontalCamSpeed(30.0f)
    , m_verticalCamSpeed(-30.0f)
{}

void ATestDefaultPawn::SetupPlayerInputComponent(UInputComponent* input)
{
    checkf(input != nullptr, TEXT("The input component for the default pawn was not created"));

    // bind movement axes
    input->BindAxis(TEXT("Forward"), this, &ATestDefaultPawn::OnMoveForward);
    input->BindAxis(TEXT("Right"), this, &ATestDefaultPawn::OnMoveRight);

    // bind camera axes
    input->BindAxis(TEXT("LookUp"), this, &ATestDefaultPawn::OnLookUp);
    input->BindAxis(TEXT("LookRight"), this, &ATestDefaultPawn::OnLookRight);
}

void ATestDefaultPawn::OnMoveForward(float value)
{
    if (value != 0.0f)
    {
        // we move along the controller view direction
        const FRotator rot = GetControlRotation();
        const FVector dir = FRotationMatrix(rot).GetScaledAxis(EAxis::X);

        AddMovementInput(dir, value);
    }
}

void ATestDefaultPawn::OnMoveRight(float value)
{
    if (value != 0.0f)
    {
        // we move perpendicular to the controller view direction
        const FRotator rot = GetControlRotation();
        const FVector dir = FRotationMatrix(rot).GetScaledAxis(EAxis::Y);

        AddMovementInput(dir, value);
    }
}

void ATestDefaultPawn::OnLookUp(float value)
{
    if (value != 0.0f)
    {
        // wo rotate the camera to look up or down
        float deltaTime = GetWorld()->GetDeltaSeconds();
        AddControllerPitchInput(m_verticalCamSpeed * value * deltaTime);
    }
}

void ATestDefaultPawn::OnLookRight(float value)
{
    if (value != 0.0f)
    {
        // we rotate the camera to look left or right
        float deltaTime = GetWorld()->GetDeltaSeconds();
        AddControllerYawInput(m_horizontalCamSpeed * value * deltaTime);
    }
}

We only override the setup function to add our bindings. Notice the AddMovementInput function to move the pawn. This uses the MovementComponent and its settings, which also means that any speed or acceleration settings have to be done on it. It also uses the delta time for a frame independent speed and normalizes the overall direction per frame to avoid the old classic where diagonal movement is about 1.4 times faster. You can of course perform your own movement calculations and just use SetActorLocation and SetActorRotation.

We don’t have a handy component for the camera movement so we just add a relative pitch and yaw rotation. The camera speed variables can also be used to invert the camera horizontally or vertically if we just negate them. The deltaTime is very important or the camera controls would be frame-rate dependent.

Handling multiple Pawns

I have mentioned possessing pawns multiple times so lets use it to jump between two instances of the pawn we just created. First of we have to add the other pawn to the map. Click the folder icon in the “Content Browser” to switch from the regular assets to our C++ classes.

Click drag the pawn class into the viewport to add it to the map.

Now we have the pawn in our map. You might have wondered what decides where the initial pawn for the player controller is spawned and that is the “Player Start” in the map. You can move it arround in the viewport if you want to change the starting location.

We want to switch pawns with tab so we add the input event in the settings.

PlayerController.h

    // switch to the pawn under the cursor
    void OnSwitchPawn();

    class ATestDefaultPawn* m_otherPawn;

We just add a new function for the PawnSwitch and a member to hold the other pawn.

PlayerController.cpp

ATestPlayerController::ATestPlayerController()
    : m_rotationSpeed(100.f)
    , m_rotationTag(TEXT("Rotate"))
    , m_shouldRotate(true)
    , m_otherPawn(nullptr)
{
    // we want the Tick function to be called for our PlayerController
    PrimaryActorTick.bCanEverTick = true;

    // we reserve some memory in our array
    m_actors.Reserve(10);
}

void ATestPlayerController::BeginPlay()
{
    // we also call the BeginPlay for the parent class
    Super::BeginPlay();

    // we get every actor with our rotate tag
    UGameplayStatics::GetAllActorsWithTag(GetWorld(), m_rotationTag, m_actors);

    // we iterate over the two pawns in the scene and choose the one that we are not controlling
    const ATestDefaultPawn* currPawn = Cast<ATestDefaultPawn>(GetPawn());
    TActorIterator<ATestDefaultPawn> itr(GetWorld());
    for (; itr; ++itr)
    {
        ATestDefaultPawn* pawn = *itr;
        if(pawn != currPawn)
        {
            m_otherPawn = pawn;
        }
    }

    // generally display is the better log level but we use warning for now because it is highlighted in the console output
    UE_LOG(LogTemp, Warning, TEXT("We found %d Actors with the %s tag"), m_actors.Num(), *(m_rotationTag.ToString()))
}

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

    InputComponent->BindAction(TEXT("ToggleRotation"), IE_Pressed, this, &ATestPlayerController::OnToggleRotaion);
    InputComponent->BindAction(TEXT("SwitchPawn"), IE_Pressed, this, &ATestPlayerController::OnSwitchPawn);
}

void ATestPlayerController::OnSwitchPawn()
{
    // we remember our current pawn
    ATestDefaultPawn* pawn = Cast<ATestDefaultPawn>(GetPawn());

    // the possess function automatically calls and UnPossess before actually possessing the pawn so we don't have to do it
    Possess(m_otherPawn);
    m_otherPawn = pawn;
}

We use the actor iterator to parse the 2 pawns in the scene and remember the one we are not controlling. The actual switch is done with the Possess function. The Cast function is the Unreal wrapper for a dynamic_cast. We can now jump between both pawns with tab. And only the pawn we are controlling is movable with W,A,S,D so the pawn input components also works like we wanted. We can’t see the pawn we are currently controlling since the camera is inside the mesh and all inward facing triangles are culled.

Next Time

The next part will deal with the UI and how to communicate between C++ and blueprints. I will probably interject a separate post on how to debug in Visual Studio with Unreal since I haven’t gotten to that yet.

comments powered by Disqus