Udemy: Unreal Engine C The Ultimate Game Developer Course: Section 5 - jgoffeney/Cesium4Unreal GitHub Wiki

Back

The Pawn Class

A Pawn is an Actor that can be possessed and receive control input.

For this project an object Critter of type Pawn is created. Within the constructor the RootComponent is initialized as a USceneComponent. Then a UStaticMeshComponent is instantiated and attached to the root component. Then a camera component is added with location and rotation set relative to the root.

The Critter class is converted into a blueprint but when added to the scene it doesn't do anything because the player is still attached to the default scene camera. The GameModeBase class is converted into the blueprint CritterGameMode_BP and within it is the property Default Pawn Class which is use to select which Pawn the player is attached to. By setting the value to to Critter_BP it still does not work correctly. Within the Critter constructor set AutoPossessPlayer to the value of EAutoReceiveInput::Player0. Finally within the editor in Settings->World Settings->DefaultGameMode change it to CritterGameMode_BP. Note: the new pawn object does not have any built in movement so when you hit play your view is within the camera but stuck in place.

Constructor

ACritter::ACritter()
{
 	// 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;

	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
	meshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
	meshComponent->SetupAttachment(GetRootComponent());

	cameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
	cameraComponent->SetupAttachment(GetRootComponent());
	cameraComponent->SetRelativeLocation(FVector(-300, 0, 300));
	cameraComponent->SetRelativeRotation(FRotator(-45, 0, 0));

        AutoPossessPlayer = EAutoReceiveInput::Player0;
}

Pawn Movement Input

Go to Project Settings->Engine->Input->Bindings to see Action Mappings and Axis Mappings. Action Mappings define things like key presses and release. Axis Mappings are more continuous and like Tick functions to monitor input status (like 1 for pressed and 0 when not pressed) but also things like a range of values for touch sensitivity or thumb stick positions.

We are creating axis mappings and a new one is created by pressing the '+'. You can set a name and then specify the input source (keyboard) and specific input (key). This can be done multiple times. The scale value next to the input specifies how the value is applied. This means you can use the same binding but with positive and negative scales to to map forward and backward movement (or left and right).

Essentially the setting above defines a global event which can then be listened for and processed within our code. The 'W' key gets pressed and a MoveForward event is generated. It is up to the code to convert that into actual forward movement.

The Pawn function SetupPlayerInputComponent is where you take the mapping defined above and tell the class what to do with it. You have to define listener functions (regular functions to treat as delegates but really just passed as pointers).

First pass

This version has a velocity vector that is used to update the position based on the last position within Tick. The value DeltaTime is used to ensure the same amount of velocity change independent of framerate (low frame rate = high delta time and opposite is true).

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

	FVector updatedLocation = GetActorLocation() + currentVelocity * DeltaTime;

	SetActorLocation(updatedLocation);
}

// Called to bind functionality to input
void ACritter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(TEXT("MoveForward"), this, &ACritter::_moveForward);
	PlayerInputComponent->BindAxis(TEXT("MoveRight"), this, &ACritter::_moveRight);
}

void ACritter::_moveForward(float value) {
	currentVelocity.X += FMath::Clamp(value, -1.0f, 1.0f) * maxSpeed;
}

void ACritter::_moveRight(float value) {
	currentVelocity.Y += FMath::Clamp(value, -1.0f, 1.0f) * maxSpeed;
}

Second Pass

Continuing on we essentially created a new project (I used 4.26 to ensure it would work with the assets we downloaded). This time we create the class CollisionCritter and add a sphere collider. We also created getters and setters for the mesh and sphere components.

Creating the sphere component of type USphereComponent is similar to the mesh component added previously. In addition we set the radius via its method InitSphereRadius() and then give is a collision profile of Pawn via the method SetCollisionProfileName.

When setting up the mesh component its mesh type can be set in the C++ code rather than the preferred editor method. The ConstuctorHelpers::ObjectFinder type takes a template parameter to construct a new instance of the type via a file path. Then the actual object is retreived from the Object() method. Then the sphere is moved relative to the pawn origin and resized to match the collider.

NOTE: sometimes Unreal decides not to update correctly after compiling. Try to restart. This is apparently a limitation of the editor that does not handle changes to the constructor for a class on the fly. Looks like it has been around for years.

// Sets default values
AColliderCritter::AColliderCritter()
{
 	// 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;

	RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
	sphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("SphereComponent"));
	sphereComponent->SetupAttachment(GetRootComponent());
	sphereComponent->InitSphereRadius(40);
	sphereComponent->SetCollisionProfileName(TEXT("Pawn"));

	meshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
	meshComponent->SetupAttachment(GetRootComponent());
	static ConstructorHelpers::FObjectFinder<UStaticMesh> meshComponentAsset(
		TEXT("StaticMesh'/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere'"));
	if (meshComponentAsset.Succeeded()) {
		meshComponent->SetStaticMesh(meshComponentAsset.Object);
		meshComponent->SetRelativeLocation(FVector(0, 0, -40));
		meshComponent->SetWorldScale3D(FVector(0.8));
	}
}

Now rather manage the direction vectors manually we can use the GetActorForwardVector() and GetActorRightVector() to get the x/y movement components and then apply it with AddMovementInput().

// Called to bind functionality to input
void AColliderCritter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);

	PlayerInputComponent->BindAxis(TEXT("MoveForward"), this, &AColliderCritter::_moveForward);
	PlayerInputComponent->BindAxis(TEXT("MoveRight"), this, &AColliderCritter::_moveRight);
}

void AColliderCritter::_moveForward(float value) {
	FVector forwardVector = GetActorForwardVector();
	AddMovementInput(value * forwardVector);
}

void AColliderCritter::_moveRight(float value) {
	FVector rightVector = GetActorRightVector();
	AddMovementInput(value * rightVector);
}

Then to have the player automatically possess the pawn add the following to the constructor:

AutoPossessPlayer = EAutoReceiveInput::Player0;

SpringArmCamera

The next step is to create a 3rd person follow camera. A spring arm connects the pawn and the camera. The following code in the constructor instantiates the spring arm, sets its tilt and length and then attaches the camera to the end if it.

springArmComponent = CreateDefaultSubobject<USpringArmComponent>(TEXT("SpringArmComponent"));
springArmComponent->SetupAttachment(GetRootComponent());
springArmComponent->SetRelativeRotation(FRotator(-45, 0, 0));     // Set arm to point down toward player
springArmComponent->TargetArmLength = 400;
springArmComponent->bEnableCameraLag = true;
springArmComponent->CameraLagSpeed = 3;
	
cameraComponent = CreateDefaultSubobject<UCameraComponent>(TEXT("CameraComponent"));
cameraComponent->SetupAttachment(springArmComponent, USpringArmComponent::SocketName);

At this point the camera/arm is setup but the pawn does not move which is addressed in the next section.

Pawn Movement Component

In this section a movement component is being created. Apparently the issue with the previous code is we call AddMovementInput() but there is nothing to do anything with it.

The first step is to create a new C++ class based on the PawnMovementComponent class. This is not a normal thing to do but is done to demonstrate how it works. Usually you would just use a Character rather than a Pawn.

First we added a PawnMovementComponent instance to CollisionCritter and then set its UpdatedComponent to the root. But then we made the sphereComponent the actual root so all movement will affect it directly.

Looking in CollisionCritter->MoveForward() and examining Pawn::AddMovementInput() you can see the function will update the movement component if a movement component exists. For a Pawn it does not by default but that is why we are making one.

The general idea for a PawnMovementComponent is it has a version of the tick function. We use the AddInputVector() function in CollisionCritter which basically queue up movements and then within the tick of PawnMovementComponent we can apply it.

Pawn Camera Rotation

The next step is to extend the movement of the CollisionCritter to include moving the camera via the mouse.

Add Axis Mapping

Go to Project Settings->Engine->Input->AxisMappings. Add two axis mappings of CameraPitch and CameraYaw mapped to mouse Y and mouse X, respectively.

Within ColliderCriter create a FVector2D to store the values for the CameraPitch and CameraYaw as Y and X respectively. Now in Tick() get the actor rotation add the yaw value and then set te actor rotation to turn the critter with the mouse. For the pitch instead get the spring arm pitch, add the mouse pitch, clamp the values to prevent sliding under the critter and then set the spring arm's world rotation. This rotates the world rather than the camera giving you that chase camera feel.

// Called every frame
void AColliderCritter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	FRotator newRotation = GetActorRotation();
	newRotation.Yaw += _cameraInput.X;
	SetActorRotation(newRotation);

	FRotator springArmRotation = springArmComponent->GetComponentRotation();
	springArmRotation.Pitch = FMath::Clamp(springArmRotation.Pitch +=_cameraInput.Y, -80.f, -15.f);
	springArmComponent->SetWorldRotation(springArmRotation);

}

Environmental Assets

The final section downloads the Sun Temple project from the Unreal Engine Learn section. By opening the project and selecting the Content->Maps folder and selecting the Migrate options from the right-click menu you can select the Content folder in another project and import it into that other project.

By going into Project Settings->Maps and Modes the Sun Temple map can be set as the default map for the editor and game.