548 lines
15 KiB
C++
548 lines
15 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
|
|
#include "CombatCharacter.h"
|
|
#include "Components/CapsuleComponent.h"
|
|
#include "Components/WidgetComponent.h"
|
|
#include "GameFramework/CharacterMovementComponent.h"
|
|
#include "GameFramework/SpringArmComponent.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
#include "Camera/CameraComponent.h"
|
|
#include "EnhancedInputSubsystems.h"
|
|
#include "EnhancedInputComponent.h"
|
|
#include "CombatLifeBar.h"
|
|
#include "Engine/DamageEvents.h"
|
|
#include "TimerManager.h"
|
|
#include "Engine/LocalPlayer.h"
|
|
#include "CombatPlayerController.h"
|
|
|
|
ACombatCharacter::ACombatCharacter()
|
|
{
|
|
PrimaryActorTick.bCanEverTick = true;
|
|
|
|
// bind the attack montage ended delegate
|
|
OnAttackMontageEnded.BindUObject(this, &ACombatCharacter::AttackMontageEnded);
|
|
|
|
// Set size for collision capsule
|
|
GetCapsuleComponent()->InitCapsuleSize(35.0f, 90.0f);
|
|
|
|
// Configure character movement
|
|
GetCharacterMovement()->MaxWalkSpeed = 400.0f;
|
|
|
|
// create the camera boom
|
|
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
|
|
CameraBoom->SetupAttachment(RootComponent);
|
|
|
|
CameraBoom->TargetArmLength = DefaultCameraDistance;
|
|
CameraBoom->bUsePawnControlRotation = true;
|
|
CameraBoom->bEnableCameraLag = true;
|
|
CameraBoom->bEnableCameraRotationLag = true;
|
|
|
|
// create the orbiting camera
|
|
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
|
|
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
|
|
FollowCamera->bUsePawnControlRotation = false;
|
|
|
|
// create the life bar widget component
|
|
LifeBar = CreateDefaultSubobject<UWidgetComponent>(TEXT("LifeBar"));
|
|
LifeBar->SetupAttachment(RootComponent);
|
|
|
|
// set the player tag
|
|
Tags.Add(FName("Player"));
|
|
}
|
|
|
|
void ACombatCharacter::Move(const FInputActionValue& Value)
|
|
{
|
|
// input is a Vector2D
|
|
FVector2D MovementVector = Value.Get<FVector2D>();
|
|
|
|
// route the input
|
|
DoMove(MovementVector.X, MovementVector.Y);
|
|
}
|
|
|
|
void ACombatCharacter::Look(const FInputActionValue& Value)
|
|
{
|
|
FVector2D LookAxisVector = Value.Get<FVector2D>();
|
|
|
|
// route the input
|
|
DoLook(LookAxisVector.X, LookAxisVector.Y);
|
|
}
|
|
|
|
void ACombatCharacter::ComboAttackPressed()
|
|
{
|
|
// route the input
|
|
DoComboAttackStart();
|
|
}
|
|
|
|
void ACombatCharacter::ChargedAttackPressed()
|
|
{
|
|
// route the input
|
|
DoChargedAttackStart();
|
|
}
|
|
|
|
void ACombatCharacter::ChargedAttackReleased()
|
|
{
|
|
// route the input
|
|
DoChargedAttackEnd();
|
|
}
|
|
|
|
void ACombatCharacter::ToggleCamera()
|
|
{
|
|
// call the BP hook
|
|
BP_ToggleCamera();
|
|
}
|
|
|
|
void ACombatCharacter::DoMove(float Right, float Forward)
|
|
{
|
|
if (GetController() != nullptr)
|
|
{
|
|
// find out which way is forward
|
|
const FRotator Rotation = GetController()->GetControlRotation();
|
|
const FRotator YawRotation(0, Rotation.Yaw, 0);
|
|
|
|
// get forward vector
|
|
const FVector ForwardDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
|
|
|
|
// get right vector
|
|
const FVector RightDirection = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
|
|
|
|
// add movement
|
|
AddMovementInput(ForwardDirection, Forward);
|
|
AddMovementInput(RightDirection, Right);
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::DoLook(float Yaw, float Pitch)
|
|
{
|
|
if (GetController() != nullptr)
|
|
{
|
|
// add yaw and pitch input to controller
|
|
AddControllerYawInput(Yaw);
|
|
AddControllerPitchInput(Pitch);
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::DoComboAttackStart()
|
|
{
|
|
// are we already playing an attack animation?
|
|
if (bIsAttacking)
|
|
{
|
|
// cache the input time so we can check it later
|
|
CachedAttackInputTime = GetWorld()->GetTimeSeconds();
|
|
|
|
return;
|
|
}
|
|
|
|
// perform a combo attack
|
|
ComboAttack();
|
|
}
|
|
|
|
void ACombatCharacter::DoComboAttackEnd()
|
|
{
|
|
// stub
|
|
}
|
|
|
|
void ACombatCharacter::DoChargedAttackStart()
|
|
{
|
|
// raise the charging attack flag
|
|
bIsChargingAttack = true;
|
|
|
|
if (bIsAttacking)
|
|
{
|
|
// cache the input time so we can check it later
|
|
CachedAttackInputTime = GetWorld()->GetTimeSeconds();
|
|
|
|
return;
|
|
}
|
|
|
|
ChargedAttack();
|
|
}
|
|
|
|
void ACombatCharacter::DoChargedAttackEnd()
|
|
{
|
|
// lower the charging attack flag
|
|
bIsChargingAttack = false;
|
|
|
|
// if we've done the charge loop at least once, release the charged attack right away
|
|
if (bHasLoopedChargedAttack)
|
|
{
|
|
CheckChargedAttack();
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::ResetHP()
|
|
{
|
|
// reset the current HP total
|
|
CurrentHP = MaxHP;
|
|
|
|
// update the life bar
|
|
LifeBarWidget->SetLifePercentage(1.0f);
|
|
}
|
|
|
|
void ACombatCharacter::ComboAttack()
|
|
{
|
|
// raise the attacking flag
|
|
bIsAttacking = true;
|
|
|
|
// reset the combo count
|
|
ComboCount = 0;
|
|
|
|
// notify enemies they are about to be attacked
|
|
NotifyEnemiesOfIncomingAttack();
|
|
|
|
// play the attack montage
|
|
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
|
{
|
|
const float MontageLength = AnimInstance->Montage_Play(ComboAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
|
|
|
// subscribe to montage completed and interrupted events
|
|
if (MontageLength > 0.0f)
|
|
{
|
|
// set the end delegate for the montage
|
|
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ComboAttackMontage);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
void ACombatCharacter::ChargedAttack()
|
|
{
|
|
// raise the attacking flag
|
|
bIsAttacking = true;
|
|
|
|
// reset the charge loop flag
|
|
bHasLoopedChargedAttack = false;
|
|
|
|
// notify enemies they are about to be attacked
|
|
NotifyEnemiesOfIncomingAttack();
|
|
|
|
// play the charged attack montage
|
|
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
|
{
|
|
const float MontageLength = AnimInstance->Montage_Play(ChargedAttackMontage, 1.0f, EMontagePlayReturnType::MontageLength, 0.0f, true);
|
|
|
|
// subscribe to montage completed and interrupted events
|
|
if (MontageLength > 0.0f)
|
|
{
|
|
// set the end delegate for the montage
|
|
AnimInstance->Montage_SetEndDelegate(OnAttackMontageEnded, ChargedAttackMontage);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::AttackMontageEnded(UAnimMontage* Montage, bool bInterrupted)
|
|
{
|
|
// reset the attacking flag
|
|
bIsAttacking = false;
|
|
|
|
// check if we have a non-stale cached input
|
|
if (GetWorld()->GetTimeSeconds() - CachedAttackInputTime <= AttackInputCacheTimeTolerance)
|
|
{
|
|
// are we holding the charged attack button?
|
|
if (bIsChargingAttack)
|
|
{
|
|
// do a charged attack
|
|
ChargedAttack();
|
|
}
|
|
else
|
|
{
|
|
// do a regular attack
|
|
ComboAttack();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::DoAttackTrace(FName DamageSourceBone)
|
|
{
|
|
// sweep for objects in front of the character to be hit by the attack
|
|
TArray<FHitResult> OutHits;
|
|
|
|
// start at the provided socket location, sweep forward
|
|
const FVector TraceStart = GetMesh()->GetSocketLocation(DamageSourceBone);
|
|
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * MeleeTraceDistance);
|
|
|
|
// check for pawn and world dynamic collision object types
|
|
FCollisionObjectQueryParams ObjectParams;
|
|
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
|
|
ObjectParams.AddObjectTypesToQuery(ECC_WorldDynamic);
|
|
|
|
// use a sphere shape for the sweep
|
|
FCollisionShape CollisionShape;
|
|
CollisionShape.SetSphere(MeleeTraceRadius);
|
|
|
|
// ignore self
|
|
FCollisionQueryParams QueryParams;
|
|
QueryParams.AddIgnoredActor(this);
|
|
|
|
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
|
|
{
|
|
// iterate over each object hit
|
|
for (const FHitResult& CurrentHit : OutHits)
|
|
{
|
|
// check if we've hit a damageable actor
|
|
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
|
|
|
|
if (Damageable)
|
|
{
|
|
// knock upwards and away from the impact normal
|
|
const FVector Impulse = (CurrentHit.ImpactNormal * -MeleeKnockbackImpulse) + (FVector::UpVector * MeleeLaunchImpulse);
|
|
|
|
// pass the damage event to the actor
|
|
Damageable->ApplyDamage(MeleeDamage, this, CurrentHit.ImpactPoint, Impulse);
|
|
|
|
// call the BP handler to play effects, etc.
|
|
DealtDamage(MeleeDamage, CurrentHit.ImpactPoint);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::CheckCombo()
|
|
{
|
|
// are we playing a non-charge attack animation?
|
|
if (bIsAttacking && !bIsChargingAttack)
|
|
{
|
|
// is the last attack input not stale?
|
|
if (GetWorld()->GetTimeSeconds() - CachedAttackInputTime <= ComboInputCacheTimeTolerance)
|
|
{
|
|
// consume the attack input so we don't accidentally trigger it twice
|
|
CachedAttackInputTime = 0.0f;
|
|
|
|
// increase the combo counter
|
|
++ComboCount;
|
|
|
|
// do we still have a combo section to play?
|
|
if (ComboCount < ComboSectionNames.Num())
|
|
{
|
|
// notify enemies they are about to be attacked
|
|
NotifyEnemiesOfIncomingAttack();
|
|
|
|
// jump to the next combo section
|
|
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
|
{
|
|
AnimInstance->Montage_JumpToSection(ComboSectionNames[ComboCount], ComboAttackMontage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::CheckChargedAttack()
|
|
{
|
|
// raise the looped charged attack flag
|
|
bHasLoopedChargedAttack = true;
|
|
|
|
// jump to either the loop or the attack section depending on whether we're still holding the charge button
|
|
if (UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance())
|
|
{
|
|
AnimInstance->Montage_JumpToSection(bIsChargingAttack ? ChargeLoopSection : ChargeAttackSection, ChargedAttackMontage);
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::NotifyEnemiesOfIncomingAttack()
|
|
{
|
|
// sweep for objects in front of the character to be hit by the attack
|
|
TArray<FHitResult> OutHits;
|
|
|
|
// start at the actor location, sweep forward
|
|
const FVector TraceStart = GetActorLocation();
|
|
const FVector TraceEnd = TraceStart + (GetActorForwardVector() * DangerTraceDistance);
|
|
|
|
// check for pawn object types only
|
|
FCollisionObjectQueryParams ObjectParams;
|
|
ObjectParams.AddObjectTypesToQuery(ECC_Pawn);
|
|
|
|
// use a sphere shape for the sweep
|
|
FCollisionShape CollisionShape;
|
|
CollisionShape.SetSphere(DangerTraceRadius);
|
|
|
|
// ignore self
|
|
FCollisionQueryParams QueryParams;
|
|
QueryParams.AddIgnoredActor(this);
|
|
|
|
if (GetWorld()->SweepMultiByObjectType(OutHits, TraceStart, TraceEnd, FQuat::Identity, ObjectParams, CollisionShape, QueryParams))
|
|
{
|
|
// iterate over each object hit
|
|
for (const FHitResult& CurrentHit : OutHits)
|
|
{
|
|
// check if we've hit a damageable actor
|
|
ICombatDamageable* Damageable = Cast<ICombatDamageable>(CurrentHit.GetActor());
|
|
|
|
if (Damageable)
|
|
{
|
|
// notify the enemy
|
|
Damageable->NotifyDanger(GetActorLocation(), this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::ApplyDamage(float Damage, AActor* DamageCauser, const FVector& DamageLocation, const FVector& DamageImpulse)
|
|
{
|
|
// pass the damage event to the actor
|
|
FDamageEvent DamageEvent;
|
|
const float ActualDamage = TakeDamage(Damage, DamageEvent, nullptr, DamageCauser);
|
|
|
|
// only process knockback and effects if we received nonzero damage
|
|
if (ActualDamage > 0.0f)
|
|
{
|
|
// apply the knockback impulse
|
|
GetCharacterMovement()->AddImpulse(DamageImpulse, true);
|
|
|
|
// is the character ragdolling?
|
|
if (GetMesh()->IsSimulatingPhysics())
|
|
{
|
|
// apply an impulse to the ragdoll
|
|
GetMesh()->AddImpulseAtLocation(DamageImpulse * GetMesh()->GetMass(), DamageLocation);
|
|
}
|
|
|
|
// pass control to BP to play effects, etc.
|
|
ReceivedDamage(ActualDamage, DamageLocation, DamageImpulse.GetSafeNormal());
|
|
}
|
|
|
|
}
|
|
|
|
void ACombatCharacter::HandleDeath()
|
|
{
|
|
// disable movement while we're dead
|
|
GetCharacterMovement()->DisableMovement();
|
|
|
|
// enable full ragdoll physics
|
|
GetMesh()->SetSimulatePhysics(true);
|
|
|
|
// hide the life bar
|
|
LifeBar->SetHiddenInGame(true);
|
|
|
|
// pull back the camera
|
|
GetCameraBoom()->TargetArmLength = DeathCameraDistance;
|
|
|
|
// schedule respawning
|
|
GetWorld()->GetTimerManager().SetTimer(RespawnTimer, this, &ACombatCharacter::RespawnCharacter, RespawnTime, false);
|
|
}
|
|
|
|
void ACombatCharacter::ApplyHealing(float Healing, AActor* Healer)
|
|
{
|
|
// stub
|
|
}
|
|
|
|
void ACombatCharacter::NotifyDanger(const FVector& DangerLocation, AActor* DangerSource)
|
|
{
|
|
// stub
|
|
}
|
|
|
|
void ACombatCharacter::RespawnCharacter()
|
|
{
|
|
// destroy the character and let it be respawned by the Player Controller
|
|
Destroy();
|
|
}
|
|
|
|
float ACombatCharacter::TakeDamage(float Damage, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
|
|
{
|
|
// only process damage if the character is still alive
|
|
if (CurrentHP <= 0.0f)
|
|
{
|
|
return 0.0f;
|
|
}
|
|
|
|
// reduce the current HP
|
|
CurrentHP -= Damage;
|
|
|
|
// have we run out of HP?
|
|
if (CurrentHP <= 0.0f)
|
|
{
|
|
// die
|
|
HandleDeath();
|
|
}
|
|
else
|
|
{
|
|
// update the life bar
|
|
LifeBarWidget->SetLifePercentage(CurrentHP / MaxHP);
|
|
|
|
// enable partial ragdoll physics, but keep the pelvis vertical
|
|
GetMesh()->SetPhysicsBlendWeight(0.5f);
|
|
GetMesh()->SetBodySimulatePhysics(PelvisBoneName, false);
|
|
}
|
|
|
|
// return the received damage amount
|
|
return Damage;
|
|
}
|
|
|
|
void ACombatCharacter::Landed(const FHitResult& Hit)
|
|
{
|
|
Super::Landed(Hit);
|
|
|
|
// is the character still alive?
|
|
if (CurrentHP >= 0.0f)
|
|
{
|
|
// disable ragdoll physics
|
|
GetMesh()->SetPhysicsBlendWeight(0.0f);
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::BeginPlay()
|
|
{
|
|
Super::BeginPlay();
|
|
|
|
// get the life bar from the widget component
|
|
LifeBarWidget = Cast<UCombatLifeBar>(LifeBar->GetUserWidgetObject());
|
|
check(LifeBarWidget);
|
|
|
|
// initialize the camera
|
|
GetCameraBoom()->TargetArmLength = DefaultCameraDistance;
|
|
|
|
// save the relative transform for the mesh so we can reset the ragdoll later
|
|
MeshStartingTransform = GetMesh()->GetRelativeTransform();
|
|
|
|
// set the life bar color
|
|
LifeBarWidget->SetBarColor(LifeBarColor);
|
|
|
|
// reset HP to maximum
|
|
ResetHP();
|
|
}
|
|
|
|
void ACombatCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
|
{
|
|
Super::EndPlay(EndPlayReason);
|
|
|
|
// clear the respawn timer
|
|
GetWorld()->GetTimerManager().ClearTimer(RespawnTimer);
|
|
}
|
|
|
|
void ACombatCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
|
|
{
|
|
Super::SetupPlayerInputComponent(PlayerInputComponent);
|
|
|
|
// Set up action bindings
|
|
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent))
|
|
{
|
|
// Moving
|
|
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Move);
|
|
|
|
// Looking
|
|
EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Look);
|
|
EnhancedInputComponent->BindAction(MouseLookAction, ETriggerEvent::Triggered, this, &ACombatCharacter::Look);
|
|
|
|
// Combo Attack
|
|
EnhancedInputComponent->BindAction(ComboAttackAction, ETriggerEvent::Started, this, &ACombatCharacter::ComboAttackPressed);
|
|
|
|
// Charged Attack
|
|
EnhancedInputComponent->BindAction(ChargedAttackAction, ETriggerEvent::Started, this, &ACombatCharacter::ChargedAttackPressed);
|
|
EnhancedInputComponent->BindAction(ChargedAttackAction, ETriggerEvent::Completed, this, &ACombatCharacter::ChargedAttackReleased);
|
|
|
|
// Camera Side Toggle
|
|
EnhancedInputComponent->BindAction(ToggleCameraAction, ETriggerEvent::Triggered, this, &ACombatCharacter::ToggleCamera);
|
|
}
|
|
}
|
|
|
|
void ACombatCharacter::NotifyControllerChanged()
|
|
{
|
|
Super::NotifyControllerChanged();
|
|
|
|
// update the respawn transform on the Player Controller
|
|
if (ACombatPlayerController* PC = Cast<ACombatPlayerController>(GetController()))
|
|
{
|
|
PC->SetRespawnTransform(GetActorTransform());
|
|
}
|
|
}
|
|
|