840 lines
28 KiB
C++
840 lines
28 KiB
C++
// 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;
|
||
} |