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
andEnemy
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
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
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:
.jpg)
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:
.jpg)
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.