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.
// we include our DefaultPawn
#include "TestDefaultPawn.h"
ATestProjectGameModeBase::ATestProjectGameModeBase()
{
// use our custom PlayerController class
PlayerControllerClass = ATestPlayerController::StaticClass();
// use our DefaultPawn class
DefaultPawnClass = ATestDefaultPawn::StaticClass();
}
/**
* The DefaultPawn for the Test Project
*/
UCLASS()
class TESTPROJECT_API ATestDefaultPawn : public ADefaultPawn
{
GENERATED_BODY()
virtual void BeginPlay() override;
};
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.
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.
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.
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.
We will mostly replicate the default behavior to get a feeling for the camera and movement controls.
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.
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.
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.
// 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.
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.
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