// Copyright Epic Games, Inc. All Rights Reserved. #include "LyraGameplayAbility_RangedWeapon.h" #include "Weapons/LyraRangedWeaponInstance.h" #include "Physics/LyraCollisionChannels.h" #include "LyraLogChannels.h" #include "AIController.h" #include "Messages/LyraVerbMessage.h" #include "NativeGameplayTags.h" #include "GameFramework/GameplayMessageSubsystem.h" #include "Weapons/LyraWeaponStateComponent.h" #include "Teams/LyraTeamSubsystem.h" #include "AbilitySystemComponent.h" #include "GameFramework/PlayerController.h" #include "AbilitySystem/LyraGameplayEffectContext.h" #include "AbilitySystem/LyraGameplayAbilityTargetData_SingleTargetHit.h" #include "DrawDebugHelpers.h" namespace LyraConsoleVariables { static float DrawBulletTracesDuration = 0.0f; static FAutoConsoleVariableRef CVarDrawBulletTraceDuraton( TEXT("lyra.Weapon.DrawBulletTraceDuration"), DrawBulletTracesDuration, TEXT("Should we do debug drawing for bullet traces (if above zero, sets how long (in seconds))"), ECVF_Default); static float DrawBulletHitDuration = 0.0f; static FAutoConsoleVariableRef CVarDrawBulletHits( TEXT("lyra.Weapon.DrawBulletHitDuration"), DrawBulletHitDuration, TEXT("Should we do debug drawing for bullet impacts (if above zero, sets how long (in seconds))"), ECVF_Default); static float DrawBulletHitRadius = 3.0f; static FAutoConsoleVariableRef CVarDrawBulletHitRadius( TEXT("lyra.Weapon.DrawBulletHitRadius"), DrawBulletHitRadius, TEXT("When bullet hit debug drawing is enabled (see DrawBulletHitDuration), how big should the hit radius be? (in uu)"), ECVF_Default); } // Weapon fire will be blocked/canceled if the player has this tag UE_DEFINE_GAMEPLAY_TAG_STATIC(TAG_WeaponFireBlocked, "Ability.Weapon.NoFiring"); ////////////////////////////////////////////////////////////////////// FVector VRandConeNormalDistribution(const FVector& Dir, const float ConeHalfAngleRad, const float Exponent) { if (ConeHalfAngleRad > 0.f) { const float ConeHalfAngleDegrees = FMath::RadiansToDegrees(ConeHalfAngleRad); // consider the cone a concatenation of two rotations. one "away" from the center line, and another "around" the circle // apply the exponent to the away-from-center rotation. a larger exponent will cluster points more tightly around the center const float FromCenter = FMath::Pow(FMath::FRand(), Exponent); const float AngleFromCenter = FromCenter * ConeHalfAngleDegrees; const float AngleAround = FMath::FRand() * 360.0f; FRotator Rot = Dir.Rotation(); FQuat DirQuat(Rot); FQuat FromCenterQuat(FRotator(0.0f, AngleFromCenter, 0.0f)); FQuat AroundQuat(FRotator(0.0f, 0.0, AngleAround)); FQuat FinalDirectionQuat = DirQuat * AroundQuat * FromCenterQuat; FinalDirectionQuat.Normalize(); return FinalDirectionQuat.RotateVector(FVector::ForwardVector); } else { return Dir.GetSafeNormal(); } } ULyraGameplayAbility_RangedWeapon::ULyraGameplayAbility_RangedWeapon(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { SourceBlockedTags.AddTag(TAG_WeaponFireBlocked); } ULyraRangedWeaponInstance* ULyraGameplayAbility_RangedWeapon::GetWeaponInstance() const { return Cast(GetAssociatedEquipment()); } bool ULyraGameplayAbility_RangedWeapon::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const { bool bResult = Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags); if (bResult) { if (GetWeaponInstance() == nullptr) { UE_LOG(LogLyraAbilitySystem, Error, TEXT("Weapon ability %s cannot be activated because there is no associated ranged weapon (equipment instance=%s but needs to be derived from %s)"), *GetPathName(), *GetPathNameSafe(GetAssociatedEquipment()), *ULyraRangedWeaponInstance::StaticClass()->GetName()); bResult = false; } } return bResult; } int32 ULyraGameplayAbility_RangedWeapon::FindFirstPawnHitResult(const TArray& HitResults) { for (int32 Idx = 0; Idx < HitResults.Num(); ++Idx) { const FHitResult& CurHitResult = HitResults[Idx]; if (CurHitResult.HitObjectHandle.DoesRepresentClass(APawn::StaticClass())) { // If we hit a pawn, we're good return Idx; } else { AActor* HitActor = CurHitResult.HitObjectHandle.FetchActor(); if ((HitActor != nullptr) && (HitActor->GetAttachParentActor() != nullptr) && (Cast(HitActor->GetAttachParentActor()) != nullptr)) { // If we hit something attached to a pawn, we're good return Idx; } } } return INDEX_NONE; } void ULyraGameplayAbility_RangedWeapon::AddAdditionalTraceIgnoreActors(FCollisionQueryParams& TraceParams) const { if (AActor* Avatar = GetAvatarActorFromActorInfo()) { // Ignore any actors attached to the avatar doing the shooting TArray AttachedActors; Avatar->GetAttachedActors(/*out*/ AttachedActors); TraceParams.AddIgnoredActors(AttachedActors); } } ECollisionChannel ULyraGameplayAbility_RangedWeapon::DetermineTraceChannel(FCollisionQueryParams& TraceParams, bool bIsSimulated) const { return Lyra_TraceChannel_Weapon; } FHitResult ULyraGameplayAbility_RangedWeapon::WeaponTrace(const FVector& StartTrace, const FVector& EndTrace, float SweepRadius, bool bIsSimulated, OUT TArray& OutHitResults) const { TArray HitResults; FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(WeaponTrace), /*bTraceComplex=*/ true, /*IgnoreActor=*/ GetAvatarActorFromActorInfo()); TraceParams.bReturnPhysicalMaterial = true; AddAdditionalTraceIgnoreActors(TraceParams); //TraceParams.bDebugQuery = true; const ECollisionChannel TraceChannel = DetermineTraceChannel(TraceParams, bIsSimulated); if (SweepRadius > 0.0f) { GetWorld()->SweepMultiByChannel(HitResults, StartTrace, EndTrace, FQuat::Identity, TraceChannel, FCollisionShape::MakeSphere(SweepRadius), TraceParams); } else { GetWorld()->LineTraceMultiByChannel(HitResults, StartTrace, EndTrace, TraceChannel, TraceParams); } FHitResult Hit(ForceInit); if (HitResults.Num() > 0) { // Filter the output list to prevent multiple hits on the same actor; // this is to prevent a single bullet dealing damage multiple times to // a single actor if using an overlap trace for (FHitResult& CurHitResult : HitResults) { auto Pred = [&CurHitResult](const FHitResult& Other) { return Other.HitObjectHandle == CurHitResult.HitObjectHandle; }; if (!OutHitResults.ContainsByPredicate(Pred)) { OutHitResults.Add(CurHitResult); } } Hit = OutHitResults.Last(); } else { Hit.TraceStart = StartTrace; Hit.TraceEnd = EndTrace; } return Hit; } FVector ULyraGameplayAbility_RangedWeapon::GetWeaponTargetingSourceLocation() const { // Use Pawn's location as a base APawn* const AvatarPawn = Cast(GetAvatarActorFromActorInfo()); check(AvatarPawn); const FVector SourceLoc = AvatarPawn->GetActorLocation(); const FQuat SourceRot = AvatarPawn->GetActorQuat(); FVector TargetingSourceLocation = SourceLoc; //@TODO: Add an offset from the weapon instance and adjust based on pawn crouch/aiming/etc... return TargetingSourceLocation; } FTransform ULyraGameplayAbility_RangedWeapon::GetTargetingTransform(APawn* SourcePawn, ELyraAbilityTargetingSource Source) const { check(SourcePawn); AController* SourcePawnController = SourcePawn->GetController(); ULyraWeaponStateComponent* WeaponStateComponent = (SourcePawnController != nullptr) ? SourcePawnController->FindComponentByClass() : nullptr; // The caller should determine the transform without calling this if the mode is custom! check(Source != ELyraAbilityTargetingSource::Custom); const FVector ActorLoc = SourcePawn->GetActorLocation(); FQuat AimQuat = SourcePawn->GetActorQuat(); AController* Controller = SourcePawn->Controller; FVector SourceLoc; double FocalDistance = 1024.0f; FVector FocalLoc; FVector CamLoc; FRotator CamRot; bool bFoundFocus = false; if ((Controller != nullptr) && ((Source == ELyraAbilityTargetingSource::CameraTowardsFocus) || (Source == ELyraAbilityTargetingSource::PawnTowardsFocus) || (Source == ELyraAbilityTargetingSource::WeaponTowardsFocus))) { // Get camera position for later bFoundFocus = true; APlayerController* PC = Cast(Controller); if (PC != nullptr) { PC->GetPlayerViewPoint(/*out*/ CamLoc, /*out*/ CamRot); } else { SourceLoc = GetWeaponTargetingSourceLocation(); CamLoc = SourceLoc; CamRot = Controller->GetControlRotation(); } // Determine initial focal point to FVector AimDir = CamRot.Vector().GetSafeNormal(); FocalLoc = CamLoc + (AimDir * FocalDistance); // Move the start and focal point up in front of pawn if (PC) { const FVector WeaponLoc = GetWeaponTargetingSourceLocation(); CamLoc = FocalLoc + (((WeaponLoc - FocalLoc) | AimDir) * AimDir); FocalLoc = CamLoc + (AimDir * FocalDistance); } //Move the start to be the HeadPosition of the AI else if (AAIController* AIController = Cast(Controller)) { CamLoc = SourcePawn->GetActorLocation() + FVector(0, 0, SourcePawn->BaseEyeHeight); } if (Source == ELyraAbilityTargetingSource::CameraTowardsFocus) { // If we're camera -> focus then we're done return FTransform(CamRot, CamLoc); } } if ((Source == ELyraAbilityTargetingSource::WeaponForward) || (Source == ELyraAbilityTargetingSource::WeaponTowardsFocus)) { SourceLoc = GetWeaponTargetingSourceLocation(); } else { // Either we want the pawn's location, or we failed to find a camera SourceLoc = ActorLoc; } if (bFoundFocus && ((Source == ELyraAbilityTargetingSource::PawnTowardsFocus) || (Source == ELyraAbilityTargetingSource::WeaponTowardsFocus))) { // Return a rotator pointing at the focal point from the source return FTransform((FocalLoc - SourceLoc).Rotation(), SourceLoc); } // If we got here, either we don't have a camera or we don't want to use it, either way go forward return FTransform(AimQuat, SourceLoc); } FHitResult ULyraGameplayAbility_RangedWeapon::DoSingleBulletTrace(const FVector& StartTrace, const FVector& EndTrace, float SweepRadius, bool bIsSimulated, OUT TArray& OutHits) const { #if ENABLE_DRAW_DEBUG if (LyraConsoleVariables::DrawBulletTracesDuration > 0.0f) { static float DebugThickness = 1.0f; DrawDebugLine(GetWorld(), StartTrace, EndTrace, FColor::Red, false, LyraConsoleVariables::DrawBulletTracesDuration, 0, DebugThickness); } #endif // ENABLE_DRAW_DEBUG FHitResult Impact; // Trace and process instant hit if something was hit // First trace without using sweep radius if (FindFirstPawnHitResult(OutHits) == INDEX_NONE) { Impact = WeaponTrace(StartTrace, EndTrace, /*SweepRadius=*/ 0.0f, bIsSimulated, /*out*/ OutHits); } if (FindFirstPawnHitResult(OutHits) == INDEX_NONE) { // If this weapon didn't hit anything with a line trace and supports a sweep radius, try that if (SweepRadius > 0.0f) { TArray SweepHits; Impact = WeaponTrace(StartTrace, EndTrace, SweepRadius, bIsSimulated, /*out*/ SweepHits); // If the trace with sweep radius enabled hit a pawn, check if we should use its hit results const int32 FirstPawnIdx = FindFirstPawnHitResult(SweepHits); if (SweepHits.IsValidIndex(FirstPawnIdx)) { // If we had a blocking hit in our line trace that occurs in SweepHits before our // hit pawn, we should just use our initial hit results since the Pawn hit should be blocked bool bUseSweepHits = true; for (int32 Idx = 0; Idx < FirstPawnIdx; ++Idx) { const FHitResult& CurHitResult = SweepHits[Idx]; auto Pred = [&CurHitResult](const FHitResult& Other) { return Other.HitObjectHandle == CurHitResult.HitObjectHandle; }; if (CurHitResult.bBlockingHit && OutHits.ContainsByPredicate(Pred)) { bUseSweepHits = false; break; } } if (bUseSweepHits) { OutHits = SweepHits; } } } } return Impact; } void ULyraGameplayAbility_RangedWeapon::PerformLocalTargeting(OUT TArray& OutHits) { APawn* const AvatarPawn = Cast(GetAvatarActorFromActorInfo()); ULyraRangedWeaponInstance* WeaponData = GetWeaponInstance(); if (AvatarPawn && AvatarPawn->IsLocallyControlled() && WeaponData) { FRangedWeaponFiringInput InputData; InputData.WeaponData = WeaponData; InputData.bCanPlayBulletFX = (AvatarPawn->GetNetMode() != NM_DedicatedServer); //@TODO: Should do more complicated logic here when the player is close to a wall, etc... const FTransform TargetTransform = GetTargetingTransform(AvatarPawn, ELyraAbilityTargetingSource::CameraTowardsFocus); InputData.AimDir = TargetTransform.GetUnitAxis(EAxis::X); InputData.StartTrace = TargetTransform.GetTranslation(); InputData.EndAim = InputData.StartTrace + InputData.AimDir * WeaponData->GetMaxDamageRange(); #if ENABLE_DRAW_DEBUG if (LyraConsoleVariables::DrawBulletTracesDuration > 0.0f) { static float DebugThickness = 2.0f; DrawDebugLine(GetWorld(), InputData.StartTrace, InputData.StartTrace + (InputData.AimDir * 100.0f), FColor::Yellow, false, LyraConsoleVariables::DrawBulletTracesDuration, 0, DebugThickness); } #endif TraceBulletsInCartridge(InputData, /*out*/ OutHits); } } void ULyraGameplayAbility_RangedWeapon::TraceBulletsInCartridge(const FRangedWeaponFiringInput& InputData, OUT TArray& OutHits) { ULyraRangedWeaponInstance* WeaponData = InputData.WeaponData; check(WeaponData); const int32 BulletsPerCartridge = WeaponData->GetBulletsPerCartridge(); for (int32 BulletIndex = 0; BulletIndex < BulletsPerCartridge; ++BulletIndex) { const float BaseSpreadAngle = WeaponData->GetCalculatedSpreadAngle(); const float SpreadAngleMultiplier = WeaponData->GetCalculatedSpreadAngleMultiplier(); const float ActualSpreadAngle = BaseSpreadAngle * SpreadAngleMultiplier; const float HalfSpreadAngleInRadians = FMath::DegreesToRadians(ActualSpreadAngle * 0.5f); const FVector BulletDir = VRandConeNormalDistribution(InputData.AimDir, HalfSpreadAngleInRadians, WeaponData->GetSpreadExponent()); const FVector EndTrace = InputData.StartTrace + (BulletDir * WeaponData->GetMaxDamageRange()); FVector HitLocation = EndTrace; TArray AllImpacts; FHitResult Impact = DoSingleBulletTrace(InputData.StartTrace, EndTrace, WeaponData->GetBulletTraceSweepRadius(), /*bIsSimulated=*/ false, /*out*/ AllImpacts); const AActor* HitActor = Impact.GetActor(); if (HitActor) { #if ENABLE_DRAW_DEBUG if (LyraConsoleVariables::DrawBulletHitDuration > 0.0f) { DrawDebugPoint(GetWorld(), Impact.ImpactPoint, LyraConsoleVariables::DrawBulletHitRadius, FColor::Red, false, LyraConsoleVariables::DrawBulletHitRadius); } #endif if (AllImpacts.Num() > 0) { OutHits.Append(AllImpacts); } HitLocation = Impact.ImpactPoint; } // Make sure there's always an entry in OutHits so the direction can be used for tracers, etc... if (OutHits.Num() == 0) { if (!Impact.bBlockingHit) { // Locate the fake 'impact' at the end of the trace Impact.Location = EndTrace; Impact.ImpactPoint = EndTrace; } OutHits.Add(Impact); } } } void ULyraGameplayAbility_RangedWeapon::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) { // Bind target data callback UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get(); check(MyAbilityComponent); OnTargetDataReadyCallbackDelegateHandle = MyAbilityComponent->AbilityTargetDataSetDelegate(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()).AddUObject(this, &ThisClass::OnTargetDataReadyCallback); // Update the last firing time ULyraRangedWeaponInstance* WeaponData = GetWeaponInstance(); check(WeaponData); WeaponData->UpdateFiringTime(); Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData); } void ULyraGameplayAbility_RangedWeapon::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) { if (IsEndAbilityValid(Handle, ActorInfo)) { if (ScopeLockCount > 0) { WaitingToExecute.Add(FPostLockDelegate::CreateUObject(this, &ThisClass::EndAbility, Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled)); return; } UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get(); check(MyAbilityComponent); // When ability ends, consume target data and remove delegate MyAbilityComponent->AbilityTargetDataSetDelegate(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()).Remove(OnTargetDataReadyCallbackDelegateHandle); MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()); Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled); } } void ULyraGameplayAbility_RangedWeapon::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& InData, FGameplayTag ApplicationTag) { UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get(); check(MyAbilityComponent); if (const FGameplayAbilitySpec* AbilitySpec = MyAbilityComponent->FindAbilitySpecFromHandle(CurrentSpecHandle)) { FScopedPredictionWindow ScopedPrediction(MyAbilityComponent); // Take ownership of the target data to make sure no callbacks into game code invalidate it out from under us FGameplayAbilityTargetDataHandle LocalTargetDataHandle(MoveTemp(const_cast(InData))); const bool bShouldNotifyServer = CurrentActorInfo->IsLocallyControlled() && !CurrentActorInfo->IsNetAuthority(); if (bShouldNotifyServer) { MyAbilityComponent->CallServerSetReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey(), LocalTargetDataHandle, ApplicationTag, MyAbilityComponent->ScopedPredictionKey); } const bool bIsTargetDataValid = true; bool bProjectileWeapon = false; #if WITH_SERVER_CODE if (!bProjectileWeapon) { if (AController* Controller = GetControllerFromActorInfo()) { if (Controller->GetLocalRole() == ROLE_Authority) { // Confirm hit markers if (ULyraWeaponStateComponent* WeaponStateComponent = Controller->FindComponentByClass()) { TArray HitReplaces; for (uint8 i = 0; (i < LocalTargetDataHandle.Num()) && (i < 255); ++i) { if (FGameplayAbilityTargetData_SingleTargetHit* SingleTargetHit = static_cast(LocalTargetDataHandle.Get(i))) { if (SingleTargetHit->bHitReplaced) { HitReplaces.Add(i); } } } WeaponStateComponent->ClientConfirmTargetData(LocalTargetDataHandle.UniqueId, bIsTargetDataValid, HitReplaces); } } } } #endif //WITH_SERVER_CODE // See if we still have ammo if (bIsTargetDataValid && CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo)) { // We fired the weapon, add spread ULyraRangedWeaponInstance* WeaponData = GetWeaponInstance(); check(WeaponData); WeaponData->AddSpread(); // Let the blueprint do stuff like apply effects to the targets OnRangedWeaponTargetDataReady(LocalTargetDataHandle); } else { UE_LOG(LogLyraAbilitySystem, Warning, TEXT("Weapon ability %s failed to commit (bIsTargetDataValid=%d)"), *GetPathName(), bIsTargetDataValid ? 1 : 0); K2_EndAbility(); } } // We've processed the data MyAbilityComponent->ConsumeClientReplicatedTargetData(CurrentSpecHandle, CurrentActivationInfo.GetActivationPredictionKey()); } void ULyraGameplayAbility_RangedWeapon::StartRangedWeaponTargeting() { check(CurrentActorInfo); AActor* AvatarActor = CurrentActorInfo->AvatarActor.Get(); check(AvatarActor); UAbilitySystemComponent* MyAbilityComponent = CurrentActorInfo->AbilitySystemComponent.Get(); check(MyAbilityComponent); AController* Controller = GetControllerFromActorInfo(); check(Controller); ULyraWeaponStateComponent* WeaponStateComponent = Controller->FindComponentByClass(); FScopedPredictionWindow ScopedPrediction(MyAbilityComponent, CurrentActivationInfo.GetActivationPredictionKey()); TArray FoundHits; PerformLocalTargeting(/*out*/ FoundHits); // Fill out the target data from the hit results FGameplayAbilityTargetDataHandle TargetData; TargetData.UniqueId = WeaponStateComponent ? WeaponStateComponent->GetUnconfirmedServerSideHitMarkerCount() : 0; if (FoundHits.Num() > 0) { const int32 CartridgeID = FMath::Rand(); for (const FHitResult& FoundHit : FoundHits) { FLyraGameplayAbilityTargetData_SingleTargetHit* NewTargetData = new FLyraGameplayAbilityTargetData_SingleTargetHit(); NewTargetData->HitResult = FoundHit; NewTargetData->CartridgeID = CartridgeID; TargetData.Add(NewTargetData); } } // Send hit marker information const bool bProjectileWeapon = false; if (!bProjectileWeapon && (WeaponStateComponent != nullptr)) { WeaponStateComponent->AddUnconfirmedServerSideHitMarkers(TargetData, FoundHits); } // Process the target data immediately OnTargetDataReadyCallback(TargetData, FGameplayTag()); }