
The Gladiator source (.cpp) file
Because source files always contain more than 20x more code than the header files, I would like to follow a different approach here in explaining the code. I will break down the source file into blocks, one by one.
The includes
As we mentioned earlier, any C++ file or even header file must start with the include
statements. You don't have to include everything; some of the include
statements will be there by default but others might be needed while you are building up the code.
Even if your game example is different and you wanted to have different functionalities, you might need to include more headers.
#include "Bellz.h" #include "Gladiator.h" #include "GameDataTables.h" #include "PaperSpriteComponent.h" #include "GameDataTables.h"
As you can see, now the included header files have been increased to include those we have formed from the auto-generated source file.
Because the game will be reading data from Excel sheets, I managed to import the GameDataTables
header file, so we will be able to deal and work with the data table classes. For the same reason, I managed to import the GameDataTables
header file, so we'll be able to get the data through the object instance of the data itself.
Because we've made a Sprite2D
component within the header file, I needed to be able to control this sprite at any given moment and also needed to be able to control anything related to this sprite component at the constructor while building our character. To be able to do all of that, I need to be able to access that component, which can be done through the PaperSpriteComponent
header file.
Keep in mind that, if you are not able to load this header file, it might be because Paper2D is not in your include
path, so you need to add several additional include
paths in the project settings of your game project.

The constructor
You might be familiar with the term constructor. It has exactly the same function within Unreal games. It will be execute the set of commands you enter once the object instance has been created.
AGladiator::AGladiator() { //Set size for collision capsule GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f); TotalHealth = 100.f; AttackRange = 25.f; jumppingVelocity = 600.f; //set our turn rates for input BaseTurnRate = 45.f; BaseLookUpRate = 45.f; //Don't rotate when the controller rotates. Let that just affect the camera. bUseControllerRotationPitch = false; bUseControllerRotationYaw = false; bUseControllerRotationRoll = false; //Configure character movement GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input... GetCharacterMovement()->RotationRate = FRotator(0.0f, 540.0f, 0.0f); // ...at this rotation rate GetCharacterMovement()->JumpZVelocity = jumppingVelocity; GetCharacterMovement()->AirControl = 0.2f; //Create a camera boom (pulls in towards the player if there is a collision) CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom")); CameraBoom->AttachTo(RootComponent); CameraBoom->TargetArmLength = 300.0f; // The camera follows at this distance behind the character CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller //Create a follow camera FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera")); FollowCamera->AttachTo(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm EffectSprite = CreateDefaultSubobject<UPaperSpriteComponent>(TEXT("ClawEffect")); EffectSprite->AttachTo(CameraBoom); //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++) IsStillAlive = true; IsAttacking = false; WeaponIndex = 1; //by default the inputs should be enabled, in case there is something ned to be tested OnSetPlayerController(true); }
Inside the constructor, we have to put all the logic that will be applied directly in the following three cases:
- If you switch back directly to the editor after writing the code, you should see that the changes that have been made inside the constructor have already taken place
- Once you create a blueprint based on the class, all the code within the constructor will be executed after creating the blueprint instance in the editor
- Once you spawn a blueprint based on the class at runtime (while the game is running), all the constructor logic will already have been applied (unless you changed it inside the editor)
Because of the nature of the constructor, I managed to put any default value, add any default component, or add any default objects, inside it. As you can see from the constructor logic, I have done the following, in order:
- Applied a size (width and height) to the
capsule
component of thecharacter
class. - Added a default value of
100
to the total health variable holder, and added a default value of25
to the attack range variable. - Applied a jump velocity to the character (this value is inherited from the base character class). Feel free to experiment with values, I found
400
to600
works well for me. - Applied default values for the
BaseTurnRate
and theBaseLookupRate
values, so I now have some values to control the camera when necessary to control it. The higher the values, the faster the camera moves; the lower the values, the slower the camera moves. - Then I worked on applying values to some of the default members of the character base class. Those values include:
- Setting
boolOrientRotationToMovement
to true; that way I make sure the character mesh will be rotated in the direction of the movement - Settting
RotationRate
variable to make the default rotation rate for the character - Applying the default velocity value we stored earlier to the
JumpZVelocity
variable - Adding some
AirControl
value to make sure the character behaves well
- Setting
- Then I started to work with the spring arm component called
CameraBoom
that we declared within the header file. I started by creating the component itself, to add it to a category of its own name. Then I attached it to the root of the object, added aTargetArmLength
value of300
to it, and finally I set the Boolean namedbUsePawnControlRotation
totrue
to make sure that the spring arm will be rotated based on the controller. - While I'm creating the new components and applying some parameters to them, it is time to create the camera and attach it to the player controller. After creating the camera component as
FollowCamera
and assigning it to a category, I attached it to the spring arm component (as a child) and finally disabled itsbUsePawnControlRotation
component to make sure that the camera doesn't rotate relatively to the spring arm component. - Now I have created a paper sprite component (
Paper2D
) and assigned it to theEffectSprite
component; that way I make sure it will always be rotated with the mouse/gamepad, which applies the camera rotations based on theCameraBoom
component attached to the player. - Then I set some of the variables I have to their default values, including the Boolean
IsStillAlive
to mark the player as alive at the beginning, and then I set the value of theIsAttacking
variable tofalse
, because it makes sense that, once the game starts there is definitely no attack! Finally, I set theWeaponIndex
component to the first weapon I want to be used. - Finally, we call the method,
OnSetPlayerController
and pass atrue
value to it to make sure that once the game starts, the player, by default, has control over the camera. This could be changed later based on the level, the scenario, and so on.
BeginPlay
BeginPlay
is a little different to OnConstruct
, which is the constructor executed once the object has been created, which means it is executed either once the instance has been made at runtime, or inside the editor while building the game. Beginplay
is only executed at the start of the game, or once the object is instantiated at runtime, as long as it has never been executed inside the editor.
void AGladiator::BeginPlay() { //Ask the datamanager to get all the tables datat at once and store them //AGameDataTables dataHolder; for (TActorIterator<AGameDataTables> ActorItr(GetWorld()); ActorItr; ++ActorItr) { if (ActorItr) { //print theinstance name to screen //GEngine->AddOnScreenDebugMessage(-1, 10.f, FColor::Green, ActorItr->GetName()); //Call the fetch to the tables, now we get all the datat stored. Why? simply because keep readin everytime from the table itself is going to cost over your memory //but the most safe method, is just to read all the data at once, and then keep getting whatever needed values from the storage we've . TablesInstance = *ActorItr; TablesInstance->OnFetchAllTables(); } } }
Because BeginPlay()
is the first thing to take place for the class instance once the game runs, I didn't have much to do for now except getting the current data table instance from the level so I can store it and use the data from it. Some of the data is useful for the player, such as the weapons data, and that made it important to keep an instance of it inside the player controller.
Using the TActorIterator
type was my choice, as it is the best way yet within Unreal to look for objects of an X type within the level. I used it to look for the type AGameDataTables
, which is a class type we are going to create later, and once I found it, I called its member function OnFetchAllTables
which will get all the table data and store it.
Feel free to comment this part, and not even use it until you reach the data table chapter. It might make more sense there but again, it makes the game go back and forth between classes. However, each chapter here has to be as independent as possible.
SetupPlayerInputComponent
This is one more default method that Unreal adds by default you can totally ignore calling it at all, but as long as we have inputs required it is the best place to bind them.
void AGladiator::SetupPlayerInputComponent(class UInputComponent* InputComponent) { //Set up gameplay key bindings check(InputComponent); InputComponent->BindAction("Jump", IE_Pressed, this, &ACharacter::Jump); InputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping); InputComponent->BindAction("Attack", IE_Released, this, &AGladiator::OnAttack); InputComponent->BindAction("ChangeWeapon", IE_Released, this, &AGladiator::OnChangeWeapon); InputComponent->BindAxis("MoveForward", this, &AGladiator::MoveForward); InputComponent->BindAxis("MoveRight", this, &AGladiator::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, &AGladiator::TurnAtRate); InputComponent->BindAxis("LookUp", this, &APawn::AddControllerPitchInput); InputComponent->BindAxis("LookUpRate", this, &AGladiator::LookUpAtRate); }
SetupPlayerInputController
is one of the most important functions from the base character class. This is the function responsible for assigning the keys from the input settings (the ones we set in Chapter 1, Preparing for a Big Project, inside the project settings), to functions within this class (or maybe other classes if you wish).
As you may remember, we added two types of key input in Chapter 1, Preparing for a Big Project, which were:
Actions
Axis
Now, it is time to bind the inputs and in order to do this we use a function based on the key input type, and that means that, as we have two key input types, we also have two types of binding functions:
BindAction
BindAxis
Both interfaces look the same; you have to pass the key name that we have defined inside the input settings and pass the function that should be called in that input event. It is not only everything that you can pass, but also a specific type of the input event. For example, you can fire the function on when the input key is released or pressed.
Jump
This is a very short, but useful function. Lots of people used to map the jump
function with the key in the SetupPlayerInputComponent
directly, but I always like to have an independent function for jump processing.
void AGladiator::Jump() { if (IsControlable && !IsAttacking) { bPressedJump = true; JumpKeyHoldTime = 0.0f; } }
As you can see, I made sure first that the player is in control and is not in attacking mode. If both conditions are met, then I can go ahead and change some parameters, which will apply the jump directly from the character base class.
StopJumping
When a jump takes place, lots of things will depend on it. Some logic will be executed but, most importantly, animations will be played based on that action. That is the reason behind adding another function to be called when a jump is done, because other animations will need them.
void AGladiator::StopJumping() { if (IsControlable) { bPressedJump = false; JumpKeyHoldTime = 0.0f; } }
The StopJumping
function is not very complex, and in fact it only does the exact opposite of the jump
function.
OnAttack
While there isn't too much to do when the player hits the Attack button, changing the status of the attack itself is enough to drive the animations inside the animation blueprint.
void AGladiator::OnAttack() { if (IsControlable) { IsAttacking = true; } }
The OnAttack
function is simple here, as it is only going to set the Boolean value of the attack to true
. This value is going to be used from the animation blueprint in order to display the attack animation.
The attack effect itself is measured by the overlapping spheres of the weapons and that's the reason behind not having too much logic within the attacking function.
OnPostAttack
Here is another reverse of a function; a function that is only doing the opposite of the previous function.
void AGladiator::OnPostAttack() { IsAttacking = false; }
A good implementation to be used here is to make them both one function with a Boolean parameter to be passed. Then, based on the case, we pass a true
or false
value. But I prefer to make both separate functions, just in case I need to add something special for either of the two functions.
OnChangeWeapon
Sometimes in such a game, the player needs to switch between weapons; this might make more sense with a very large game with tons of different weapons, but I wanted to add it here in a minimal fashion as it is sometimes essential to have it.
void AGladiator::OnChangeWeapon() { if (IsControlable) { if (WeaponIndex < TablesInstance->AllWeaponsData.Num()) { WeaponIndex++; } else { WeaponIndex = 1; } } }
The main idea here is that, whenever the player switches between weapons, all that we do is change the weapon index, and based on that index we can:
- Load a different weapon mesh
- Load different weapon data from the data tables (information that includes the weapon's name, icon, damage, and so on)
So as you can see, every time the player hits the Change Weapon key, this function gets called. The function is very simple. It checks the current weapon index
variable and, as long as it is less than the amount of the weapons found on the game data table, it adds 1 to the current weapon index
variable. If it is not, then it sets the weapon index
variable to 1
to start from the first weapon in the table again.
TurnAtRate
This is one of the functions mapped to a certain input. Its main job it to rotate the camera up and down, but the good part of it is that it is mapped to ready-made code from the Unreal Engine Character
class.
void AGladiator::TurnAtRate(float Rate) { if (IsControlable) { //calculate delta for this frame from the rate information AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds()); } }
The base character class has a method called AddControllerYawInput
. It is meant to apply a yaw rotation to the controller. Here we make sure this method is going to be called when the proper input calls the TurnAtRate
method and applies it to the value of the BaseTurnRate
float we have created in the header file.
LookUpAtRate
As we mapped some keys to move the camera up and down, it makes sense to add a similar functionality to move the camera from side to side:
void AGladiator::LookUpAtRate(float Rate) { if (IsControlable) { //calculate delta for this frame from the rate information AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds()); } }
We are going to do with the pitch as we did with the yaw. The base character class has a method called AddControllerPitchInput
to apply a yaw rotation to the controller. Here we make sure that, every time this method is called, when the proper input calls the TurnAtRate
method and applies it to the value of the BaseLookUpRate
float we have created in the header file.
OnSetPlayerController
This simple function is very effective as it is going to control almost all player inputs. This function's main job is to change the value of the Boolean called IsControlable
. The value of this Boolean is checked before any player input to verify whether the player has access to the input or not; based on that, the game will process the player input and translate it to functions.
void AGladiator::OnSetPlayerController(bool status) { IsControlable = status; }
For example, this function is called by the end of the constructor with the value of true
to make sure that, once the object instance is created, the player has control over it. At the same time, when the player tries to do any input, such as attack, you'll find that the attack
method called AGladiator::OnAttack()
does nothing before it checks for the Boolean IsControlable
to see if it is true
or false
.
OnChangeHealthByAmount
Now let's do something interesting. It is not only interesting because we are going to do a math function that will decrease and increase the player health, but also because it will be calling a function from the blueprint, yes, C++ code calling blueprint logic!
void AGladiator::OnChangeHealthByAmount(float usedAmount) { TotalHealth -= usedAmount; FOutputDeviceNull ar; this->CallFunctionByNameWithArguments(TEXT("ApplyGetDamageEffect"), ar, NULL, true); }
The player health by default is 100
, stored in the variable named TotalHealth
; this value is not going to stay as is during the gameplay.
The player has to take some damage from the enemy's attacks and eventually lose some health. Or maybe you want to add some new features to the game such as health pickups; then you need a quick way to be able to change the current health value, which will be eventually displayed through the UI.
That's the main job of this function: it takes a specified amount of health, and subtracts it from the main health.
As you can see, there is a call for an internal function being made using the method CallFunctionByNameWithArguments
; this means that the C++ code is going to call the blueprint function.
Let's be clear, anything that can be done using C++, can be done using blueprints, but sometimes accessing components and applying some settings is easier via blueprints. The idea here was to present a way that you can call a blueprint function from within the C++ code.
The function being called here, which is called ApplyGetDamageEffect
, is meant to display an overall screen effect that shows that the player has taken some damage at that moment. We will build its logic in the blueprint later in this chapter, but let's be clear about what exactly this function is going to do in order:
- Get the level post-processing volume that is dedicated to displaying the hit effect and save a reference to it so it becomes easier and faster the next time we call the function, as we will already have a reference for the volume and then will display this volume effect.
- Get the
sprite
component we created in the header file, enable it, and play some fade animations for it. So yeah, making fade effects based on afloat
value is a lot easier through blueprints and timeline nodes. - Finally we create some camera shakes to simulate the damage to the player.
Once we're done with the C++ code and have created our blueprint based on that class, we will be working on beautifying it and adding some blueprint logic. That's where we will implement the logic for the ApplyGetDamageEffect
method.
MoveForward
Now we come to a very important part of the controller system, where we will be writing the code to move the player forward and back, using the keys we have mapped earlier at the top of the class.
void AGladiator::MoveForward(float Value) { if ((Controller != NULL) && (Value != 0.0f) && IsControlable && !IsAttacking) { //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); } }
The AGladiator::MoveForward
function is called when the move forward
input key is triggered. This function has to first check whether the controller exists, and the value of the movement is not 0, as well as whether the player has control now or not. Finally, it checks to see whether the player is not in attacking mode. If all the conditions are met, then the function will get the rotation of the controller and the yaw rotation of the controller, and that's very important information in order to decide which forward direction will be used to move.
After getting the direction and storing it as a vector as the Direction
variable, it is a good time to apply the movement using the base class method, AddMovementInput
, to apply the movement with the value that came from the input key, and the direction we just calculated.
Understand that the value passed might be positive or negative, and that will define the direction: forward or backward.
MoveRight
As we've created a function to move the player along the front-back axis, we have to create another one for the side to side movement.
void AGladiator::MoveRight(float Value) { if ((Controller != NULL) && (Value != 0.0f) && IsControlable && !IsAttacking) { //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); } }
The AGladiator:: MoveRight
function is called when the move right
input key is triggered. This function has to check first whether the controller exists and the value of the movement is not 0, as well as whether the player has control now or not. Finally, it checks to see whether the player is not in attacking mode. If all the conditions are met, then the function will get the rotation of the controller and the yaw rotation of the controller; this is very important, in order to decide which direction is the right way to move.
As with the move forward
function, after getting the direction and storing it as a vector as the Direction
variable, it is a good time to apply the movement using the base class method called AddMovementInput
with the value that came from the input key, and the direction we just calculated.
Understand that the value passed might be positive or negative, and that will define the direction: right or left.