super-duper-spoon/Source/Shooter/ShooterCharacter.cpp

1274 lines
33 KiB
C++

// Fill out your copyright notice in the Description page of Project Settings.
#include "ShooterCharacter.h"
#include "Weapon.h"
#include "Item.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/WidgetComponent.h"
#include "Engine/SkeletalMeshSocket.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystemComponent.h"
#include "Sound/SoundCue.h"
#include "Ammo.h"
#include "BulletHitInterface.h"
#include "Enemy.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
// Sets default values
AShooterCharacter::AShooterCharacter() :
// Base rates for turning/looking up
BaseTurnRate(45.f),
BaseLookUpRate(45.f),
// Turn rates for aiming/not aiming
HipTurnRate(90.f),
HipLookUpRate(90.f),
AimingTurnRate(20.f),
AimingLookUpRate(20.f),
// Mouse look sensitivity scale factors
MouseHipTurnRate(1.0f),
MouseHipLookUpRate(1.0f),
MouseAimingTurnRate(0.6f),
MouseAimingLookUpRate(0.6f),
// True when aiming the weapon
bAiming(false),
// Camera field of view values
CameraDefaultFOV(0.f), // Set in BeginPlay
CameraZoomedFOV(20.f),
CameraCurrentFOV(0.f),
ZoomInterpSpeed(20.f),
// Crosshair spread factors
CrosshairSpreadMultiplier(0.f),
CrosshairVelocityFactor(0.f),
CrosshairInAirFactor(0.f),
CrosshairAimFactor(0.f),
CrosshairShootingFactor(0.f),
// Bullet fire timer variables
ShootTimeDuration(0.05f),
bFiringBullet(false),
// Automatic fire
bFireButtonPressed(false),
bShouldFire(true),
// Item trace variables
bShouldTraceForItems(false),
// Camera interp location variables
CameraInterpDistance(150.f),
CameraInterpElevation(55.f),
// Starting ammo amounts
Starting9mmAmmo(85),
StartingARAmmo(120),
// Combat variables
CombatState(ECombatState::ECS_Unoccupied),
bCrouching(false),
BaseMovementSpeed(650.f),
CrouchMovementSpeed(300.f),
StandingCapsuleHalfHeight(88.f),
CrouchingCapsuleHalfHeight(54.f),
CrouchingMeshZOffset(0.f),
BaseGroundFriction(2.f),
CrouchingGroundFriction(100.f),
bAimingButtonPressed(false),
bShouldPlayPickupSound(true),
bShouldPlayEquipSound(true),
PickupSoundResetTime(0.2f),
EquipSoundResetTime(0.2f),
// Icon animation property
HighlightedSlot(-1)
{
// 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;
// Create a camera boom (pulls in towards the character if there is a collision)
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
// The camera follows at this distance behind the character
CameraBoom->TargetArmLength = 240.f;
// Rotate the arm based on the controller
CameraBoom->bUsePawnControlRotation = true;
CameraBoom->SocketOffset = FVector(0.f, 35.f, 70.f);
// Create a follow camera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName);
// Camera does not rotate relative to arm
FollowCamera->bUsePawnControlRotation = false;
// Don't rotate when the controller does. Let the controller only affect the camera.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false;
GetCharacterMovement()->bOrientRotationToMovement = false; // Character moves in the direction of input....
GetCharacterMovement()->RotationRate = FRotator(0.f, 540.f, 0.f); // ... at this rotation rate
GetCharacterMovement()->JumpZVelocity = 600.f;
GetCharacterMovement()->AirControl = 0.2f;
// Create Hand Scene Component
HandSceneComponent = CreateDefaultSubobject<USceneComponent>(TEXT("HandSceneComp"));
// Create interpolation components
WeaponInterpComp = CreateDefaultSubobject<USceneComponent>(TEXT("Weapon Interpolation Component"));
WeaponInterpComp->SetupAttachment(GetFollowCamera());
InterpComp1 = CreateDefaultSubobject<USceneComponent>(TEXT("Interpolation Component 1"));
InterpComp1->SetupAttachment(GetFollowCamera());
InterpComp2 = CreateDefaultSubobject<USceneComponent>(TEXT("Interpolation Component 2"));
InterpComp2->SetupAttachment(GetFollowCamera());
InterpComp3 = CreateDefaultSubobject<USceneComponent>(TEXT("Interpolation Component 3"));
InterpComp3->SetupAttachment(GetFollowCamera());
InterpComp4 = CreateDefaultSubobject<USceneComponent>(TEXT("Interpolation Component 4"));
InterpComp4->SetupAttachment(GetFollowCamera());
InterpComp5 = CreateDefaultSubobject<USceneComponent>(TEXT("Interpolation Component 5"));
InterpComp5->SetupAttachment(GetFollowCamera());
InterpComp6 = CreateDefaultSubobject<USceneComponent>(TEXT("Interpolation Component 6"));
InterpComp6->SetupAttachment(GetFollowCamera());
}
// Called when the game starts or when spawned
void AShooterCharacter::BeginPlay()
{
Super::BeginPlay();
if (FollowCamera)
{
CameraDefaultFOV = FollowCamera->FieldOfView;
CameraCurrentFOV = CameraDefaultFOV;
}
// Spawn the default weapon and equip it
EquipWeapon(SpawnDefaultWeapon());
Inventory.Add(EquippedWeapon);
EquippedWeapon->SetSlotIndex(0);
EquippedWeapon->SetCharacter(this);
InitializeAmmoMap();
GetCharacterMovement()->MaxWalkSpeed = BaseMovementSpeed;
// Create FInterpLocation structs for each interp location. Add to array
InitializeInterpLocations();
}
// Called to bind functionality to input
void AShooterCharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
check(PlayerInputComponent);
PlayerInputComponent->BindAxis("MoveForward", this, &AShooterCharacter::MoveForward);
PlayerInputComponent->BindAxis("MoveRight", this, &AShooterCharacter::MoveRight);
PlayerInputComponent->BindAxis("TurnRate", this, &AShooterCharacter::TurnAtRate);
PlayerInputComponent->BindAxis("LookUpRate", this, &AShooterCharacter::LookUpAtRate);
PlayerInputComponent->BindAxis("Turn", this, &AShooterCharacter::Turn);
PlayerInputComponent->BindAxis("LookUp", this, &AShooterCharacter::LookUp);
PlayerInputComponent->BindAction("Jump", IE_Pressed, this, &AShooterCharacter::Jump);
PlayerInputComponent->BindAction("Jump", IE_Released, this, &ACharacter::StopJumping);
PlayerInputComponent->BindAction("FireButton", IE_Pressed, this, &AShooterCharacter::FireButtonPressed);
PlayerInputComponent->BindAction("FireButton", IE_Released, this, &AShooterCharacter::FireButtonReleased);
PlayerInputComponent->BindAction("Aiming", IE_Pressed, this, &AShooterCharacter::AimingButtonPressed);
PlayerInputComponent->BindAction("Aiming", IE_Released, this, &AShooterCharacter::AimingButtonReleased);
PlayerInputComponent->BindAction("Select", IE_Pressed, this, &AShooterCharacter::SelectButtonPressed);
PlayerInputComponent->BindAction("Select", IE_Released, this, &AShooterCharacter::SelectButtonReleased);
PlayerInputComponent->BindAction("Reload", IE_Pressed, this, &AShooterCharacter::ReloadButtonPressed);
PlayerInputComponent->BindAction("Crouch", IE_Pressed, this, &AShooterCharacter::CrouchButtonPressed);
PlayerInputComponent->BindAction("FKey", IE_Pressed, this, &AShooterCharacter::KeyFPressed);
PlayerInputComponent->BindAction("1Key", IE_Pressed, this, &AShooterCharacter::Key1Pressed);
PlayerInputComponent->BindAction("2Key", IE_Pressed, this, &AShooterCharacter::Key2Pressed);
PlayerInputComponent->BindAction("3Key", IE_Pressed, this, &AShooterCharacter::Key3Pressed);
PlayerInputComponent->BindAction("4Key", IE_Pressed, this, &AShooterCharacter::Key4Pressed);
PlayerInputComponent->BindAction("5Key", IE_Pressed, this, &AShooterCharacter::Key5Pressed);
}
// Called every frame
void AShooterCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Handle interpolation for zoom when aiming
CameraInterpZoom(DeltaTime);
// Change look sensitivity based on aiming
SetLookRates();
// Calculate crosshair spread multiplier
CalculateCrosshairSpread(DeltaTime);
TraceForItems();
// Interpolate capsule half height based on crouching/standing
InterpCapsuleHalfHeight(DeltaTime);
}
void AShooterCharacter::MoveForward(float Value)
{
if (Controller && (Value != 0))
{
// Find out which way is forward
const FRotator Rotation{ Controller->GetControlRotation() };
const FRotator YawRotation{ 0, Rotation.Yaw, 0 };
const FVector Direction{ FRotationMatrix{YawRotation}.GetUnitAxis(EAxis::X) };
AddMovementInput(Direction, Value);
}
}
void AShooterCharacter::MoveRight(float Value)
{
if (Controller && (Value != 0))
{
// Find out which way is forward
const FRotator Rotation{ Controller->GetControlRotation() };
const FRotator YawRotation{ 0, Rotation.Yaw, 0 };
const FVector Direction{ FRotationMatrix{YawRotation}.GetUnitAxis(EAxis::Y) };
AddMovementInput(Direction, Value);
}
}
void AShooterCharacter::TurnAtRate(float Rate)
{
// Calculate delta for this frame from the rate information
AddControllerYawInput(Rate * BaseTurnRate * GetWorld()->GetDeltaSeconds()); // def/sec * sec/frame
}
void AShooterCharacter::LookUpAtRate(float Rate)
{
AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds()); // def/sec * sec/frame
}
void AShooterCharacter::Turn(float Value)
{
float TurnScaleFactor;
if (bAiming)
{
TurnScaleFactor = MouseAimingTurnRate;
}
else
{
TurnScaleFactor = MouseHipTurnRate;
}
AddControllerYawInput(Value * TurnScaleFactor);
}
void AShooterCharacter::LookUp(float Value)
{
float LookUpScaleFactor;
if (bAiming)
{
LookUpScaleFactor = MouseAimingLookUpRate;
}
else
{
LookUpScaleFactor = MouseHipLookUpRate;
}
AddControllerPitchInput(Value * LookUpScaleFactor);
}
void AShooterCharacter::AimingButtonPressed()
{
bAimingButtonPressed = true;
if (CombatState != ECombatState::ECS_Reloading && CombatState != ECombatState::ECS_Equipping)
{
Aim();
}
}
void AShooterCharacter::AimingButtonReleased()
{
bAimingButtonPressed = false;
StopAiming();
}
void AShooterCharacter::Aim()
{
bAiming = true;
GetCharacterMovement()->MaxWalkSpeed = CrouchMovementSpeed;
}
void AShooterCharacter::StopAiming()
{
bAiming = false;
if (!bCrouching)
{
GetCharacterMovement()->MaxWalkSpeed = BaseMovementSpeed;
}
}
void AShooterCharacter::PickupAmmo(AAmmo* Ammo)
{
// Check to see if AmmoMap contains Ammo's AmmoType
if (AmmoMap.Find(Ammo->GetAmmoType()))
{
// Get amount of ammo in our AmmoMap for Ammo's type
int32 AmmoCount{ AmmoMap[Ammo->GetAmmoType()] };
AmmoCount += Ammo->GetItemCount();
// Set amount of ammo in our AmmoMap for Ammo's type
AmmoMap[Ammo->GetAmmoType()] = AmmoCount;
}
if (EquippedWeapon->GetAmmoType() == Ammo->GetAmmoType())
{
// Check to see if the gun is empty
if (EquippedWeapon->GetAmmo() == 0)
{
ReloadWeapon();
}
}
Ammo->Destroy();
}
void AShooterCharacter::InitializeInterpLocations()
{
struct FInterpLocation WeaponLocation{ WeaponInterpComp, 0 };
InterpLocations.Add(WeaponLocation);
struct FInterpLocation InerpLoc1{ InterpComp1, 0 };
InterpLocations.Add(InerpLoc1);
struct FInterpLocation InerpLoc2{ InterpComp2, 0 };
InterpLocations.Add(InerpLoc2);
struct FInterpLocation InerpLoc3{ InterpComp3, 0 };
InterpLocations.Add(InerpLoc3);
struct FInterpLocation InerpLoc4{ InterpComp4, 0 };
InterpLocations.Add(InerpLoc4);
struct FInterpLocation InerpLoc5{ InterpComp5, 0 };
InterpLocations.Add(InerpLoc5);
struct FInterpLocation InerpLoc6{ InterpComp6, 0 };
InterpLocations.Add(InerpLoc6);
}
void AShooterCharacter::ResetPickupSoundTimer()
{
bShouldPlayPickupSound = true;
}
void AShooterCharacter::ResetEquipSoundTimer()
{
bShouldPlayEquipSound = true;
}
void AShooterCharacter::KeyFPressed()
{
if (EquippedWeapon->GetSlotIndex() == 0) return;
ExchangeInventoryItems(EquippedWeapon->GetSlotIndex(), 0);
}
void AShooterCharacter::Key1Pressed()
{
if (EquippedWeapon->GetSlotIndex() == 1) return;
ExchangeInventoryItems(EquippedWeapon->GetSlotIndex(), 1);
}
void AShooterCharacter::Key2Pressed()
{
if (EquippedWeapon->GetSlotIndex() == 2) return;
ExchangeInventoryItems(EquippedWeapon->GetSlotIndex(), 2);
}
void AShooterCharacter::Key3Pressed()
{
if (EquippedWeapon->GetSlotIndex() == 3) return;
ExchangeInventoryItems(EquippedWeapon->GetSlotIndex(), 3);
}
void AShooterCharacter::Key4Pressed()
{
if (EquippedWeapon->GetSlotIndex() == 4) return;
ExchangeInventoryItems(EquippedWeapon->GetSlotIndex(), 4);
}
void AShooterCharacter::Key5Pressed()
{
if (EquippedWeapon->GetSlotIndex() == 5) return;
ExchangeInventoryItems(EquippedWeapon->GetSlotIndex(), 5);
}
void AShooterCharacter::ExchangeInventoryItems(int32 CurrentItemIndex, int32 NewItemIndex)
{
if (CurrentItemIndex == NewItemIndex
|| NewItemIndex >= Inventory.Num()
|| (CombatState != ECombatState::ECS_Unoccupied && CombatState != ECombatState::ECS_Equipping))
return;
if (bAiming)
{
StopAiming();
}
auto OldEquippedWeapon = EquippedWeapon;
auto NewWeapon = Cast<AWeapon>(Inventory[NewItemIndex]);
EquipWeapon(NewWeapon);
OldEquippedWeapon->SetItemState(EItemState::EIS_PickedUp);
NewWeapon->SetItemState(EItemState::EIS_Equipped);
CombatState = ECombatState::ECS_Equipping;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && EquipMontage)
{
AnimInstance->Montage_Play(EquipMontage, 1.0f);
AnimInstance->Montage_JumpToSection(FName("Equip"));
}
NewWeapon->PlayEquipSound(true);
}
int32 AShooterCharacter::GetEmptyInventorySlot()
{
for (int32 i = 0; i < Inventory.Num(); ++i)
{
if (Inventory[i] == nullptr)
return i;
}
if (Inventory.Num() < INVENTORY_CAPACITY)
{
return Inventory.Num();
}
return -1; // Inventory full
}
void AShooterCharacter::HighlightInventorySlot()
{
const int32 EmptySlot{ GetEmptyInventorySlot() };
if (EmptySlot != -1)
{
HighlightIconDelegate.Broadcast(EmptySlot, true);
HighlightedSlot = EmptySlot;
}
}
EPhysicalSurface AShooterCharacter::GetSurfaceType()
{
FHitResult HitResult;
const FVector Start{ GetActorLocation() };
const FVector End{ Start + FVector(0.f, 0.f, -400.f) };
FCollisionQueryParams QueryParams;
QueryParams.bReturnPhysicalMaterial = true;
GetWorld()->LineTraceSingleByChannel(HitResult, Start, End, ECollisionChannel::ECC_Visibility, QueryParams);
return UPhysicalMaterial::DetermineSurfaceType(HitResult.PhysMaterial.Get());
}
void AShooterCharacter::UnhighlightInventorySlot()
{
if (HighlightedSlot != -1)
{
HighlightIconDelegate.Broadcast(HighlightedSlot, false);
HighlightedSlot = -1;
}
}
int32 AShooterCharacter::GetInterpLocationIndex()
{
int32 LowestIndex = 1;
int32 LowestCount = INT_MAX;
for (int32 i = 1; i < InterpLocations.Num(); ++i)
{
if (InterpLocations[i].ItemCount < LowestCount)
{
LowestIndex = i;
LowestCount = InterpLocations[i].ItemCount;
}
}
return LowestIndex;
}
void AShooterCharacter::IncrementInterpLocItemCount(int32 Index, int32 Amount)
{
if (Amount < -1 || Amount > 1) return;
if (Index < InterpLocations.Num())
{
InterpLocations[Index].ItemCount += Amount;
}
}
void AShooterCharacter::StartPickupSoundTimer()
{
bShouldPlayPickupSound = false;
GetWorldTimerManager().SetTimer(PickupSoundTimer, this, &AShooterCharacter::ResetPickupSoundTimer, PickupSoundResetTime);
}
void AShooterCharacter::StartEquipSoundTimer()
{
bShouldPlayEquipSound = false;
GetWorldTimerManager().SetTimer(EquipSoundTimer, this, &AShooterCharacter::ResetEquipSoundTimer, EquipSoundResetTime);
}
void AShooterCharacter::CameraInterpZoom(float DeltaTime)
{
// Set current camera field of view
if (bAiming)
{
// Interpolate to zoomed FOV
CameraCurrentFOV = FMath::FInterpTo(CameraCurrentFOV, CameraZoomedFOV,
DeltaTime, ZoomInterpSpeed);
}
else
{
// Interpolate to default FOV
CameraCurrentFOV = FMath::FInterpTo(CameraCurrentFOV, CameraDefaultFOV,
DeltaTime, ZoomInterpSpeed);
}
if (FollowCamera)
{
FollowCamera->SetFieldOfView(CameraCurrentFOV);
}
}
void AShooterCharacter::SetLookRates()
{
if (bAiming)
{
BaseTurnRate = AimingTurnRate;
BaseLookUpRate = AimingLookUpRate;
}
else
{
BaseTurnRate = HipTurnRate;
BaseLookUpRate = HipLookUpRate;
}
}
void AShooterCharacter::FireButtonPressed()
{
bFireButtonPressed = true;
FireWeapon();
}
void AShooterCharacter::FireButtonReleased()
{
bFireButtonPressed = false;
}
void AShooterCharacter::FireWeapon()
{
if (!EquippedWeapon) return;
if (CombatState != ECombatState::ECS_Unoccupied) return;
if (!WeaponHasAmmo()) return;
PlayFireSound();
SendBullet();
PlayGunFireMontage();
EquippedWeapon->DecrementAmmo();
StartCrosshairBulletFire();
StartFireTimer();
if (EquippedWeapon->GetWeaponType() == EWeaponType::EWT_Pistol)
{
// Start moving slide timer
EquippedWeapon->StartSlideTimer();
}
}
void AShooterCharacter::StartFireTimer()
{
if (!EquippedWeapon) return;
CombatState = ECombatState::ECS_FireTimerInProgress;
GetWorldTimerManager().SetTimer(AutoFireTimer, this, &AShooterCharacter::AutoFireReset,
EquippedWeapon->GetAutoFireRate());
}
void AShooterCharacter::AutoFireReset()
{
if (!EquippedWeapon) return;
CombatState = ECombatState::ECS_Unoccupied;
if (WeaponHasAmmo())
{
if (bFireButtonPressed && EquippedWeapon->GetAutomatic())
{
FireWeapon();
}
}
else
{
ReloadWeapon();
}
}
void AShooterCharacter::PlayFireSound()
{
// Play fire sound
if (!EquippedWeapon) return;
if (EquippedWeapon->GetFireSound())
{
UGameplayStatics::PlaySound2D(this, EquippedWeapon->GetFireSound());
}
}
void AShooterCharacter::SendBullet()
{
// Send bullet
const USkeletalMeshSocket* BarrelSocket = EquippedWeapon->GetItemMesh()->GetSocketByName("BarrelSocket");
if (!BarrelSocket) return;
const FTransform SocketTransform = BarrelSocket->GetSocketTransform(EquippedWeapon->GetItemMesh());
if (!EquippedWeapon) return;
if (EquippedWeapon->GetMuzzleFlash())
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), EquippedWeapon->GetMuzzleFlash(), SocketTransform);
}
FHitResult BeamHitResult;
bool bBeamEnd = GetBeamEndLocation(SocketTransform.GetLocation(), BeamHitResult);
if (!bBeamEnd) return;
// Spawn impact particles after updating BeamEndPoint
// Does hit actor implement BulletHitInterface?
bool spawnGenericImpactParticles = false;
if (IsValid(BeamHitResult.GetActor()))
{
if (IBulletHitInterface* BulletHitInterface = Cast<IBulletHitInterface>(BeamHitResult.GetActor()))
{
BulletHitInterface->BulletHit_Implementation(BeamHitResult);
}
else
{
spawnGenericImpactParticles = true;
}
if (AEnemy* HitEnemy = Cast<AEnemy>(BeamHitResult.GetActor()))
{
float damageToApply;
if (BeamHitResult.BoneName.ToString() == HitEnemy->GetHeadBone())
{
damageToApply = EquippedWeapon->GetHeadshotDamage();
HitEnemy->ShowHitNumber(damageToApply, BeamHitResult.Location, true);
}
else
{
damageToApply = EquippedWeapon->GetDamage();
HitEnemy->ShowHitNumber(damageToApply, BeamHitResult.Location, false);
}
UGameplayStatics::ApplyDamage(HitEnemy,
damageToApply,
GetController(), this,
UDamageType::StaticClass());
}
}
else
{
spawnGenericImpactParticles = true;
}
if (ImpactParticles && spawnGenericImpactParticles)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles,
BeamHitResult.Location);
}
// Spawn smoke trail particles
if (BeamParticles)
{
UParticleSystemComponent* Beam = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), BeamParticles,
SocketTransform);
if (Beam)
{
Beam->SetVectorParameter("Target", BeamHitResult.Location);
}
}
}
void AShooterCharacter::PlayGunFireMontage()
{
// Play gun fire montage
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && HipFireMontage)
{
AnimInstance->Montage_Play(HipFireMontage);
AnimInstance->Montage_JumpToSection(FName("StartFire"));
}
}
bool AShooterCharacter::GetBeamEndLocation(const FVector& MuzzleSocketLocation, FHitResult& OutHitResult)
{
FVector OutBeamLocation;
// Check for crosshair trace hit
FHitResult CrosshairHitResult;
bool bCrosshairHit = TraceUnderCrosshairs(CrosshairHitResult, OutBeamLocation);
// Perform a second trace, this time from the gun barrel
const FVector WeaponTraceStart{ MuzzleSocketLocation };
const FVector StartToEnd{ OutBeamLocation - MuzzleSocketLocation };
const FVector WeaponTraceEnd{ MuzzleSocketLocation + StartToEnd * 1.25f };
GetWorld()->LineTraceSingleByChannel(OutHitResult, WeaponTraceStart, WeaponTraceEnd,
ECollisionChannel::ECC_Visibility);
if (!OutHitResult.bBlockingHit) // Object between barrel and BeamEndPoint ?
{
OutHitResult.Location = OutBeamLocation;
return false;
}
return true;
}
bool AShooterCharacter::TraceUnderCrosshairs(FHitResult& OutHitResult, FVector& OutHitLocation)
{
// Get Viewport Size
FVector2D ViewportSize;
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(ViewportSize);
}
// Get screen space location of crosshairs
FVector2D CrosshairLocation{ ViewportSize.X / 2.f, ViewportSize.Y / 2.f };
// Get world position and direction of crosshairs
FVector CrosshairWorldPosition;
FVector CrosshairWorldDirection;
bool bScreenToWorld = UGameplayStatics::DeprojectScreenToWorld(
UGameplayStatics::GetPlayerController(this, 0),
CrosshairLocation, CrosshairWorldPosition, CrosshairWorldDirection);
if (bScreenToWorld)
{
// Trace from Crosshair world location outward
const FVector Start{ CrosshairWorldPosition };
const FVector End{ Start + CrosshairWorldDirection * 50'000.f };
OutHitLocation = End;
GetWorld()->LineTraceSingleByChannel(OutHitResult, Start, End, ECollisionChannel::ECC_Visibility);
if (OutHitResult.bBlockingHit)
{
OutHitLocation = OutHitResult.Location;
return true;
}
}
return false;
}
void AShooterCharacter::StartCrosshairBulletFire()
{
bFiringBullet = true;
GetWorldTimerManager().SetTimer(CrosshairShootTimer, this,
&AShooterCharacter::FinishCrosshairBulletFire, ShootTimeDuration);
}
void AShooterCharacter::FinishCrosshairBulletFire()
{
bFiringBullet = false;
}
void AShooterCharacter::CalculateCrosshairSpread(float DeltaTime)
{
FVector2D WalkSpeedRange{ 0.f, 600.f };
FVector2D VelocityMultiplierRange{ 0.f, 1.f };
FVector Velocity{ GetVelocity() };
Velocity.Z = 0.f;
// Calculate crosshair velocity factor
CrosshairVelocityFactor = FMath::GetMappedRangeValueClamped(WalkSpeedRange,
VelocityMultiplierRange, Velocity.Size());
// Calculate crosshair in air factor
if (GetCharacterMovement()->IsFalling())
{
// Spread the crosshairs quickly while in air
CrosshairInAirFactor = FMath::FInterpTo(CrosshairInAirFactor, 2.25f,
DeltaTime, 20.f);
}
else // Character in on the ground
{
// Spread the crosshairs slowly when on the ground
CrosshairInAirFactor = FMath::FInterpTo(CrosshairInAirFactor, 0,
DeltaTime, 2.25f);
}
// Calculate crosshair aim factor
if (bAiming)
{
// Group the crosshairs when aiming
CrosshairAimFactor = FMath::FInterpTo(CrosshairAimFactor, 0.6,
DeltaTime, 30.f);
}
else
{
// Spread the crosshairs when not aiming
CrosshairAimFactor = FMath::FInterpTo(CrosshairAimFactor, 0,
DeltaTime, 30.f);
}
// True 0.05s after firing
if (bFiringBullet)
{
CrosshairShootingFactor = FMath::FInterpTo(CrosshairShootingFactor, 0.3f,
DeltaTime, 60.f);
}
else
{
CrosshairShootingFactor = FMath::FInterpTo(CrosshairShootingFactor, 0.f,
DeltaTime, 60.f);
}
CrosshairSpreadMultiplier = 0.5f
+ CrosshairVelocityFactor
+ CrosshairInAirFactor
+ CrosshairShootingFactor
- CrosshairAimFactor;
}
void AShooterCharacter::TraceForItems()
{
if (bShouldTraceForItems)
{
FHitResult ItemTraceResult;
FVector HitLocation;
TraceUnderCrosshairs(ItemTraceResult, HitLocation);
TraceHitItem = Cast<AItem>(ItemTraceResult.GetActor());
if (TraceHitItem && ItemTraceResult.bBlockingHit)
{
// If an item was being looked at from last frame, we should hide its widget
if (OldTraceHitItem != TraceHitItem)
{
UnhighlightInventorySlot();
SetItemPickupWidgetVisibility(OldTraceHitItem, false);
}
if (TraceHitItem && TraceHitItem->GetItemState() == EItemState::EIS_EquipInterping)
TraceHitItem = nullptr;
const auto TraceHitWeapon = Cast<AWeapon>(TraceHitItem);
if (TraceHitWeapon && HighlightedSlot == -1)
HighlightInventorySlot();
// Show the new item's widget
SetItemPickupWidgetVisibility(TraceHitItem, true);
if (Inventory.Num() >= INVENTORY_CAPACITY)
{
// Inventory is full
TraceHitItem->SetCharacterInventoryFull(true);
}
else
{
TraceHitItem->SetCharacterInventoryFull(false);
}
OldTraceHitItem = TraceHitItem;
}
else if (OldTraceHitItem) // Hide widget for the item from last frame
{
UnhighlightInventorySlot();
SetItemPickupWidgetVisibility(OldTraceHitItem, false);
OldTraceHitItem = nullptr;
}
}
else if (TraceHitItem) // Hide widget for the item from last frame
{
UnhighlightInventorySlot();
SetItemPickupWidgetVisibility(TraceHitItem, false);
TraceHitItem = nullptr;
}
}
void AShooterCharacter::SetItemPickupWidgetVisibility(AItem* Item, bool visibility)
{
if (Item && Item->GetPickupWidget())
{
Item->GetPickupWidget()->SetVisibility(visibility);
visibility ?
Item->EnableCustomDepth() :
Item->DisableCustomDepth();
}
}
AWeapon* AShooterCharacter::SpawnDefaultWeapon()
{
// Check the TSubclassOf variable
if (DefaultWeaponClass)
{
// Spawn the weapon
return GetWorld()->SpawnActor<AWeapon>(DefaultWeaponClass);
}
return nullptr;
}
void AShooterCharacter::EquipWeapon(AWeapon* WeaponToEquip, bool bSwapping)
{
// Get the hand socket
const USkeletalMeshSocket* HandSocket = GetMesh()->GetSocketByName(FName("RightHandSocket"));
if (HandSocket && WeaponToEquip)
{
// Attach the weapon to the hand socket RightHandSocket
HandSocket->AttachActor(WeaponToEquip, GetMesh());
if (EquippedWeapon == nullptr)
{
// -1 == no EquippedWeapon yet. No need to reverse the icon animation
EquipItemDelegate.Broadcast(-1, WeaponToEquip->GetSlotIndex());
}
else if (!bSwapping)
{
EquipItemDelegate.Broadcast(EquippedWeapon->GetSlotIndex(), WeaponToEquip->GetSlotIndex());
}
EquippedWeapon = WeaponToEquip;
EquippedWeapon->SetItemState(EItemState::EIS_Equipped);
EquippedWeapon->DisableGlowMaterial();
EquippedWeapon->DisableCustomDepth();
}
}
void AShooterCharacter::DropWeapon()
{
if (EquippedWeapon)
{
FDetachmentTransformRules DetachmentTransformRules(EDetachmentRule::KeepWorld, true);
EquippedWeapon->GetItemMesh()->DetachFromComponent(DetachmentTransformRules);
EquippedWeapon->SetItemState(EItemState::EIS_Falling);
EquippedWeapon->ThrowWeapon();
}
}
void AShooterCharacter::SelectButtonPressed()
{
if (CombatState != ECombatState::ECS_Unoccupied) return;
if (TraceHitItem)
{
TraceHitItem->StartItemCurve(this, true);
TraceHitItem = nullptr;
}
}
void AShooterCharacter::SelectButtonReleased()
{
}
void AShooterCharacter::SwapWeapon(AWeapon* WeaponToSwap)
{
if (EquippedWeapon->GetSlotIndex() <= Inventory.Num() - 1)
{
Inventory[EquippedWeapon->GetSlotIndex()] = WeaponToSwap;
WeaponToSwap->SetSlotIndex(EquippedWeapon->GetSlotIndex());
}
DropWeapon();
EquipWeapon(WeaponToSwap, true);
}
void AShooterCharacter::InitializeAmmoMap()
{
AmmoMap.Add(EAmmoType::EAT_9mm, Starting9mmAmmo);
AmmoMap.Add(EAmmoType::EAT_AR, StartingARAmmo);
}
bool AShooterCharacter::WeaponHasAmmo()
{
if (!EquippedWeapon) return false;
return (EquippedWeapon->GetAmmo() > 0);
}
float AShooterCharacter::GetCrosshairSpreadMultiplier() const
{
return CrosshairSpreadMultiplier;
}
void AShooterCharacter::IncrementOverlappedItemCount(int8 Amount)
{
if (OverlappedItemCount + Amount <= 0)
{
OverlappedItemCount = 0;
bShouldTraceForItems = false;
}
else
{
OverlappedItemCount += Amount;
bShouldTraceForItems = true;
}
}
// No longer needed; AItem has GetInterpLocation
//FVector AShooterCharacter::GetCameraInterpLocation()
//{
// const FVector CameraWorldLocation{ FollowCamera->GetComponentLocation() };
// const FVector CameraForward{ FollowCamera->GetForwardVector() };
//
// // Desired = CameraWorldLocation + Forward * A + Up * B
// return CameraWorldLocation + CameraForward * CameraInterpDistance
// + FVector(0.f, 0.f, CameraInterpElevation);
//}
void AShooterCharacter::GetPickupItem(AItem* Item)
{
Item->PlayEquipSound();
auto Weapon = Cast<AWeapon>(Item);
if (Weapon)
{
if (Inventory.Num() < INVENTORY_CAPACITY)
{
Weapon->SetSlotIndex(Inventory.Num());
Inventory.Add(Weapon);
Weapon->SetItemState(EItemState::EIS_PickedUp);
}
else // Inventory is full! Swap with EquippedWeapon
{
SwapWeapon(Weapon);
}
}
auto Ammo = Cast<AAmmo>(Item);
if (Ammo)
{
PickupAmmo(Ammo);
}
}
FInterpLocation AShooterCharacter::GetInterpLocation(int32 Index)
{
if (Index <= InterpLocations.Num())
{
return InterpLocations[Index];
}
return FInterpLocation();
}
void AShooterCharacter::ReloadButtonPressed()
{
ReloadWeapon();
}
void AShooterCharacter::ReloadWeapon()
{
if (CombatState != ECombatState::ECS_Unoccupied) return;
if (!EquippedWeapon) return;
// Do we have ammo for the correct type
if (CarryingAmmo() && !EquippedWeapon->ClipIsFull())
{
if (bAiming) StopAiming();
CombatState = ECombatState::ECS_Reloading;
UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
if (AnimInstance && ReloadMontage)
{
AnimInstance->Montage_Play(ReloadMontage);
AnimInstance->Montage_JumpToSection(EquippedWeapon->GetReloadMontageSection());
}
}
}
void AShooterCharacter::FinishReloading()
{
if (!EquippedWeapon) return;
CombatState = ECombatState::ECS_Unoccupied;
if (const auto AmmoType { EquippedWeapon->GetAmmoType() }; AmmoMap.Contains(AmmoType))
{
int32 CarriedAmmo = AmmoMap[AmmoType];
if (const int32 MagEmptySpace = EquippedWeapon->GetMagazineCapacity() - EquippedWeapon->GetAmmo();
MagEmptySpace > CarriedAmmo)
{
// Reload the mag with all the ammo we're carrying
EquippedWeapon->ReloadAmmo(CarriedAmmo);
CarriedAmmo = 0;
AmmoMap.Add(AmmoType, CarriedAmmo);
}
else
{
// Fill the mag
EquippedWeapon->ReloadAmmo(MagEmptySpace);
CarriedAmmo -= MagEmptySpace;
AmmoMap.Add(AmmoType, CarriedAmmo);
}
}
if (bAimingButtonPressed)
{
Aim();
}
if (WeaponHasAmmo())
{
if (bFireButtonPressed && EquippedWeapon->GetAutomatic())
{
FireWeapon();
}
}
}
void AShooterCharacter::FinishEquipping()
{
CombatState = ECombatState::ECS_Unoccupied;
if (bAimingButtonPressed)
{
Aim();
}
if (WeaponHasAmmo())
{
if (bFireButtonPressed && EquippedWeapon->GetAutomatic())
{
FireWeapon();
}
}
}
bool AShooterCharacter::CarryingAmmo()
{
if (!EquippedWeapon) return false;
if (const auto AmmoType = EquippedWeapon->GetAmmoType(); AmmoMap.Contains(AmmoType))
{
return AmmoMap[AmmoType] > 0;
}
return false;
}
void AShooterCharacter::GrabClip()
{
if (!EquippedWeapon) return;
if (!HandSceneComponent) return;
// Index for the clip bone on the Equipped weapon
int32 ClipBoneIndex{ EquippedWeapon->GetItemMesh()->GetBoneIndex(EquippedWeapon->GetClipBoneName()) };
// Store the transform of the clip
ClipTransform = EquippedWeapon->GetItemMesh()->GetBoneTransform(ClipBoneIndex);
HandSceneComponent->SetWorldTransform(ClipTransform);
// Attach Scene component to hand bone
FAttachmentTransformRules AttachmentRules(EAttachmentRule::KeepRelative, true);
HandSceneComponent->AttachToComponent(GetMesh(), AttachmentRules, FName(TEXT("Hand_L")));
EquippedWeapon->SetMovingClip(true);
}
void AShooterCharacter::ReleaseClip()
{
EquippedWeapon->SetMovingClip(false);
}
void AShooterCharacter::CrouchButtonPressed()
{
if (!GetCharacterMovement()->IsFalling())
{
bCrouching = !bCrouching;
}
if (bCrouching)
{
GetCharacterMovement()->MaxWalkSpeed = CrouchMovementSpeed;
GetCharacterMovement()->GroundFriction = CrouchingGroundFriction;
}
else
{
GetCharacterMovement()->MaxWalkSpeed = BaseMovementSpeed;
GetCharacterMovement()->GroundFriction = BaseGroundFriction;
}
}
void AShooterCharacter::Jump()
{
if (bCrouching)
{
bCrouching = false;
GetCharacterMovement()->MaxWalkSpeed = BaseMovementSpeed;
GetCharacterMovement()->GroundFriction = BaseGroundFriction;
}
else
{
Super::Jump();
}
}
void AShooterCharacter::InterpCapsuleHalfHeight(float DeltaTime)
{
float TargetCapsuleHalfHeight{};
// float TargetMeshOffset{};
if (bCrouching)
{
TargetCapsuleHalfHeight = CrouchingCapsuleHalfHeight;
// TargetMeshOffset = CrouchingMeshZOffset;
}
else
{
TargetCapsuleHalfHeight = StandingCapsuleHalfHeight;
// TargetMeshOffset = 0;
}
const float InterpHalfHeight{ FMath::FInterpTo(
GetCapsuleComponent()->GetScaledCapsuleHalfHeight(),
TargetCapsuleHalfHeight, DeltaTime, 20.f)};
// const double InterpMeshOffset{ FMath::FInterpTo(
// GetMesh()->GetRelativeLocation().Z,
// TargetMeshOffset, DeltaTime, 20.f)};
// Negative value if crouching, positive if standing
const float DeltaCapsuleHalfHeight{ InterpHalfHeight - GetCapsuleComponent()->GetScaledCapsuleHalfHeight() };
// const double DeltaMeshOffset{ -DeltaCapsuleHalfHeight + (InterpMeshOffset - GetMesh()->GetRelativeLocation().Z) };
const FVector MeshOffset{0.f, 0.f, -DeltaCapsuleHalfHeight};
GetMesh()->AddLocalOffset(MeshOffset);
GetCapsuleComponent()->SetCapsuleHalfHeight(InterpHalfHeight);
}