top of page

Moving to Unreal Engine



With the recent drama with Unity adding install fees, I decided to have another go at Unreal Engine.


While browsing the unreal marketplace, I came across the character asset for the upcoming souls-like game, Black Myth: Wukong, and decided I would try and replicate the player controller as an intro to Unreal Engine. Thankfully, the asset already came with an animation blueprint, so I didn't have to make my own. I tried, but it was a little too complicated for how little experience I have in Unreal.


I started by creating a class called "SCharacter", with the base class of "ACharacter". I used this script to setup the player input with the new enhanced input component. The actions I setup were the move, look and jump actions. I also setup other basics like the camera component and the boom arm component.


SCharacter.h

class THIRDPERSON_API ASCharacter : public ACharacter
{
	GENERATED_BODY()

protected:

	USpringArmComponent* SpringArmComp;

	UPROPERTY(EditAnywhere)
	UCameraComponent* CameraComp;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Input")
	UInputAction* AttackAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	UInputAction* AttackCancelAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	UInputAction* MoveAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	UInputAction* LookAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Input")
	UInputAction* JumpAction;

	UPROPERTY(EditAnywhere, BlueprintReadOnly)
	UInputMappingContext* InputMapping;

	void OnAttack(const FInputActionValue& value);
	void OnAttackCancel(const FInputActionValue& value);
	void Move(const FInputActionValue& value);
	void Look(const FInputActionValue& value);

	UPROPERTY(VisibleAnywhere)
	UAttackComponent* AttackComponent;


public:
	// Sets default values for this character's properties
	ASCharacter();

	UCameraComponent* GetCameraComp();
	//APlayerCameraManager* GetCameraManager();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};



SCharacter.cpp


#include "SCharacter.h"
#include "GameFramework/SpringArmComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "EnhancedInputComponent.h"
#include "InputAction.h"
#include "InputActionValue.h"
#include "InputMappingContext.h"
#include "EnhancedInputSubsystems.h"

#include "AttackComponent.h"

// Sets default values
ASCharacter::ASCharacter()
{
 	// Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	bUseControllerRotationPitch = false;
	bUseControllerRotationYaw = false;
	bUseControllerRotationRoll = false;

	//Rotation
	GetCharacterMovement()->bOrientRotationToMovement = true;
	GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f);

	//Movement
	GetCharacterMovement()->MaxWalkSpeed = 710.0f;
	GetCharacterMovement()->MaxAcceleration = 2000.0f;
	GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;

	//Jumping
	GetCharacterMovement()->GravityScale = 2.0f;
	GetCharacterMovement()->JumpZVelocity = 600.0f;

	SpringArmComp = CreateDefaultSubobject<USpringArmComponent>("SpringArmComp");
	SpringArmComp->SetupAttachment(RootComponent);
	SpringArmComp->bUsePawnControlRotation = true;
	SpringArmComp->TargetArmLength = 400.0f;

	CameraComp = CreateDefaultSubobject<UCameraComponent>("CameraComp");
	CameraComp->SetupAttachment(SpringArmComp);
	CameraComp->bUsePawnControlRotation = false;

	AttackComponent = CreateDefaultSubobject<UAttackComponent>("AttackComp");
}

//Getters
UCameraComponent* ASCharacter::GetCameraComp() { return CameraComp; }
//APlayerCameraManager* ASCharacter::GetCameraManager() { return  }

// Called when the game starts or when spawned
void ASCharacter::BeginPlay()
{
	Super::BeginPlay();
	
	//Add input mapping context
	if (APlayerController* PlayerController = Cast<APlayerController>(Controller))
	{
		if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer()))
		{
			Subsystem->AddMappingContext(InputMapping, 0);
		}
	}
}

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

}

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

	UEnhancedInputComponent* Input = Cast<UEnhancedInputComponent>(PlayerInputComponent);
	Input->BindAction(AttackAction, ETriggerEvent::Triggered, this, &ASCharacter::OnAttack);
	Input->BindAction(AttackCancelAction, ETriggerEvent::Triggered, this, &ASCharacter::OnAttackCancel);

	Input->BindAction(LookAction, ETriggerEvent::Triggered, this, &ASCharacter::Look);

	Input->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ASCharacter::Move);

	Input->BindAction(JumpAction, ETriggerEvent::Triggered, this, &ASCharacter::Jump);
}

void ASCharacter::OnAttack(const FInputActionValue& value)
{
	AttackComponent->Attack();
}

void ASCharacter::OnAttackCancel(const FInputActionValue& value)
{
	AttackComponent->FinishAttack();
}

void ASCharacter::Move(const FInputActionValue& value)
{
	FVector2D MoveVector = value.Get<FVector2D>();

	if (Controller != nullptr)
	{
		FRotator Rotation = Controller->GetControlRotation();
		FRotator YawRotation(0, Rotation.Yaw, 0);

		FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
		FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);

		AddMovementInput(ForwardDirection, MoveVector.Y);
		AddMovementInput(RightDirection, MoveVector.X);
	}
}

void ASCharacter::Look(const FInputActionValue& value)
{
	FVector2D LookVector = value.Get<FVector2D>();

	if (Controller != nullptr) {
		AddControllerYawInput(LookVector.X);
		AddControllerPitchInput(LookVector.Y);
	}
}


Now that the character can move smoothly, I started working on a new attack component. To do this, I created a new class called "AttackComponent" which derives from "UActorComponent". This script handles everything to do with attacking, like animations, dealing damage and effects when an enemy is hit.





AttackComponent.h


class THIRDPERSON_API UAttackComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	// Sets default values for this component's properties
	UAttackComponent();

protected:
	// Called when the game starts
	virtual void BeginPlay() override;

	ASCharacter* player;
	APlayerController* controller;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
	UAnimMontage* AttackMonatgeOne;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
	UAnimMontage* AttackMonatgeTwo;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
	UAnimMontage* AttackMonatgeThree;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Animation")
	UAnimMontage* AttackMonatgeFour;

	//Timer handles
	FTimerHandle TimerHandle_Attack;

	//Attack timers
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
	float AttackTimer_A;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
	float AttackTimer_B;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
	float AttackTimer_C;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
	float AttackTimer_D;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Animation")
	float ResetAttackTimer;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack Properties")
	float AttackForce;

	UPROPERTY(EditAnywhere)
	TSubclassOf<UCameraShakeBase> CameraShake;

	FTimerHandle TimerHandle_Pause;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Attack Properties")
	float AttackPauseTimer;

	float pauseTimer;

	void PlayAttackMontage(UAnimMontage* anim);
	
	void PlayAttack();
	void ResetAttacks();

	void AttackPause();
	void AttackUnpause();

	uint32 AttackNumber;
	bool bIsAttacking;
	bool bIsInAttackAnim;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Attack Properties")
	bool CanHoldToAttack;

public:	

	// Called every frame
	virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;

	void Attack();

	//Finich the current attack
	void FinishAttack();
	//Stop attacks immediately
	void StopAttack();

};



AttackComponent.cpp


#include "AttackComponent.h"
#include "SCharacter.h"
#include "Components/BoxComponent.h"
#include "SCameraShake.h"
#include "Kismet/GameplayStatics.h"

// Sets default values for this component's properties
UAttackComponent::UAttackComponent()
{
	PrimaryComponentTick.bCanEverTick = true;
	PrimaryComponentTick.bTickEvenWhenPaused = true;

	player = Cast<ASCharacter>(GetOwner());

	controller = UGameplayStatics::GetPlayerController(GetWorld(), 0);

	AttackNumber = 1;
	bIsAttacking = false;
	bIsInAttackAnim = false;

	CanHoldToAttack = false;

	AttackTimer_A = 1.17f;
	AttackTimer_B = 1.09f;
	AttackTimer_C = 1.05f;
	AttackTimer_D = 1.23f;
	ResetAttackTimer = 0.75f;

	AttackForce = 50.0f;

	AttackPauseTimer = 0.2f;
}

// Called when the game starts
void UAttackComponent::BeginPlay()
{
	Super::BeginPlay();
}

void UAttackComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (GetWorld() && GetWorld()->IsPaused())
	{
		pauseTimer -= DeltaTime;

		if (pauseTimer <= 0)
		{
			AttackUnpause();
		}
	}
}

void UAttackComponent::PlayAttackMontage(UAnimMontage* anim)
{
	if (player != nullptr)
	{
		player->PlayAnimMontage(anim);
	}
}

void UAttackComponent::PlayAttack()
{
	bIsInAttackAnim = false;

	if (bIsAttacking && CanHoldToAttack == true)
		Attack();
	else
		GetWorld()->GetTimerManager().SetTimer(TimerHandle_Attack, this, &UAttackComponent::ResetAttacks, ResetAttackTimer);
}

void UAttackComponent::ResetAttacks()
{
	AttackNumber = 1;
}

void UAttackComponent::Attack()
{
	bIsAttacking = true;	

	if (!bIsInAttackAnim) {

		FVector forwardVector = player->GetActorForwardVector();
		forwardVector.Normalize();

		TArray<FHitResult> hits;
		FVector start = player->GetActorLocation();
		FVector end = start + player->GetActorForwardVector() * 300.0f;

		FCollisionObjectQueryParams collisionParams;
		collisionParams.AddObjectTypesToQuery(ECollisionChannel::ECC_Pawn);

		float SphereRadius = 80.0f;
		FCollisionShape shape;
		shape.SetSphere(SphereRadius);

		bool hasHitObject = GetWorld()->SweepMultiByObjectType(hits, start, end, FQuat::Identity, collisionParams, shape);

		FColor hitColour = hasHitObject ? FColor::Green : FColor::Red;

		for (FHitResult hit : hits)
		{
			AActor* HitActor = hit.GetActor();

			if (HitActor)
			{
				UBoxComponent* box = HitActor->GetComponentByClass<UBoxComponent>();

				if (box)
				{
					FVector force = HitActor->GetActorLocation() - player->GetActorLocation();
					force.Normalize();
					box->AddForce(force * (AttackForce * 10000.0f));

					//APlayerController* controller = UGameplayStatics::GetPlayerController(GetWorld(), 0);

					if (controller)
					{
						//Play camera shake
						controller->PlayerCameraManager->StartCameraShake(CameraShake);
					}

					//Slightly pause game when attacking
					AttackPause();

					DrawDebugSphere(GetWorld(), hit.ImpactPoint, SphereRadius, 16, hitColour, false, 2.0f);
				}
			}
		}

		DrawDebugLine(GetWorld(), start, end, hitColour, false, 2.0f, 0, 2.0f);

		switch (AttackNumber)
		{
		case 1:
			PlayAttackMontage(AttackMonatgeOne);
			AttackNumber = 2;
			bIsInAttackAnim = true;
			GetWorld()->GetTimerManager().SetTimer(TimerHandle_Attack, this, &UAttackComponent::PlayAttack, AttackTimer_A);
			break;
		case 2:
			PlayAttackMontage(AttackMonatgeTwo);
			AttackNumber = 3;
			bIsInAttackAnim = true;
			GetWorld()->GetTimerManager().SetTimer(TimerHandle_Attack, this, &UAttackComponent::PlayAttack, AttackTimer_B);
			break;
		case 3:
			PlayAttackMontage(AttackMonatgeThree);
			AttackNumber = 4;
			bIsInAttackAnim = true;
			GetWorld()->GetTimerManager().SetTimer(TimerHandle_Attack, this, &UAttackComponent::PlayAttack, AttackTimer_C);
			break;
		case 4:
			PlayAttackMontage(AttackMonatgeFour);
			AttackNumber = 1;
			bIsInAttackAnim = true;
			GetWorld()->GetTimerManager().SetTimer(TimerHandle_Attack, this, &UAttackComponent::PlayAttack, AttackTimer_D);
			break;
		default:
			break;
		}
	}
}

void UAttackComponent::FinishAttack()
{
	bIsAttacking = false;
}

void UAttackComponent::StopAttack()
{
	FinishAttack();

	if (player != nullptr)
	{
		player->StopAnimMontage();
	}
}

void UAttackComponent::AttackPause()
{
	pauseTimer = AttackPauseTimer;
	controller->SetPause(true);
}

void UAttackComponent::AttackUnpause()
{
	controller->SetPause(false);
}

It's not much, but it's a start. Here are the results so far...


bottom of page