
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...