// Copyright 2016 Mookie. All Rights Reserved. #include "EBBallisticImpactComponent.h" #include "EBMathematicalBallistics.h" #include "EBUnitConversions.h" #include "Engine/Engine.h" #include "Kismet/KismetMathLibrary.h" UEBBallisticImpactComponent::UEBBallisticImpactComponent() { PrimaryComponentTick.bCanEverTick = false; bEnableBallisticCalculations = true; bUseMathematicalPenetration = false; } void UEBBallisticImpactComponent::BeginPlay() { Super::BeginPlay(); } FVector UEBBallisticImpactComponent::CalculateBallisticImpact( const FVector& ImpactLocation, const FVector& ProjectileVelocity, const FMathematicalBulletProperties& BulletProperties, UPhysicalMaterial* HitMaterial, float& OutPenetrationDepth, bool& bOutDidPenetrate, FVector& OutExitLocation) { OutPenetrationDepth = 0.0f; bOutDidPenetrate = false; OutExitLocation = ImpactLocation; // SANITY CHECK: Validate input parameters if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z)) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid impact location")); return ImpactLocation; } if (!FMath::IsFinite(ProjectileVelocity.X) || !FMath::IsFinite(ProjectileVelocity.Y) || !FMath::IsFinite(ProjectileVelocity.Z) || ProjectileVelocity.IsNearlyZero()) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid projectile velocity")); return ImpactLocation; } if (!bEnableBallisticCalculations || !HitMaterial) { return ImpactLocation; } // Get material response FEBMaterialResponseMapEntry MaterialResponse = GetMaterialResponse(HitMaterial); if (MaterialResponse.NeverPenetrate) { return ImpactLocation; } // Calculate penetration depth if (bUseMathematicalPenetration) { UEBMaterialPropertiesAsset* MaterialProps = GetMaterialProperties(HitMaterial); if (MaterialProps) { float VelocityMPS = FEBUnitConversions::CMPSToMPS(ProjectileVelocity.Size()); // Convert cm/s (Unreal units) to m/s float ImpactAngle = CalculateImpactAngle(ProjectileVelocity, FVector::UpVector); // Simplified OutPenetrationDepth = UEBMathematicalBallistics::CalculatePenetrationDepth( BulletProperties, MaterialProps->MaterialProperties, VelocityMPS, ImpactAngle ); } } else { // Use artistic approach float BaseDepth = ProjectileVelocity.Size() * 0.01f; // Simple velocity-based calculation OutPenetrationDepth = BaseDepth * MaterialResponse.PenetrationDepthMultiplier; } // SANITY CHECK: Validate penetration depth if (!FMath::IsFinite(OutPenetrationDepth) || OutPenetrationDepth < 0.0f) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid penetration depth %.3f, setting to 0"), OutPenetrationDepth); OutPenetrationDepth = 0.0f; } // SANITY CHECK: Clamp to reasonable maximum (5 meters) const float MaxPenetrationDepth = 500.0f; // 5 meters in cm if (OutPenetrationDepth > MaxPenetrationDepth) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Penetration depth %.1f cm exceeds maximum, clamping to %.1f cm"), OutPenetrationDepth, MaxPenetrationDepth); OutPenetrationDepth = MaxPenetrationDepth; } // Check if penetration occurred float MinPenetrationThreshold = 1.0f; // 1cm minimum bOutDidPenetrate = OutPenetrationDepth > MinPenetrationThreshold; if (bOutDidPenetrate) { // Calculate exit location FVector PenetrationDirection = ProjectileVelocity.GetSafeNormal(); FVector PenetrationVector = PenetrationDirection * OutPenetrationDepth; // SANITY CHECK: Validate penetration vector if (!FMath::IsFinite(PenetrationVector.X) || !FMath::IsFinite(PenetrationVector.Y) || !FMath::IsFinite(PenetrationVector.Z)) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid penetration vector calculated")); OutExitLocation = ImpactLocation; bOutDidPenetrate = false; } else { OutExitLocation = ImpactLocation + PenetrationVector; } } // Fire event OnBallisticImpact.Broadcast(ImpactLocation, FVector::UpVector, HitMaterial, OutPenetrationDepth, bOutDidPenetrate); return OutExitLocation; } FVector UEBBallisticImpactComponent::CalculateRicochet( const FVector& ImpactLocation, const FVector& ImpactNormal, const FVector& ProjectileVelocity, const FMathematicalBulletProperties& BulletProperties, UPhysicalMaterial* HitMaterial, float& OutEnergyRetained, bool& bOutDidRicochet) { OutEnergyRetained = 0.0f; bOutDidRicochet = false; // SANITY CHECK: Validate input parameters if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z)) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid impact location")); return ProjectileVelocity; } if (!FMath::IsFinite(ImpactNormal.X) || !FMath::IsFinite(ImpactNormal.Y) || !FMath::IsFinite(ImpactNormal.Z) || ImpactNormal.IsNearlyZero()) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid impact normal")); return ProjectileVelocity; } if (!FMath::IsFinite(ProjectileVelocity.X) || !FMath::IsFinite(ProjectileVelocity.Y) || !FMath::IsFinite(ProjectileVelocity.Z) || ProjectileVelocity.IsNearlyZero()) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid projectile velocity")); return ProjectileVelocity; } if (!bEnableBallisticCalculations || !HitMaterial) { return ProjectileVelocity; } // Get material response FEBMaterialResponseMapEntry MaterialResponse = GetMaterialResponse(HitMaterial); if (MaterialResponse.NeverRicochet) { return ProjectileVelocity; } // Calculate impact angle float ImpactAngle = CalculateImpactAngle(ProjectileVelocity, ImpactNormal); // Simple ricochet probability based on angle float RicochetProbability = FMath::Clamp((90.0f - ImpactAngle) / 90.0f, 0.0f, 1.0f); RicochetProbability *= MaterialResponse.RicochetProbabilityMultiplier; // Random chance for ricochet if (FMath::RandRange(0.0f, 1.0f) < RicochetProbability) { bOutDidRicochet = true; // Calculate ricochet direction using reflection FVector RicochetDirection = UKismetMathLibrary::GetReflectionVector(ProjectileVelocity, ImpactNormal); // SANITY CHECK: Ensure reflection vector is valid if (!FMath::IsFinite(RicochetDirection.X) || !FMath::IsFinite(RicochetDirection.Y) || !FMath::IsFinite(RicochetDirection.Z)) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid reflection vector, using simple reflection")); RicochetDirection = ProjectileVelocity - 2.0f * FVector::DotProduct(ProjectileVelocity, ImpactNormal) * ImpactNormal; } // Apply some randomness based on material response if (MaterialResponse.RicochetSpread > 0.0f) { // SAFETY: Clamp spread to reasonable range to prevent extreme deviations float SafeSpread = FMath::Clamp(MaterialResponse.RicochetSpread, 0.0f, 45.0f); FVector RandomOffset = FMath::VRand() * FMath::DegreesToRadians(SafeSpread); RicochetDirection = (RicochetDirection + RandomOffset).GetSafeNormal(); } // Calculate energy retention with additional safety clamping OutEnergyRetained = FMath::Clamp(MaterialResponse.RicochetRestitution, 0.0f, 0.95f); // Never allow full energy retention // SANITY CHECK: Validate ricochet direction if (!FMath::IsFinite(RicochetDirection.X) || !FMath::IsFinite(RicochetDirection.Y) || !FMath::IsFinite(RicochetDirection.Z) || RicochetDirection.IsNearlyZero()) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid ricochet direction calculated")); return ProjectileVelocity; } // Apply velocity reduction with additional safety checks float OriginalSpeed = ProjectileVelocity.Size(); // SAFETY: Clamp original speed to prevent calculation with invalid values if (!FMath::IsFinite(OriginalSpeed) || OriginalSpeed <= 0.0f) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid original speed %.3f"), OriginalSpeed); return ProjectileVelocity; } float NewSpeed = OriginalSpeed * OutEnergyRetained; // SANITY CHECK: Validate new speed if (!FMath::IsFinite(NewSpeed) || NewSpeed < 0.0f) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid ricochet speed %.3f"), NewSpeed); return ProjectileVelocity; } // SAFETY: Additional clamp to prevent excessive speeds float MaxSafeSpeed = OriginalSpeed * 0.9f; // Never exceed 90% of original speed NewSpeed = FMath::Min(NewSpeed, MaxSafeSpeed); FVector NewVelocity = RicochetDirection * NewSpeed; // SANITY CHECK: Validate final ricochet velocity if (!FMath::IsFinite(NewVelocity.X) || !FMath::IsFinite(NewVelocity.Y) || !FMath::IsFinite(NewVelocity.Z)) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid final ricochet velocity")); return ProjectileVelocity; } // SAFETY: Final velocity magnitude check float FinalSpeed = NewVelocity.Size(); if (!FMath::IsFinite(FinalSpeed) || FinalSpeed > OriginalSpeed) { UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Ricochet velocity %.1f exceeds original %.1f, clamping"), FinalSpeed, OriginalSpeed); NewVelocity = NewVelocity.GetSafeNormal() * (OriginalSpeed * 0.8f); } // Fire event OnBallisticRicochet.Broadcast(ImpactLocation, RicochetDirection, HitMaterial, OutEnergyRetained); return NewVelocity; } return ProjectileVelocity; } UEBMaterialPropertiesAsset* UEBBallisticImpactComponent::GetMaterialProperties(UPhysicalMaterial* PhysicalMaterial) { if (!PhysicalMaterial) { return nullptr; } // For now, use naming convention to find associated material properties // In production, you'd want a more robust system FString AssetName = PhysicalMaterial->GetName() + TEXT("_BallisticProps"); FString PackageName = PhysicalMaterial->GetPackage()->GetName() + TEXT("_BallisticProps"); return LoadObject(nullptr, *PackageName, nullptr, LOAD_NoWarn | LOAD_Quiet); } FEBMaterialResponseMapEntry UEBBallisticImpactComponent::GetMaterialResponse(UPhysicalMaterial* PhysicalMaterial) { FEBMaterialResponseMapEntry DefaultResponse; if (!MaterialResponseMap || !PhysicalMaterial) { // CRITICAL FIX: Use conservative default values for unmapped materials to prevent infinite speed issues DefaultResponse.RicochetRestitution = 0.3f; // Lower energy retention for safety DefaultResponse.RicochetProbabilityMultiplier = 0.5f; // Reduced ricochet probability DefaultResponse.RicochetSpread = 5.0f; // Add some randomness to prevent perfect reflections DefaultResponse.NeverRicochet = false; DefaultResponse.PenetrationDepthMultiplier = 0.8f; // Slightly reduced penetration // Log warning about unmapped material UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Material '%s' not found in MaterialResponseMap, using safe defaults"), PhysicalMaterial ? *PhysicalMaterial->GetName() : TEXT("NULL")); return DefaultResponse; } if (FEBMaterialResponseMapEntry* Found = MaterialResponseMap->Map.Find(PhysicalMaterial)) { return *Found; } // CRITICAL FIX: Use conservative default values for unmapped materials to prevent infinite speed issues DefaultResponse.RicochetRestitution = 0.3f; // Lower energy retention for safety DefaultResponse.RicochetProbabilityMultiplier = 0.5f; // Reduced ricochet probability DefaultResponse.RicochetSpread = 5.0f; // Add some randomness to prevent perfect reflections DefaultResponse.NeverRicochet = false; DefaultResponse.PenetrationDepthMultiplier = 0.8f; // Slightly reduced penetration // Log warning about unmapped material UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Material '%s' not found in MaterialResponseMap, using safe defaults"), *PhysicalMaterial->GetName()); return DefaultResponse; } FVector UEBBallisticImpactComponent::CalculatePenetrationVector( const FVector& ImpactLocation, const FVector& ImpactNormal, const FVector& ProjectileVelocity, float PenetrationDepth) { FVector PenetrationDirection = ProjectileVelocity.GetSafeNormal(); return ImpactLocation + (PenetrationDirection * PenetrationDepth); } float UEBBallisticImpactComponent::CalculateImpactAngle(const FVector& ProjectileVelocity, const FVector& SurfaceNormal) { FVector NormalizedVelocity = ProjectileVelocity.GetSafeNormal(); FVector NormalizedSurfaceNormal = SurfaceNormal.GetSafeNormal(); float DotProduct = FVector::DotProduct(-NormalizedVelocity, NormalizedSurfaceNormal); float AngleRadians = FMath::Acos(FMath::Clamp(DotProduct, -1.0f, 1.0f)); return FMath::RadiansToDegrees(AngleRadians); }