// Copyright 2016 Mookie. All Rights Reserved. #include "EBBullet.h" float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEnumAsByte CollisionChannel) { bool Hit; FHitResult HitResult; TArray 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); // Generate spalling fragments if conditions are met if (HasAuthority() && ShouldGenerateSpalling(Velocity, PhysMaterial)) { // Entry spalling GenerateSpallFragments(HitResult.Location, Velocity, HitResult.Normal, 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 GenerateSpallFragments(exitLoc, ExitSpallVelocity, -exitNormal, PhysMaterial, HitResult.GetActor()); } } // 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 AEBBullet::GetAttachedActorsRecursive(AActor* Actor, uint16 Depth, TArray VisitedActors) const { //TODO: limit depth TArray Attached; Actor->GetAttachedActors(Attached); TArray 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; }