// Fill out your copyright notice in the Description page of Project Settings. #include "ShooterCharacter.h" #include "Item.h" #include "Camera/CameraComponent.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" // 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 bShouldFire(true), bFireButtonPressed(false), AutomaticFireRate(0.1f), // Item trace variables bShouldTraceForItems(false) { // 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); // The camera follows at this distance behind the character CameraBoom->TargetArmLength = 180.f; // Rotate the arm based on the controller CameraBoom->bUsePawnControlRotation = true; CameraBoom->SocketOffset = FVector(0.f, 50.f, 70.f); // Create a follow camera FollowCamera = CreateDefaultSubobject(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; } // Called when the game starts or when spawned void AShooterCharacter::BeginPlay() { Super::BeginPlay(); if (FollowCamera) { CameraDefaultFOV = FollowCamera->FieldOfView; CameraCurrentFOV = CameraDefaultFOV; } } 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::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) { // Spawn impact particles after updating BeamEndPoint if (ImpactParticles) { UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles, BeamEnd); } // Spawn smoke trail particles if (BeamParticles) { UParticleSystemComponent* Beam = UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), BeamParticles, SocketTransform); if (Beam) { Beam->SetVectorParameter("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); // Perform a second trace, this time from the gun barrel FHitResult WeaponTraceHit; const FVector WeaponTraceStart{ MuzzleSocketLocation }; const FVector StartToEnd{ OutBeamLocation - MuzzleSocketLocation }; 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); } if (FollowCamera) { FollowCamera->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()) { // 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::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) { AShooterCharacter::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 }; // 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::TraceForItems() { static AItem* HitItem; if (bShouldTraceForItems) { FHitResult ItemTraceResult; FVector HitLocation; TraceUnderCrosshairs(ItemTraceResult, HitLocation); if (ItemTraceResult.bBlockingHit) { // If an item was being looked at from last frame, we should hide its widget SetItemPickupWidgetVisibility(HitItem, false); // Show the new item's widget HitItem = Cast(ItemTraceResult.GetActor()); SetItemPickupWidgetVisibility(HitItem, true); } else if (HitItem) // Hide widget for the item from last frame { SetItemPickupWidgetVisibility(HitItem, false); HitItem = nullptr; } } else if (HitItem) // Hide widget for the item from last frame { SetItemPickupWidgetVisibility(HitItem, false); HitItem = nullptr; } } void AShooterCharacter::SetItemPickupWidgetVisibility(AItem* Item, bool visibility) { if (Item && Item->GetPickupWidget()) { Item->GetPickupWidget()->SetVisibility(visibility); } } // 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(); } // 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("Aiming", IE_Pressed, this, &AShooterCharacter::AimingButtonPressed); PlayerInputComponent->BindAction("Aiming", IE_Released, this, &AShooterCharacter::AimingButtonReleased); } float AShooterCharacter::GetCrosshairSpreadMultiplier() const { return CrosshairSpreadMultiplier; } void AShooterCharacter::IncrementOverlappedItemCount(int8 Amount) { if (OverlappedItemCount + Amount <= 0) { OverlappedItemCount = 0; bShouldTraceForItems = false; } else { OverlappedItemCount += Amount; bShouldTraceForItems = true; } }