#include "ShooterCharacter.h" #include "GameFramework/SpringArmComponent.h" #include "Camera/CameraComponent.h" #include "GameFramework/CharacterMovementComponent.h" #include "Kismet/GameplayStatics.h" #include "Sound/SoundCue.h" #include "Engine/SkeletalMeshSocket.h" #include "DrawDebugHelpers.h" #include "Particles/ParticleSystemComponent.h" #include "Item.h" #include "Weapon.h" #include "Components/WidgetComponent.h" #include "Components/SphereComponent.h" #include "Components/BoxComponent.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.2f), MouseAimingLookUpRate(0.2f), // true when aiming the weapon bAiming(false), // Camera field of view values CameraDefaultFOV(0.f), // set in BeginPlay CameraZoomedFOV(35.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 variables AutomaticFireRate(0.1f), bShouldFire(true), bFireButtonPressed(false), // Item trace variables bShouldTraceForItems(false), OverlappedItemCount(0) { // 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(TEXT("CameraBoom")); CameraBoom->SetupAttachment(RootComponent); CameraBoom->TargetArmLength = 180.f; // The camera follows at this distance behind the character CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller CameraBoom->SocketOffset = FVector(0.f, 50.f, 70.f); // Create a follow camera FollowCamera = CreateDefaultSubobject(TEXT("FollowCamera")); FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach camera to end of boom FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm // Don't rotate when the controller rotates. Let the controller only affect the camera. bUseControllerRotationPitch = false; bUseControllerRotationYaw = true; bUseControllerRotationRoll = false; // Configure character movement 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; } // Called when the game starts or when spawned void AShooterCharacter::BeginPlay() { Super::BeginPlay(); if (FollowCamera) { CameraDefaultFOV = GetFollowCamera()->FieldOfView; CameraCurrentFOV = CameraDefaultFOV; } // Spawn the default weapon and equip it EquipWeapon(SpawnDefaultWeapon()); } void AShooterCharacter::MoveForward(float Value) { if ((Controller != nullptr) && (Value != 0.0f)) { // 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 != nullptr) && (Value != 0.0f)) { // find out which way is right 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()); // deg/sec * sec/frame } void AShooterCharacter::LookUpAtRate(float Rate) { AddControllerPitchInput(Rate * BaseLookUpRate * GetWorld()->GetDeltaSeconds()); // deg/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::FireWeapon() { if (FireSound) { UGameplayStatics::PlaySound2D(this, FireSound); } const USkeletalMeshSocket* BarrelSocket = GetMesh()->GetSocketByName("BarrelSocket"); if (BarrelSocket) { const FTransform SocketTransform = BarrelSocket->GetSocketTransform(GetMesh()); if (MuzzleFlash) { UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), MuzzleFlash, SocketTransform); } FVector BeamEnd; bool bBeamEnd = GetBeamEndLocation( SocketTransform.GetLocation(), BeamEnd); if (bBeamEnd) { if (ImpactParticles) { UGameplayStatics::SpawnEmitterAtLocation( GetWorld(), ImpactParticles, BeamEnd); } UParticleSystemComponent* Beam = UGameplayStatics::SpawnEmitterAtLocation( GetWorld(), BeamParticles, SocketTransform); if (Beam) { Beam->SetVectorParameter(FName("Target"), BeamEnd); } } } UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance(); if (AnimInstance && HipFireMontage) { AnimInstance->Montage_Play(HipFireMontage); AnimInstance->Montage_JumpToSection(FName("StartFire")); } // Start bullet fire timer for crosshairs StartCrosshairBulletFire(); } bool AShooterCharacter::GetBeamEndLocation( const FVector& MuzzleSocketLocation, FVector& OutBeamLocation) { // Check for crosshair trace hit FHitResult CrosshairHitResult; bool bCrosshairHit = TraceUnderCrosshairs(CrosshairHitResult, OutBeamLocation); if (bCrosshairHit) { // Tentative beam location - still need to trace from gun OutBeamLocation = CrosshairHitResult.Location; } else // no crosshair trace hit { // OutBeamLocation is the End location for the line trace } // Perform a second trace, this time from the gun barrel FHitResult WeaponTraceHit; const FVector WeaponTraceStart{ MuzzleSocketLocation }; const FVector StartToEnd{ OutBeamLocation - WeaponTraceStart }; const FVector WeaponTraceEnd{ MuzzleSocketLocation + StartToEnd * 1.25f }; GetWorld()->LineTraceSingleByChannel( WeaponTraceHit, WeaponTraceStart, WeaponTraceEnd, ECollisionChannel::ECC_Visibility); if (WeaponTraceHit.bBlockingHit) // object between barrel and BeamEndPoint? { OutBeamLocation = WeaponTraceHit.Location; return true; } return false; } void AShooterCharacter::AimingButtonPressed() { bAiming = true; } void AShooterCharacter::AimingButtonReleased() { bAiming = false; } 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); } GetFollowCamera()->SetFieldOfView(CameraCurrentFOV); } void AShooterCharacter::SetLookRates() { if (bAiming) { BaseTurnRate = AimingTurnRate; BaseLookUpRate = AimingLookUpRate; } else { BaseTurnRate = HipTurnRate; BaseLookUpRate = HipLookUpRate; } } 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()) // is in air? { // Spread the crosshairs slowly while in air CrosshairInAirFactor = FMath::FInterpTo( CrosshairInAirFactor, 2.25f, DeltaTime, 2.25f); } else // Character is on the ground { // Shrink the crosshairs rapidly while on the ground CrosshairInAirFactor = FMath::FInterpTo( CrosshairInAirFactor, 0.f, DeltaTime, 30.f); } // Calculate crosshair aim factor if (bAiming) // Are we aiming? { // Shrink crosshairs a small amount very quickly CrosshairAimFactor = FMath::FInterpTo( CrosshairAimFactor, 0.6f, DeltaTime, 30.f); } else // Not aiming { // Spread crosshairs back to normal very quickly CrosshairAimFactor = FMath::FInterpTo( CrosshairAimFactor, 0.f, DeltaTime, 30.f); } // True 0.05 second 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 - CrosshairAimFactor + CrosshairShootingFactor; } void AShooterCharacter::StartCrosshairBulletFire() { bFiringBullet = true; GetWorldTimerManager().SetTimer( CrosshairShootTimer, this, &AShooterCharacter::FinishCrosshairBulletFire, ShootTimeDuration); } void AShooterCharacter::FinishCrosshairBulletFire() { bFiringBullet = false; } void AShooterCharacter::FireButtonPressed() { bFireButtonPressed = true; StartFireTimer(); } void AShooterCharacter::FireButtonReleased() { bFireButtonPressed = false; } void AShooterCharacter::StartFireTimer() { if (bShouldFire) { FireWeapon(); bShouldFire = false; GetWorldTimerManager().SetTimer( AutoFireTimer, this, &AShooterCharacter::AutoFireReset, AutomaticFireRate); } } void AShooterCharacter::AutoFireReset() { bShouldFire = true; if (bFireButtonPressed) { StartFireTimer(); } } 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); FVector CrosshairWorldPosition; FVector CrosshairWorldDirection; // Get world position and direction of crosshairs 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; } float AShooterCharacter::GetCrosshairSpreadMultiplier() const { return CrosshairSpreadMultiplier; } void AShooterCharacter::TraceForItems() { if (bShouldTraceForItems) { FHitResult ItemTraceResult; FVector HitLocation; TraceUnderCrosshairs(ItemTraceResult, HitLocation); if (ItemTraceResult.bBlockingHit) { TraceHitItem = Cast(ItemTraceResult.Actor); if (TraceHitItem && TraceHitItem->GetPickupWidget()) { // Show Item's Pickup Widget TraceHitItem->GetPickupWidget()->SetVisibility(true); } // We hit an AItem last frame if (TraceHitItemLastFrame) { if (TraceHitItem != TraceHitItemLastFrame) { // We are hitting a different AItem this frame from last frame // Or AItem is null. TraceHitItemLastFrame->GetPickupWidget()->SetVisibility(false); } } // Store a reference to HitItem for next frame TraceHitItemLastFrame = TraceHitItem; } } else if (TraceHitItemLastFrame) { // No longer overlapping any items, // Item last frame should not show widget TraceHitItemLastFrame->GetPickupWidget()->SetVisibility(false); } } void AShooterCharacter::IncrementOverlappedItemCount(int8 Amount) { if (OverlappedItemCount + Amount <= 0) { OverlappedItemCount = 0; bShouldTraceForItems = false; } else { OverlappedItemCount += Amount; bShouldTraceForItems = true; } } AWeapon* AShooterCharacter::SpawnDefaultWeapon() { // Check the TSubclassOf variable if (DefaultWeaponClass) { // Spawn the Weapon return GetWorld()->SpawnActor(DefaultWeaponClass); } return nullptr; } void AShooterCharacter::EquipWeapon(AWeapon* WeaponToEquip) { if (WeaponToEquip) { // Set AreaSphere to ignore all Collision Channels WeaponToEquip->GetAreaSphere()->SetCollisionResponseToAllChannels( ECollisionResponse::ECR_Ignore); // Set CollisionBox to ignore all Collision Channels WeaponToEquip->GetCollisionBox()->SetCollisionResponseToAllChannels( ECollisionResponse::ECR_Ignore); // Get the Hand Socket const USkeletalMeshSocket* HandSocket = GetMesh()->GetSocketByName( FName("RightHandSocket")); if (HandSocket) { // Attach the Weapon to the hand socket RightHandSocket HandSocket->AttachActor(WeaponToEquip, GetMesh()); } // Set EquippedWeapon to the newly spawned Weapon EquippedWeapon = WeaponToEquip; EquippedWeapon->SetItemState(EItemState::EIS_Equipped); } } 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 (TraceHitItem) { auto TraceHitWeapon = Cast(TraceHitItem); SwapWeapon(TraceHitWeapon); } } void AShooterCharacter::SelectButtonReleased() { } void AShooterCharacter::SwapWeapon(AWeapon* WeaponToSwap) { DropWeapon(); EquipWeapon(WeaponToSwap); TraceHitItem = nullptr; TraceHitItemLastFrame = nullptr; } // 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); // Check OverlappedItemCount, then trace for items TraceForItems(); } // 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, &ACharacter::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("AimingButton", IE_Pressed, this, &AShooterCharacter::AimingButtonPressed); PlayerInputComponent->BindAction("AimingButton", IE_Released, this, &AShooterCharacter::AimingButtonReleased); PlayerInputComponent->BindAction("Select", IE_Pressed, this, &AShooterCharacter::SelectButtonPressed); PlayerInputComponent->BindAction("Select", IE_Released, this, &AShooterCharacter::SelectButtonReleased); }