We have a design for our game and an Unreal project set up for our game. It's now time to dive into the actual game code.
In this chapter, we'll be making a game character that moves around the world, defining our game data, and prototyping a basic combat system for the game. We will cover the following topics in this chapter:
This particular chapter is the most C++ heavy portion of the book, and provides the basic framework that the rest of the book is going to use. Because this chapter provides much of the backend of our game, the code in this chapter must work to completion before moving on to the rest of the content in the book. If you bought this book because you are a programmer looking for more background in creating a framework for an RPG, this chapter is for you! If you bought this book because you are a designer, and care more about building upon the framework rather than programming it from scratch, you will probably be more into the upcoming chapters because those chapters contain less C++ and more UMG and Blueprints. No matter who you are, it is a good idea to download the source code provided through the directions located in the preface of the book in case you get stuck or would like to skip chapters based on your interests.
The very first thing we are going to do is create a new Pawn class. In Unreal, the Pawn is the representation of a character. It handles the movement, physics, and rendering of a character.
Here's how our character pawn is going to work. The player is divided into two parts: there's the Pawn that, as mentioned earlier, is responsible for handling the movement, physics, and rendering. Then there's the Player Controller, responsible for translating the player's input into making the Pawn perform what the player wants.
Now, let's create the actual pawn.
Create a new C++ class and select Character
as the parent class for it. We will be deriving this class from the Character
class because Character
has a lot of built-in move functions that we can use for our field player. Name the class RPGCharacter
. Open RPGCharacter.h
and change the class definition using the following code:
UCLASS(config = Game) class ARPGCharacter : public ACharacter { GENERATED_BODY() /** Camera boom positioning the camera behind the character */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true")) class USpringArmComponent* CameraBoom; /** Follow camera */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera, meta = (AllowPrivateAccess = "true")) class UCameraComponent* FollowCamera; public: ARPGCharacter(); /**Base turn rate, in deg/sec. Other scaling may affect final turn rate.*/ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Camera) float BaseTurnRate; protected: /** Called for forwards/backward input */ void MoveForward(float Value); /** Called for side to side input */ void MoveRight(float Value); /** * Called via input to turn at a given rate. * @param Rate This is a normalized rate, i.e. 1.0 means 100% of desired turn rate */ void TurnAtRate(float Rate); protected: // APawn interface virtual void SetupPlayerInputComponent(class UInputComponent* InputComponent) override; // End of APawn interface public: /** Returns CameraBoom subobject **/ FORCEINLINE class USpringArmComponent* GetCameraBoom() const { return CameraBoom; } /** Returns FollowCamera subobject **/ FORCEINLINE class UCameraComponent* GetFollowCamera() const { return FollowCamera; } };
Next, open RPGCharacter.cpp
and replace it with the following code:
#include "RPG.h" #include "RPGCharacter.h" ARPGCharacter::ARPGCharacter() { // Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); // set our turn rates for input BaseTurnRate = 45.f; // Don't rotate when the controller rotates. //Let that just affect the camera. bUseControllerRotationPitch = false; bUseControllerRotationYaw = false; bUseControllerRotationRoll = false; // Configure character movement // Character moves in the direction of input... GetCharacterMovement()->bOrientRotationToMovement = true; // ...at this rotation rate GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // Create a camera boom CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom")); CameraBoom->SetupAttachment(RootComponent); // The camera follows at this distance behind the character CameraBoom->TargetArmLength = 300.0f; CameraBoom->RelativeLocation = FVector(0.f, 0.f, 500.f);// Rotate the arm based on the controller CameraBoom->bUsePawnControlRotation = true; // Create a follow camera FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera")); FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Camera does not rotate relative to arm FollowCamera->bUsePawnControlRotation = false; FollowCamera->RelativeRotation = FRotator(-45.f, 0.f, 0.f); /* Note: The skeletal mesh and anim blueprint references on the Mesh component (inherited from Character) are set in the derived blueprint asset named MyCharacter (to avoid direct content references in C++)*/ } ////////////////////////////////////////////////////////////////// // Input void ARPGCharacter::SetupPlayerInputComponent(class UInputComponent* InputComponent) { // Set up gameplay key bindings check(InputComponent); InputComponent->BindAxis("MoveForward", this, &ARPGCharacter::MoveForward); InputComponent->BindAxis("MoveRight", this, &ARPGCharacter::MoveRight); /* We have 2 versions of the rotation bindings to handle different kinds of devices differently "turn" handles devices that provide an absolute delta, such as a mouse. "turnrate" is for devices that we choose to treat as a rate of change, such as an analog joystick*/ InputComponent->BindAxis("Turn", this, &APawn::AddControllerYawInput); InputComponent->BindAxis("TurnRate", this, &ARPGCharacter::TurnAtRate); } void ARPGCharacter::TurnAtRate(float Rate) { // calculate delta for this frame from the rate information AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds()); } void ARPGCharacter::MoveForward(float Value) { if ((Controller != NULL) && (Value != 0.0f)) { // find out which way is forward const FRotator Rotation = Controller->GetControlRotation(); const FRotator YawRotation(0, Rotation.Yaw, 0); // get forward vector const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X); AddMovementInput(Direction, Value); } } void ARPGCharacter::MoveRight(float Value) { if ((Controller != NULL) && (Value != 0.0f)) { // find out which way is right const FRotator Rotation = Controller->GetControlRotation(); const FRotator YawRotation(0, Rotation.Yaw, 0); // get right vector const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y); // add movement in that direction AddMovementInput(Direction, Value); } }
If you have ever created and worked with the C++ ThirdPerson game template, you will notice that we are not reinventing the wheel here. The RPGCharacter
class should look familiar because it is a modified version of the ThirdPerson Character
class code given to us when we create a C++ ThirdPerson template, provided for us to use by Epic Games.
Since we are not creating a fast-paced action game and are simply using the Pawn as an RPG character to maneuver out in the field, we eliminated mechanics that are often associated with action games, such as jumping. But we kept in the code that is important to us, which is the ability to move forward, backward, left, and right; rotational behaviors of the pawn; a camera that will follow the character around in an isometric view; a collision capsule for the character to be able to collide with collidable objects; configuration for character movement; and a camera boom, which will allow the camera to move closer to the character in case it runs into collisions such as a wall or other meshes, important for not blocking a player's view. If you want to edit the character mechanics, feel free to do so by following the comments in the code to change the values of some specific mechanics such as TargetArmLength
to change the distance of the camera from the player, or adding jumping, which can be seen in the ThirdPerson character template that came with the Engine.
Because we derived the RPGCharacter
class from the Character
class, its default camera is not rotated for an isometric view; instead, the camera rotations and locations are zeroed out by default to the pawn's location. So what we did was add a CameraBoom
relative location in RPGCharacter.cpp
(CameraBoom->RelativeLocation = FVector(0.f, 0.f, 500.f);
); this offsets the camera 500 units up on the z axis. Along with rotating the camera that follows the player -45 units on the pitch (FollowCamera->RelativeRotation = FRotator(-45.f, 0.f, 0.f);
), we get a traditional isometric view. If you would like to edit these values to customize your camera even more, it is suggested; for instance, if you still think your camera is too close to the player, you can simply change the relative location of CameraBoom
on the z axis to a value higher than 500 units, and/or adjust TargetArmLength
to something larger than 300.
Lastly, if you take a look at the MoveForward
and MoveRight
movement functions, you will notice that no movement is being added to the pawn unless the value that is passed to MoveForward
or MoveRight
is not equal to 0. Later on in this chapter, we will bind keys W, A, S, and D to these functions and set each one of these inputs to pass a scalar of 1 or -1 to the corresponding movement function as values. This 1 or -1 value is then used as a multiplier to the direction of the pawn, which will then allow the player to move in a specific direction based on its walk speed. For instance, if we set W as a keybind to MoveForward
with a scalar of 1, and S as a keybind to MoveFoward
with a scalar of -1, when the player presses W, the value in the MoveFoward
function will be equal to 1 and cause the pawn to move in the positive forward direction as a result. Alternatively, if the player presses the S key, -1 is then passed into the value used by the MoveForward
function, which will cause the pawn to move in the negative forward direction (in other words, backwards). Similar logic can be said about the MoveRight
function, which is why we don't have a MoveLeft
function—simply because pressing the A key would cause the player to move in the negative right direction, which is in fact left.
Now, in order to use this pawn as a player character, we need to set up a new game mode class. This game mode will then specify the default pawn and player controller classes to use. We'll also be able to make a Blueprint of the game mode and override these defaults.
Create a new class and choose GameMode
as the parent class. Name this new class RPGGameMode
(if RPGGameMode
already exists in your project, simply navigate to your C++ source directory and proceed to open up RPGGameMode.h
, as listed in the next step).
Open RPGGameMode.h
and change the class definition using the following code:
UCLASS() class RPG_API ARPGGameMode : public AGameMode { GENERATED_BODY() ARPGGameMode( const class FObjectInitializer& ObjectInitializer ); };
Just as we've done before, we're just defining a constructor for the CPP file to implement.
We're going to implement that constructor now in RPGGameMode.cpp
:
#include "RPGCharacter.h" ARPGGameMode::ARPGGameMode( const class FObjectInitializer& ObjectInitializer ) : Super( ObjectInitializer ) { DefaultPawnClass = ARPGCharacter::StaticClass(); }
Here, we include the RPGCharacter.h
file so that we can reference these classes. Then, in the constructor, we set the class as the default class to use for the Pawn.
Now, if you compile this code, you should be able to assign your new game mode class as the default game mode. To do this, go to Edit | Project Settings, find the Default Modes box, expand the Default GameMode drop-down menu, and select RPGGameMode.
However, we don't necessarily want to use this class directly. Instead, if we make a Blueprint, we can expose the properties of the game mode that can be modified in the Blueprint.
So, let's make a new Blueprint Class in Content | Blueprints, pick RPGGameMode
as its parent class, and call it DefaultRPGGameMode
:
If you open the Blueprint and navigate to the Defaults tab, you can modify the settings for the game mode for Default Pawn Class, HUD Class, Player Controller Class, and more settings:
However, we still have one extra step before we can test our new Pawn. If you run the game, you will not see the Pawn at all. In fact, it will appear as if nothing is happening. We need to give our Pawn a skinned mesh and also make the camera follow the pawn.
For now, we're just going to import the prototype character that comes with the ThirdPerson sample. To do this, make a new project based on the ThirdPerson sample. Locate the ThirdPersonCharacter
Blueprint class in Content | ThirdPersonCPP | Blueprints and migrate it to the Content
folder of your RPG project by right-clicking on the ThirdPersonCharacter
Blueprint class and navigating to Asset Actions | Migrate…. This action should copy ThirdPersonCharacter
with all its assets into your RPG project:
Now, let's create a new Blueprint for our Pawn. Create a new Blueprint class and select RPGCharacter as the parent class. Name it FieldPlayer.
Expand Mesh located in the Details tab when selecting the Mesh component from the Components tab and choose SK_Mannequin as the skeletal mesh for the pawn. Next, expand Animation and choose ThirdPerson_AnimBP as the animation Blueprint to use. You will most likely need to move your character's mesh down the z axis so that the bottom of the character's feet meet the bottom of the collision capsule. Also be sure that the character mesh is facing the same direction that the blue arrow in the component is facing. You may need to rotate the character on the z axis as well to ensure that the character is facing the right direction:
Finally, open your game mode Blueprint and change the pawn to your new FieldPlayer Blueprint.
Now, our character will be visible, but we may not be able to move it yet because we have not bound keys to any of our movement variables. To do so, go into Project Settings and locate Input. Expand Bindings and then expand Axis Mappings. Add an axis mapping by pressing the + button. Call the first axis mapping MoveRight, which should match the MoveRight
movement variable you created earlier in this chapter. Add two key bindings for MoveRight by pressing the + button. Let one of those keys be A with a scale of -1 and other be D with a scale of 1. Add another axis mapping for MoveForward; only this time, have a W key binding with a scale of 1 and an S key binding with a scale of -1:
Once you play test, you should see your character moving and animating using the W, A, S, and D keys you bound to the player.
When you run the game, the camera should track the player in an overhead view. Now that we have a character that can explore the game world, let's take a look at defining characters and party members.
18.118.12.50