This commit is contained in:
2025-07-04 01:09:56 -07:00
parent 61a7656cb1
commit a72231ee28
13 changed files with 1240 additions and 64 deletions
@@ -5,21 +5,45 @@ bool AEBBullet::CollisionFilter_Implementation(FHitResult HitResult) const{
};
FHitResult AEBBullet::FilterHits(TArray<FHitResult> Results, bool &hit) const{
TArray<FHitResult> OutResults;
for (FHitResult Result : Results) {
if (Result.bBlockingHit) {
hit = true;
return Result;
}else{
if (CollisionFilter(Result)) {
hit = true;
return Result;
}
}
if (Results.Num() == 0) {
hit = false;
return FHitResult();
}
hit = false;
return FHitResult(); //blank
// Sort hits by distance to get the closest valid hit
Results.Sort([](const FHitResult& A, const FHitResult& B) {
return A.Distance < B.Distance;
});
// Filter out invalid hits and duplicates
TArray<FHitResult> ValidHits;
for (const FHitResult& Result : Results) {
// Skip if not a blocking hit or if filter rejects it
if (!Result.bBlockingHit || !CollisionFilter(Result)) {
continue;
}
// Skip if this hit is essentially the same as a previous one (duplicate/overlapping geometry)
bool IsDuplicate = false;
for (const FHitResult& ValidHit : ValidHits) {
float Distance = FVector::Dist(Result.Location, ValidHit.Location);
if (Distance < 0.5f && Result.GetComponent() == ValidHit.GetComponent()) { // 0.5cm tolerance
IsDuplicate = true;
break;
}
}
if (!IsDuplicate) {
ValidHits.Add(Result);
}
}
if (ValidHits.Num() == 0) {
hit = false;
return FHitResult();
}
// Return the closest valid hit
hit = true;
return ValidHits[0];
};
@@ -2,6 +2,7 @@
#include "EBBallisticImpactComponent.h"
#include "EBMathematicalBallistics.h"
#include "EBUnitConversions.h"
#include "Engine/Engine.h"
#include "Kismet/KismetMathLibrary.h"
@@ -49,7 +50,7 @@ FVector UEBBallisticImpactComponent::CalculateBallisticImpact(
UEBMaterialPropertiesAsset* MaterialProps = GetMaterialProperties(HitMaterial);
if (MaterialProps)
{
float VelocityMPS = ProjectileVelocity.Size() * 0.3048f; // feet to meters
float VelocityMPS = FEBUnitConversions::CMPSToMPS(ProjectileVelocity.Size()); // Convert cm/s (Unreal units) to m/s
float ImpactAngle = CalculateImpactAngle(ProjectileVelocity, FVector::UpVector); // Simplified
OutPenetrationDepth = UEBMathematicalBallistics::CalculatePenetrationDepth(
@@ -1,5 +1,7 @@
// Copyright 2018 Mookie. All Rights Reserved.
#include "EBBarrel.h"
#include "EBBullet.h"
#include "EngineUtils.h"
UEBBarrel::UEBBarrel() {
PrimaryComponentTick.bCanEverTick = true;
@@ -240,4 +242,88 @@ void UEBBarrel::ApplyRecoil_Implementation(UPrimitiveComponent* Component, FVect
if (Component->IsSimulatingPhysics()) {
Component->AddImpulseAtLocation(Impulse, InLocation);
}
}
// Enhanced Debug Control Functions
void UEBBarrel::SetBulletDebugCategory(const FString& CategoryName, bool bEnabled)
{
// Find all bullets fired from this barrel and update their debug settings
UWorld* World = GetWorld();
if (!World)
{
return;
}
for (TActorIterator<AEBBullet> ActorItr(World); ActorItr; ++ActorItr)
{
AEBBullet* Bullet = *ActorItr;
if (Bullet && Bullet->GetFiringBarrel() == this)
{
Bullet->ToggleDebugCategory(CategoryName, bEnabled);
}
}
}
void UEBBarrel::SetAllBulletDebugCategories(bool bEnabled)
{
// Update all debug categories for bullets from this barrel
SetBulletDebugCategory(TEXT("Trajectory"), bEnabled);
SetBulletDebugCategory(TEXT("Impact"), bEnabled);
SetBulletDebugCategory(TEXT("Physics"), bEnabled);
SetBulletDebugCategory(TEXT("Performance"), bEnabled);
SetBulletDebugCategory(TEXT("Ballistics"), bEnabled);
SetBulletDebugCategory(TEXT("Spalling"), bEnabled);
SetBulletDebugCategory(TEXT("Pooling"), bEnabled);
}
void UEBBarrel::ClearAllBulletDebugDisplay()
{
// Clear debug display for all bullets from this barrel
UWorld* World = GetWorld();
if (!World)
{
return;
}
for (TActorIterator<AEBBullet> ActorItr(World); ActorItr; ++ActorItr)
{
AEBBullet* Bullet = *ActorItr;
if (Bullet && Bullet->GetFiringBarrel() == this)
{
Bullet->ClearDebugDisplay();
}
}
}
FString UEBBarrel::GetBulletDebugInfo() const
{
// Collect debug information from all bullets fired from this barrel
UWorld* World = GetWorld();
if (!World)
{
return TEXT("No world available");
}
int32 BulletCount = 0;
int32 DebugEnabledCount = 0;
FString DebugInfo = TEXT("Barrel Debug Info:\n");
for (TActorIterator<AEBBullet> ActorItr(World); ActorItr; ++ActorItr)
{
AEBBullet* Bullet = *ActorItr;
if (Bullet && Bullet->GetFiringBarrel() == this)
{
BulletCount++;
if (Bullet->DebugEnabled)
{
DebugEnabledCount++;
}
}
}
DebugInfo += FString::Printf(TEXT("Total Bullets: %d\n"), BulletCount);
DebugInfo += FString::Printf(TEXT("Debug Enabled: %d\n"), DebugEnabledCount);
DebugInfo += FString::Printf(TEXT("Impact Debug: %s\n"), DebugImpactInfo ? TEXT("ON") : TEXT("OFF"));
return DebugInfo;
}
@@ -494,4 +494,57 @@ void AEBBullet::ConfigureAsSpallFragment(float FragmentMass, float FragmentVeloc
// 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;
}
+511 -10
View File
@@ -2,13 +2,22 @@
#include "EBBullet.h"
#include "EBBarrel.h"
#include "EBUnitConversions.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "PhysicalMaterials/PhysicalMaterial.h"
#include "DrawDebugHelpers.h"
// Initialize static debug counter
int32 AEBBullet::DebugMessageCounter = 0;
void AEBBullet::CreateDebugImpactWidget(FVector Location, bool bRicochet, bool bPenetration, FVector ImpactVelocity, float PenetrationDepth, UPhysicalMaterial* Material)
{
if (!DebugEnabled || !DebugImpact)
{
return;
}
UEBBarrel* Barrel = GetFiringBarrel();
if (!Barrel || !Barrel->DebugImpactInfo)
{
@@ -21,44 +30,536 @@ void AEBBullet::CreateDebugImpactWidget(FVector Location, bool bRicochet, bool b
return;
}
// Add to debug tracking
DebugImpactLocations.Add(Location);
DebugImpactTimes.Add(World->GetTimeSeconds());
// Clean up old debug data
float CurrentTime = World->GetTimeSeconds();
for (int32 i = DebugImpactLocations.Num() - 1; i >= 0; i--)
{
if (CurrentTime - DebugImpactTimes[i] > ImpactDebugTime)
{
DebugImpactLocations.RemoveAt(i);
DebugImpactTimes.RemoveAt(i);
}
}
// Build comprehensive debug information
FString ImpactType;
FLinearColor ImpactColor;
if (bRicochet)
{
ImpactType = TEXT("RICOCHET");
ImpactColor = ImpactColorRicochet;
}
else if (bPenetration)
{
ImpactType = TEXT("PENETRATION");
ImpactColor = ImpactColorPenetration;
}
else
{
ImpactType = TEXT("STOPPED");
ImpactColor = ImpactColorStopped;
}
FString MaterialName = Material ? Material->GetName() : TEXT("Unknown");
float VelocityMPS = ImpactVelocity.Size();
float VelocityKMH = VelocityMPS * 0.036f; // Convert cm/s to km/h
float VelocityCMPS = ImpactVelocity.Size();
float VelocityMPS = FEBUnitConversions::CMPSToMPS(VelocityCMPS);
float VelocityKMH = FEBUnitConversions::CMPSToKMH(VelocityCMPS);
float KineticEnergy = FEBUnitConversions::CalculateKineticEnergyJoules(GetEffectiveMass(), VelocityMPS);
FString DebugInfo = FString::Printf(TEXT(
"Impact: %s | Material: %s | Velocity: %.1f m/s (%.1f km/h) | Mass: %.3f kg | Diameter: %.2f cm | Energy: %.1f J | Penetration: %.2f cm"
),
*ImpactType,
*MaterialName,
VelocityMPS / 100.0f, // Convert cm/s to m/s for display
VelocityMPS,
VelocityKMH,
GetEffectiveMass(),
GetEffectiveDiameter(),
0.5f * GetEffectiveMass() * FMath::Pow(VelocityMPS / 100.0f, 2.0f), // Kinetic energy in Joules
KineticEnergy,
PenetrationDepth
);
// Use simple on-screen debug message instead of UMG widget
if (GEngine)
// Display debug information
if (UseOnScreenMessages && GEngine)
{
GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Yellow, DebugInfo);
// Manage message count
if (DebugMessageCounter > MaxOnScreenMessages)
{
DebugMessageCounter = 0;
}
GEngine->AddOnScreenDebugMessage(DebugMessageCounter++, ImpactDebugTime,
FColor(ImpactColor.R * 255, ImpactColor.G * 255, ImpactColor.B * 255), DebugInfo);
}
// Also draw debug text in world space
DrawDebugString(World, Location + FVector(0, 0, 50), DebugInfo, nullptr, FColor::Yellow, 10.0f, false, 1.2f);
if (UseWorldSpaceText)
{
DrawDebugString(World, Location + FVector(0, 0, 50), DebugInfo, nullptr,
FColor(ImpactColor.R * 255, ImpactColor.G * 255, ImpactColor.B * 255),
ImpactDebugTime, false, WorldTextScale);
}
}
void AEBBullet::DrawTrajectoryDebug(FVector StartLocation, FVector EndLocation, FVector CurrentVelocity, float DeltaTime)
{
if (!DebugEnabled || !DebugTrajectory)
{
return;
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
float CurrentTime = World->GetTimeSeconds();
// Add trajectory point
if (ShowTrail)
{
DebugTrajectoryPoints.Add(StartLocation);
DebugTrajectoryTimes.Add(CurrentTime);
// Clean up old trajectory points
for (int32 i = DebugTrajectoryPoints.Num() - 1; i >= 0; i--)
{
if (CurrentTime - DebugTrajectoryTimes[i] > DebugTrailTime)
{
DebugTrajectoryPoints.RemoveAt(i);
DebugTrajectoryTimes.RemoveAt(i);
}
}
// Draw trajectory trail
if (DebugTrajectoryPoints.Num() > 1)
{
for (int32 i = 1; i < DebugTrajectoryPoints.Num(); i++)
{
float Alpha = FMath::Clamp((CurrentTime - DebugTrajectoryTimes[i]) / DebugTrailTime, 0.0f, 1.0f);
float VelocityNormalized = FMath::Clamp(CurrentVelocity.Size() / 100000.0f, 0.0f, 1.0f); // Normalize to ~1000m/s
FLinearColor TrailColor = FMath::Lerp(DebugTrailColorSlow, DebugTrailColorFast, VelocityNormalized);
TrailColor.A = 1.0f - Alpha;
float LineThickness = DebugTrailWidth > 0 ? DebugTrailWidth : 1.0f;
DrawDebugLine(World, DebugTrajectoryPoints[i-1], DebugTrajectoryPoints[i],
FColor(TrailColor.R * 255, TrailColor.G * 255, TrailColor.B * 255, TrailColor.A * 255),
false, -1, 0, LineThickness);
}
}
}
// Show velocity vectors
if (ShowVelocityVectors)
{
FVector VelocityVector = CurrentVelocity * VelocityVectorScale;
DrawDebugDirectionalArrow(World, StartLocation, StartLocation + VelocityVector,
50.0f, FColor::Blue, false, DebugTrailTime, 0, 2.0f);
// Add velocity magnitude text
FString VelocityText = FString::Printf(TEXT("%.1f m/s"),
FEBUnitConversions::CMPSToMPS(CurrentVelocity.Size()));
DrawDebugString(World, StartLocation + FVector(0, 0, 30), VelocityText, nullptr,
FColor::Blue, DebugTrailTime, false, WorldTextScale);
}
// Show path prediction
if (ShowPathPrediction && PathPredictionSteps > 0)
{
FVector PredictLocation = StartLocation;
FVector PredictVelocity = CurrentVelocity;
float PredictDeltaTime = DeltaTime * 5.0f; // Predict further ahead
for (int32 i = 0; i < PathPredictionSteps; i++)
{
FVector NextLocation = PredictLocation + PredictVelocity * PredictDeltaTime;
// Apply gravity and drag prediction (simplified)
FVector GravityForce = OverrideGravity ? Gravity : FVector(0, 0, -980);
PredictVelocity += GravityForce * PredictDeltaTime;
DrawDebugLine(World, PredictLocation, NextLocation, FColor::Yellow, false,
DebugTrailTime, 0, 1.0f);
DrawDebugSphere(World, NextLocation, 2.0f, 8, FColor::Yellow, false, DebugTrailTime);
PredictLocation = NextLocation;
}
}
}
void AEBBullet::DrawImpactDebug(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, bool bRicochet, bool bPenetration, float PenetrationDepth, UPhysicalMaterial* Material)
{
if (!DebugEnabled || !DebugImpact)
{
return;
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
// Choose color based on impact type
FLinearColor ImpactColor;
if (bRicochet)
{
ImpactColor = ImpactColorRicochet;
}
else if (bPenetration)
{
ImpactColor = ImpactColorPenetration;
}
else
{
ImpactColor = ImpactColorStopped;
}
FColor DebugColor(ImpactColor.R * 255, ImpactColor.G * 255, ImpactColor.B * 255);
// Show impact points
if (ShowImpactPoints)
{
DrawDebugSphere(World, ImpactLocation, ImpactPointSize, 12, DebugColor, false, ImpactDebugTime);
}
// Show impact normals
if (ShowImpactNormals)
{
DrawDebugDirectionalArrow(World, ImpactLocation, ImpactLocation + ImpactNormal * 50.0f,
20.0f, DebugColor, false, ImpactDebugTime, 0, 2.0f);
}
// Show penetration depth
if (ShowPenetrationDepth && bPenetration)
{
FVector PenetrationVector = -ImpactNormal * PenetrationDepth;
DrawDebugLine(World, ImpactLocation, ImpactLocation + PenetrationVector,
FColor::Green, false, ImpactDebugTime, 0, 3.0f);
FString PenetrationText = FString::Printf(TEXT("Pen: %.1f cm"), PenetrationDepth);
DrawDebugString(World, ImpactLocation + PenetrationVector + FVector(0, 0, 20),
PenetrationText, nullptr, FColor::Green, ImpactDebugTime, false, WorldTextScale);
}
// Show ricochet angles
if (ShowRicochetAngles && bRicochet)
{
FVector ReflectedVelocity = ImpactVelocity - 2 * FVector::DotProduct(ImpactVelocity, ImpactNormal) * ImpactNormal;
DrawDebugDirectionalArrow(World, ImpactLocation, ImpactLocation + ReflectedVelocity.GetSafeNormal() * 75.0f,
30.0f, FColor::Yellow, false, ImpactDebugTime, 0, 2.0f);
float RicochetAngle = FMath::Acos(FVector::DotProduct(-ImpactVelocity.GetSafeNormal(), ImpactNormal));
FString AngleText = FString::Printf(TEXT("Angle: %.1f°"), FMath::RadiansToDegrees(RicochetAngle));
DrawDebugString(World, ImpactLocation + FVector(0, 0, 40), AngleText, nullptr,
FColor::Yellow, ImpactDebugTime, false, WorldTextScale);
}
// Show material information
if (ShowMaterialInfo && Material)
{
FString MaterialInfo = FString::Printf(TEXT("Material: %s"), *Material->GetName());
DrawDebugString(World, ImpactLocation + FVector(0, 0, 60), MaterialInfo, nullptr,
DebugColor, ImpactDebugTime, false, WorldTextScale);
}
}
void AEBBullet::DrawPhysicsDebug(FVector Location, FVector DragForce, FVector GravityForce, FVector WindForce, float AirDensity, float SpeedOfSound)
{
if (!DebugEnabled || !DebugPhysics)
{
return;
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
// Show drag forces
if (ShowDragForces && !DragForce.IsNearlyZero())
{
FVector DragVector = DragForce * ForceVectorScale;
DrawDebugDirectionalArrow(World, Location, Location + DragVector,
10.0f, FColor::Red, false, PhysicsDebugTime, 0, 1.5f);
FString DragText = FString::Printf(TEXT("Drag: %.1f N"), DragForce.Size());
DrawDebugString(World, Location + DragVector + FVector(0, 0, 10), DragText, nullptr,
FColor::Red, PhysicsDebugTime, false, WorldTextScale);
}
// Show gravity effect
if (ShowGravityEffect && !GravityForce.IsNearlyZero())
{
FVector GravityVector = GravityForce * ForceVectorScale;
DrawDebugDirectionalArrow(World, Location, Location + GravityVector,
10.0f, FColor::Purple, false, PhysicsDebugTime, 0, 1.5f);
FString GravityText = FString::Printf(TEXT("Gravity: %.1f N"), GravityForce.Size());
DrawDebugString(World, Location + GravityVector + FVector(0, 0, 10), GravityText, nullptr,
FColor::Purple, PhysicsDebugTime, false, WorldTextScale);
}
// Show wind effect
if (ShowWindEffect && !WindForce.IsNearlyZero())
{
FVector WindVector = WindForce * ForceVectorScale;
DrawDebugDirectionalArrow(World, Location, Location + WindVector,
10.0f, FColor::Cyan, false, PhysicsDebugTime, 0, 1.5f);
FString WindText = FString::Printf(TEXT("Wind: %.1f N"), WindForce.Size());
DrawDebugString(World, Location + WindVector + FVector(0, 0, 10), WindText, nullptr,
FColor::Cyan, PhysicsDebugTime, false, WorldTextScale);
}
// Show atmospheric data
if (ShowAtmosphericData)
{
FString AtmosphericInfo = FString::Printf(TEXT(
"Air Density: %.3f kg/m³\nSpeed of Sound: %.1f m/s\nAltitude: %.1f m"
),
AirDensity,
FEBUnitConversions::CMPSToMPS(SpeedOfSound),
FEBUnitConversions::CMToM(GetAltitude(World, Location))
);
DrawDebugString(World, Location + FVector(0, 0, 80), AtmosphericInfo, nullptr,
FColor::White, PhysicsDebugTime, false, WorldTextScale);
}
}
void AEBBullet::DrawBallisticsDebug(FVector Location, FVector BulletVelocity, float KineticEnergy, float DragCoefficient, float MachNumber)
{
if (!DebugEnabled || !DebugBallistics)
{
return;
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
// Show energy calculations
if (ShowEnergyCalculations)
{
FString EnergyInfo = FString::Printf(TEXT(
"Kinetic Energy: %.1f J\nMomentum: %.3f kg⋅m/s\nPower: %.1f W"
),
KineticEnergy,
GetEffectiveMass() * FEBUnitConversions::CMPSToMPS(BulletVelocity.Size()),
KineticEnergy * BulletVelocity.Size() / 100.0f // Simplified power calculation
);
DrawDebugString(World, Location + FVector(0, 0, 100), EnergyInfo, nullptr,
FColor::Orange, BallisticsDebugTime, false, WorldTextScale);
}
// Show mathematical comparison
if (ShowMathematicalComparison && UseMathematicalPhysics)
{
FString MathInfo = FString::Printf(TEXT(
"Mathematical Mode: ON\nDrag Coefficient: %.3f\nMach Number: %.2f"
),
DragCoefficient,
MachNumber
);
DrawDebugString(World, Location + FVector(0, 0, 120), MathInfo, nullptr,
FColor::Green, BallisticsDebugTime, false, WorldTextScale);
}
// Show ballistic coefficient
if (ShowBallisticCoefficient)
{
float BallisticCoefficient = GetEffectiveMass() / (DragCoefficient * GetEffectiveDiameter() * GetEffectiveDiameter());
FString BCInfo = FString::Printf(TEXT("Ballistic Coefficient: %.3f"), BallisticCoefficient);
DrawDebugString(World, Location + FVector(0, 0, 140), BCInfo, nullptr,
FColor::Magenta, BallisticsDebugTime, false, WorldTextScale);
}
}
void AEBBullet::DrawSpallDebug(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, TArray<FVector> FragmentVelocities)
{
if (!DebugEnabled || !DebugSpalling)
{
return;
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
FColor PrimaryColor(SpallColorPrimary.R * 255, SpallColorPrimary.G * 255, SpallColorPrimary.B * 255);
FColor FragmentColor(SpallColorFragment.R * 255, SpallColorFragment.G * 255, SpallColorFragment.B * 255);
// Show spall generation
if (ShowSpallGeneration)
{
DrawDebugSphere(World, ImpactLocation, 8.0f, 16, PrimaryColor, false, SpallDebugTime);
FString SpallInfo = FString::Printf(TEXT("Spall Generated: %d fragments"), FragmentVelocities.Num());
DrawDebugString(World, ImpactLocation + FVector(0, 0, 70), SpallInfo, nullptr,
PrimaryColor, SpallDebugTime, false, WorldTextScale);
}
// Show fragment trajectories
if (ShowFragmentTrajectories)
{
for (int32 i = 0; i < FragmentVelocities.Num(); i++)
{
FVector FragmentDirection = FragmentVelocities[i].GetSafeNormal();
FVector FragmentEndpoint = ImpactLocation + FragmentDirection * 100.0f;
DrawDebugDirectionalArrow(World, ImpactLocation, FragmentEndpoint,
15.0f, FragmentColor, false, SpallDebugTime, 0, 1.0f);
}
}
// Show spall cones
if (ShowSpallCones && FragmentVelocities.Num() > 0)
{
// Calculate cone parameters
FVector ConeDirection = ImpactNormal;
float ConeAngle = FMath::DegreesToRadians(45.0f); // 45 degree cone
float ConeLength = 80.0f;
// Draw cone outline
int32 ConeSides = 8;
TArray<FVector> ConePoints;
for (int32 i = 0; i < ConeSides; i++)
{
float Angle = (2.0f * PI * i) / ConeSides;
FVector RadialDirection = FVector(FMath::Cos(Angle), FMath::Sin(Angle), 0);
FVector ConePoint = ImpactLocation + ConeDirection * ConeLength + RadialDirection * FMath::Tan(ConeAngle) * ConeLength;
ConePoints.Add(ConePoint);
// Draw lines from impact point to cone edge
DrawDebugLine(World, ImpactLocation, ConePoint, PrimaryColor, false, SpallDebugTime, 0, 0.5f);
}
// Draw cone base
for (int32 i = 0; i < ConeSides; i++)
{
int32 NextIndex = (i + 1) % ConeSides;
DrawDebugLine(World, ConePoints[i], ConePoints[NextIndex], PrimaryColor, false, SpallDebugTime, 0, 0.5f);
}
}
}
void AEBBullet::DrawPerformanceDebug(FVector Location, int32 TraceCount, float FrameTime, int32 PooledBullets)
{
if (!DebugEnabled || !DebugPerformance)
{
return;
}
UWorld* World = GetWorld();
if (!World)
{
return;
}
FString PerformanceInfo;
// Show trace count
if (ShowTraceCount)
{
PerformanceInfo += FString::Printf(TEXT("Traces: %d\n"), TraceCount);
}
// Show frame timing
if (ShowFrameTiming)
{
float FPS = FrameTime > 0 ? 1.0f / FrameTime : 0.0f;
PerformanceInfo += FString::Printf(TEXT("Frame Time: %.2f ms (%.1f FPS)\n"), FrameTime * 1000.0f, FPS);
}
// Show pooling stats
if (ShowPoolingStats && EnablePooling)
{
PerformanceInfo += FString::Printf(TEXT("Pooled Bullets: %d/%d\n"), PooledBullets, MaxPoolSize);
}
if (!PerformanceInfo.IsEmpty())
{
DrawDebugString(World, Location + FVector(0, 0, 160), PerformanceInfo, nullptr,
FColor::Cyan, PerformanceDebugTime, false, WorldTextScale);
}
}
void AEBBullet::ClearDebugDisplay()
{
DebugTrajectoryPoints.Empty();
DebugTrajectoryTimes.Empty();
DebugImpactLocations.Empty();
DebugImpactTimes.Empty();
DebugTraceCounter = 0;
DebugMessageCounter = 0;
}
void AEBBullet::ToggleDebugCategory(const FString& CategoryName, bool bEnabled)
{
if (CategoryName == TEXT("Trajectory"))
{
DebugTrajectory = bEnabled;
}
else if (CategoryName == TEXT("Impact"))
{
DebugImpact = bEnabled;
}
else if (CategoryName == TEXT("Physics"))
{
DebugPhysics = bEnabled;
}
else if (CategoryName == TEXT("Performance"))
{
DebugPerformance = bEnabled;
}
else if (CategoryName == TEXT("Ballistics"))
{
DebugBallistics = bEnabled;
}
else if (CategoryName == TEXT("Spalling"))
{
DebugSpalling = bEnabled;
}
else if (CategoryName == TEXT("Pooling"))
{
DebugPooling = bEnabled;
}
}
FString AEBBullet::GetDebugInfoString() const
{
return FString::Printf(TEXT(
"Bullet Debug Info:\n"
"Enabled: %s\n"
"Trajectory: %s | Impact: %s | Physics: %s\n"
"Performance: %s | Ballistics: %s | Spalling: %s\n"
"Trace Count: %d | Active Trajectory Points: %d\n"
"Active Impact Points: %d"
),
DebugEnabled ? TEXT("Yes") : TEXT("No"),
DebugTrajectory ? TEXT("On") : TEXT("Off"),
DebugImpact ? TEXT("On") : TEXT("Off"),
DebugPhysics ? TEXT("On") : TEXT("Off"),
DebugPerformance ? TEXT("On") : TEXT("Off"),
DebugBallistics ? TEXT("On") : TEXT("Off"),
DebugSpalling ? TEXT("On") : TEXT("Off"),
DebugTraceCounter,
DebugTrajectoryPoints.Num(),
DebugImpactLocations.Num()
);
}
@@ -1,6 +1,7 @@
// Copyright 2016 Mookie. All Rights Reserved.
#include "EBMathematicalBallistics.h"
#include "EBUnitConversions.h"
#include "Engine/Engine.h"
#include "Math/UnrealMathUtility.h"
@@ -13,8 +14,8 @@ float UEBMathematicalBallistics::CalculatePenetrationDepth(
// Calculate kinetic energy in joules
float KineticEnergy = CalculateKineticEnergy(BulletProps, VelocityMPS);
// Calculate sectional density
float SectionalDensity = BulletProps.GetSectionalDensity();
// Calculate sectional density in proper SI units (kg/m²)
float SectionalDensity = BulletProps.GetSectionalDensityKgPerM2();
// Calculate hardness ratio
float HardnessRatio = CalculateHardnessRatio(BulletProps.BulletHardness, MaterialProps.MaterialHardness);
@@ -27,14 +28,28 @@ float UEBMathematicalBallistics::CalculatePenetrationDepth(
// Modified Taylor-Hopkinson equation for penetration depth
// P = (K * E * SD * HF * AF * SF) / (ρ * σ)
// Where: P = penetration depth, K = constant, E = kinetic energy, SD = sectional density
// HF = hardness factor, AF = angle factor, SF = shape factor, ρ = density, σ = yield strength
// Where: P = penetration depth (cm), K = constant, E = kinetic energy (J), SD = sectional density (kg/m²)
// HF = hardness factor, AF = angle factor, SF = shape factor, ρ = density (g/cm³), σ = yield strength (MPa)
float Constant = 0.0012f; // Empirical constant
float Penetration = (Constant * KineticEnergy * SectionalDensity * HardnessRatio * AngleFactor * ShapeFactor) /
(MaterialProps.DensityGPerCm3 * MaterialProps.YieldStrengthMPa);
// Unit conversion factors for proper dimensional analysis:
// KineticEnergy is in Joules (kg⋅m²/s²)
// SectionalDensity is in kg/m²
// DensityGPerCm3 needs conversion: g/cm³ = 1000 kg/m³
// YieldStrengthMPa is in MPa = 10⁶ Pa = 10⁶ N/m²
return FMath::Max(0.0f, Penetration);
float DensityKgPerM3 = FEBUnitConversions::GPerCM3ToKGPerM3(MaterialProps.DensityGPerCm3);
float YieldStrengthPa = FEBUnitConversions::MPaToPa(MaterialProps.YieldStrengthMPa);
// Empirical constant adjusted for proper units (results in meters)
float Constant = 2.0f; // Adjusted for dimensional consistency
float PenetrationM = (Constant * KineticEnergy * SectionalDensity * HardnessRatio * AngleFactor * ShapeFactor) /
(DensityKgPerM3 * YieldStrengthPa);
// Convert from meters to centimeters for consistency with Unreal Engine units
float PenetrationCm = FEBUnitConversions::MetersToCM(PenetrationM);
return FMath::Max(0.0f, PenetrationCm);
}
float UEBMathematicalBallistics::CalculateResidualVelocity(
@@ -178,10 +193,15 @@ float UEBMathematicalBallistics::CalculateTaylorHopkinsonLimit(
{
// Taylor-Hopkinson limit: t = K * ρ * σ / (SD * V^2)
// Rearranged to solve for limiting thickness
float SectionalDensity = BulletProps.GetSectionalDensity();
float SectionalDensity = BulletProps.GetSectionalDensityKgPerM2();
float Constant = 0.001f; // Empirical constant
return Constant * MaterialProps.DensityGPerCm3 * MaterialProps.YieldStrengthMPa / SectionalDensity;
// Convert to proper units: result in cm
float DensityKgPerM3 = MaterialProps.DensityGPerCm3 * 1000.0f;
float YieldStrengthPa = MaterialProps.YieldStrengthMPa * 1000000.0f;
float ThicknessM = Constant * DensityKgPerM3 * YieldStrengthPa / SectionalDensity;
return ThicknessM * 100.0f; // Convert to cm
}
float UEBMathematicalBallistics::CalculateRechtIpsonVelocity(
@@ -194,13 +214,21 @@ float UEBMathematicalBallistics::CalculateRechtIpsonVelocity(
// Where k is a constant, σ is yield strength, t is thickness, ρ is bullet density, A is cross-sectional area
float Constant = 2.0f; // Empirical constant
float CrossSectionArea = BulletProps.GetCrossSectionCm2();
float BulletDensity = BulletProps.GetMassKg() / (CrossSectionArea * BulletProps.LengthInches * 2.54f); // Rough approximation
float BallisticLimit = FMath::Sqrt(Constant * MaterialProps.YieldStrengthMPa * ThicknessCM /
(BulletDensity * CrossSectionArea));
// Convert all units to SI for calculation
float CrossSectionAreaM2 = BulletProps.GetCrossSectionCm2() * 0.0001f; // cm² to m²
float ThicknessM = ThicknessCM * 0.01f; // cm to m
float YieldStrengthPa = MaterialProps.YieldStrengthMPa * 1000000.0f; // MPa to Pa
return BallisticLimit;
// Calculate bullet density (kg/m³)
float BulletVolumeM3 = CrossSectionAreaM2 * BulletProps.LengthInches * 0.0254f; // Convert length to meters
float BulletDensityKgPerM3 = BulletProps.GetMassKg() / BulletVolumeM3;
// Calculate ballistic limit velocity in m/s
float BallisticLimitMPS = FMath::Sqrt(Constant * YieldStrengthPa * ThicknessM /
(BulletDensityKgPerM3 * CrossSectionAreaM2));
return BallisticLimitMPS;
}
float UEBMathematicalBallistics::CalculateCriticalRicochetAngle(
@@ -2,9 +2,110 @@
// Copyright 2016 Mookie. All Rights Reserved.
#include "EasyBallistics.h"
#include "EBBullet.h"
#include "Engine/World.h"
#include "Engine/Engine.h"
#include "EngineUtils.h"
#define LOCTEXT_NAMESPACE "FEasyBallisticsModule"
// Console commands for debug control
static FAutoConsoleCommand EBDebugToggleCommand(
TEXT("eb.debug.toggle"),
TEXT("Toggle EasyBallistics debug display on/off"),
FConsoleCommandDelegate::CreateStatic([]()
{
if (GEngine && GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull))
{
UWorld* World = GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull);
for (TActorIterator<AEBBullet> ActorItr(World); ActorItr; ++ActorItr)
{
AEBBullet* Bullet = *ActorItr;
if (Bullet)
{
Bullet->DebugEnabled = !Bullet->DebugEnabled;
}
}
UE_LOG(LogTemp, Warning, TEXT("EasyBallistics debug toggled"));
}
})
);
static FAutoConsoleCommand EBDebugCategoryCommand(
TEXT("eb.debug.category"),
TEXT("Toggle specific debug category: eb.debug.category <CategoryName> <0|1>"),
FConsoleCommandWithArgsDelegate::CreateStatic([](const TArray<FString>& Args)
{
if (Args.Num() < 2)
{
UE_LOG(LogTemp, Warning, TEXT("Usage: eb.debug.category <CategoryName> <0|1>"));
UE_LOG(LogTemp, Warning, TEXT("Categories: Trajectory, Impact, Physics, Performance, Ballistics, Spalling, Pooling"));
return;
}
FString CategoryName = Args[0];
bool bEnabled = FCString::Atoi(*Args[1]) != 0;
if (GEngine && GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull))
{
UWorld* World = GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull);
for (TActorIterator<AEBBullet> ActorItr(World); ActorItr; ++ActorItr)
{
AEBBullet* Bullet = *ActorItr;
if (Bullet)
{
Bullet->ToggleDebugCategory(CategoryName, bEnabled);
}
}
UE_LOG(LogTemp, Warning, TEXT("EasyBallistics debug category '%s' set to %s"), *CategoryName, bEnabled ? TEXT("ON") : TEXT("OFF"));
}
})
);
static FAutoConsoleCommand EBDebugClearCommand(
TEXT("eb.debug.clear"),
TEXT("Clear all EasyBallistics debug display"),
FConsoleCommandDelegate::CreateStatic([]()
{
if (GEngine && GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull))
{
UWorld* World = GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull);
for (TActorIterator<AEBBullet> ActorItr(World); ActorItr; ++ActorItr)
{
AEBBullet* Bullet = *ActorItr;
if (Bullet)
{
Bullet->ClearDebugDisplay();
}
}
UE_LOG(LogTemp, Warning, TEXT("EasyBallistics debug display cleared"));
}
})
);
static FAutoConsoleCommand EBDebugInfoCommand(
TEXT("eb.debug.info"),
TEXT("Print debug information for all bullets"),
FConsoleCommandDelegate::CreateStatic([]()
{
if (GEngine && GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull))
{
UWorld* World = GEngine->GetWorldFromContextObject(GEngine->GameViewport, EGetWorldErrorMode::LogAndReturnNull);
int32 BulletCount = 0;
for (TActorIterator<AEBBullet> ActorItr(World); ActorItr; ++ActorItr)
{
AEBBullet* Bullet = *ActorItr;
if (Bullet)
{
FString DebugInfo = Bullet->GetDebugInfoString();
UE_LOG(LogTemp, Warning, TEXT("Bullet %d: %s"), BulletCount++, *DebugInfo);
}
}
UE_LOG(LogTemp, Warning, TEXT("Total active bullets: %d"), BulletCount);
}
})
);
void FEasyBallisticsModule::StartupModule()
{
// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module
+17 -5
View File
@@ -6,19 +6,31 @@ float AEBBullet::PenetrationTrace(FVector StartLocation, FVector EndLocation, TW
FCollisionQueryParams QueryParams;
QueryParams.bTraceComplex = TraceComplex;
QueryParams.bFindInitialOverlaps = true;
QueryParams.AddIgnoredActor(this); // Ignore self
QueryParams.AddIgnoredActors(IgnoredActors); // Ignore other ignored actors
FHitResult Result;
TArray<FHitResult> MultiResults;
switch (PenTraceType) {
case(EPenTraceType::PT_BackTrace): {
bool Hit = GetWorld()->LineTraceSingleByChannel(Result, EndLocation, StartLocation, CollisionChannel, QueryParams);
if (!Hit) return 1.0f;
ExitNormal = Result.Normal;
ExitLocation = Result.Location;
return (1.0f - Result.Time);
// Use multi-trace to handle multiple surfaces between start and end
bool Hit = GetWorld()->LineTraceMultiByChannel(MultiResults, EndLocation, StartLocation, CollisionChannel, QueryParams);
if (!Hit || MultiResults.Num() == 0) return 1.0f;
// Find the first blocking hit when tracing backwards
for (const FHitResult& HitResult : MultiResults) {
if (HitResult.bBlockingHit) {
ExitNormal = HitResult.Normal;
ExitLocation = HitResult.Location;
return (1.0f - HitResult.Time);
}
}
return 1.0f;
}
case(EPenTraceType::PT_ByComponent): {
if (!Component.IsValid()) return 1.0f;
bool Hit = Component->LineTraceComponent(Result, EndLocation, StartLocation, QueryParams);
if (!Hit) return 1.0f;
ExitNormal = Result.Normal;
+86 -8
View File
@@ -101,18 +101,56 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
#endif
float GrazingAngle = FMath::Pow(dot, GrazingAngleExponent);
FVector PenetrationVector = RandomStream.VRandCone(Velocity, penEnterSpread);
PenetrationVector = FMath::Lerp(PenetrationVector, -HitResult.Normal, FMath::Lerp(penNormalization, penNormalizationGrazing, GrazingAngle));
// 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;
float PenetrationDepth = -FVector::DotProduct(PenetrationVector, HitResult.Normal) * PenetrationDistance;
// 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) {
BlockTIme = PenetrationTrace(HitResult.Location - (HitResult.Normal * CollisionMargin), HitResult.Location + PenetrationVector * PenetrationDistance, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal);
}
// 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) {
@@ -141,11 +179,51 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
//penetration
float RemainingEnergy = FMath::Pow(1.0f - BlockTIme, 2.0f);
SetActorLocation(exitLoc + exitNormal * CollisionMargin);
NewVelocity = RandomStream.VRandCone(PenetrationVector, penExitSpread * (1.0f - RemainingEnergy));
NewVelocity = FMath::Lerp(NewVelocity, Velocity.GetSafeNormal(), RemainingEnergy);
NewVelocity *= RemainingEnergy * Velocity.Size();
// 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;
// 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
}
+13
View File
@@ -37,6 +37,19 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") float DebugArrowSize = 100.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") bool DebugImpactInfo = false;
// Enhanced Debug Controls
UFUNCTION(BlueprintCallable, Category = "EBBarrel|Debug")
void SetBulletDebugCategory(const FString& CategoryName, bool bEnabled);
UFUNCTION(BlueprintCallable, Category = "EBBarrel|Debug")
void SetAllBulletDebugCategories(bool bEnabled);
UFUNCTION(BlueprintCallable, Category = "EBBarrel|Debug")
void ClearAllBulletDebugDisplay();
UFUNCTION(BlueprintPure, Category = "EBBarrel|Debug")
FString GetBulletDebugInfo() const;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Bullet inherits barrel velocity, only works with physics enabled or with additional velocity set")) float InheritVelocity = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Amount of recoil applied to the barrel, only works with physics enabled")) float RecoilMultiplier = 1.0f;
+111 -9
View File
@@ -43,12 +43,74 @@ public:
UPROPERTY(BlueprintReadWrite, Category = "State") bool OwnerSafe=false;
// Enhanced Debug System
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") bool DebugEnabled;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") float DebugTrailTime=1.0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") float DebugTrailWidth=0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") FLinearColor DebugTrailColorFast = FLinearColor(0, 1, 0, 1);
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") FLinearColor DebugTrailColorSlow = FLinearColor(1, 0, 0, 1);
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") bool DebugPooling;
// Debug Categories
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Categories") bool DebugTrajectory = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Categories") bool DebugImpact = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Categories") bool DebugPhysics = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Categories") bool DebugPerformance = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Categories") bool DebugBallistics = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Categories") bool DebugSpalling = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Categories") bool DebugPooling = false;
// Trajectory Debug Options
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) bool ShowTrail = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) bool ShowVelocityVectors = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) bool ShowPathPrediction = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) float DebugTrailTime = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) float DebugTrailWidth = 0.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) FLinearColor DebugTrailColorFast = FLinearColor(0, 1, 0, 1);
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) FLinearColor DebugTrailColorSlow = FLinearColor(1, 0, 0, 1);
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) float VelocityVectorScale = 0.01f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) int32 PathPredictionSteps = 10;
// Impact Debug Options
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) bool ShowImpactPoints = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) bool ShowPenetrationDepth = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) bool ShowRicochetAngles = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) bool ShowMaterialInfo = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) bool ShowImpactNormals = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) float ImpactDebugTime = 10.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) float ImpactPointSize = 5.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) FLinearColor ImpactColorPenetration = FLinearColor(0, 1, 0, 1);
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) FLinearColor ImpactColorRicochet = FLinearColor(1, 1, 0, 1);
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Impact", meta = (EditCondition = "DebugEnabled && DebugImpact")) FLinearColor ImpactColorStopped = FLinearColor(1, 0, 0, 1);
// Physics Debug Options
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Physics", meta = (EditCondition = "DebugEnabled && DebugPhysics")) bool ShowDragForces = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Physics", meta = (EditCondition = "DebugEnabled && DebugPhysics")) bool ShowGravityEffect = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Physics", meta = (EditCondition = "DebugEnabled && DebugPhysics")) bool ShowWindEffect = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Physics", meta = (EditCondition = "DebugEnabled && DebugPhysics")) bool ShowAtmosphericData = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Physics", meta = (EditCondition = "DebugEnabled && DebugPhysics")) float PhysicsDebugTime = 5.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Physics", meta = (EditCondition = "DebugEnabled && DebugPhysics")) float ForceVectorScale = 0.001f;
// Performance Debug Options
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Performance", meta = (EditCondition = "DebugEnabled && DebugPerformance")) bool ShowTraceCount = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Performance", meta = (EditCondition = "DebugEnabled && DebugPerformance")) bool ShowFrameTiming = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Performance", meta = (EditCondition = "DebugEnabled && DebugPerformance")) bool ShowPoolingStats = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Performance", meta = (EditCondition = "DebugEnabled && DebugPerformance")) float PerformanceDebugTime = 3.0f;
// Ballistics Debug Options
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Ballistics", meta = (EditCondition = "DebugEnabled && DebugBallistics")) bool ShowEnergyCalculations = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Ballistics", meta = (EditCondition = "DebugEnabled && DebugBallistics")) bool ShowMathematicalComparison = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Ballistics", meta = (EditCondition = "DebugEnabled && DebugBallistics")) bool ShowBallisticCoefficient = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Ballistics", meta = (EditCondition = "DebugEnabled && DebugBallistics")) float BallisticsDebugTime = 8.0f;
// Spalling Debug Options
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Spalling", meta = (EditCondition = "DebugEnabled && DebugSpalling")) bool ShowSpallGeneration = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Spalling", meta = (EditCondition = "DebugEnabled && DebugSpalling")) bool ShowFragmentTrajectories = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Spalling", meta = (EditCondition = "DebugEnabled && DebugSpalling")) bool ShowSpallCones = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Spalling", meta = (EditCondition = "DebugEnabled && DebugSpalling")) float SpallDebugTime = 5.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Spalling", meta = (EditCondition = "DebugEnabled && DebugSpalling")) FLinearColor SpallColorPrimary = FLinearColor(1, 0.5f, 0, 1);
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Spalling", meta = (EditCondition = "DebugEnabled && DebugSpalling")) FLinearColor SpallColorFragment = FLinearColor(0.8f, 0.4f, 0, 1);
// Debug Display Options
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Display") bool UseWorldSpaceText = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Display") bool UseOnScreenMessages = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Display") float WorldTextScale = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Display") int32 MaxOnScreenMessages = 10;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World") FVector Wind;
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Select atmosphere model")) EEBAtmosphereType AtmosphereType;
@@ -82,8 +144,8 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (EditCondition = "Shotgun")) float ShotSpread=0.01;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (EditCondition = "Shotgun")) float ShotVelocitySpread = 0.01;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") float MuzzleVelocityMin = 100000.0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") float MuzzleVelocityMax = 100000.0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Minimum muzzle velocity in cm/s (Unreal units)")) float MuzzleVelocityMin = 91440.0; // ~3000 fps in cm/s
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Maximum muzzle velocity in cm/s (Unreal units)")) float MuzzleVelocityMax = 91440.0; // ~3000 fps in cm/s
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Maximum bullet spread, in radians", ClampMin = "0")) float Spread = 0.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Spread bias, higher is more accurate on average", ClampMin = "0")) float SpreadBias = 0.0f;
// Physics Mode Toggle
@@ -162,7 +224,7 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Allow components to collide, intended for use with trigger volumes. Do not use for actual collisions.")) bool AllowComponentCollisions = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") TEnumAsByte<ECollisionChannel> TraceChannel;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") bool TraceComplex;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") float CollisionMargin=1.0;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Margin to prevent surface clipping (cm). Lower values improve accuracy but may cause clipping.", ClampMin = "0.01", ClampMax = "5.0")) float CollisionMargin = 0.1;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Bullets with lower velocity will automatically despawn on impact, never despawn if set to zero or negative")) float DespawnVelocity=100.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") TArray<AActor*> IgnoredActors;
@@ -170,7 +232,7 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (EditCondition = "DoFirstStepImmediately")) bool RandomFirstStepDelta = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation") bool FixedStep = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (EditCondition = "FixedStep", ClampMin = "0")) float FixedStepSeconds = 0.1;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation") int MaxTracesPerStep = 8;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (ToolTip = "Maximum number of surfaces to process per frame. Higher values allow penetrating more walls but may impact performance.", ClampMin = "1", ClampMax = "20")) int MaxTracesPerStep = 12;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace") bool Retrace = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace") bool RetraceOnAnotherChannel = false;
@@ -242,12 +304,40 @@ public:
UFUNCTION(BlueprintCallable, Category = "EBBullet|Mathematical Physics")
FMathematicalMaterialProperties GetMaterialProperties(UPhysicalMaterial* Material) const;
// Enhanced Debug Functions
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void CreateDebugImpactWidget(FVector Location, bool bRicochet, bool bPenetration, FVector ImpactVelocity, float PenetrationDepth, UPhysicalMaterial* Material);
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void DrawTrajectoryDebug(FVector StartLocation, FVector EndLocation, FVector CurrentVelocity, float DeltaTime);
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void DrawImpactDebug(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, bool bRicochet, bool bPenetration, float PenetrationDepth, UPhysicalMaterial* Material);
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void DrawPhysicsDebug(FVector Location, FVector DragForce, FVector GravityForce, FVector WindForce, float AirDensity, float SpeedOfSound);
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void DrawBallisticsDebug(FVector Location, FVector BulletVelocity, float KineticEnergy, float DragCoefficient, float MachNumber);
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void DrawSpallDebug(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, TArray<FVector> FragmentVelocities);
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void DrawPerformanceDebug(FVector Location, int32 TraceCount, float FrameTime, int32 PooledBullets);
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void ClearDebugDisplay();
UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug")
void ToggleDebugCategory(const FString& CategoryName, bool bEnabled);
UFUNCTION(BlueprintPure, Category = "EBBullet|Debug")
class UEBBarrel* GetFiringBarrel() const { return FiringBarrel; }
UFUNCTION(BlueprintPure, Category = "EBBullet|Debug")
FString GetDebugInfoString() const;
// Spalling Functions
UFUNCTION(BlueprintCallable, Category = "EBBullet|Spalling")
void GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, UPhysicalMaterial* Material, AActor* HitActor);
@@ -286,6 +376,9 @@ private:
TArray<AActor*> GetAttachedActorsRecursive(AActor* Actor, uint16 Depth = 0, TArray<AActor*> VisitedActors = TArray<AActor*>()) const;
float PenetrationTrace(FVector start, FVector end, TWeakObjectPtr<UPrimitiveComponent,FWeakObjectPtr> comp, EPenTraceType penType, TEnumAsByte<ECollisionChannel> channel, FVector &exitLoc, FVector &exitNormal);
// Helper function to process multiple hits in sequence (for close walls)
bool ProcessSequentialHits(TArray<FHitResult>& AllHits, FVector& CurrentVelocity, float DeltaTime, int32& ProcessedHitCount);
float GetCurveValue(const UCurveFloat* curve, float in, float deflt) const;
@@ -304,6 +397,15 @@ private:
class UEBBarrel* FiringBarrel;
// Debug tracking variables
TArray<FVector> DebugTrajectoryPoints;
TArray<float> DebugTrajectoryTimes;
TArray<FVector> DebugImpactLocations;
TArray<float> DebugImpactTimes;
int32 DebugTraceCounter;
float DebugFrameStartTime;
static int32 DebugMessageCounter;
float GetAltitude(UWorld* World, FVector Location) const;
float GetAltitudePressure(float AltitudeMeter) const;
float GetAltitudeTemperature(float AltitudeMeter) const;
@@ -111,10 +111,19 @@ struct FMathematicalBulletProperties
{
return SectionalDensity;
}
// SD = Weight(grains) / (7000 * Diameter^2(inches))
// Traditional SD = Weight(grains) / (7000 * Diameter^2(inches)) - dimensionless
return GrainWeight / (7000.0f * DiameterInches * DiameterInches);
}
// Calculate sectional density in proper SI units (kg/m²) for mathematical ballistics
float GetSectionalDensityKgPerM2() const
{
float MassKg = GetMassKg();
float DiameterM = DiameterInches * 0.0254f; // Convert inches to meters
float CrossSectionM2 = 3.14159f * (DiameterM/2.0f) * (DiameterM/2.0f);
return MassKg / CrossSectionM2;
}
// Calculate mass in kilograms
float GetMassKg() const
{
@@ -0,0 +1,168 @@
// Copyright 2016 Mookie. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
/**
* Centralized unit conversion utilities for EasyBallistics
* This class ensures consistent unit conversions throughout the plugin
*/
class EASYBALLISTICS_API FEBUnitConversions
{
public:
// === VELOCITY CONVERSIONS ===
/** Convert feet per second to meters per second */
static float FPSToMPS(float FPS) { return FPS * 0.3048f; }
/** Convert meters per second to feet per second */
static float MPSToFPS(float MPS) { return MPS / 0.3048f; }
/** Convert centimeters per second (Unreal units) to meters per second */
static float CMPSToMPS(float CMPS) { return CMPS / 100.0f; }
/** Convert meters per second to centimeters per second (Unreal units) */
static float MPSToCMPS(float MPS) { return MPS * 100.0f; }
/** Convert centimeters per second to kilometers per hour */
static float CMPSToKMH(float CMPS) { return (CMPS / 100.0f) * 3.6f; }
/** Convert feet per second to centimeters per second (Unreal units) */
static float FPSToCMPS(float FPS) { return FPS * 30.48f; }
/** Convert centimeters per second (Unreal units) to feet per second */
static float CMPSToFPS(float CMPS) { return CMPS / 30.48f; }
// === DISTANCE CONVERSIONS ===
/** Convert inches to centimeters */
static float InchesToCM(float Inches) { return Inches * 2.54f; }
/** Convert centimeters to inches */
static float CMToInches(float CM) { return CM / 2.54f; }
/** Convert meters to centimeters */
static float MetersToCM(float Meters) { return Meters * 100.0f; }
/** Convert centimeters to meters */
static float CMToMeters(float CM) { return CM / 100.0f; }
/** Alias for CMToMeters for compatibility */
static float CMToM(float CM) { return CMToMeters(CM); }
/** Convert feet to meters */
static float FeetToMeters(float Feet) { return Feet * 0.3048f; }
/** Convert meters to feet */
static float MetersToFeet(float Meters) { return Meters / 0.3048f; }
// === MASS CONVERSIONS ===
/** Convert grains to kilograms */
static float GrainsToKG(float Grains) { return Grains * 0.0647989f / 1000.0f; }
/** Convert kilograms to grains */
static float KGToGrains(float KG) { return KG * 1000.0f / 0.0647989f; }
/** Convert grains to grams */
static float GrainsToGrams(float Grains) { return Grains * 0.0647989f; }
/** Convert grams to grains */
static float GrainsToGrains(float Grams) { return Grams / 0.0647989f; }
// === DENSITY CONVERSIONS ===
/** Convert g/cm³ to kg/m³ */
static float GPerCM3ToKGPerM3(float GPerCM3) { return GPerCM3 * 1000.0f; }
/** Convert kg/m³ to g/cm³ */
static float KGPerM3ToGPerCM3(float KGPerM3) { return KGPerM3 / 1000.0f; }
// === PRESSURE CONVERSIONS ===
/** Convert MPa to Pa */
static float MPaToPa(float MPa) { return MPa * 1000000.0f; }
/** Convert Pa to MPa */
static float PaToMPa(float Pa) { return Pa / 1000000.0f; }
/** Convert PSI to MPa */
static float PSIToMPa(float PSI) { return PSI * 0.00689476f; }
/** Convert MPa to PSI */
static float MPaToPSI(float MPa) { return MPa / 0.00689476f; }
// === ENERGY CONVERSIONS ===
/** Convert foot-pounds to Joules */
static float FtLbsToJoules(float FtLbs) { return FtLbs * 1.35582f; }
/** Convert Joules to foot-pounds */
static float JoulesToFtLbs(float Joules) { return Joules / 1.35582f; }
// === ANGLE CONVERSIONS ===
/** Convert degrees to radians */
static float DegreesToRadians(float Degrees) { return Degrees * PI / 180.0f; }
/** Convert radians to degrees */
static float RadiansToDegrees(float Radians) { return Radians * 180.0f / PI; }
// === AREA CONVERSIONS ===
/** Convert square centimeters to square meters */
static float CM2ToM2(float CM2) { return CM2 * 0.0001f; }
/** Convert square meters to square centimeters */
static float M2ToCM2(float M2) { return M2 * 10000.0f; }
/** Convert square inches to square centimeters */
static float Inch2ToCM2(float Inch2) { return Inch2 * 6.4516f; }
/** Convert square centimeters to square inches */
static float CM2ToInch2(float CM2) { return CM2 / 6.4516f; }
// === HELPER FUNCTIONS ===
/** Calculate kinetic energy in Joules from mass (kg) and velocity (m/s) */
static float CalculateKineticEnergyJoules(float MassKG, float VelocityMPS)
{
return 0.5f * MassKG * VelocityMPS * VelocityMPS;
}
/** Calculate sectional density in kg/m² from mass (kg) and diameter (m) */
static float CalculateSectionalDensityKGPerM2(float MassKG, float DiameterM)
{
float RadiusM = DiameterM / 2.0f;
float CrossSectionM2 = PI * RadiusM * RadiusM;
return MassKG / CrossSectionM2;
}
/** Calculate cross-sectional area in m² from diameter in m */
static float CalculateCrossSectionM2(float DiameterM)
{
float RadiusM = DiameterM / 2.0f;
return PI * RadiusM * RadiusM;
}
// === VALIDATION FUNCTIONS ===
/** Validate that a velocity value is reasonable for ballistics (returns true if valid) */
static bool IsValidBallisticVelocity(float VelocityMPS)
{
return VelocityMPS >= 0.0f && VelocityMPS <= 2000.0f; // 0 to 2000 m/s is reasonable range
}
/** Validate that a mass value is reasonable for bullets (returns true if valid) */
static bool IsValidBulletMass(float MassKG)
{
return MassKG >= 0.001f && MassKG <= 0.1f; // 1g to 100g is reasonable range
}
/** Validate that a diameter value is reasonable for bullets (returns true if valid) */
static bool IsValidBulletDiameter(float DiameterCM)
{
return DiameterCM >= 0.1f && DiameterCM <= 5.0f; // 1mm to 50mm is reasonable range
}
};