diff --git a/Source/EasyBallistics/Private/CollisionFilter.cpp b/Source/EasyBallistics/Private/CollisionFilter.cpp index 55e3168..e2f2158 100644 --- a/Source/EasyBallistics/Private/CollisionFilter.cpp +++ b/Source/EasyBallistics/Private/CollisionFilter.cpp @@ -5,21 +5,45 @@ bool AEBBullet::CollisionFilter_Implementation(FHitResult HitResult) const{ }; FHitResult AEBBullet::FilterHits(TArray Results, bool &hit) const{ - TArray 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 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]; }; \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp b/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp index 811f87a..660ee01 100644 --- a/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp +++ b/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp @@ -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( diff --git a/Source/EasyBallistics/Private/EBBarrel.cpp b/Source/EasyBallistics/Private/EBBarrel.cpp index a204895..6eccb7a 100644 --- a/Source/EasyBallistics/Private/EBBarrel.cpp +++ b/Source/EasyBallistics/Private/EBBarrel.cpp @@ -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 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 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 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; } \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBBullet.cpp b/Source/EasyBallistics/Private/EBBullet.cpp index 35b34bc..8985f63 100644 --- a/Source/EasyBallistics/Private/EBBullet.cpp +++ b/Source/EasyBallistics/Private/EBBullet.cpp @@ -494,4 +494,57 @@ void AEBBullet::ConfigureAsSpallFragment(float FragmentMass, float FragmentVeloc // Safety settings OwnerSafe = false; +} + +bool AEBBullet::ProcessSequentialHits(TArray& 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; } \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBBulletDebug.cpp b/Source/EasyBallistics/Private/EBBulletDebug.cpp index a2fda44..0feaa03 100644 --- a/Source/EasyBallistics/Private/EBBulletDebug.cpp +++ b/Source/EasyBallistics/Private/EBBulletDebug.cpp @@ -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 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 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() + ); } \ No newline at end of file diff --git a/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp b/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp index 93947db..74f4a2a 100644 --- a/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp +++ b/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp @@ -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( diff --git a/Source/EasyBallistics/Private/EasyBallistics.cpp b/Source/EasyBallistics/Private/EasyBallistics.cpp index 8de34ae..3163646 100644 --- a/Source/EasyBallistics/Private/EasyBallistics.cpp +++ b/Source/EasyBallistics/Private/EasyBallistics.cpp @@ -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 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 <0|1>"), + FConsoleCommandWithArgsDelegate::CreateStatic([](const TArray& Args) + { + if (Args.Num() < 2) + { + UE_LOG(LogTemp, Warning, TEXT("Usage: eb.debug.category <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 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 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 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 diff --git a/Source/EasyBallistics/Private/Pentrace.cpp b/Source/EasyBallistics/Private/Pentrace.cpp index 073fc4c..6317370 100644 --- a/Source/EasyBallistics/Private/Pentrace.cpp +++ b/Source/EasyBallistics/Private/Pentrace.cpp @@ -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 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; diff --git a/Source/EasyBallistics/Private/Trace.cpp b/Source/EasyBallistics/Private/Trace.cpp index e6b4891..ab9a721 100644 --- a/Source/EasyBallistics/Private/Trace.cpp +++ b/Source/EasyBallistics/Private/Trace.cpp @@ -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 } diff --git a/Source/EasyBallistics/Public/EBBarrel.h b/Source/EasyBallistics/Public/EBBarrel.h index 37d44c5..bc0b728 100644 --- a/Source/EasyBallistics/Public/EBBarrel.h +++ b/Source/EasyBallistics/Public/EBBarrel.h @@ -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; diff --git a/Source/EasyBallistics/Public/EBBullet.h b/Source/EasyBallistics/Public/EBBullet.h index 2f18c23..a2280cc 100644 --- a/Source/EasyBallistics/Public/EBBullet.h +++ b/Source/EasyBallistics/Public/EBBullet.h @@ -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 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 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 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 GetAttachedActorsRecursive(AActor* Actor, uint16 Depth = 0, TArray VisitedActors = TArray()) const; float PenetrationTrace(FVector start, FVector end, TWeakObjectPtr comp, EPenTraceType penType, TEnumAsByte channel, FVector &exitLoc, FVector &exitNormal); + + // Helper function to process multiple hits in sequence (for close walls) + bool ProcessSequentialHits(TArray& 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 DebugTrajectoryPoints; + TArray DebugTrajectoryTimes; + TArray DebugImpactLocations; + TArray 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; diff --git a/Source/EasyBallistics/Public/EBBulletProperties.h b/Source/EasyBallistics/Public/EBBulletProperties.h index fdbfe3e..115bc25 100644 --- a/Source/EasyBallistics/Public/EBBulletProperties.h +++ b/Source/EasyBallistics/Public/EBBulletProperties.h @@ -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 { diff --git a/Source/EasyBallistics/Public/EBUnitConversions.h b/Source/EasyBallistics/Public/EBUnitConversions.h new file mode 100644 index 0000000..6032445 --- /dev/null +++ b/Source/EasyBallistics/Public/EBUnitConversions.h @@ -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 + } +}; \ No newline at end of file