Files
BallisticsDocs/Source/EasyBallistics/Private/Trace.cpp
T
2025-07-10 01:02:24 -07:00

401 lines
16 KiB
C++

// Copyright 2016 Mookie. All Rights Reserved.
#include "EBBullet.h"
float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEnumAsByte<ECollisionChannel> CollisionChannel) {
bool Hit;
FHitResult HitResult;
TArray<FHitResult> Results;
FCollisionResponseParams ResponseParameters;
FCollisionQueryParams CollisionParameters;
CollisionParameters.bTraceComplex = TraceComplex;
CollisionParameters.bReturnPhysicalMaterial = true;
CollisionParameters.AddIgnoredActor(this);
CollisionParameters.AddIgnoredActors(IgnoredActors);
CollisionParameters.bReturnFaceIndex = true;
if (OwnerSafe) {
CollisionParameters.AddIgnoredActors(GetSafeLaunchIgnoredActors(GetOwner()));
}
FVector TraceDistance = (PreviousVelocity + Velocity)*0.5*delta;
GetWorld()->LineTraceMultiByChannel(Results, start, start + TraceDistance, CollisionChannel, CollisionParameters, ResponseParameters);
if (Results.Num() > 0) {
HitResult = FilterHits(Results, Hit);
}
else { Hit = false; }
if (Hit) {
//Reduce velocity
Velocity = FMath::Lerp(PreviousVelocity, Velocity, HitResult.Time);
bool Ricochet = false;
bool Penetration = false;
FVector exitLoc;
FVector exitNormal;
FVector NewVelocity = Velocity;
//material mods
bool neverPenetrate = false;
bool neverRicochet = false;
float penDepthMultiplier = 1.0f;
float penNormalization = PenetrationNormalization;
float penNormalizationGrazing = PenetrationNormalizationGrazing;
float penEnterSpread = PenetrationEntryAngleSpread;
float penExitSpread = PenetrationExitAngleSpread;
float ricProbMultiplier = 1.0f;
float ricRestitution = RicochetRestitution;
float ricFriction = RicochetFriction;
float ricSpread = RicochetSpread;
EPenTraceType PenTraceType = DefaultPenTraceType;
UPhysicalMaterial* PhysMaterial = HitResult.PhysMaterial.Get();
if (PhysMaterial) {
//material response modifiers
if (MaterialResponseMap != nullptr) {
FEBMaterialResponseMapEntry* ResponseEntry = MaterialResponseMap->Map.Find(PhysMaterial);
if (ResponseEntry != nullptr) {
neverPenetrate = ResponseEntry->NeverPenetrate;
neverRicochet = ResponseEntry->NeverRicochet;
PenTraceType = ResponseEntry->PenTraceType;
penDepthMultiplier = ResponseEntry->PenetrationDepthMultiplier;
penNormalization = PenetrationNormalization + ResponseEntry->PenetrationNormalization;
penNormalizationGrazing = PenetrationNormalizationGrazing + ResponseEntry->PenetrationNormalizationGrazing;
penEnterSpread = PenetrationEntryAngleSpread + ResponseEntry->PenetrationEntryAngleSpread;
penExitSpread = PenetrationExitAngleSpread + ResponseEntry->PenetrationExitAngleSpread;
ricProbMultiplier = ResponseEntry->RicochetProbabilityMultiplier;
ricRestitution = FMath::Lerp(RicochetRestitution, ResponseEntry->RicochetRestitution, ResponseEntry->RicochetRestitutionInfluence);
ricFriction = FMath::Lerp(RicochetFriction, ResponseEntry->RicochetFriction, ResponseEntry->RicochetFrictionInfluence);
ricSpread = RicochetSpread + ResponseEntry->RicochetSpread;
}
}
if (MaterialDensityControlsPenetrationDepth) {
penDepthMultiplier /= PhysMaterial->Density;
}
if (MaterialRestitutionControlsRicochet) {
ricRestitution = FMath::Clamp(ricRestitution * PhysMaterial->Restitution, 0.0f, 0.95f);
if (PhysMaterial->Restitution > 1.5f) {
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Material '%s' has very high restitution %.2f, clamping ricochet restitution to %.2f"),
*PhysMaterial->GetName(), PhysMaterial->Restitution, ricRestitution);
}
}
}
float dot = FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal) + 1.0f;
FVector cross = FVector::CrossProduct(Velocity.GetSafeNormal(), HitResult.Normal);
FVector flat = HitResult.Normal.RotateAngleAxis(-90.0f, cross);
#ifdef WITH_EDITOR
if (DebugEnabled) {
FColor DebugColor = FColor::MakeRedToGreenColorFromScalar(Velocity.Size() / MuzzleVelocityMax);
DrawDebugLine(GetWorld(), start, HitResult.Location, DebugColor, false, DebugTrailTime, 0, DebugTrailWidth);
};
#endif
float GrazingAngle = FMath::Pow(dot, GrazingAngleExponent);
// Improved penetration vector calculation to prevent sideways exits
FVector VelocityDirection = Velocity.GetSafeNormal();
FVector SurfaceNormal = HitResult.Normal;
// Calculate the component of velocity that's perpendicular to the surface
FVector VelocityParallel = VelocityDirection - FVector::DotProduct(VelocityDirection, SurfaceNormal) * SurfaceNormal;
FVector VelocityPerpendicular = VelocityDirection - VelocityParallel;
// Base penetration direction should be primarily forward through the material
FVector BasePenetrationDirection = VelocityPerpendicular.GetSafeNormal();
if (BasePenetrationDirection.Size() < 0.1f) {
// If velocity is parallel to surface, use surface normal
BasePenetrationDirection = -SurfaceNormal;
}
// Add controlled random spread
FVector PenetrationVector = RandomStream.VRandCone(BasePenetrationDirection, penEnterSpread);
// Blend with surface normal based on normalization settings, but limit lateral deviation
float NormalizationFactor = FMath::Lerp(penNormalization, penNormalizationGrazing, GrazingAngle);
PenetrationVector = FMath::Lerp(PenetrationVector, -SurfaceNormal, NormalizationFactor);
// Ensure the penetration vector doesn't deviate too much from forward direction
float ForwardComponent = FVector::DotProduct(PenetrationVector.GetSafeNormal(), VelocityDirection);
if (ForwardComponent < 0.3f) { // If penetration is too sideways, correct it
PenetrationVector = FMath::Lerp(PenetrationVector, VelocityDirection, 0.7f);
}
PenetrationVector = PenetrationVector.GetSafeNormal();
float PenetrationDistance = FMath::Lerp(MinPenetration, MaxPenetration, RandomStream.FRand()) * FMath::Pow((Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax) * 0.5f)), 2.0f) * penDepthMultiplier;
// Calculate angle factor to reduce penetration for grazing impacts
float AngleFactor = FMath::Abs(FVector::DotProduct(PenetrationVector.GetSafeNormal(), HitResult.Normal));
AngleFactor = FMath::Max(AngleFactor, 0.1f); // Prevent division by zero, minimum 10% effectiveness
// PenetrationDepth is the actual depth achieved, accounting for impact angle
float PenetrationDepth = PenetrationDistance * AngleFactor;
float BlockTIme = 1.0f;
if (PenetrationDistance > 0.0f) {
if (!neverPenetrate) {
// Improved penetration trace with better start location calculation
FVector PenetrationStart = HitResult.Location + HitResult.Normal * 0.1f; // Small offset to avoid self-intersection
FVector PenetrationEnd = HitResult.Location + PenetrationVector * PenetrationDistance;
BlockTIme = PenetrationTrace(PenetrationStart, PenetrationEnd, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal);
}
}
if (BlockTIme >= 0.999999f) {
//no pen
SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin);
float ricThreshold = 1.0f;
if (SpeedControlsRicochetProbability) { ricThreshold *= Velocity.Size() / MuzzleVelocityMax; };
if (!neverRicochet && RandomStream.FRand() * ricThreshold < FMath::Lerp(RicochetProbability * ricProbMultiplier, RicochetProbabilityGrazing * ricProbMultiplier, GrazingAngle)) {
//bounce
FVector bounceAngle = flat * dot * (1.0f - ricFriction);
bounceAngle += HitResult.Normal * (1.0f - dot) * ricRestitution;
// CRITICAL FIX: Normalize bounceAngle before using with VRandCone to prevent infinite velocity
FVector normalizedBounceAngle = bounceAngle.GetSafeNormal();
FVector ricochetDirection = RandomStream.VRandCone(normalizedBounceAngle, ricSpread);
// Apply proper velocity scaling with safety clamps
float ricochetSpeed = Velocity.Size() * FMath::Clamp(ricRestitution, 0.0f, 1.0f);
NewVelocity = ricochetDirection * ricochetSpeed;
Ricochet = true;
OwnerSafe = false;
}
else {
//stopped
NewVelocity = FVector(0, 0, 0);
}
}
else {
//penetration
float RemainingEnergy = FMath::Pow(1.0f - BlockTIme, 2.0f);
SetActorLocation(exitLoc + exitNormal * CollisionMargin);
// Improved exit velocity calculation to maintain forward momentum
FVector OriginalDirection = Velocity.GetSafeNormal();
// Calculate exit direction with controlled spread
FVector ExitDirection = RandomStream.VRandCone(PenetrationVector, penExitSpread * (1.0f - RemainingEnergy));
// Ensure exit direction has significant forward component
float ExitForwardComponent = FVector::DotProduct(ExitDirection.GetSafeNormal(), OriginalDirection);
if (ExitForwardComponent < 0.5f) {
// If exit direction is too sideways, blend more with original direction
ExitDirection = FMath::Lerp(ExitDirection, OriginalDirection, 0.6f);
}
// Blend with original velocity direction to maintain trajectory continuity
NewVelocity = FMath::Lerp(ExitDirection, OriginalDirection, RemainingEnergy * 0.8f);
NewVelocity = NewVelocity.GetSafeNormal() * RemainingEnergy * Velocity.Size();
Penetration = true;
OwnerSafe = false;
// X-ray trajectory debug visualization
if (DebugEnabled && DebugTrajectory && ShowXRayTrajectory)
{
DrawXRayTrajectory(HitResult.Location, exitLoc, PenetrationVector, PenetrationDepth, PhysMaterial);
}
// CRITICAL FIX: Ensure trace continues after penetration
// Calculate how much of the original trace distance was used for penetration
float PenetrationTraceDistance = (exitLoc - HitResult.Location).Size();
float OriginalTraceDistance = (start + (PreviousVelocity + Velocity)*0.5*delta - start).Size();
if (OriginalTraceDistance > 0.1f) {
// Adjust HitResult.Time to account for penetration distance
float PenetrationTimeFraction = FMath::Clamp(PenetrationTraceDistance / OriginalTraceDistance, 0.0f, 0.9f);
HitResult.Time = FMath::Min(HitResult.Time + PenetrationTimeFraction, 0.95f); // Leave some time for continuation
}
#ifdef WITH_EDITOR
// Debug visualization for penetration issues
if (DebugEnabled) {
// Draw entry point (red)
DrawDebugSphere(GetWorld(), HitResult.Location, 2.0f, 8, FColor::Red, false, DebugTrailTime);
// Draw exit point (green)
DrawDebugSphere(GetWorld(), exitLoc, 2.0f, 8, FColor::Green, false, DebugTrailTime);
// Draw penetration path (yellow)
DrawDebugLine(GetWorld(), HitResult.Location, exitLoc, FColor::Yellow, false, DebugTrailTime, 0, 1.0f);
// Draw exit normal (blue)
DrawDebugLine(GetWorld(), exitLoc, exitLoc + exitNormal * 10.0f, FColor::Blue, false, DebugTrailTime, 0, 1.0f);
}
#endif
}
//response
FVector Impulse = (Velocity - NewVelocity) * Mass * ImpulseMultiplier;
if (AddImpulse && HitResult.Component->IsSimulatingPhysics()) {
HitResult.Component->AddImpulseAtLocation(Impulse, HitResult.Location, HitResult.BoneName);
}
// New Ballistic Impact System Integration
if (UseNewImpactSystem && BallisticImpactComponent && BulletPropertiesAsset) {
// Calculate impact using new system
float NewPenetrationDepth;
bool bNewDidPenetrate;
FVector NewExitLocation;
FVector NewExitLoc = BallisticImpactComponent->CalculateBallisticImpact(
HitResult.Location,
Velocity,
BulletPropertiesAsset->BulletProperties,
PhysMaterial,
NewPenetrationDepth,
bNewDidPenetrate,
NewExitLocation
);
// Calculate ricochet using new system
float NewEnergyRetained;
bool bNewDidRicochet;
FVector NewRicochetVelocity = BallisticImpactComponent->CalculateRicochet(
HitResult.Location,
HitResult.Normal,
Velocity,
BulletPropertiesAsset->BulletProperties,
PhysMaterial,
NewEnergyRetained,
bNewDidRicochet
);
// Override legacy system results with new system
if (bNewDidPenetrate) {
Penetration = true;
PenetrationDepth = NewPenetrationDepth;
exitLoc = NewExitLocation;
}
if (bNewDidRicochet) {
Ricochet = true;
NewVelocity = NewRicochetVelocity;
}
}
//impact actual
if (HasAuthority()) {
OnImpact(Ricochet, Penetration, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), NewVelocity, Impulse, PenetrationDepth, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult);
}
else {
OnNetPredictedImpact(Ricochet, Penetration, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), NewVelocity, Impulse, PenetrationDepth, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult);
}
// Create debug impact widget if enabled
CreateDebugImpactWidget(HitResult.Location, Ricochet, Penetration, Velocity, PenetrationDepth, PhysMaterial);
bool bDidSpallThisImpact = false;
// Generate spalling fragments if conditions are met
if (HasAuthority() && ShouldGenerateSpalling(Velocity, PhysMaterial))
{
bDidSpallThisImpact = true;
// Entry spalling
{
float EntryImpactAngleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Abs(FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal))));
GenerateSpallFragments(HitResult.Location, Velocity, HitResult.Normal,
0.0f, EntryImpactAngleDeg,
PhysMaterial, HitResult.GetActor());
}
// Exit spalling (backspall) for penetration - typically more dangerous
if (Penetration && (exitLoc - HitResult.Location).Size() > 1.0f)
{
// Exit spalling velocity is based on remaining velocity after penetration
// Backspall typically has higher fragment velocity than entry spalling
FVector ExitSpallVelocity = NewVelocity * 1.2f; // Amplify for more realistic backspall effect
{
float ExitImpactAngleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Abs(FVector::DotProduct(NewVelocity.GetSafeNormal(), -exitNormal))));
GenerateSpallFragments(exitLoc, ExitSpallVelocity, -exitNormal,
PenetrationDepth, ExitImpactAngleDeg,
PhysMaterial, HitResult.GetActor());
}
}
}
if (bDidSpallThisImpact)
{
bHasSpalled = true;
NewVelocity = FVector::ZeroVector;
}
// SAFETY: Clamp velocity to prevent infinite speeds and numerical instability
float MaxSafeVelocity = MuzzleVelocityMax * 10.0f; // Allow up to 10x muzzle velocity as safety limit
if (!NewVelocity.IsNearlyZero() && NewVelocity.Size() > MaxSafeVelocity)
{
NewVelocity = NewVelocity.GetSafeNormal() * MaxSafeVelocity;
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Velocity clamped from %.1f to %.1f to prevent infinite speed (Ricochet=%s, Penetration=%s)"),
Velocity.Size(), NewVelocity.Size(), Ricochet ? TEXT("true") : TEXT("false"), Penetration ? TEXT("true") : TEXT("false"));
}
Velocity = NewVelocity;
if ((Velocity.Size() < DespawnVelocity) || (!Ricochet && !Penetration && (DespawnVelocity>0.0f))){
Deactivate();
}
CanRetrace = false;
}
else {
//prepare for time travel
if (Retrace) {
CanRetrace = true;
LastTraceStart = start;
LastTraceDelta = delta;
LastTracePrevVelocity = PreviousVelocity;
LastTraceVelocity = Velocity;
}
SetActorLocation(start + TraceDistance);
HitResult.Time = 1.0f;
OnTrace(start, GetActorLocation());
#ifdef WITH_EDITOR
if (DebugEnabled) {
FLinearColor Color = GetDebugColor(Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax)*0.5f));
DrawDebugLine(GetWorld(), start, start + TraceDistance, Color.ToFColor(true), false, DebugTrailTime, 0, 0);
}
}
#endif
return delta*(1.0f - HitResult.Time);
}
TArray<AActor*> AEBBullet::GetAttachedActorsRecursive(AActor* Actor, uint16 Depth, TArray<AActor*> VisitedActors) const {
//TODO: limit depth
TArray<AActor*> Attached;
Actor->GetAttachedActors(Attached);
TArray<AActor*> AttachedRecursive;
for (AActor* ActorRecursive : Attached) { // Skip already visited actors to avoid infinite recursion
if (!VisitedActors.Contains(ActorRecursive)) {
VisitedActors.Add(ActorRecursive);
AttachedRecursive += GetAttachedActorsRecursive(ActorRecursive, Depth+1, VisitedActors);
VisitedActors.Remove(ActorRecursive); // Remove from visited actors to allow other branches to visit it
}
}
Attached += AttachedRecursive;
return Attached;
}