401 lines
16 KiB
C++
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;
|
|
} |