Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Unreal Engine 5 Game Development with C++ Scripting

You're reading from   Unreal Engine 5 Game Development with C++ Scripting Become a professional game developer and create fully functional, high-quality games

Arrow left icon
Product type Paperback
Published in Aug 2023
Publisher Packt
ISBN-13 9781804613931
Length 384 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Author (1):
Arrow left icon
Zhenyu George Li Zhenyu George Li
Author Profile Icon Zhenyu George Li
Zhenyu George Li
Arrow right icon
View More author details
Toc

Table of Contents (18) Chapters Close

Preface 1. Part 1: Getting Started with Unreal C++ Scripting
2. Chapter 1: Creating Your First Unreal C++ Game FREE CHAPTER 3. Chapter 2: Editing C++ Code in Visual Studio 4. Chapter 3: Learning C++ and Object-Oriented Programming 5. Chapter 4: Investigating the Shooter Game’s Generated Project and C++ Code 6. Part 2: C++ Scripting for Unreal Engine
7. Chapter 5: Learning How to Use UE Gameplay Framework Base Classes 8. Chapter 6: Creating Game Actors 9. Chapter 7: Controlling Characters 10. Chapter 8: Handling Collisions 11. Chapter 9: Improving C++ Code Quality 12. Part 3: Making a Complete Multiplayer Game
13. Chapter 10: Making Pangaea a Network Multiplayer Game 14. Chapter 11: Controlling the Game Flow 15. Chapter 12: Polishing and Packaging the Game 16. Index 17. Other Books You May Enjoy

Refactoring code

You probably felt a little uncomfortable with the redundant code blocks we wrote in the previous chapters; for example, the PlayerAvatar and Enemy classes have some identical attribute variables and functions. Can we instead combine them and maintain only one copy of code for those replicated variables and functions? Yes, and this will require us to refactor the code.

Code refactoring helps improve the internal structure of code so that it becomes more readable, maintainable, and efficient. The process includes algorithm optimization, duplicate removal, and code simplification.

For the Pangaea project, we identified two refactoring tasks:

  • Combining the two animation instance classes
  • Adding a parent class for the PlayerAvatar and Enemy classes

Let’s start with the first task.

Combining the PlayerAvatarAnimInstance and EnemyAnimInstance classes

Open and compare the PlayerAvatarAnimInstance.h, PlayerAvatarAnimInstance.cpp, EnemyAnimInstance.h, and EnemyAnimInstance.cpp files – you should find that the two pairs of header and source code files are almost identical, except for the class names.

For the Pangaea game, we know that the game design won’t be changed in the future, so we can combine these two classes. We can create a new class called PangaeaAnimInstance, with the class files called PangaeaAnimInstance.h and PangaeaAnimInstance.cpp. Of course, the old files can be removed from the project (if you have forgotten how to remove code files, please refer to the Recompiling C++ Projects section of Chapter 5).

Here is the code for PangaeaAnimInstance.h:

#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimInstance.h"
#include "PangaeaAnimInstance.generated.h"
UENUM(BlueprintType)
enum class ECharacterState : uint8
{
  Locomotion,
  Attack,
  Hit,
  Die
};
UCLASS()
class PANGAEA_API UPangaeaAnimInstance : public UAnimInstance
{
  GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PangaeaAnimInstance Params")
  float Speed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PangaeaAnimInstance Params")
  ECharacterState State;
  UFUNCTION(BlueprintCallable)
  void OnStateAnimationEnds();
};

Here is the code for PangaeaAnimInstance.cpp:

#include "PangaeaAnimInstance.h"
#include "PangaeaCharacter.h"
void UPangaeaAnimInstance::OnStateAnimationEnds()
{
if (State == ECharacterState::Attack)
{
  State = ECharacterState::Locomotion;
}
else
{
auto character = Cast<APangaeaCharacter>(GetOwningActor());
if (State == ECharacterState::Hit)
{
  if (character->GetHealthPoints() > 0.0f)
  {
    State = ECharacterState::Locomotion;
  }
  else
  {
    State = ECharacterState::Die;
  }
}
else if (State == ECharacterState::Die)
{
  character->DieProcess();
}
}
}

The previous code for the PangaeaAnimInstance class combined the redundant code in both PlayerAvatarAnimInstance and EnemyAnimInstance into one place. Since the two animation blueprints, ABP_Player and ABP_Enemy, were created based on the old animation instance classes, they both need to be reparented to the new PangaeAnimInstance class.

To do that, open ABP_Player in the Animation Blueprint Editor and select File | Reparent Blueprint from the main menu:

Figure 9.1 – Reparenting ABP_Player to PangaeaAnimInstance.png

Figure 9.1 – Reparenting ABP_Player to PangaeaAnimInstance.png

Then, choose PangaeaAnimInstance from the Reparent Blueprint list.

Unfortunately, you have to re-set up the state machine, variables, and animations for the blueprint (follow the steps introduced in the Creating the State Machine on ABP_PlayerAvatar section of Chapter 6 to do this).

Figure 9.2 illustrates the different class diagrams before and after the refactoring process:

Figure 9.2 – Class diagrams before and after refactoring PlayerAvatarAnimInstance and EnemyAnimInstance into PangaeaAnimInstance

Figure 9.2 – Class diagrams before and after refactoring PlayerAvatarAnimInstance and EnemyAnimInstance into PangaeaAnimInstance

Next, we want to refactor the PlayerAvatar and Enemy classes to avoid having replicated attribute variables and functions.

Making PangaeaCharacter the parent class of APlayerAvatar and AEnemy

By investigating the APlayerAvatar and AEnemy classes, we can draw the class diagram, which includes information about the class variables and functions:

Figure 9.3 – Pangaea actors class diagram before the refactoring

Figure 9.3 – Pangaea actors class diagram before the refactoring

In the diagram, + indicates that the subsequent variable or function is publicly accessible, and indicates that the subsequent variable or function is only privately accessible.

As you can see, the APlayerAvatar class and the AEnemy class have some variables and functions in common, for instance, the HealthPoints variable and the GetHealthPoints function. The two copies of the same code may potentially cause inconsistency and confusion, which will make it harder to maintain the code. Therefore, we want to refactor these two classes and their relationship.

To refactor the class structure of APlayerAvatar and AEnemy, we can change and make the APangaeaCharacter class (which was already created with the Pangaea project) the parent class that is inherited by classes. Then, we can move the common variables and functions from the two child classes into the parent class:

Figure 9.4 – Pangaea actors class diagram after the refactoring

Figure 9.4 – Pangaea actors class diagram after the refactoring

As you can see, the common variables and functions are now written only once in the APangaea Character class, which helps avoid ambiguity and improves the code quality for maintenance.

We will present the code for the .h and .cpp files for the three refactored classes: APangaeaCharacter, APlayerAvatar, and AEnemy.

Here is the code for PangaeaCharacter.h:

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "PangaeaCharacter.generated.h"
UCLASS(Blueprintable)
class APangaeaCharacter : public ACharacter
{
GENERATED_BODY()
public:
APangaeaCharacter();
UPROPERTY(EditAnywhere, Category = "Pangaea Character Params")
int HealthPoints = 100;
UPROPERTY(EditAnywhere, Category = "Pangaea Character Params")
float Strength = 5;
UPROPERTY(EditAnywhere, Category = "Pangaea Character Params")
float Armer = 1;
UPROPERTY(EditAnywhere, Category = "Pangaea Character Params")
float AttackRange = 200.0f;
UPROPERTY(EditAnywhere, Category = "Pangaea Character Params")
float AttackInterval = 3.0f;
public :
virtual void Tick(float DeltaTime) override;
UFUNCTION(BlueprintCallable,
          Category = "Pangaea|Character",
          meta = (DisplayName = "Get HP"))
int GetHealthPoints();
UFUNCTION(BlueprintCallable, Category = "Pangaea|Character")
bool IsKilled();
UFUNCTION(BlueprintCallable, Category = "Pangaea|Character")
bool CanAttack();
virtual void Attack();
virtual void Hit(int damage);
virtual void DieProcess();
protected:
virtual void BeginPlay() override;
class UPangaeaAnimInstance* _AnimInstance;
int _HealthPoints;
float _AttackCountingDown;
};

In PangaeaCharacter.h, you may have noticed the new UPangaeaAnimInstance-type variable, _AnimInstance, of the APangaeaCharacter class. This variable is used as a cache variable. We will explain it in the Refining code section.

We also changed the APlayerAvatar and AEnemy classes’ parent to be APangaeaCharacter and removed those variables and functions that were already moved to the parent class.

The code for PangaeaCharacter.cpp is provided here:

#include "PangaeaCharacter.h"
#include "PangaeaAnimInstance.h"
#include "UObject/ConstructorHelpers.h"
#include "Camera/CameraComponent.h"
#include "Components/DecalComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/SpringArmComponent.h"
#include "Materials/Material.h"
#include "Engine/World.h"
APangaeaCharacter::APangaeaCharacter()
{
    PrimaryActorTick.bCanEverTick = true;
}
void APangaeaCharacter::BeginPlay()
{
    Super::BeginPlay();
    _AnimInstance = Cast<UPangaeaAnimInstance>(
                             GetMesh()->GetAnimInstance());
    _HealthPoints = HealthPoints;
}
void APangaeaCharacter::Tick(float DeltaSeconds)
{
    Super::Tick(DeltaSeconds);
}
int APangaeaCharacter::GetHealthPoints()
{
    return _HealthPoints;
}
bool APangaeaCharacter::IsKilled()
{
    return (_HealthPoints <= 0.0f);
}
bool APangaeaCharacter::CanAttack()
{
    return (_AttackCountingDown <= 0.0f &&
     _AnimInstance->State == ECharacterState::Locomotion);
}
void APangaeaCharacter::Attack()
{
    _AttackCountingDown = AttackInterval;
}
void APangaeaCharacter::Hit(int damage)
{
    _HealthPoints -= damage;
    _AnimInstance->State = ECharacterState::Hit;
if (IsKilled()) {
          PrimaryActorTick.bCanEverTick = false;
}
}
void APangaeaCharacter::DieProcess()
{
    PrimaryActorTick.bCanEverTick = false;
    Destroy();
    GEngine->ForceGarbageCollection(true);
}

Next up is the code for PlayerAvatar.h:

#pragma once
#include "CoreMinimal.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "PangaeaCharacter.h"
#include "Weapon.h"
#include "PlayerAvatar.generated.h"
UCLASS(Blueprintable)
class PANGAEA_API APlayerAvatar : public APangaeaCharacter
{
GENERATED_BODY()
public:
APlayerAvatar();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
virtual void SetupPlayerInputComponent(
       class UInputComponent* PlayerInputComponent) override;
UFUNCTION(BlueprintCallable,
                Category = "Pangaea|PlayerAvatar")
void AttachWeapon(AWeapon* Weapon);
UFUNCTION(BlueprintCallable,
               Category = "Pangaea|PlayerAvatar")
void DropWeapon();
void Attack() override;
FORCEINLINE
class UCameraComponent* GetCameraComponet() const
{ return _CameraComponent; }
FORCEINLINE
class USpringArmComponent* GetSringArmComponet() const { return _SpringArmComponent; }
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly,
  Category = "Camera", meta = (AllowPrivateAccess = "true"))
USpringArmComponent* _SpringArmComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly,
  Category = "Camera", meta = (AllowPrivateAccess = "true"))
UCameraComponent* _CameraComponent;
};

The implementation code for PlayerAvatar.cpp is provided here:

#include "PlayerAvatar.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "PangaeaAnimInstance.h"
APlayerAvatar::APlayerAvatar()
{
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = false;
bUseControllerRotationRoll = false;
auto characterMovement = GetCharacterMovement();
characterMovement->bOrientRotationToMovement = true;
characterMovement->RotationRate = FRotator(0.f, 640.f, 0.f);
characterMovement->bConstrainToPlane = true;
characterMovement->bSnapToPlaneAtStart = true;
_SpringArmComponent =
         CreateDefaultSubobject<USpringArmComponent>(
                              TEXT("SpringArm"));
_SpringArmComponent->SetupAttachment(RootComponent);
_SpringArmComponent->SetUsingAbsoluteRotation(true);
_SpringArmComponent->TargetArmLength = 800.f;
_SpringArmComponent->SetRelativeRotation(
                              FRotator(-60.f, 0.f, 0.f));
_SpringArmComponent->bDoCollisionTest = false;
CameraComponent =
     CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
_CameraComponent->SetupAttachment(_SpringArmComponent,
                             USpringArmComponent::SocketName);
_CameraComponent->bUsePawnControlRotation = false;
}
void APlayerAvatar::BeginPlay()
{
Super::BeginPlay();
}
void APlayerAvatar::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
_AnimInstance->Speed =
            GetCharacterMovement()->Velocity.Size2D();
if (_AttackCountingDown == AttackInterval)
{
  _AnimInstance->State = ECharacterState::Attack;
}
if (_AttackCountingDown > 0.0f)
{
  _AttackCountingDown -= DeltaTime;
}
}
void APlayerAvatar::SetupPlayerInputComponent(
                    UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
}
void APlayerAvatar::AttachWeapon(AWeapon* Weapon)
{
Weapon->AttachToComponent(GetMesh(),
   FAttachmentTransformRules::SnapToTargetIncludingScale,
   FName("hand_rSocket"));
}
void APlayerAvatar::DropWeapon()
{
TArray<AActor*> attachedActors;
GetAttachedActors(attachedActors, true);
for (int i = 0; i < attachedActors.Num(); ++i)
{
  attachedActors[i]->DetachFromActor(
              FDetachmentTransformRules::KeepWorldTransform);
  attachedActors[i]->SetActorRotation(FQuat::Identity);
  AWeapon* weapon = Cast<AWeapon>(attachedActors[i]);
  if (weapon != nullptr)
  {
    weapon->Holder = nullptr;
  }
}
}
void APlayerAvatar::Attack()
{
  APangaeaCharacter::Attack();
}

Lastly, we’ll modify the code for the AEnemy class. Here is the code for Enemy.h:

#pragma once
#include "CoreMinimal.h"
#include "PangaeaCharacter.h"
#include "Weapon.h"
#include "Enemy.generated.h"
UCLASS()
class PANGAEA_API AEnemy : public APangaeaCharacter
{
  GENERATED_BODY()
public:
  AEnemy();
protected:
  virtual void BeginPlay() override;
  APawn* _chasedTarget = nullptr;
  UClass* _WeaponClass;
  AWeapon* _Weapon;
public:
  virtual void Tick(float DeltaTime) override;
  void Attack() override;
  void DieProcess() override;
  UFUNCTION(BlueprintCallable, Category = "Pangaea|Enemy")
  void Chase(APawn* targetPawn);
private:
  UPROPERTY(VisibleAnywhere, BlueprintReadOnly,
                      meta = (AllowPrivateAccess = "true"))
  class UPawnSensingComponent* PawnSensingComponent;
};

The implementation code for Enemy.cpp is as follows:

#include "Enemy.h"
#include "Perception/PawnSensingComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnemyController.h"
#include "PangaeaAnimInstance.h"
AEnemy::AEnemy()
{
PawnSensingComponent =
  CreateDefaultSubobject<UPawnSensingComponent>(
                                    TEXT("PawnSensor"));
static ConstructorHelpers::FObjectFinder<UBlueprint>
blueprint_finder(
TEXT("Blueprint'/Game/TopDown/Blueprints/BP_Hammer.BP_Hammer'"));
_WeaponClass = (UClass*)blueprint_finder.Object->GeneratedClass;
}
void AEnemy::BeginPlay()
{
Super::BeginPlay();
_Weapon = Cast<AWeapon>(GetWorld()->SpawnActor(_WeaponClass));
_Weapon->Holder = this;
_Weapon->AttachToComponent(GetMesh(),
     FAttachmentTransformRules::SnapToTargetIncludingScale,
     FName("hand_rSocket"));
}
void AEnemy::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
_AnimInstance->Speed =
      GetCharacterMovement()->Velocity.Size2D();
if (_AttackCountingDown == AttackInterval)
{
  _AnimInstance->State = ECharacterState::Attack;
}
if (_AttackCountingDown > 0.0f)
{
  _AttackCountingDown -= DeltaTime;
}
if (_chasedTarget != nullptr &&
    _AnimInstance->State == ECharacterState::Locomotion)
{
auto enemyController =
       Cast<AEnemyController>(GetController());
enemyController->MakeAttackDecision(_chasedTarget);
}
}
void AEnemy::Chase(APawn* targetPawn)
{
if (targetPawn != nullptr &&
    _AnimInstance->State == ECharacterState::Locomotion)
{
auto enemyController =
    Cast<AEnemyController>(GetController());
enemyController->MoveToActor(targetPawn, 90.0f);
}
_chasedTarget = targetPawn;
}
void AEnemy::DieProcess()
{
  Super::DieProcess();
  _Weapon->Destroy();
}
void AEnemy::Attack()
{
  APangaeaCharacter::Attack();
  GetController()->StopMovement();
}

The previously presented header and .cpp files demonstrated the code implementation for adding the APangaeaCharacter class and refactoring the APlayerAvatar and AEnemy classes. Moreover, we employed _AnimInstance as a cache variable to avoid constantly retrieving the character’s animation instance.

In fact, using caching variables for improved performance is a process of refinement. Let’s proceed to explain and carry out additional refinement tasks.

lock icon The rest of the chapter is locked
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Visually different images