Files
BallisticsDocs/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp
T
2025-07-04 02:33:40 -07:00

341 lines
12 KiB
C++

// 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<UEBMaterialPropertiesAsset>(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);
}