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

840 lines
28 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2018 Mookie. All Rights Reserved.
#include "EBBullet.h"
#include "EBBarrel.h"
#include "EBUnitConversions.h"
#include "Kismet/KismetMathLibrary.h"
// Sets default values
AEBBullet::AEBBullet() {
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
SetActorTickEnabled(true);
bHasSpalled = false;
Collision = CreateDefaultSubobject<USphereComponent>(TEXT("Collision"));
RootComponent = Collision;
Collision->SetCollisionProfileName(TEXT("BlockAllDynamic"));
SetTickGroup(ETickingGroup::TG_PrePhysics);
// Set bullet lifetime to 10 seconds
InitialLifeSpan = 10.0f;
// Initialize firing barrel pointer
FiringBarrel = nullptr;
// Create the new ballistic impact component
BallisticImpactComponent = CreateDefaultSubobject<UEBBallisticImpactComponent>(TEXT("BallisticImpactComponent"));
BallisticImpactComponent->MaterialResponseMap = MaterialResponseMap;
}
// Called when the game starts or when spawned
void AEBBullet::BeginPlay() {
SetActorEnableCollision(AllowComponentCollisions);
// Update ballistic impact component with current settings
if (BallisticImpactComponent) {
BallisticImpactComponent->MaterialResponseMap = MaterialResponseMap;
BallisticImpactComponent->bUseMathematicalPenetration = UseMathematicalPhysics;
BallisticImpactComponent->bEnableBallisticCalculations = UseNewImpactSystem;
}
if(!IsRecycled){
Super::BeginPlay();
IsRecycled = true;
}
else{
ReceiveBeginPlay();
}
if (SafeLaunch) {
OwnerSafe = true;
}
if (DoFirstStepImmediately) {
float DeltaTime = GetWorld()->GetDeltaSeconds();
if (RandomFirstStepDelta) {
DeltaTime *= RandomStream.FRand();
};
if (FixedStep) {
Step(FixedStepSeconds);
}
else {
Step(DeltaTime);
}
}
}
// Called every frame
void AEBBullet::Tick(float DeltaTime) {
Super::Tick(DeltaTime);
if (FixedStep) {
AccumulatedDelta += DeltaTime;
while (AccumulatedDelta >= FixedStepSeconds) {
Step(FixedStepSeconds);
AccumulatedDelta -= FixedStepSeconds;
}
}
else {
Step(DeltaTime);
}
}
void AEBBullet::Step(float DeltaTime) {
FVector start = GetActorLocation();
bool sendUpdate = false;
// SANITY CHECK: Validate delta time to prevent simulation instability
if (DeltaTime <= 0.0f || DeltaTime > 1.0f || !FMath::IsFinite(DeltaTime))
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid DeltaTime %.6f, skipping step"), DeltaTime);
return;
}
// SANITY CHECK: Distance-based cleanup - bullets that travel too far should be destroyed
FVector SpawnLocation = GetFiringBarrel() ? GetFiringBarrel()->GetComponentLocation() : FVector::ZeroVector;
float MaxTravelDistance = IsSpallFragment ? 50000.0f : 1000000.0f; // 500m for fragments, 10km for regular bullets
float TravelDistance = FVector::Dist(start, SpawnLocation);
if (TravelDistance > MaxTravelDistance)
{
UE_LOG(LogTemp, Log, TEXT("EBBullet: Destroying bullet that traveled too far (%.1f cm > %.1f cm)"), TravelDistance, MaxTravelDistance);
Deactivate();
return;
}
// SANITY CHECK: Validate current location
if (!start.IsZero() && (!FMath::IsFinite(start.X) || !FMath::IsFinite(start.Y) || !FMath::IsFinite(start.Z)))
{
UE_LOG(LogTemp, Error, TEXT("EBBullet: Invalid position detected, destroying bullet"));
Deactivate();
return;
}
// SANITY CHECK: Validate velocity before processing
if (!FMath::IsFinite(Velocity.X) || !FMath::IsFinite(Velocity.Y) || !FMath::IsFinite(Velocity.Z) ||
FMath::IsNaN(Velocity.X) || FMath::IsNaN(Velocity.Y) || FMath::IsNaN(Velocity.Z))
{
UE_LOG(LogTemp, Error, TEXT("EBBullet: Invalid velocity detected, destroying bullet"));
Deactivate();
return;
}
// SANITY CHECK: Validate mass and physics properties
float EffectiveMass = GetEffectiveMass();
if (EffectiveMass <= 0.0f || !FMath::IsFinite(EffectiveMass))
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid mass %.6f, using default 0.004kg"), EffectiveMass);
Mass = 0.004f; // Default to 4 grams
}
// Initialize debug tracking
if (DebugEnabled)
{
DebugTraceCounter++;
DebugFrameStartTime = GetWorld()->GetTimeSeconds();
}
if (Retrace && CanRetrace) {
//time travel
float remainingTime = LastTraceDelta;
int remainingSteps = MaxTracesPerStep;
FVector PreviousVelocity = LastTracePrevVelocity;
SetActorLocation(LastTraceStart);
Velocity = LastTraceVelocity;
do {
if (RetraceOnAnotherChannel) {
remainingTime = Trace(GetActorLocation(),
PreviousVelocity,
remainingTime,
RetraceChannel);
}
else {
remainingTime = Trace(GetActorLocation(),
PreviousVelocity,
remainingTime,
TraceChannel);
}
PreviousVelocity = Velocity;
remainingSteps -= 1;
if (remainingTime > 0.0f) { sendUpdate = true; };
} while (remainingTime > 0.0f && remainingSteps > 0);
}
CanRetrace = false;
FVector PreviousVelocity = Velocity;
FVector CurrentLocation = GetActorLocation();
Velocity = UpdateVelocity(GetWorld(), CurrentLocation, Velocity, DeltaTime);
// Debug trajectory visualization
if (DebugEnabled && DebugTrajectory)
{
FVector EndLocation = CurrentLocation + Velocity * DeltaTime;
DrawTrajectoryDebug(start, EndLocation, Velocity, DeltaTime);
}
// Debug physics forces
if (DebugEnabled && DebugPhysics)
{
// Calculate forces for debug display
FVector GravityForce = OverrideGravity ? Gravity : FVector(0, 0, GetWorld()->GetGravityZ());
GravityForce *= GetEffectiveMass() * DeltaTime;
FVector WindVelocity = GetWind(GetWorld(), CurrentLocation);
FVector RelativeVelocity = Velocity - WindVelocity;
float AirDensity = GetAirDensity(GetWorld(), CurrentLocation);
float SpeedOfSound = GetSpeedOfSound(GetWorld(), CurrentLocation);
float MachNumber = RelativeVelocity.Size() / SpeedOfSound;
float DragCoeff = GetEffectiveDragCoefficient(MachNumber);
// Calculate drag force: F = 0.5 * Cd * ρ * A * v²
float CrossSectionArea = PI * FMath::Pow(GetEffectiveDiameter() / 200.0f, 2.0f); // Convert cm to m and get area
FVector DragForce = -RelativeVelocity.GetSafeNormal() * 0.5f * DragCoeff * AirDensity * CrossSectionArea * FMath::Pow(RelativeVelocity.Size() / 100.0f, 2.0f);
DragForce *= DeltaTime;
FVector WindForce = (WindVelocity - Velocity) * 0.1f * DeltaTime; // Simplified wind effect
DrawPhysicsDebug(CurrentLocation, DragForce, GravityForce, WindForce, AirDensity, SpeedOfSound);
}
// Debug ballistics calculations
if (DebugEnabled && DebugBallistics)
{
float VelocityMPS = FEBUnitConversions::CMPSToMPS(Velocity.Size());
float KineticEnergy = FEBUnitConversions::CalculateKineticEnergyJoules(GetEffectiveMass(), VelocityMPS);
float SpeedOfSound = GetSpeedOfSound(GetWorld(), CurrentLocation);
float MachNumber = Velocity.Size() / SpeedOfSound;
float DragCoeff = GetEffectiveDragCoefficient(MachNumber);
DrawBallisticsDebug(CurrentLocation, Velocity, KineticEnergy, DragCoeff, MachNumber);
}
//trace
float remainingTime = DeltaTime;
int remainingSteps = MaxTracesPerStep;
do {
remainingTime = Trace(GetActorLocation(),
PreviousVelocity,
remainingTime,
TraceChannel
);
PreviousVelocity = Velocity;
remainingSteps -= 1;
if (remainingTime > 0.0f) { sendUpdate = true; };
} while (remainingTime > 0.0f && remainingSteps > 0);
if (sendUpdate) {
if (ReliableReplication) {
VelocityChangeBroadcastReliable(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),GetActorLocation()), Velocity);
}
else {
VelocityChangeBroadcast(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(), GetActorLocation()), Velocity);
}
}
if(SafeDelay <= 0.0f){
OwnerSafe = false;
}
else {
SafeDelay -= DeltaTime;
}
if (RotateActor) {
FRotator NewRot = UKismetMathLibrary::MakeRotFromX(Velocity);
NewRot.Roll = GetActorRotation().Roll;
SetActorRotation(NewRot);
}
// Debug performance metrics
if (DebugEnabled && DebugPerformance)
{
float FrameTime = GetWorld()->GetTimeSeconds() - DebugFrameStartTime;
int32 PooledBullets = EnablePooling ? Pooled.Num() : 0;
DrawPerformanceDebug(GetActorLocation(), DebugTraceCounter, FrameTime, PooledBullets);
}
}
float AEBBullet::GetCurveValue(const UCurveFloat* curve, float in, float deflt) const {
if (curve == nullptr) return deflt;
return curve->GetFloatValue(in);
}
// Bullet-type specific scaling for spalling behaviour
float AEBBullet::GetSpallFactorByBulletType() const
{
if (!BulletPropertiesAsset)
{
return 1.0f;
}
switch (BulletPropertiesAsset->BulletProperties.BulletType)
{
case EBulletType::BT_ArmorPiercing:
case EBulletType::BT_ArmorPiercingIncendiary:
return 1.4f; // More prone to cause spall
case EBulletType::BT_FullMetalJacket:
case EBulletType::BT_Match:
case EBulletType::BT_Tracer:
return 1.0f; // Baseline
case EBulletType::BT_HollowPoint:
case EBulletType::BT_SoftPoint:
case EBulletType::BT_Frangible:
return 0.6f; // Less spall (expands/deforms instead)
case EBulletType::BT_Wadcutter:
case EBulletType::BT_SemiWadcutter:
return 0.8f; // Moderate
default:
return 1.0f;
}
}
void AEBBullet::ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) {
Super::ApplyWorldOffset(InOffset, bWorldShift);
LastTraceStart += InOffset;
}
// Mathematical Physics Function Implementations
float AEBBullet::GetEffectiveMass() const
{
if (UseMathematicalPhysics && BulletPropertiesAsset)
{
return BulletPropertiesAsset->BulletProperties.GetMassKg();
}
return Mass;
}
float AEBBullet::GetEffectiveDiameter() const
{
if (UseMathematicalPhysics && BulletPropertiesAsset)
{
return BulletPropertiesAsset->BulletProperties.GetDiameterCm();
}
return Diameter;
}
float AEBBullet::GetEffectiveDragCoefficient(float MachNumber) const
{
if (UseMathematicalPhysics && BulletPropertiesAsset)
{
return UEBMathematicalBallistics::CalculateDragCoefficient(
BulletPropertiesAsset->BulletProperties, MachNumber);
}
// Use artistic drag calculation
float DragCoeff = GetCurveValue(MachDragCurve, MachNumber, 0.5f);
return DragCoeff * FormFactor;
}
float AEBBullet::CalculateMathematicalPenetration(UPhysicalMaterial* Material, float VelocityMPS, float ImpactAngle) const
{
if (!UseMathematicalPhysics || !BulletPropertiesAsset)
{
return 0.0f;
}
FMathematicalMaterialProperties MaterialProps = GetMaterialProperties(Material);
return UEBMathematicalBallistics::CalculatePenetrationDepth(
BulletPropertiesAsset->BulletProperties,
MaterialProps,
VelocityMPS,
ImpactAngle
);
}
float AEBBullet::CalculateMathematicalRicochetProbability(UPhysicalMaterial* Material, float VelocityMPS, float ImpactAngle) const
{
if (!UseMathematicalPhysics || !BulletPropertiesAsset)
{
return 0.0f;
}
FMathematicalMaterialProperties MaterialProps = GetMaterialProperties(Material);
return UEBMathematicalBallistics::CalculateRicochetProbability(
BulletPropertiesAsset->BulletProperties,
MaterialProps,
VelocityMPS,
ImpactAngle
);
}
FMathematicalMaterialProperties AEBBullet::GetMaterialProperties(UPhysicalMaterial* Material) const
{
// Default properties for unknown materials
FMathematicalMaterialProperties DefaultProps;
if (!Material || !MaterialResponseMap)
{
return DefaultProps;
}
// Check if we have custom properties for this material
if (MaterialResponseMap->Map.Contains(Material))
{
const FEBMaterialResponseMapEntry& Entry = MaterialResponseMap->Map[Material];
if (Entry.UseMathematicalProperties)
{
return Entry.MathematicalProperties;
}
}
// If we have a material properties asset, use that as fallback
if (MaterialPropertiesAsset)
{
return MaterialPropertiesAsset->MaterialProperties;
}
return DefaultProps;
}
// Spalling Implementation
bool AEBBullet::ShouldGenerateSpalling(FVector ImpactVelocity, UPhysicalMaterial* Material) const
{
if (bHasSpalled)
{
return false;
}
if (!EnableSpalling || IsSpallFragment || !Material || !MaterialResponseMap)
{
return false;
}
const FEBMaterialResponseMapEntry* ResponseEntry = MaterialResponseMap->Map.Find(Material);
if (!ResponseEntry || !ResponseEntry->EnableSpalling)
{
return false;
}
float ImpactSpeed = ImpactVelocity.Size();
// Apply bullet-type scaling (AP increases spall likelihood, HP decreases)
float BulletTypeFactor = GetSpallFactorByBulletType();
// Calculate dynamic spalling threshold based on material properties
float MaterialSpallResistance = Material->Density * 0.1f; // Denser materials resist spalling more
float EffectiveThreshold = ResponseEntry->SpallVelocityThreshold * (1.0f + MaterialSpallResistance);
// Reduce/increase threshold according to bullet type
EffectiveThreshold /= BulletTypeFactor;
// Check if bullet has enough kinetic energy to cause spalling
float ImpactEnergy = 0.5f * GetEffectiveMass() * ImpactSpeed * ImpactSpeed;
float MinimumSpallEnergy = 50.0f * Material->Density; // Energy threshold scales with material density
MinimumSpallEnergy /= BulletTypeFactor;
return ImpactSpeed >= EffectiveThreshold && ImpactEnergy >= MinimumSpallEnergy;
}
void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal,
float PlateThicknessCM, float ImpactAngleDegrees,
UPhysicalMaterial* Material, AActor* HitActor)
{
// SANITY CHECK: Validate input parameters
if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z) ||
!FMath::IsFinite(ImpactVelocity.X) || !FMath::IsFinite(ImpactVelocity.Y) || !FMath::IsFinite(ImpactVelocity.Z) ||
!FMath::IsFinite(ImpactNormal.X) || !FMath::IsFinite(ImpactNormal.Y) || !FMath::IsFinite(ImpactNormal.Z))
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid spalling parameters, skipping fragment generation"));
return;
}
// Calculate cone half-angle (deg) based on empirical model θ = 25° + 10°·sin(impactAngle)
float ConeHalfAngleDeg = 25.0f + 10.0f * FMath::Sin(FMath::DegreesToRadians(ImpactAngleDegrees));
ConeHalfAngleDeg = FMath::Clamp(ConeHalfAngleDeg, 20.0f, 45.0f);
float ConeHalfAngleRad = FMath::DegreesToRadians(ConeHalfAngleDeg);
// Determine total spall mass from target plate (NOT bullet) if we know thickness
float TotalSpallMassKg = 0.0f;
if (PlateThicknessCM > 0.01f && Material)
{
float t_m = PlateThicknessCM * 0.01f; // cm → m
float r_m = t_m * FMath::Tan(ConeHalfAngleRad);
float Volume_m3 = (1.0f/3.0f) * PI * r_m * r_m * t_m; // cone volume
// Density assume UPhysicalMaterial::Density (kg/m³). If unreal default is g/cm³ convert, but here treat as kg/m³.
float TargetDensity = Material->Density > 1.0f ? Material->Density : Material->Density * 1000.0f; // fallback
TotalSpallMassKg = Volume_m3 * TargetDensity;
// Clamp to at most the bullet mass *2 to keep extremes sane
TotalSpallMassKg = FMath::Clamp(TotalSpallMassKg, 0.001f, GetEffectiveMass() * 2.0f);
}
if (TotalSpallMassKg <= 0.0f)
{
// Fallback: use percentage of bullet mass (old behaviour ~20%)
TotalSpallMassKg = GetEffectiveMass() * 0.2f;
}
// SANITY CHECK: Prevent excessive fragment generation for performance
static int32 GlobalFragmentCount = 0;
static float LastFrameTime = 0.0f;
float CurrentFrameTime = GetWorld()->GetTimeSeconds();
// Reset counter every second
if (CurrentFrameTime - LastFrameTime > 1.0f)
{
GlobalFragmentCount = 0;
LastFrameTime = CurrentFrameTime;
}
const int32 MaxFragmentsPerSecond = 200; // Performance limit
if (GlobalFragmentCount > MaxFragmentsPerSecond)
{
UE_LOG(LogTemp, Log, TEXT("EBBullet: Fragment generation limit reached this second (%d), skipping"), MaxFragmentsPerSecond);
return;
}
const FEBMaterialResponseMapEntry* ResponseEntry = MaterialResponseMap->Map.Find(Material);
if (!ResponseEntry)
{
return;
}
TSubclassOf<AEBBullet> FragmentClass = ResponseEntry->SpallFragmentClass;
if (!FragmentClass)
{
FragmentClass = GetClass();
}
int32 FragmentCount;
float SpreadAngleRad;
// Check if we should use mathematical spalling calculations
bool UseMathematicalSpalling = UseMathematicalPhysics && BulletPropertiesAsset &&
ResponseEntry->UseMathematicalProperties &&
ResponseEntry->MathematicalProperties.EnableMathematicalSpalling;
if (UseMathematicalSpalling)
{
// Use mathematical ballistics for spalling calculations
float VelocityMPS = ImpactVelocity.Size() / 100.0f; // Convert cm/s to m/s
FragmentCount = UEBMathematicalBallistics::CalculateSpallFragmentCount(
BulletPropertiesAsset->BulletProperties,
ResponseEntry->MathematicalProperties,
VelocityMPS,
ImpactAngleDegrees
);
float ConeAngle = UEBMathematicalBallistics::CalculateSpallConeAngle(
BulletPropertiesAsset->BulletProperties,
ResponseEntry->MathematicalProperties,
VelocityMPS,
ImpactAngleDegrees
);
SpreadAngleRad = FMath::DegreesToRadians(ConeAngle);
}
else
{
// Use artistic spalling calculations (original physics-based approach)
float ImpactKineticEnergy = 0.5f * GetEffectiveMass() * ImpactVelocity.Size() * ImpactVelocity.Size();
float AngleEffectiveness = FMath::Cos(FMath::DegreesToRadians(ImpactAngleDegrees));
float MaterialHardness = Material ? FMath::Clamp(Material->Density * 0.001f, 0.1f, 5.0f) : 1.0f;
float BaseFragmentCount = FMath::Sqrt(ImpactKineticEnergy / 1000.0f) * MaterialHardness * AngleEffectiveness;
FragmentCount = FMath::Clamp(FMath::RoundToInt(BaseFragmentCount), 1, ResponseEntry->SpallFragmentCount);
float PhysicalSpreadAngle = FMath::Lerp(15.0f, 60.0f, FMath::Sin(FMath::DegreesToRadians(ImpactAngleDegrees)));
SpreadAngleRad = FMath::DegreesToRadians(PhysicalSpreadAngle);
}
// SANITY CHECK: Clamp fragment count to reasonable limits
const int32 MaxFragmentsPerImpact = 20; // Absolute maximum per single impact
float BulletTypeFactor = GetSpallFactorByBulletType();
FragmentCount = FMath::Clamp(FMath::RoundToInt(FragmentCount * BulletTypeFactor), 0, MaxFragmentsPerImpact);
if (FragmentCount <= 0)
{
return;
}
// Distribute parent bullet's mass among fragments to ensure conservation
TArray<float> MassRatios;
float TotalMassRatioPart = 0.f;
for (int32 i = 0; i < FragmentCount; ++i)
{
float ratio;
if (UseMathematicalSpalling)
{
ratio = UEBMathematicalBallistics::CalculateSpallFragmentMass(BulletPropertiesAsset->BulletProperties, ResponseEntry->MathematicalProperties, i, FragmentCount);
}
else
{
// Using a simple power law for mass distribution. Smaller fragments are more common.
ratio = FMath::Pow(RandomStream.FRand(), 2.0f) + 0.1f;
}
MassRatios.Add(ratio);
TotalMassRatioPart += ratio;
}
// Normalize the mass ratios so they sum to 1.0
if (TotalMassRatioPart > 0.f)
{
for (int32 i = 0; i < FragmentCount; ++i)
{
MassRatios[i] /= TotalMassRatioPart;
}
}
else // Should not happen if FragmentCount > 0, but as a fallback
{
for (int32 i = 0; i < FragmentCount; ++i)
{
MassRatios.Add(1.0f / FragmentCount);
}
}
// Base spalling direction (combination of reflection and surface normal)
FVector ReflectionDirection = UKismetMathLibrary::GetReflectionVector(ImpactVelocity.GetSafeNormal(), ImpactNormal);
FVector SpallBaseDirection = FMath::Lerp(ImpactNormal, ReflectionDirection, 0.3f).GetSafeNormal();
// SANITY CHECK: Validate spall direction
if (!FMath::IsFinite(SpallBaseDirection.X) || !FMath::IsFinite(SpallBaseDirection.Y) || !FMath::IsFinite(SpallBaseDirection.Z) ||
SpallBaseDirection.IsNearlyZero())
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid spall direction calculated, using impact normal"));
SpallBaseDirection = ImpactNormal.GetSafeNormal();
}
for (int32 i = 0; i < FragmentCount; i++)
{
float FragmentMass;
float FragmentSpeed;
float FragmentMassRatio = MassRatios[i];
// Determine fragment mass using Mott distribution (simple inverse CDF)
float xi = RandomStream.FRand();
float m_char = TotalSpallMassKg / FragmentCount; // characteristic mass, rough
float fragMassMott = FMath::Square(-FMath::Loge(FMath::Clamp(1.0f - xi, 0.0001f, 0.9999f))) * m_char;
FragmentMass = FMath::Clamp(fragMassMott, 0.0001f, TotalSpallMassKg * 0.5f);
// Compute velocity using a Gurney-like energy share
float VelocityMPS;
if (UseMathematicalSpalling)
{
float VelocityMPS_impact = ImpactVelocity.Size() / 100.0f;
VelocityMPS = UEBMathematicalBallistics::CalculateSpallFragmentVelocity(
BulletPropertiesAsset->BulletProperties,
ResponseEntry->MathematicalProperties,
VelocityMPS_impact,
FragmentMass / TotalSpallMassKg);
}
else
{
float AvailableEnergy = 0.5f * TotalSpallMassKg * FMath::Square(ImpactVelocity.Size() / 100.0f) * 0.15f; // 15% of bullet KE
VelocityMPS = FMath::Sqrt(2.0f * (AvailableEnergy / FragmentCount) / FragmentMass);
}
FragmentSpeed = VelocityMPS * 100.0f; // m/s → cm/s
// SANITY CHECK: Validate fragment properties
if (FragmentMass <= 0.0f || !FMath::IsFinite(FragmentMass))
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid fragment mass %.6f, skipping fragment %d"), FragmentMass, i);
continue;
}
if (FragmentSpeed <= 0.0f || !FMath::IsFinite(FragmentSpeed) || FragmentSpeed > 500000.0f) // Max 5km/s
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid fragment speed %.1f, clamping to reasonable range"), FragmentSpeed);
FragmentSpeed = FMath::Clamp(FragmentSpeed, 100.0f, 100000.0f); // 1-1000 m/s range
}
// SANITY CHECK: Check global fragment limit before spawning
if (GlobalFragmentCount >= MaxFragmentsPerSecond)
{
UE_LOG(LogTemp, Log, TEXT("EBBullet: Global fragment limit reached, stopping fragment generation"));
break;
}
// Fragment direction with dispersion
FVector FragmentDirection = RandomStream.VRandCone(SpallBaseDirection, SpreadAngleRad);
FVector FragmentVelocity = FragmentDirection * FragmentSpeed;
// SANITY CHECK: Validate fragment velocity
if (!FMath::IsFinite(FragmentVelocity.X) || !FMath::IsFinite(FragmentVelocity.Y) || !FMath::IsFinite(FragmentVelocity.Z) ||
FragmentVelocity.IsNearlyZero())
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid fragment velocity, skipping fragment %d"), i);
continue;
}
FVector SpawnLocation = ImpactLocation + ImpactNormal * 2.0f;
if (HasAuthority())
{
// Create spawn transform
FTransform SpawnTransform;
SpawnTransform.SetLocation(SpawnLocation);
SpawnTransform.SetRotation(FragmentVelocity.Rotation().Quaternion());
// Spawn fragment using deferred spawn to set properties before BeginPlay
AEBBullet* Fragment = GetWorld()->SpawnActorDeferred<AEBBullet>(FragmentClass, SpawnTransform, GetOwner(), GetInstigator());
if (Fragment)
{
// Configure fragment properties before finishing spawn
Fragment->Velocity = FragmentVelocity;
Fragment->Mass = FragmentMass;
Fragment->OwnerSafe = false;
Fragment->IsSpallFragment = true;
// CRITICAL: Disable spalling on fragments to prevent chain spalling
Fragment->EnableSpalling = false;
// Set fragment-specific properties for proper behavior
Fragment->SetFiringBarrel(GetFiringBarrel());
Fragment->DespawnVelocity = FMath::Max(DespawnVelocity * 0.5f, 50.0f); // Lower threshold for fragments
Fragment->InitialLifeSpan = FMath::Min(InitialLifeSpan, 5.0f); // Shorter lifespan for fragments
// Inherit material response map but disable spalling
if (Fragment->MaterialResponseMap == nullptr)
{
Fragment->MaterialResponseMap = MaterialResponseMap;
}
// Set up velocity parameters to ensure fragment uses its assigned velocity
Fragment->MuzzleVelocityMin = FragmentVelocity.Size();
Fragment->MuzzleVelocityMax = FragmentVelocity.Size();
Fragment->Spread = 0.0f; // No additional spread
// Configure for fragment physics
Fragment->Mass = FragmentMass;
Fragment->Diameter = GetEffectiveDiameter() * 0.3f; // Smaller diameter for fragments
// Set ignored actors to prevent immediate collisions
Fragment->IgnoredActors.Add(HitActor);
Fragment->IgnoredActors.Add(this);
Fragment->IgnoredActors.Append(IgnoredActors);
// Ensure proper velocity assignment by setting RandomStream seed
Fragment->RandomStream.GenerateNewSeed();
// Finish spawning to trigger BeginPlay with all properties set
Fragment->FinishSpawning(SpawnTransform);
// Double-check velocity assignment after spawn
if (Fragment->Velocity.Size() < FragmentVelocity.Size() * 0.8f)
{
Fragment->Velocity = FragmentVelocity;
}
// SANITY CHECK: Increment global fragment counter
GlobalFragmentCount++;
// Debug logging for spall fragment creation
if (DebugEnabled)
{
UE_LOG(LogTemp, Warning, TEXT("Spall Fragment Created: Velocity=%.1f cm/s, Mass=%.4f kg, IsSpallFragment=%s, EnableSpalling=%s"),
Fragment->Velocity.Size(), Fragment->Mass,
Fragment->IsSpallFragment ? TEXT("true") : TEXT("false"),
Fragment->EnableSpalling ? TEXT("true") : TEXT("false"));
}
}
else
{
UE_LOG(LogTemp, Warning, TEXT("EBBullet: Failed to spawn spall fragment"));
}
}
OnSpallFragmentGenerated(SpawnLocation, FragmentVelocity, FragmentMass, Material);
}
// ----- Reduce parent bullet (mass & diameter) to account for material lost -----
float ParentMassKg = GetEffectiveMass();
float RemainingMassKg = FMath::Max(ParentMassKg - TotalSpallMassKg, 0.001f);
if (RemainingMassKg < ParentMassKg)
{
float MassScale = RemainingMassKg / ParentMassKg;
Mass = RemainingMassKg;
// Diameter scales with cube-root of mass for similar density
float DiamScale = FMath::Pow(MassScale, 1.0f / 3.0f);
Diameter *= DiamScale;
}
}
void AEBBullet::OnSpallFragmentGenerated_Implementation(FVector FragmentLocation, FVector FragmentVelocity, float FragmentMass, UPhysicalMaterial* Material)
{
}
void AEBBullet::ConfigureAsSpallFragment(float FragmentMass, float FragmentVelocity)
{
// Mark as spall fragment
IsSpallFragment = true;
EnableSpalling = false;
// Set fragment properties
Mass = FragmentMass;
Diameter = GetEffectiveDiameter() * 0.3f; // Smaller fragments
// Set velocity parameters
float VelocitySize = FragmentVelocity;
MuzzleVelocityMin = VelocitySize;
MuzzleVelocityMax = VelocitySize;
Spread = 0.0f;
// Fragment lifecycle
DespawnVelocity = FMath::Max(DespawnVelocity * 0.5f, 50.0f);
InitialLifeSpan = FMath::Min(InitialLifeSpan, 5.0f);
// Safety settings
OwnerSafe = false;
}
bool AEBBullet::ProcessSequentialHits(TArray<FHitResult>& AllHits, FVector& CurrentVelocity, float DeltaTime, int32& ProcessedHitCount)
{
const float MinWallSeparation = 5.0f; // Minimum distance (cm) between walls to consider them separate
const int32 MaxSequentialHits = 5; // Maximum hits to process in one trace step
ProcessedHitCount = 0;
bool AnyPenetration = false;
// Sort hits by distance
AllHits.Sort([](const FHitResult& A, const FHitResult& B) {
return A.Distance < B.Distance;
});
for (int32 i = 0; i < AllHits.Num() && ProcessedHitCount < MaxSequentialHits; i++)
{
const FHitResult& Hit = AllHits[i];
// Skip if this hit is too close to the previous one (likely same wall)
if (ProcessedHitCount > 0)
{
float DistanceToPrevious = FVector::Dist(Hit.Location, AllHits[ProcessedHitCount-1].Location);
if (DistanceToPrevious < MinWallSeparation)
{
continue;
}
}
// Check if we have enough energy to continue penetrating
float CurrentSpeed = CurrentVelocity.Size();
if (CurrentSpeed < DespawnVelocity)
{
break; // Not enough energy to continue
}
// Process this hit like a normal penetration
// This is a simplified version - in practice you'd want to call the full penetration logic
float VelocityLoss = FMath::Clamp(Hit.Distance / 100.0f, 0.1f, 0.8f); // Lose velocity based on wall thickness
CurrentVelocity *= (1.0f - VelocityLoss);
ProcessedHitCount++;
AnyPenetration = true;
// Add small random deflection to simulate material effects
if (CurrentVelocity.Size() > 0.1f)
{
FVector RandomDeflection = RandomStream.VRandCone(CurrentVelocity.GetSafeNormal(), FMath::DegreesToRadians(2.0f));
CurrentVelocity = RandomDeflection * CurrentVelocity.Size();
}
}
return AnyPenetration;
}