diff --git a/Source/EasyBallistics/Private/Acceleration.cpp b/Source/EasyBallistics/Private/Acceleration.cpp index 180fc93..8e5241c 100644 --- a/Source/EasyBallistics/Private/Acceleration.cpp +++ b/Source/EasyBallistics/Private/Acceleration.cpp @@ -23,10 +23,25 @@ FVector AEBBullet::UpdateVelocity_Implementation(UWorld* World, FVector Location //drag FVector relVel = (NewVelocity - GetWind(World, Location)); float speed = relVel.Size(); - float mach = speed / speedOfSound; + + // Safety check: Prevent division by zero and clamp mach number to reasonable range + float safeMach = (speedOfSound > 0.1f) ? (speed / speedOfSound) : 0.0f; + float mach = FMath::Clamp(safeMach, 0.0f, 50.0f); // Clamp to prevent extreme values + float profile = FMath::Pow(Diameter / 200.0f, 2.0f)*3.141592f; float drag = GetCurveValue(MachDragCurve, mach, 0.25f)*FMath::Pow(speed / 100.0f, 2.0f)*profile*air*FormFactor*50.0f; - NewVelocity -= relVel.GetSafeNormal() * drag / Mass * DeltaTime / WorldScale; + + // Safety check: Ensure WorldScale is not zero + float safeWorldScale = FMath::Max(WorldScale, 0.01f); + NewVelocity -= relVel.GetSafeNormal() * drag / Mass * DeltaTime / safeWorldScale; + + // SAFETY: Prevent infinite or NaN velocities from physics calculations + if (!FMath::IsFinite(NewVelocity.X) || !FMath::IsFinite(NewVelocity.Y) || !FMath::IsFinite(NewVelocity.Z) || + FMath::IsNaN(NewVelocity.X) || FMath::IsNaN(NewVelocity.Y) || FMath::IsNaN(NewVelocity.Z)) + { + UE_LOG(LogTemp, Error, TEXT("EBBullet: Invalid velocity detected in UpdateVelocity, using previous velocity")); + return PreviousVelocity; + } return NewVelocity; } diff --git a/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp b/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp index 660ee01..469033c 100644 --- a/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp +++ b/Source/EasyBallistics/Private/EBBallisticImpactComponent.cpp @@ -31,6 +31,20 @@ FVector UEBBallisticImpactComponent::CalculateBallisticImpact( bOutDidPenetrate = false; OutExitLocation = ImpactLocation; + // SANITY CHECK: Validate input parameters + if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid impact location")); + return ImpactLocation; + } + + if (!FMath::IsFinite(ProjectileVelocity.X) || !FMath::IsFinite(ProjectileVelocity.Y) || !FMath::IsFinite(ProjectileVelocity.Z) || + ProjectileVelocity.IsNearlyZero()) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid projectile velocity")); + return ImpactLocation; + } + if (!bEnableBallisticCalculations || !HitMaterial) { return ImpactLocation; @@ -68,6 +82,22 @@ FVector UEBBallisticImpactComponent::CalculateBallisticImpact( OutPenetrationDepth = BaseDepth * MaterialResponse.PenetrationDepthMultiplier; } + // SANITY CHECK: Validate penetration depth + if (!FMath::IsFinite(OutPenetrationDepth) || OutPenetrationDepth < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid penetration depth %.3f, setting to 0"), OutPenetrationDepth); + OutPenetrationDepth = 0.0f; + } + + // SANITY CHECK: Clamp to reasonable maximum (5 meters) + const float MaxPenetrationDepth = 500.0f; // 5 meters in cm + if (OutPenetrationDepth > MaxPenetrationDepth) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Penetration depth %.1f cm exceeds maximum, clamping to %.1f cm"), + OutPenetrationDepth, MaxPenetrationDepth); + OutPenetrationDepth = MaxPenetrationDepth; + } + // Check if penetration occurred float MinPenetrationThreshold = 1.0f; // 1cm minimum bOutDidPenetrate = OutPenetrationDepth > MinPenetrationThreshold; @@ -76,7 +106,19 @@ FVector UEBBallisticImpactComponent::CalculateBallisticImpact( { // Calculate exit location FVector PenetrationDirection = ProjectileVelocity.GetSafeNormal(); - OutExitLocation = ImpactLocation + (PenetrationDirection * OutPenetrationDepth); + FVector PenetrationVector = PenetrationDirection * OutPenetrationDepth; + + // SANITY CHECK: Validate penetration vector + if (!FMath::IsFinite(PenetrationVector.X) || !FMath::IsFinite(PenetrationVector.Y) || !FMath::IsFinite(PenetrationVector.Z)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Invalid penetration vector calculated")); + OutExitLocation = ImpactLocation; + bOutDidPenetrate = false; + } + else + { + OutExitLocation = ImpactLocation + PenetrationVector; + } } // Fire event @@ -97,6 +139,27 @@ FVector UEBBallisticImpactComponent::CalculateRicochet( OutEnergyRetained = 0.0f; bOutDidRicochet = false; + // SANITY CHECK: Validate input parameters + if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid impact location")); + return ProjectileVelocity; + } + + if (!FMath::IsFinite(ImpactNormal.X) || !FMath::IsFinite(ImpactNormal.Y) || !FMath::IsFinite(ImpactNormal.Z) || + ImpactNormal.IsNearlyZero()) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid impact normal")); + return ProjectileVelocity; + } + + if (!FMath::IsFinite(ProjectileVelocity.X) || !FMath::IsFinite(ProjectileVelocity.Y) || !FMath::IsFinite(ProjectileVelocity.Z) || + ProjectileVelocity.IsNearlyZero()) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid projectile velocity")); + return ProjectileVelocity; + } + if (!bEnableBallisticCalculations || !HitMaterial) { return ProjectileVelocity; @@ -125,18 +188,73 @@ FVector UEBBallisticImpactComponent::CalculateRicochet( // Calculate ricochet direction using reflection FVector RicochetDirection = UKismetMathLibrary::GetReflectionVector(ProjectileVelocity, ImpactNormal); + // SANITY CHECK: Ensure reflection vector is valid + if (!FMath::IsFinite(RicochetDirection.X) || !FMath::IsFinite(RicochetDirection.Y) || !FMath::IsFinite(RicochetDirection.Z)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid reflection vector, using simple reflection")); + RicochetDirection = ProjectileVelocity - 2.0f * FVector::DotProduct(ProjectileVelocity, ImpactNormal) * ImpactNormal; + } + // Apply some randomness based on material response if (MaterialResponse.RicochetSpread > 0.0f) { - FVector RandomOffset = FMath::VRand() * MaterialResponse.RicochetSpread; + // SAFETY: Clamp spread to reasonable range to prevent extreme deviations + float SafeSpread = FMath::Clamp(MaterialResponse.RicochetSpread, 0.0f, 45.0f); + FVector RandomOffset = FMath::VRand() * FMath::DegreesToRadians(SafeSpread); RicochetDirection = (RicochetDirection + RandomOffset).GetSafeNormal(); } - // Calculate energy retention - OutEnergyRetained = MaterialResponse.RicochetRestitution; + // Calculate energy retention with additional safety clamping + OutEnergyRetained = FMath::Clamp(MaterialResponse.RicochetRestitution, 0.0f, 0.95f); // Never allow full energy retention - // Apply velocity reduction - FVector NewVelocity = RicochetDirection * (ProjectileVelocity.Size() * OutEnergyRetained); + // SANITY CHECK: Validate ricochet direction + if (!FMath::IsFinite(RicochetDirection.X) || !FMath::IsFinite(RicochetDirection.Y) || !FMath::IsFinite(RicochetDirection.Z) || + RicochetDirection.IsNearlyZero()) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid ricochet direction calculated")); + return ProjectileVelocity; + } + + // Apply velocity reduction with additional safety checks + float OriginalSpeed = ProjectileVelocity.Size(); + + // SAFETY: Clamp original speed to prevent calculation with invalid values + if (!FMath::IsFinite(OriginalSpeed) || OriginalSpeed <= 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid original speed %.3f"), OriginalSpeed); + return ProjectileVelocity; + } + + float NewSpeed = OriginalSpeed * OutEnergyRetained; + + // SANITY CHECK: Validate new speed + if (!FMath::IsFinite(NewSpeed) || NewSpeed < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid ricochet speed %.3f"), NewSpeed); + return ProjectileVelocity; + } + + // SAFETY: Additional clamp to prevent excessive speeds + float MaxSafeSpeed = OriginalSpeed * 0.9f; // Never exceed 90% of original speed + NewSpeed = FMath::Min(NewSpeed, MaxSafeSpeed); + + FVector NewVelocity = RicochetDirection * NewSpeed; + + // SANITY CHECK: Validate final ricochet velocity + if (!FMath::IsFinite(NewVelocity.X) || !FMath::IsFinite(NewVelocity.Y) || !FMath::IsFinite(NewVelocity.Z)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Invalid final ricochet velocity")); + return ProjectileVelocity; + } + + // SAFETY: Final velocity magnitude check + float FinalSpeed = NewVelocity.Size(); + if (!FMath::IsFinite(FinalSpeed) || FinalSpeed > OriginalSpeed) + { + UE_LOG(LogTemp, Warning, TEXT("EBBallisticRicochet: Ricochet velocity %.1f exceeds original %.1f, clamping"), + FinalSpeed, OriginalSpeed); + NewVelocity = NewVelocity.GetSafeNormal() * (OriginalSpeed * 0.8f); + } // Fire event OnBallisticRicochet.Broadcast(ImpactLocation, RicochetDirection, HitMaterial, OutEnergyRetained); @@ -168,6 +286,17 @@ FEBMaterialResponseMapEntry UEBBallisticImpactComponent::GetMaterialResponse(UPh if (!MaterialResponseMap || !PhysicalMaterial) { + // CRITICAL FIX: Use conservative default values for unmapped materials to prevent infinite speed issues + DefaultResponse.RicochetRestitution = 0.3f; // Lower energy retention for safety + DefaultResponse.RicochetProbabilityMultiplier = 0.5f; // Reduced ricochet probability + DefaultResponse.RicochetSpread = 5.0f; // Add some randomness to prevent perfect reflections + DefaultResponse.NeverRicochet = false; + DefaultResponse.PenetrationDepthMultiplier = 0.8f; // Slightly reduced penetration + + // Log warning about unmapped material + UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Material '%s' not found in MaterialResponseMap, using safe defaults"), + PhysicalMaterial ? *PhysicalMaterial->GetName() : TEXT("NULL")); + return DefaultResponse; } @@ -176,6 +305,17 @@ FEBMaterialResponseMapEntry UEBBallisticImpactComponent::GetMaterialResponse(UPh return *Found; } + // CRITICAL FIX: Use conservative default values for unmapped materials to prevent infinite speed issues + DefaultResponse.RicochetRestitution = 0.3f; // Lower energy retention for safety + DefaultResponse.RicochetProbabilityMultiplier = 0.5f; // Reduced ricochet probability + DefaultResponse.RicochetSpread = 5.0f; // Add some randomness to prevent perfect reflections + DefaultResponse.NeverRicochet = false; + DefaultResponse.PenetrationDepthMultiplier = 0.8f; // Slightly reduced penetration + + // Log warning about unmapped material + UE_LOG(LogTemp, Warning, TEXT("EBBallisticImpact: Material '%s' not found in MaterialResponseMap, using safe defaults"), + *PhysicalMaterial->GetName()); + return DefaultResponse; } diff --git a/Source/EasyBallistics/Private/EBBullet.cpp b/Source/EasyBallistics/Private/EBBullet.cpp index 8985f63..1e672ae 100644 --- a/Source/EasyBallistics/Private/EBBullet.cpp +++ b/Source/EasyBallistics/Private/EBBullet.cpp @@ -1,5 +1,7 @@ // Copyright 2018 Mookie. All Rights Reserved. #include "EBBullet.h" +#include "EBBarrel.h" +#include "EBUnitConversions.h" // Sets default values AEBBullet::AEBBullet() { @@ -7,6 +9,9 @@ AEBBullet::AEBBullet() { PrimaryActorTick.bCanEverTick = true; SetTickGroup(ETickingGroup::TG_PrePhysics); + // Set bullet lifetime to 10 seconds + InitialLifeSpan = 10.0f; + // Initialize firing barrel pointer FiringBarrel = nullptr; @@ -75,6 +80,56 @@ void AEBBullet::Tick(float DeltaTime) { void AEBBullet::Step(float DeltaTime) { FVector start = GetActorLocation(); bool sendUpdate = false; + + // SANITY CHECK: Validate delta time to prevent simulation instability + if (DeltaTime <= 0.0f || DeltaTime > 1.0f || !FMath::IsFinite(DeltaTime)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid DeltaTime %.6f, skipping step"), DeltaTime); + return; + } + + // SANITY CHECK: Distance-based cleanup - bullets that travel too far should be destroyed + FVector SpawnLocation = GetFiringBarrel() ? GetFiringBarrel()->GetComponentLocation() : FVector::ZeroVector; + float MaxTravelDistance = IsSpallFragment ? 50000.0f : 1000000.0f; // 500m for fragments, 10km for regular bullets + float TravelDistance = FVector::Dist(start, SpawnLocation); + if (TravelDistance > MaxTravelDistance) + { + UE_LOG(LogTemp, Log, TEXT("EBBullet: Destroying bullet that traveled too far (%.1f cm > %.1f cm)"), TravelDistance, MaxTravelDistance); + Deactivate(); + return; + } + + // SANITY CHECK: Validate current location + if (!start.IsZero() && (!FMath::IsFinite(start.X) || !FMath::IsFinite(start.Y) || !FMath::IsFinite(start.Z))) + { + UE_LOG(LogTemp, Error, TEXT("EBBullet: Invalid position detected, destroying bullet")); + Deactivate(); + return; + } + + // SANITY CHECK: Validate velocity before processing + if (!FMath::IsFinite(Velocity.X) || !FMath::IsFinite(Velocity.Y) || !FMath::IsFinite(Velocity.Z) || + FMath::IsNaN(Velocity.X) || FMath::IsNaN(Velocity.Y) || FMath::IsNaN(Velocity.Z)) + { + UE_LOG(LogTemp, Error, TEXT("EBBullet: Invalid velocity detected, destroying bullet")); + Deactivate(); + return; + } + + // SANITY CHECK: Validate mass and physics properties + float EffectiveMass = GetEffectiveMass(); + if (EffectiveMass <= 0.0f || !FMath::IsFinite(EffectiveMass)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid mass %.6f, using default 0.004kg"), EffectiveMass); + Mass = 0.004f; // Default to 4 grams + } + + // Initialize debug tracking + if (DebugEnabled) + { + DebugTraceCounter++; + DebugFrameStartTime = GetWorld()->GetTimeSeconds(); + } if (Retrace && CanRetrace) { //time travel @@ -105,7 +160,51 @@ void AEBBullet::Step(float DeltaTime) { CanRetrace = false; FVector PreviousVelocity = Velocity; - Velocity = UpdateVelocity(GetWorld(), GetActorLocation(), Velocity, DeltaTime); + FVector CurrentLocation = GetActorLocation(); + Velocity = UpdateVelocity(GetWorld(), CurrentLocation, Velocity, DeltaTime); + + // Debug trajectory visualization + if (DebugEnabled && DebugTrajectory) + { + FVector EndLocation = CurrentLocation + Velocity * DeltaTime; + DrawTrajectoryDebug(start, EndLocation, Velocity, DeltaTime); + } + + // Debug physics forces + if (DebugEnabled && DebugPhysics) + { + // Calculate forces for debug display + FVector GravityForce = OverrideGravity ? Gravity : FVector(0, 0, GetWorld()->GetGravityZ()); + GravityForce *= GetEffectiveMass() * DeltaTime; + + FVector WindVelocity = GetWind(GetWorld(), CurrentLocation); + FVector RelativeVelocity = Velocity - WindVelocity; + float AirDensity = GetAirDensity(GetWorld(), CurrentLocation); + float SpeedOfSound = GetSpeedOfSound(GetWorld(), CurrentLocation); + float MachNumber = RelativeVelocity.Size() / SpeedOfSound; + float DragCoeff = GetEffectiveDragCoefficient(MachNumber); + + // Calculate drag force: F = 0.5 * Cd * ρ * A * v² + float CrossSectionArea = PI * FMath::Pow(GetEffectiveDiameter() / 200.0f, 2.0f); // Convert cm to m and get area + FVector DragForce = -RelativeVelocity.GetSafeNormal() * 0.5f * DragCoeff * AirDensity * CrossSectionArea * FMath::Pow(RelativeVelocity.Size() / 100.0f, 2.0f); + DragForce *= DeltaTime; + + FVector WindForce = (WindVelocity - Velocity) * 0.1f * DeltaTime; // Simplified wind effect + + DrawPhysicsDebug(CurrentLocation, DragForce, GravityForce, WindForce, AirDensity, SpeedOfSound); + } + + // Debug ballistics calculations + if (DebugEnabled && DebugBallistics) + { + float VelocityMPS = FEBUnitConversions::CMPSToMPS(Velocity.Size()); + float KineticEnergy = FEBUnitConversions::CalculateKineticEnergyJoules(GetEffectiveMass(), VelocityMPS); + float SpeedOfSound = GetSpeedOfSound(GetWorld(), CurrentLocation); + float MachNumber = Velocity.Size() / SpeedOfSound; + float DragCoeff = GetEffectiveDragCoefficient(MachNumber); + + DrawBallisticsDebug(CurrentLocation, Velocity, KineticEnergy, DragCoeff, MachNumber); + } //trace float remainingTime = DeltaTime; @@ -142,6 +241,14 @@ void AEBBullet::Step(float DeltaTime) { NewRot.Roll = GetActorRotation().Roll; SetActorRotation(NewRot); } + + // Debug performance metrics + if (DebugEnabled && DebugPerformance) + { + float FrameTime = GetWorld()->GetTimeSeconds() - DebugFrameStartTime; + int32 PooledBullets = EnablePooling ? Pooled.Num() : 0; + DrawPerformanceDebug(GetActorLocation(), DebugTraceCounter, FrameTime, PooledBullets); + } } float AEBBullet::GetCurveValue(const UCurveFloat* curve, float in, float deflt) const { @@ -283,6 +390,34 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel return; } + // SANITY CHECK: Validate input parameters + if (!FMath::IsFinite(ImpactLocation.X) || !FMath::IsFinite(ImpactLocation.Y) || !FMath::IsFinite(ImpactLocation.Z) || + !FMath::IsFinite(ImpactVelocity.X) || !FMath::IsFinite(ImpactVelocity.Y) || !FMath::IsFinite(ImpactVelocity.Z) || + !FMath::IsFinite(ImpactNormal.X) || !FMath::IsFinite(ImpactNormal.Y) || !FMath::IsFinite(ImpactNormal.Z)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid spalling parameters, skipping fragment generation")); + return; + } + + // SANITY CHECK: Prevent excessive fragment generation for performance + static int32 GlobalFragmentCount = 0; + static float LastFrameTime = 0.0f; + float CurrentFrameTime = GetWorld()->GetTimeSeconds(); + + // Reset counter every second + if (CurrentFrameTime - LastFrameTime > 1.0f) + { + GlobalFragmentCount = 0; + LastFrameTime = CurrentFrameTime; + } + + const int32 MaxFragmentsPerSecond = 200; // Performance limit + if (GlobalFragmentCount > MaxFragmentsPerSecond) + { + UE_LOG(LogTemp, Log, TEXT("EBBullet: Fragment generation limit reached this second (%d), skipping"), MaxFragmentsPerSecond); + return; + } + const FEBMaterialResponseMapEntry* ResponseEntry = MaterialResponseMap->Map.Find(Material); if (!ResponseEntry) { @@ -342,10 +477,27 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel SpreadAngleRad = FMath::DegreesToRadians(PhysicalSpreadAngle); } + // SANITY CHECK: Clamp fragment count to reasonable limits + const int32 MaxFragmentsPerImpact = 20; // Absolute maximum per single impact + FragmentCount = FMath::Clamp(FragmentCount, 0, MaxFragmentsPerImpact); + + if (FragmentCount <= 0) + { + return; + } + // Base spalling direction (combination of reflection and surface normal) FVector ReflectionDirection = UKismetMathLibrary::GetReflectionVector(ImpactVelocity.GetSafeNormal(), ImpactNormal); FVector SpallBaseDirection = FMath::Lerp(ImpactNormal, ReflectionDirection, 0.3f).GetSafeNormal(); + // SANITY CHECK: Validate spall direction + if (!FMath::IsFinite(SpallBaseDirection.X) || !FMath::IsFinite(SpallBaseDirection.Y) || !FMath::IsFinite(SpallBaseDirection.Z) || + SpallBaseDirection.IsNearlyZero()) + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid spall direction calculated, using impact normal")); + SpallBaseDirection = ImpactNormal.GetSafeNormal(); + } + for (int32 i = 0; i < FragmentCount; i++) { float FragmentMass; @@ -389,9 +541,37 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel FragmentSpeed = FMath::Sqrt(2.0f * FragmentKineticEnergy / FragmentMass); } + // SANITY CHECK: Validate fragment properties + if (FragmentMass <= 0.0f || !FMath::IsFinite(FragmentMass)) + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid fragment mass %.6f, skipping fragment %d"), FragmentMass, i); + continue; + } + + if (FragmentSpeed <= 0.0f || !FMath::IsFinite(FragmentSpeed) || FragmentSpeed > 500000.0f) // Max 5km/s + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid fragment speed %.1f, clamping to reasonable range"), FragmentSpeed); + FragmentSpeed = FMath::Clamp(FragmentSpeed, 100.0f, 100000.0f); // 1-1000 m/s range + } + + // SANITY CHECK: Check global fragment limit before spawning + if (GlobalFragmentCount >= MaxFragmentsPerSecond) + { + UE_LOG(LogTemp, Log, TEXT("EBBullet: Global fragment limit reached, stopping fragment generation")); + break; + } + // Fragment direction with dispersion FVector FragmentDirection = RandomStream.VRandCone(SpallBaseDirection, SpreadAngleRad); FVector FragmentVelocity = FragmentDirection * FragmentSpeed; + + // SANITY CHECK: Validate fragment velocity + if (!FMath::IsFinite(FragmentVelocity.X) || !FMath::IsFinite(FragmentVelocity.Y) || !FMath::IsFinite(FragmentVelocity.Z) || + FragmentVelocity.IsNearlyZero()) + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Invalid fragment velocity, skipping fragment %d"), i); + continue; + } FVector SpawnLocation = ImpactLocation + ImpactNormal * 2.0f; @@ -453,6 +633,9 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel Fragment->Velocity = FragmentVelocity; } + // SANITY CHECK: Increment global fragment counter + GlobalFragmentCount++; + // Debug logging for spall fragment creation if (DebugEnabled) { @@ -462,6 +645,10 @@ void AEBBullet::GenerateSpallFragments(FVector ImpactLocation, FVector ImpactVel Fragment->EnableSpalling ? TEXT("true") : TEXT("false")); } } + else + { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Failed to spawn spall fragment")); + } } OnSpallFragmentGenerated(SpawnLocation, FragmentVelocity, FragmentMass, Material); diff --git a/Source/EasyBallistics/Private/EBBulletDebug.cpp b/Source/EasyBallistics/Private/EBBulletDebug.cpp index 0feaa03..5fe1ee4 100644 --- a/Source/EasyBallistics/Private/EBBulletDebug.cpp +++ b/Source/EasyBallistics/Private/EBBulletDebug.cpp @@ -83,6 +83,12 @@ void AEBBullet::CreateDebugImpactWidget(FVector Location, bool bRicochet, bool b PenetrationDepth ); + // Fix color conversion for display + uint8 R = FMath::Clamp(FMath::RoundToInt(ImpactColor.R * 255.0f), 0, 255); + uint8 G = FMath::Clamp(FMath::RoundToInt(ImpactColor.G * 255.0f), 0, 255); + uint8 B = FMath::Clamp(FMath::RoundToInt(ImpactColor.B * 255.0f), 0, 255); + FColor DisplayColor(R, G, B); + // Display debug information if (UseOnScreenMessages && GEngine) { @@ -93,14 +99,13 @@ void AEBBullet::CreateDebugImpactWidget(FVector Location, bool bRicochet, bool b } GEngine->AddOnScreenDebugMessage(DebugMessageCounter++, ImpactDebugTime, - FColor(ImpactColor.R * 255, ImpactColor.G * 255, ImpactColor.B * 255), DebugInfo); + DisplayColor, DebugInfo); } if (UseWorldSpaceText) { DrawDebugString(World, Location + FVector(0, 0, 50), DebugInfo, nullptr, - FColor(ImpactColor.R * 255, ImpactColor.G * 255, ImpactColor.B * 255), - ImpactDebugTime, false, WorldTextScale); + DisplayColor, ImpactDebugTime, false, WorldTextScale); } } @@ -146,10 +151,15 @@ void AEBBullet::DrawTrajectoryDebug(FVector StartLocation, FVector EndLocation, FLinearColor TrailColor = FMath::Lerp(DebugTrailColorSlow, DebugTrailColorFast, VelocityNormalized); TrailColor.A = 1.0f - Alpha; + // Fix color conversion - ensure valid range [0-255] + uint8 R = FMath::Clamp(FMath::RoundToInt(TrailColor.R * 255.0f), 0, 255); + uint8 G = FMath::Clamp(FMath::RoundToInt(TrailColor.G * 255.0f), 0, 255); + uint8 B = FMath::Clamp(FMath::RoundToInt(TrailColor.B * 255.0f), 0, 255); + uint8 A = FMath::Clamp(FMath::RoundToInt(TrailColor.A * 255.0f), 0, 255); + 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); + FColor(R, G, B, A), false, -1, 0, LineThickness); } } } @@ -192,6 +202,72 @@ void AEBBullet::DrawTrajectoryDebug(FVector StartLocation, FVector EndLocation, } } +void AEBBullet::DrawXRayTrajectory(FVector EntryPoint, FVector ExitPoint, FVector PenetrationDirection, float PenetrationDepth, UPhysicalMaterial* Material) +{ + if (!DebugEnabled || !DebugTrajectory || !ShowXRayTrajectory) + { + return; + } + + UWorld* World = GetWorld(); + if (!World) + { + return; + } + + // Draw entry point marker + uint8 XR = FMath::Clamp(FMath::RoundToInt(XRayTrajectoryColor.R * 255.0f), 0, 255); + uint8 XG = FMath::Clamp(FMath::RoundToInt(XRayTrajectoryColor.G * 255.0f), 0, 255); + uint8 XB = FMath::Clamp(FMath::RoundToInt(XRayTrajectoryColor.B * 255.0f), 0, 255); + uint8 XA = FMath::Clamp(FMath::RoundToInt(XRayTrajectoryColor.A * 255.0f), 0, 255); + FColor XRayColor(XR, XG, XB, XA); + + DrawDebugSphere(World, EntryPoint, 3.0f, 8, XRayColor, false, DebugTrailTime); + + // Draw internal trajectory path (X-ray view) + FVector InternalTrajectory = EntryPoint + PenetrationDirection * PenetrationDepth; + + // Draw dashed line to represent path through material + int32 DashCount = FMath::Max(1, FMath::RoundToInt(PenetrationDepth / 5.0f)); // Dash every 5cm + for (int32 i = 0; i < DashCount; i++) + { + float StartRatio = (float)i / DashCount; + float EndRatio = FMath::Min(1.0f, (float)(i + 0.7f) / DashCount); // 70% dash, 30% gap + + FVector DashStart = FMath::Lerp(EntryPoint, InternalTrajectory, StartRatio); + FVector DashEnd = FMath::Lerp(EntryPoint, InternalTrajectory, EndRatio); + + DrawDebugLine(World, DashStart, DashEnd, XRayColor, false, DebugTrailTime, 0, 2.0f); + } + + // Draw exit point if it exists + if (!ExitPoint.IsNearlyZero() && !EntryPoint.Equals(ExitPoint, 1.0f)) + { + uint8 PR = FMath::Clamp(FMath::RoundToInt(PenetrationTrajectoryColor.R * 255.0f), 0, 255); + uint8 PG = FMath::Clamp(FMath::RoundToInt(PenetrationTrajectoryColor.G * 255.0f), 0, 255); + uint8 PB = FMath::Clamp(FMath::RoundToInt(PenetrationTrajectoryColor.B * 255.0f), 0, 255); + uint8 PA = FMath::Clamp(FMath::RoundToInt(PenetrationTrajectoryColor.A * 255.0f), 0, 255); + FColor PenetrationColor(PR, PG, PB, PA); + + DrawDebugSphere(World, ExitPoint, 3.0f, 8, PenetrationColor, false, DebugTrailTime); + + // Draw penetration path from calculated internal point to actual exit + DrawDebugLine(World, InternalTrajectory, ExitPoint, PenetrationColor, false, DebugTrailTime, 0, 2.0f); + } + + // Add material and penetration info + if (Material && UseWorldSpaceText) + { + FString MaterialInfo = FString::Printf(TEXT("Material: %s\nPenetration: %.1f cm\nPath Length: %.1f cm"), + *Material->GetName(), + PenetrationDepth, + FVector::Dist(EntryPoint, ExitPoint)); + + DrawDebugString(World, EntryPoint + FVector(0, 0, 25), MaterialInfo, nullptr, + XRayColor, DebugTrailTime, false, WorldTextScale); + } +} + void AEBBullet::DrawImpactDebug(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, bool bRicochet, bool bPenetration, float PenetrationDepth, UPhysicalMaterial* Material) { if (!DebugEnabled || !DebugImpact) @@ -220,7 +296,11 @@ void AEBBullet::DrawImpactDebug(FVector ImpactLocation, FVector ImpactVelocity, ImpactColor = ImpactColorStopped; } - FColor DebugColor(ImpactColor.R * 255, ImpactColor.G * 255, ImpactColor.B * 255); + // Fix color conversion + uint8 R = FMath::Clamp(FMath::RoundToInt(ImpactColor.R * 255.0f), 0, 255); + uint8 G = FMath::Clamp(FMath::RoundToInt(ImpactColor.G * 255.0f), 0, 255); + uint8 B = FMath::Clamp(FMath::RoundToInt(ImpactColor.B * 255.0f), 0, 255); + FColor DebugColor(R, G, B); // Show impact points if (ShowImpactPoints) @@ -400,8 +480,16 @@ void AEBBullet::DrawSpallDebug(FVector ImpactLocation, FVector ImpactVelocity, F return; } - FColor PrimaryColor(SpallColorPrimary.R * 255, SpallColorPrimary.G * 255, SpallColorPrimary.B * 255); - FColor FragmentColor(SpallColorFragment.R * 255, SpallColorFragment.G * 255, SpallColorFragment.B * 255); + // Fix color conversion for spall colors + uint8 PR = FMath::Clamp(FMath::RoundToInt(SpallColorPrimary.R * 255.0f), 0, 255); + uint8 PG = FMath::Clamp(FMath::RoundToInt(SpallColorPrimary.G * 255.0f), 0, 255); + uint8 PB = FMath::Clamp(FMath::RoundToInt(SpallColorPrimary.B * 255.0f), 0, 255); + FColor PrimaryColor(PR, PG, PB); + + uint8 FR = FMath::Clamp(FMath::RoundToInt(SpallColorFragment.R * 255.0f), 0, 255); + uint8 FG = FMath::Clamp(FMath::RoundToInt(SpallColorFragment.G * 255.0f), 0, 255); + uint8 FB = FMath::Clamp(FMath::RoundToInt(SpallColorFragment.B * 255.0f), 0, 255); + FColor FragmentColor(FR, FG, FB); // Show spall generation if (ShowSpallGeneration) diff --git a/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp b/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp index 74f4a2a..2ebdc65 100644 --- a/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp +++ b/Source/EasyBallistics/Private/EBMathematicalBallistics.cpp @@ -11,12 +11,52 @@ float UEBMathematicalBallistics::CalculatePenetrationDepth( float VelocityMPS, float ImpactAngleDegrees) { + // SANITY CHECK: Validate input parameters + if (VelocityMPS <= 0.0f || !FMath::IsFinite(VelocityMPS) || VelocityMPS > 10000.0f) // Max 10 km/s + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid velocity %.3f m/s for penetration calculation"), VelocityMPS); + return 0.0f; + } + + if (!FMath::IsFinite(ImpactAngleDegrees) || ImpactAngleDegrees < 0.0f || ImpactAngleDegrees > 90.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid impact angle %.1f degrees, clamping to valid range"), ImpactAngleDegrees); + ImpactAngleDegrees = FMath::Clamp(ImpactAngleDegrees, 0.0f, 90.0f); + } + + // SANITY CHECK: Validate material properties + if (MaterialProps.DensityGPerCm3 <= 0.0f || !FMath::IsFinite(MaterialProps.DensityGPerCm3)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid material density %.3f g/cm³"), MaterialProps.DensityGPerCm3); + return 0.0f; + } + + if (MaterialProps.YieldStrengthMPa <= 0.0f || !FMath::IsFinite(MaterialProps.YieldStrengthMPa)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid yield strength %.1f MPa"), MaterialProps.YieldStrengthMPa); + return 0.0f; + } + // Calculate kinetic energy in joules float KineticEnergy = CalculateKineticEnergy(BulletProps, VelocityMPS); + // SANITY CHECK: Validate kinetic energy + if (KineticEnergy <= 0.0f || !FMath::IsFinite(KineticEnergy)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid kinetic energy %.3f J"), KineticEnergy); + return 0.0f; + } + // Calculate sectional density in proper SI units (kg/m²) float SectionalDensity = BulletProps.GetSectionalDensityKgPerM2(); + // SANITY CHECK: Validate sectional density + if (SectionalDensity <= 0.0f || !FMath::IsFinite(SectionalDensity)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid sectional density %.6f kg/m²"), SectionalDensity); + return 0.0f; + } + // Calculate hardness ratio float HardnessRatio = CalculateHardnessRatio(BulletProps.BulletHardness, MaterialProps.MaterialHardness); @@ -40,15 +80,52 @@ float UEBMathematicalBallistics::CalculatePenetrationDepth( float DensityKgPerM3 = FEBUnitConversions::GPerCM3ToKGPerM3(MaterialProps.DensityGPerCm3); float YieldStrengthPa = FEBUnitConversions::MPaToPa(MaterialProps.YieldStrengthMPa); + // SANITY CHECK: Validate converted values + if (DensityKgPerM3 <= 0.0f || !FMath::IsFinite(DensityKgPerM3) || + YieldStrengthPa <= 0.0f || !FMath::IsFinite(YieldStrengthPa)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid converted material properties")); + return 0.0f; + } + // 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); + // SANITY CHECK: Prevent division by zero and validate all factors + float Denominator = DensityKgPerM3 * YieldStrengthPa; + if (Denominator <= 0.0f || !FMath::IsFinite(Denominator)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid denominator in penetration calculation")); + return 0.0f; + } + + float Numerator = Constant * KineticEnergy * SectionalDensity * HardnessRatio * AngleFactor * ShapeFactor; + if (!FMath::IsFinite(Numerator)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid numerator in penetration calculation")); + return 0.0f; + } + + float PenetrationM = Numerator / Denominator; + + // SANITY CHECK: Validate result before conversion + if (!FMath::IsFinite(PenetrationM) || PenetrationM < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid penetration result %.6f m"), PenetrationM); + return 0.0f; + } // Convert from meters to centimeters for consistency with Unreal Engine units float PenetrationCm = FEBUnitConversions::MetersToCM(PenetrationM); + // SANITY CHECK: Clamp to reasonable maximum (10 meters = 1000 cm) + const float MaxPenetrationCm = 1000.0f; + if (PenetrationCm > MaxPenetrationCm) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Penetration %.1f cm exceeds maximum, clamping to %.1f cm"), PenetrationCm, MaxPenetrationCm); + PenetrationCm = MaxPenetrationCm; + } + return FMath::Max(0.0f, PenetrationCm); } @@ -59,25 +136,87 @@ float UEBMathematicalBallistics::CalculateResidualVelocity( float ThicknessCM, float ImpactAngleDegrees) { + // SANITY CHECK: Validate input parameters + if (VelocityMPS <= 0.0f || !FMath::IsFinite(VelocityMPS)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid velocity %.3f m/s for residual velocity calculation"), VelocityMPS); + return 0.0f; + } + + if (ThicknessCM <= 0.0f || !FMath::IsFinite(ThicknessCM)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid thickness %.3f cm"), ThicknessCM); + return 0.0f; + } + // Calculate ballistic limit velocity float BallisticLimit = CalculateRechtIpsonVelocity(BulletProps, MaterialProps, ThicknessCM); + // SANITY CHECK: Validate ballistic limit + if (!FMath::IsFinite(BallisticLimit) || BallisticLimit < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid ballistic limit %.3f m/s"), BallisticLimit); + return 0.0f; + } + // Adjust for impact angle float AngleFactor = CalculateAngleFactor(ImpactAngleDegrees); + + // SANITY CHECK: Prevent division by zero + if (AngleFactor <= 0.001f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Angle factor too small %.6f, using minimum"), AngleFactor); + AngleFactor = 0.001f; + } + BallisticLimit = BallisticLimit / AngleFactor; + // SANITY CHECK: Validate adjusted ballistic limit + if (!FMath::IsFinite(BallisticLimit)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid adjusted ballistic limit")); + return 0.0f; + } + // If impact velocity is below ballistic limit, no penetration if (VelocityMPS <= BallisticLimit) { return 0.0f; } + // SANITY CHECK: Ensure square root argument is positive + float VelocitySquared = VelocityMPS * VelocityMPS; + float BallisticLimitSquared = BallisticLimit * BallisticLimit; + float SquareRootArgument = VelocitySquared - BallisticLimitSquared; + + if (SquareRootArgument < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Negative square root argument %.6f, velocity %.3f < ballistic limit %.3f"), + SquareRootArgument, VelocityMPS, BallisticLimit); + return 0.0f; + } + // Calculate residual velocity using Recht-Ipson equation // Vr = sqrt(V^2 - Vbl^2) - float ResidualVelocity = FMath::Sqrt(VelocityMPS * VelocityMPS - BallisticLimit * BallisticLimit); + float ResidualVelocity = FMath::Sqrt(SquareRootArgument); + + // SANITY CHECK: Validate residual velocity before applying absorption + if (!FMath::IsFinite(ResidualVelocity)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid residual velocity calculated")); + return 0.0f; + } + + // SANITY CHECK: Validate energy absorption coefficient + float EnergyAbsorption = FMath::Clamp(MaterialProps.EnergyAbsorptionCoefficient, 0.0f, 0.99f); + if (EnergyAbsorption != MaterialProps.EnergyAbsorptionCoefficient) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Energy absorption coefficient %.3f clamped to %.3f"), + MaterialProps.EnergyAbsorptionCoefficient, EnergyAbsorption); + } // Apply energy absorption factor - ResidualVelocity *= (1.0f - MaterialProps.EnergyAbsorptionCoefficient); + ResidualVelocity *= (1.0f - EnergyAbsorption); return FMath::Max(0.0f, ResidualVelocity); } @@ -209,6 +348,19 @@ float UEBMathematicalBallistics::CalculateRechtIpsonVelocity( const FMathematicalMaterialProperties& MaterialProps, float ThicknessCM) { + // SANITY CHECK: Validate input parameters + if (ThicknessCM <= 0.0f || !FMath::IsFinite(ThicknessCM)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid thickness %.3f cm for Recht-Ipson calculation"), ThicknessCM); + return 0.0f; + } + + if (MaterialProps.YieldStrengthMPa <= 0.0f || !FMath::IsFinite(MaterialProps.YieldStrengthMPa)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid yield strength %.1f MPa"), MaterialProps.YieldStrengthMPa); + return 0.0f; + } + // Recht-Ipson equation for ballistic limit velocity // Vbl = sqrt(k * σ * t / (ρ * A)) // Where k is a constant, σ is yield strength, t is thickness, ρ is bullet density, A is cross-sectional area @@ -220,13 +372,75 @@ float UEBMathematicalBallistics::CalculateRechtIpsonVelocity( float ThicknessM = ThicknessCM * 0.01f; // cm to m float YieldStrengthPa = MaterialProps.YieldStrengthMPa * 1000000.0f; // MPa to Pa + // SANITY CHECK: Validate cross-sectional area + if (CrossSectionAreaM2 <= 0.0f || !FMath::IsFinite(CrossSectionAreaM2)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid cross-sectional area %.8f m²"), CrossSectionAreaM2); + return 0.0f; + } + // Calculate bullet density (kg/m³) float BulletVolumeM3 = CrossSectionAreaM2 * BulletProps.LengthInches * 0.0254f; // Convert length to meters - float BulletDensityKgPerM3 = BulletProps.GetMassKg() / BulletVolumeM3; + + // SANITY CHECK: Validate bullet volume + if (BulletVolumeM3 <= 0.0f || !FMath::IsFinite(BulletVolumeM3)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet volume %.8f m³"), BulletVolumeM3); + return 0.0f; + } + + float BulletMassKg = BulletProps.GetMassKg(); + if (BulletMassKg <= 0.0f || !FMath::IsFinite(BulletMassKg)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet mass %.6f kg"), BulletMassKg); + return 0.0f; + } + + float BulletDensityKgPerM3 = BulletMassKg / BulletVolumeM3; + + // SANITY CHECK: Validate bullet density + if (BulletDensityKgPerM3 <= 0.0f || !FMath::IsFinite(BulletDensityKgPerM3)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet density %.3f kg/m³"), BulletDensityKgPerM3); + return 0.0f; + } + + // SANITY CHECK: Calculate and validate square root argument + float Numerator = Constant * YieldStrengthPa * ThicknessM; + float Denominator = BulletDensityKgPerM3 * CrossSectionAreaM2; + + if (Denominator <= 0.0f || !FMath::IsFinite(Denominator)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid denominator %.6f in Recht-Ipson equation"), Denominator); + return 0.0f; + } + + float SquareRootArgument = Numerator / Denominator; + + if (SquareRootArgument < 0.0f || !FMath::IsFinite(SquareRootArgument)) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid square root argument %.6f in Recht-Ipson equation"), SquareRootArgument); + return 0.0f; + } // Calculate ballistic limit velocity in m/s - float BallisticLimitMPS = FMath::Sqrt(Constant * YieldStrengthPa * ThicknessM / - (BulletDensityKgPerM3 * CrossSectionAreaM2)); + float BallisticLimitMPS = FMath::Sqrt(SquareRootArgument); + + // SANITY CHECK: Validate final result + if (!FMath::IsFinite(BallisticLimitMPS) || BallisticLimitMPS < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid ballistic limit velocity %.3f m/s"), BallisticLimitMPS); + return 0.0f; + } + + // SANITY CHECK: Clamp to reasonable maximum (5 km/s) + const float MaxBallisticLimit = 5000.0f; + if (BallisticLimitMPS > MaxBallisticLimit) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Ballistic limit %.1f m/s exceeds maximum, clamping to %.1f m/s"), + BallisticLimitMPS, MaxBallisticLimit); + BallisticLimitMPS = MaxBallisticLimit; + } return BallisticLimitMPS; } @@ -254,14 +468,44 @@ float UEBMathematicalBallistics::CalculateCriticalRicochetAngle( // Helper function implementations float UEBMathematicalBallistics::CalculateHardnessRatio(float BulletHardness, float MaterialHardness) { + // SANITY CHECK: Validate hardness values + if (!FMath::IsFinite(BulletHardness) || BulletHardness < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid bullet hardness %.3f, using default 15.0"), BulletHardness); + BulletHardness = 15.0f; // Default bullet hardness + } + + if (!FMath::IsFinite(MaterialHardness) || MaterialHardness <= 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid material hardness %.3f, using default 50.0"), MaterialHardness); + MaterialHardness = 50.0f; // Default material hardness + } + // Hardness ratio affects penetration and ricochet - return FMath::Max(0.1f, BulletHardness / MaterialHardness); + float HardnessRatio = BulletHardness / MaterialHardness; + + // SANITY CHECK: Ensure reasonable bounds + return FMath::Clamp(HardnessRatio, 0.1f, 10.0f); } float UEBMathematicalBallistics::CalculateVelocityFactor(float Velocity, float ThresholdVelocity) { + // SANITY CHECK: Validate input values + if (!FMath::IsFinite(Velocity) || Velocity < 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid velocity %.3f for velocity factor"), Velocity); + return 0.1f; // Minimum factor + } + + if (!FMath::IsFinite(ThresholdVelocity) || ThresholdVelocity <= 0.0f) + { + UE_LOG(LogTemp, Warning, TEXT("EBMathematicalBallistics: Invalid threshold velocity %.3f, using default 300.0"), ThresholdVelocity); + ThresholdVelocity = 300.0f; // Default threshold + } + // Velocity factor for various calculations - return FMath::Clamp(Velocity / ThresholdVelocity, 0.1f, 5.0f); + float VelocityFactor = Velocity / ThresholdVelocity; + return FMath::Clamp(VelocityFactor, 0.1f, 5.0f); } float UEBMathematicalBallistics::CalculateAngleFactor(float ImpactAngleDegrees) diff --git a/Source/EasyBallistics/Private/Environment.cpp b/Source/EasyBallistics/Private/Environment.cpp index fd5290c..e9dcc6f 100644 --- a/Source/EasyBallistics/Private/Environment.cpp +++ b/Source/EasyBallistics/Private/Environment.cpp @@ -22,13 +22,26 @@ float AEBBullet::GetAirDensity_Implementation(UWorld* World, FVector Location) c } float AEBBullet::GetSpeedOfSound_Implementation(UWorld* World, FVector Location) const{ + // Safety check: Ensure WorldScale is valid + float SafeWorldScale = FMath::Max(WorldScale, 0.01f); + if (!SpeedOfSoundVariesWithAltitude) { - return SeaLevelSpeedOfSound * WorldScale; + return FMath::Max(SeaLevelSpeedOfSound * SafeWorldScale, 1.0f); } float Altitude = GetAltitude(World, Location); - float soundvmp = SeaLevelSpeedOfSound / GetCurveValue(SpeedOfSoundCurve, 0, SeaLevelSpeedOfSound); - return GetCurveValue(SpeedOfSoundCurve, Altitude, SeaLevelSpeedOfSound)*WorldScale*soundvmp; + float CurveValueAtSeaLevel = GetCurveValue(SpeedOfSoundCurve, 0, SeaLevelSpeedOfSound); + + // Safety check: Prevent division by zero + if (FMath::IsNearlyZero(CurveValueAtSeaLevel, 0.01f)) { + return FMath::Max(SeaLevelSpeedOfSound * SafeWorldScale, 1.0f); + } + + float soundvmp = SeaLevelSpeedOfSound / CurveValueAtSeaLevel; + float result = GetCurveValue(SpeedOfSoundCurve, Altitude, SeaLevelSpeedOfSound) * SafeWorldScale * soundvmp; + + // Safety check: Ensure result is never zero or negative + return FMath::Max(result, 1.0f); } diff --git a/Source/EasyBallistics/Private/Pooling.cpp b/Source/EasyBallistics/Private/Pooling.cpp index f1715b9..cf17ff2 100644 --- a/Source/EasyBallistics/Private/Pooling.cpp +++ b/Source/EasyBallistics/Private/Pooling.cpp @@ -76,7 +76,11 @@ AEBBullet* AEBBullet::SpawnOrReactivate(UWorld* World, TSubclassOfIgnoredActors = Default->IgnoredActors; Recycled->SafeDelay = Default->SafeDelay; Recycled->SetLifeSpan(Default->InitialLifeSpan); - Recycled->FinishSpawning(Transform); + AEBBullet* FinishedBullet = Recycled->FinishSpawning(Transform); + if (!FinishedBullet) { + // If spawning failed, return nullptr + return nullptr; + } //if (!Recycled->HasActorBegunPlay()){ Recycled->BeginPlay(); } //Recycled->ReactivationBroadcast(UGameplayStatics::RebaseLocalOriginOntoZero(Recycled->GetWorld(), Transform.GetLocation()), BulletVelocity, BulletOwner, BulletInstigator); #ifdef WITH_EDITOR @@ -84,20 +88,24 @@ AEBBullet* AEBBullet::SpawnOrReactivate(UWorld* World, TSubclassOfAddOnScreenDebugMessage(0, 2, FColor::Green, TEXT("Recycling pooled bullet")); } #endif - return Recycled; + return FinishedBullet; } else { bullet = Cast(World->SpawnActorDeferred(BulletClass, Transform, BulletOwner, BulletInstigator)); bullet->RandomStream.GenerateNewSeed(); bullet->Velocity = BulletVelocity; - bullet->FinishSpawning(Transform); + AEBBullet* FinishedBullet = bullet->FinishSpawning(Transform); + if (!FinishedBullet) { + // If spawning failed, return nullptr + return nullptr; + } //UGameplayStatics::FinishSpawningActor(bullet, Transform); #ifdef WITH_EDITOR if (bullet->DebugPooling) { GEngine->AddOnScreenDebugMessage(0, 2, FColor::Orange, TEXT("Spawning new bullet")); } #endif - return bullet; + return FinishedBullet; } } @@ -122,7 +130,11 @@ AEBBullet* AEBBullet::SpawnOrReactivateFromBarrel(UWorld* World, TSubclassOfIgnoredActors = Default->IgnoredActors; Recycled->SafeDelay = Default->SafeDelay; Recycled->SetLifeSpan(Default->InitialLifeSpan); - Recycled->FinishSpawning(Transform); + AEBBullet* FinishedBullet = Recycled->FinishSpawning(Transform); + if (!FinishedBullet) { + // If spawning failed, return nullptr + return nullptr; + } //if (!Recycled->HasActorBegunPlay()){ Recycled->BeginPlay(); } //Recycled->ReactivationBroadcast(UGameplayStatics::RebaseLocalOriginOntoZero(Recycled->GetWorld(), Transform.GetLocation()), BulletVelocity, BulletOwner, BulletInstigator); #ifdef WITH_EDITOR @@ -130,32 +142,38 @@ AEBBullet* AEBBullet::SpawnOrReactivateFromBarrel(UWorld* World, TSubclassOfAddOnScreenDebugMessage(0, 2, FColor::Green, TEXT("Recycling pooled bullet")); } #endif - return Recycled; + return FinishedBullet; } else { bullet = Cast(World->SpawnActorDeferred(BulletClass, Transform, BulletOwner, BulletInstigator)); bullet->RandomStream.GenerateNewSeed(); bullet->Velocity = BulletVelocity; bullet->SetFiringBarrel(SourceBarrel); - bullet->FinishSpawning(Transform); + AEBBullet* FinishedBullet = bullet->FinishSpawning(Transform); + if (!FinishedBullet) { + // If spawning failed, return nullptr + return nullptr; + } //UGameplayStatics::FinishSpawningActor(bullet, Transform); #ifdef WITH_EDITOR if (bullet->DebugPooling) { GEngine->AddOnScreenDebugMessage(0, 2, FColor::Orange, TEXT("Spawning new bullet")); } #endif - return bullet; + return FinishedBullet; } } -void AEBBullet::FinishSpawning(FTransform Transform) { +AEBBullet* AEBBullet::FinishSpawning(FTransform Transform) { if(IsRecycled){ if (!HasActorBegunPlay()){ BeginPlay(); } ReactivationBroadcast(UGameplayStatics::RebaseLocalOriginOntoZero(this->GetWorld(), Transform.GetLocation()), this->Velocity, this->GetOwner(), this->GetInstigator()); + return this; }else{ - UGameplayStatics::FinishSpawningActor(this, Transform); + AActor* FinishedActor = UGameplayStatics::FinishSpawningActor(this, Transform); + return Cast(FinishedActor); } } diff --git a/Source/EasyBallistics/Private/Trace.cpp b/Source/EasyBallistics/Private/Trace.cpp index ab9a721..9fc69ab 100644 --- a/Source/EasyBallistics/Private/Trace.cpp +++ b/Source/EasyBallistics/Private/Trace.cpp @@ -84,7 +84,12 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn } if (MaterialRestitutionControlsRicochet) { - RicochetRestitution *= PhysMaterial->Restitution; + ricRestitution = FMath::Clamp(ricRestitution * PhysMaterial->Restitution, 0.0f, 0.95f); + + if (PhysMaterial->Restitution > 1.5f) { + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Material '%s' has very high restitution %.2f, clamping ricochet restitution to %.2f"), + *PhysMaterial->GetName(), PhysMaterial->Restitution, ricRestitution); + } } } @@ -164,9 +169,15 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn //bounce FVector bounceAngle = flat * dot * (1.0f - ricFriction); bounceAngle += HitResult.Normal * (1.0f - dot) * ricRestitution; - bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceAngle.Size(); - - NewVelocity = bounceAngle * Velocity.Size(); + + // CRITICAL FIX: Normalize bounceAngle before using with VRandCone to prevent infinite velocity + FVector normalizedBounceAngle = bounceAngle.GetSafeNormal(); + FVector ricochetDirection = RandomStream.VRandCone(normalizedBounceAngle, ricSpread); + + // Apply proper velocity scaling with safety clamps + float ricochetSpeed = Velocity.Size() * FMath::Clamp(ricRestitution, 0.0f, 1.0f); + NewVelocity = ricochetDirection * ricochetSpeed; + Ricochet = true; OwnerSafe = false; } @@ -200,6 +211,12 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn Penetration = true; OwnerSafe = false; + // X-ray trajectory debug visualization + if (DebugEnabled && DebugTrajectory && ShowXRayTrajectory) + { + DrawXRayTrajectory(HitResult.Location, exitLoc, PenetrationVector, PenetrationDepth, PhysMaterial); + } + // CRITICAL FIX: Ensure trace continues after penetration // Calculate how much of the original trace distance was used for penetration float PenetrationTraceDistance = (exitLoc - HitResult.Location).Size(); @@ -305,6 +322,15 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn } } + // SAFETY: Clamp velocity to prevent infinite speeds and numerical instability + float MaxSafeVelocity = MuzzleVelocityMax * 10.0f; // Allow up to 10x muzzle velocity as safety limit + if (!NewVelocity.IsNearlyZero() && NewVelocity.Size() > MaxSafeVelocity) + { + NewVelocity = NewVelocity.GetSafeNormal() * MaxSafeVelocity; + UE_LOG(LogTemp, Warning, TEXT("EBBullet: Velocity clamped from %.1f to %.1f to prevent infinite speed (Ricochet=%s, Penetration=%s)"), + Velocity.Size(), NewVelocity.Size(), Ricochet ? TEXT("true") : TEXT("false"), Penetration ? TEXT("true") : TEXT("false")); + } + Velocity = NewVelocity; if ((Velocity.Size() < DespawnVelocity) || (!Ricochet && !Penetration && (DespawnVelocity>0.0f))){ diff --git a/Source/EasyBallistics/Public/EBBullet.h b/Source/EasyBallistics/Public/EBBullet.h index a2280cc..e169ac7 100644 --- a/Source/EasyBallistics/Public/EBBullet.h +++ b/Source/EasyBallistics/Public/EBBullet.h @@ -59,10 +59,13 @@ public: 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")) bool ShowXRayTrajectory = 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")) FLinearColor XRayTrajectoryColor = FLinearColor(0, 1, 1, 0.7f); + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Trajectory", meta = (EditCondition = "DebugEnabled && DebugTrajectory")) FLinearColor PenetrationTrajectoryColor = FLinearColor(1, 0.5f, 0, 0.8f); 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; @@ -311,6 +314,9 @@ public: UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug") void DrawTrajectoryDebug(FVector StartLocation, FVector EndLocation, FVector CurrentVelocity, float DeltaTime); + UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug") + void DrawXRayTrajectory(FVector EntryPoint, FVector ExitPoint, FVector PenetrationDirection, float PenetrationDepth, UPhysicalMaterial* Material); + UFUNCTION(BlueprintCallable, Category = "EBBullet|Debug") void DrawImpactDebug(FVector ImpactLocation, FVector ImpactVelocity, FVector ImpactNormal, bool bRicochet, bool bPenetration, float PenetrationDepth, UPhysicalMaterial* Material); @@ -367,7 +373,7 @@ private: static AEBBullet* SpawnOrReactivateFromBarrel(UWorld* World, TSubclassOf BulletClass, const FTransform& Transform, FVector BulletVelocity, AActor* BulletOwner, APawn* BulletInstigator, class UEBBarrel* SourceBarrel); void DeactivateToPool(); - void FinishSpawning(FTransform Transform); + AEBBullet* FinishSpawning(FTransform Transform); void Step(float DeltaTime); diff --git a/docs/docs/api/debug-commands-reference.md b/docs/docs/api/debug-commands-reference.md new file mode 100644 index 0000000..dd04856 --- /dev/null +++ b/docs/docs/api/debug-commands-reference.md @@ -0,0 +1,368 @@ +# Debug Commands Reference + +EasyBallistics provides a comprehensive set of debug commands and visualization tools to help developers troubleshoot and optimize their ballistic systems. + +## Console Commands + +### Core Debug Commands + +#### `eb.debug.toggle` +Toggle debug display for all EasyBallistics systems. + +``` +eb.debug.toggle +``` + +**Usage**: Enables or disables all debug visualization globally. + +#### `eb.debug.category` +Toggle specific debug categories for fine-grained control. + +``` +eb.debug.category <0|1> +``` + +**Parameters**: +- `CategoryName`: The debug category to toggle +- `0|1`: Disable (0) or enable (1) the category + +**Available Categories**: +- `Trajectory` - Bullet path visualization +- `Impact` - Impact point and penetration visualization +- `Physics` - Physics forces and atmospheric effects +- `Performance` - Performance metrics and profiling +- `Ballistics` - Mathematical ballistic calculations +- `Spalling` - Secondary fragment generation +- `Pooling` - Object pooling statistics + +**Examples**: +``` +eb.debug.category Trajectory 1 # Enable trajectory visualization +eb.debug.category Impact 1 # Enable impact visualization +eb.debug.category Physics 0 # Disable physics debug +``` + +#### `eb.debug.clear` +Clear all debug visualization elements. + +``` +eb.debug.clear +``` + +**Usage**: Removes all debug lines, text, and visualization elements from the screen. + +#### `eb.debug.info` +Display comprehensive debug information for all active bullets. + +``` +eb.debug.info +``` + +**Usage**: Shows detailed information about bullet states, physics calculations, and system performance. + +### Editor-Only Commands + +#### `EasyBallistics.OpenImportExportTool` +Open the JSON Import/Export tool window. + +``` +EasyBallistics.OpenImportExportTool +``` + +**Usage**: Opens the dedicated tool for importing and exporting ballistic data as JSON files. + +## Debug Categories + +### Trajectory Debug +Visualizes bullet flight paths and trajectory information. + +**Features**: +- **Trail Visualization**: Shows bullet path with color-coded velocity +- **Velocity Vectors**: Displays velocity direction and magnitude +- **Path Prediction**: Shows predicted trajectory +- **Atmospheric Effects**: Visualizes wind and air density impacts + +**Configuration**: +```cpp +// In bullet configuration +Bullet->DebugTrajectory = true; +Bullet->ShowTrail = true; +Bullet->ShowVelocityVectors = true; +Bullet->ShowPathPrediction = true; +``` + +### Impact Debug +Shows impact points, penetration depth, and material interactions. + +**Features**: +- **Impact Points**: Marks where bullets hit surfaces +- **Penetration Depth**: Shows how deep bullets penetrate +- **Ricochet Angles**: Displays ricochet probability and angles +- **Material Information**: Shows material properties at impact +- **Surface Normals**: Visualizes surface normal vectors + +**Configuration**: +```cpp +// In bullet configuration +Bullet->DebugImpact = true; +Bullet->ShowImpactPoints = true; +Bullet->ShowPenetrationDepth = true; +Bullet->ShowRicochetAngles = true; +Bullet->ShowMaterialInfo = true; +``` + +### Physics Debug +Visualizes physics forces and atmospheric effects. + +**Features**: +- **Drag Forces**: Shows air resistance effects +- **Gravity Visualization**: Displays gravitational effects +- **Wind Effects**: Shows wind force vectors +- **Atmospheric Data**: Displays air density and temperature + +**Configuration**: +```cpp +// In bullet configuration +Bullet->DebugPhysics = true; +Bullet->ShowDragForces = true; +Bullet->ShowGravityEffect = true; +Bullet->ShowWindEffect = true; +Bullet->ShowAtmosphericData = true; +``` + +### Performance Debug +Shows performance metrics and profiling information. + +**Features**: +- **Trace Count**: Number of traces per frame +- **Frame Timing**: Performance impact measurements +- **Pooling Statistics**: Object pooling efficiency +- **Memory Usage**: Memory allocation tracking + +**Configuration**: +```cpp +// In bullet configuration +Bullet->DebugPerformance = true; +Bullet->ShowTraceCount = true; +Bullet->ShowFrameTiming = true; +Bullet->ShowPoolingStats = true; +``` + +### Ballistics Debug +Displays mathematical ballistic calculations. + +**Features**: +- **Energy Calculations**: Kinetic energy and momentum +- **Mathematical Comparison**: Artistic vs mathematical mode comparison +- **Ballistic Coefficient**: Real-time BC calculations +- **Penetration Formulas**: Shows mathematical penetration results + +**Configuration**: +```cpp +// In bullet configuration +Bullet->DebugBallistics = true; +Bullet->ShowEnergyCalculations = true; +Bullet->ShowMathematicalComparison = true; +Bullet->ShowBallisticCoefficient = true; +``` + +### Spalling Debug +Visualizes secondary fragment generation. + +**Features**: +- **Fragment Generation**: Shows where spalling occurs +- **Fragment Trajectories**: Displays fragment paths +- **Spall Cones**: Shows fragment dispersion patterns +- **Energy Distribution**: Visualizes energy transfer to fragments + +**Configuration**: +```cpp +// In bullet configuration +Bullet->DebugSpalling = true; +Bullet->ShowSpallGeneration = true; +Bullet->ShowFragmentTrajectories = true; +Bullet->ShowSpallCones = true; +``` + +## Debug Configuration + +### Blueprint Configuration + +Debug settings can be configured through Blueprint: + +```cpp +// Get bullet reference +AEBBullet* Bullet = GetBullet(); + +// Enable debug categories +Bullet->SetBulletDebugCategory("Trajectory", true); +Bullet->SetBulletDebugCategory("Impact", true); + +// Configure visualization +Bullet->DebugTrailTime = 2.0f; +Bullet->ImpactDebugTime = 5.0f; +Bullet->WorldTextScale = 1.2f; +``` + +### Barrel-Level Debug Control + +The barrel component provides debug control for all bullets it fires: + +```cpp +// Get barrel reference +UEBBarrel* Barrel = GetBarrel(); + +// Set debug categories for all bullets +Barrel->SetBulletDebugCategory("Trajectory", true); +Barrel->SetAllBulletDebugCategories(true); + +// Clear debug display +Barrel->ClearAllBulletDebugDisplay(); +``` + +## Visualization Options + +### Color Coding + +Debug visualizations use color coding to convey information: + +**Trajectory Colors**: +- **Green**: High velocity (fast bullets) +- **Yellow**: Medium velocity +- **Red**: Low velocity (slow bullets) + +**Impact Colors**: +- **Green**: Successful penetration +- **Yellow**: Ricochet +- **Red**: Stopped/blocked impact + +**Physics Colors**: +- **Blue**: Drag forces +- **Purple**: Gravity forces +- **Cyan**: Wind forces + +### Text Display Options + +Debug text can be displayed in world space or as on-screen messages: + +```cpp +// Configure text display +Bullet->UseWorldSpaceText = true; +Bullet->UseOnScreenMessages = true; +Bullet->WorldTextScale = 1.0f; +Bullet->MaxOnScreenMessages = 10; +``` + +## Performance Considerations + +### Debug Impact on Performance + +Debug visualization can impact performance, especially with many active bullets: + +- **Trajectory trails**: High impact with many bullets +- **Text rendering**: Moderate impact +- **Line drawing**: Low to moderate impact +- **On-screen messages**: Low impact + +### Optimization Tips + +1. **Use Specific Categories**: Enable only needed debug categories +2. **Limit Debug Time**: Reduce debug display duration +3. **Batch Operations**: Use barrel-level debug control +4. **Conditional Debugging**: Enable debug only during development + +```cpp +// Conditional debug enabling +#if WITH_EDITOR + Bullet->DebugEnabled = true; +#else + Bullet->DebugEnabled = false; +#endif +``` + +## Troubleshooting Debug Issues + +### Common Problems + +#### Debug Not Showing +- Check if debug is enabled globally: `eb.debug.toggle` +- Verify specific categories are enabled: `eb.debug.category 1` +- Ensure bullets are configured for debug: `Bullet->DebugEnabled = true` + +#### Performance Issues +- Disable expensive debug categories (Trajectory, Spalling) +- Reduce debug display times +- Limit number of active bullets during debug + +#### Visual Clutter +- Use `eb.debug.clear` to remove old debug elements +- Enable only essential debug categories +- Reduce text scale and message count + +### Debug Information Output + +Get comprehensive debug information: + +```cpp +// Get debug info string +FString DebugInfo = Bullet->GetDebugInfoString(); +UE_LOG(LogTemp, Warning, TEXT("%s"), *DebugInfo); +``` + +## Advanced Debug Features + +### Custom Debug Widgets + +Create custom debug widgets for impact visualization: + +```cpp +// Create custom impact widget +Bullet->CreateDebugImpactWidget( + ImpactLocation, + bRicochet, + bPenetration, + ImpactVelocity, + PenetrationDepth, + HitMaterial +); +``` + +### Debug Event Binding + +Bind to debug events for custom visualization: + +```cpp +// Bind to impact debug event +Bullet->OnImpact.AddDynamic(this, &AMyClass::OnBulletImpact); + +UFUNCTION() +void OnBulletImpact(/* impact parameters */) +{ + // Custom debug visualization + DrawCustomDebugInfo(); +} +``` + +## Best Practices + +1. **Development Only**: Use debug features only during development +2. **Selective Enabling**: Enable only necessary debug categories +3. **Performance Monitoring**: Monitor FPS impact when using debug +4. **Clean Up**: Use `eb.debug.clear` to remove debug clutter +5. **Consistent Settings**: Use barrel-level debug control for consistency + +## Integration with Profiling Tools + +EasyBallistics debug features integrate with Unreal's profiling tools: + +```cpp +// Enable profiling-friendly debug +Bullet->DebugPerformance = true; + +// Use with stat commands +// stat EasyBallistics +// stat Memory +// stat RenderThreadCommands +``` + +This comprehensive debug system provides powerful tools for developing, testing, and optimizing ballistic systems in EasyBallistics. \ No newline at end of file diff --git a/docs/docs/core-concepts/barrel-component.md b/docs/docs/core-concepts/barrel-component.md new file mode 100644 index 0000000..0eb7cf2 --- /dev/null +++ b/docs/docs/core-concepts/barrel-component.md @@ -0,0 +1,442 @@ +# Barrel Component Guide + +The `UEBBarrel` component is the heart of EasyBallistics' weapon system, managing all aspects of firing mechanics, ammunition cycling, and weapon behavior. This component provides comprehensive control over how projectiles are launched and how weapons behave. + +## Overview + +The `UEBBarrel` component serves as the interface between your weapon systems and the ballistic simulation. It handles: + +- **Firing Mechanics**: Different fire modes and timing +- **Ammunition Management**: Loading, cycling, and tracking ammunition +- **Weapon Behavior**: Spread, velocity variation, and recoil +- **Networking**: Multiplayer synchronization and prediction +- **Debug Integration**: Comprehensive debug and visualization tools + +## Basic Setup + +### Adding to Your Weapon + +```cpp +// In your weapon class header +UCLASS() +class MYGAME_API AMyWeapon : public AActor +{ + GENERATED_BODY() + +public: + AMyWeapon(); + + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Weapon") + class UEBBarrel* Barrel; +}; + +// In constructor +AMyWeapon::AMyWeapon() +{ + Barrel = CreateDefaultSubobject(TEXT("Barrel")); +} +``` + +### Blueprint Setup + +1. **Add Component**: Add `EBBarrel` component to your weapon Blueprint +2. **Position Component**: Place at the muzzle location +3. **Configure Properties**: Set up firing parameters in the Details panel + +## Fire Modes + +The barrel component supports multiple fire modes through the `EFireMode` enumeration: + +### Full Auto (`FM_Auto`) +Continuous firing while trigger is held. + +```cpp +// Configure for full auto +Barrel->FireMode = EFireMode::FM_Auto; +Barrel->FireRateMin = 600.0f; // 600 RPM minimum +Barrel->FireRateMax = 650.0f; // 650 RPM maximum +``` + +### Semi-Auto (`FM_Semiauto`) +One shot per trigger pull. + +```cpp +// Configure for semi-auto +Barrel->FireMode = EFireMode::FM_Semiauto; +Barrel->FireRateMax = 300.0f; // Maximum cyclic rate +``` + +### Burst (`FM_Burst`) +Fixed number of shots per trigger pull. + +```cpp +// Configure for burst fire +Barrel->FireMode = EFireMode::FM_Burst; +Barrel->BurstCount = 3; // 3-round burst +Barrel->BurstCooldown = 0.2f; // 200ms between bursts +``` + +### Interruptible Burst (`FM_InterBurst`) +Burst fire that can be interrupted by releasing the trigger. + +```cpp +// Configure for interruptible burst +Barrel->FireMode = EFireMode::FM_InterBurst; +Barrel->BurstCount = 5; // Up to 5 rounds +Barrel->BurstCooldown = 0.1f; // 100ms between bursts +``` + +### Manual (`FM_Manual`) +Manual firing (bolt-action style). + +```cpp +// Configure for manual operation +Barrel->FireMode = EFireMode::FM_Manual; +// Requires manual charging between shots +``` + +### Slam Fire (`FM_Slamfire`) +Fires immediately when ammunition is chambered. + +```cpp +// Configure for slam fire +Barrel->FireMode = EFireMode::FM_Slamfire; +Barrel->FireRateMax = 120.0f; // Limited by loading speed +``` + +### Gatling (`FM_Gatling`) +Variable fire rate with spool-up and spool-down mechanics. + +```cpp +// Configure for gatling operation +Barrel->FireMode = EFireMode::FM_Gatling; +Barrel->GatlingAutoSpool = true; +Barrel->GatlingSpoolUpTime = 2.0f; // 2 seconds to full speed +Barrel->GatlingSpoolDownTime = 1.5f; // 1.5 seconds to stop +Barrel->FireRateMin = 0.0f; // Stopped +Barrel->FireRateMax = 3000.0f; // 3000 RPM at full speed +``` + +## Ammunition System + +### Basic Ammunition Setup + +```cpp +// Set up ammunition array +TArray> AmmoTypes; +AmmoTypes.Add(MyBulletClass); +Barrel->SetAmmo(100, false, false, true, AmmoTypes); +``` + +### Ammunition Cycling + +Enable automatic ammunition cycling: + +```cpp +// Enable cycling with unlimited ammo +Barrel->CycleAmmo = true; +Barrel->CycleAmmoUnlimited = true; +Barrel->CycleAmmoCount = 30; // 30 rounds before cycling +``` + +### Manual Ammunition Management + +```cpp +// Manually load ammunition +Barrel->Charge(); + +// Unload chambered round +Barrel->UnloadChambered(true); + +// Check ammunition status +int32 AmmoCount = Barrel->GetAmmoCount(true); // Include chambered round +TArray> CurrentAmmo = Barrel->GetAmmo(true); +``` + +## Weapon Characteristics + +### Spread and Accuracy + +```cpp +// Configure weapon spread +Barrel->Spread = 0.01f; // 0.01 radians (~0.6 degrees) +Barrel->SpreadBias = 2.0f; // Higher bias = more accurate on average + +// Configure muzzle velocity variation +Barrel->MuzzleVelocityMultiplierMin = 0.95f; // 95% minimum velocity +Barrel->MuzzleVelocityMultiplierMax = 1.05f; // 105% maximum velocity +``` + +### Recoil and Physics + +```cpp +// Configure recoil effects +Barrel->RecoilMultiplier = 1.0f; // Full recoil +Barrel->InheritVelocity = 1.0f; // Full velocity inheritance +Barrel->AdditionalVelocity = FVector(0, 0, 0); // No additional velocity +``` + +## Advanced Features + +### Gatling Mechanics + +For weapons with variable fire rates: + +```cpp +// Manual gatling control +Barrel->GatlingSpool(true); // Start spooling up +Barrel->GatlingSpool(false); // Start spooling down + +// Check gatling status +bool bIsSpooling = Barrel->Spooling; +float SpoolPhase = Barrel->GatlingPhase; // 0.0 to 1.0 +float CurrentRPS = Barrel->GatlingRPS; // Current rounds per second +``` + +### Prediction and Targeting + +```cpp +// Predict bullet trajectory +bool bHit; +FHitResult HitResult; +FVector HitLocation; +float HitTime; +AActor* HitActor; +TArray Trajectory; + +Barrel->PredictHit( + bHit, HitResult, HitLocation, HitTime, HitActor, Trajectory, + MyBulletClass, IgnoredActors, 10.0f, 0.1f +); + +// Calculate aim direction for moving targets +FVector AimDirection; +FVector PredictedTargetLocation; +FVector PredictedIntersectionLocation; +float PredictedFlightTime; +float Error; + +Barrel->CalculateAimDirection( + MyBulletClass, TargetLocation, TargetVelocity, + AimDirection, PredictedTargetLocation, PredictedIntersectionLocation, + PredictedFlightTime, Error +); +``` + +## Events and Delegates + +### Firing Events + +```cpp +// Bind to firing events +Barrel->BeforeShotFired.AddDynamic(this, &AMyWeapon::OnBeforeShotFired); +Barrel->ShotFired.AddDynamic(this, &AMyWeapon::OnShotFired); +Barrel->AmmoDepleted.AddDynamic(this, &AMyWeapon::OnAmmoDepleted); +Barrel->ReadyToShoot.AddDynamic(this, &AMyWeapon::OnReadyToShoot); + +UFUNCTION() +void OnBeforeShotFired() +{ + // Called just before bullet is spawned + // Good for muzzle flash effects +} + +UFUNCTION() +void OnShotFired() +{ + // Called after bullet is spawned + // Good for sound effects and recoil +} + +UFUNCTION() +void OnAmmoDepleted() +{ + // Called when ammunition runs out + // Good for reload prompts +} + +UFUNCTION() +void OnReadyToShoot() +{ + // Called when weapon is ready to fire again + // Good for UI updates +} +``` + +### Custom Bullet Transformation + +```cpp +// Override bullet spawn transform +UFUNCTION(BlueprintNativeEvent) +void InitialBulletTransform(FVector InLocation, FVector InDirection, + FVector& OutLocation, FVector& OutDirection); + +void AMyWeapon::InitialBulletTransform_Implementation( + FVector InLocation, FVector InDirection, + FVector& OutLocation, FVector& OutDirection) +{ + // Modify bullet spawn location and direction + OutLocation = InLocation + FVector(0, 0, 5); // Slight offset + OutDirection = InDirection; +} +``` + +### Custom Recoil Implementation + +```cpp +// Override recoil application +UFUNCTION(BlueprintNativeEvent) +void ApplyRecoil(UPrimitiveComponent* Component, FVector InLocation, FVector Impulse); + +void AMyWeapon::ApplyRecoil_Implementation( + UPrimitiveComponent* Component, FVector InLocation, FVector Impulse) +{ + // Apply custom recoil to weapon + if (Component) + { + Component->AddImpulseAtLocation(Impulse, InLocation); + } +} +``` + +## Networking + +### Replication Setup + +```cpp +// Configure networking +Barrel->ReplicateVariables = true; +Barrel->ReplicateShotFiredEvents = true; +Barrel->ClientSideAim = true; +Barrel->ClientAimUpdateFrequency = 20.0f; +Barrel->ClientAimDistanceLimit = 1000.0f; +``` + +### Client-Side Prediction + +```cpp +// Enable client-side aiming for responsiveness +Barrel->ClientSideAim = true; +Barrel->ClientAimUpdateFrequency = 15.0f; // 15 updates per second +Barrel->ClientAimDistanceLimit = 200.0f; // 200 units prediction distance +``` + +## Debug and Visualization + +### Debug Configuration + +```cpp +// Enable debug visualization +Barrel->DebugArrowSize = 100.0f; +Barrel->DebugImpactInfo = true; + +// Control bullet debug settings +Barrel->SetBulletDebugCategory("Trajectory", true); +Barrel->SetBulletDebugCategory("Impact", true); +Barrel->SetAllBulletDebugCategories(false); // Disable all +Barrel->ClearAllBulletDebugDisplay(); // Clear display +``` + +### Debug Information + +```cpp +// Get debug information +FString DebugInfo = Barrel->GetBulletDebugInfo(); +UE_LOG(LogTemp, Warning, TEXT("Barrel Debug: %s"), *DebugInfo); +``` + +## Performance Optimization + +### Object Pooling + +All bullets fired from the barrel can use object pooling: + +```cpp +// Enable pooling on bullet class +MyBulletClass->GetDefaultObject()->EnablePooling = true; +MyBulletClass->GetDefaultObject()->MaxPoolSize = 100; +``` + +### Fire Rate Optimization + +```cpp +// Optimize fire rate calculations +Barrel->FireRateMin = Barrel->FireRateMax; // Disable randomization +``` + +## Common Patterns + +### Weapon Switching + +```cpp +// Switch between fire modes +void SwitchToSemiAuto() +{ + Barrel->SwitchFireMode(EFireMode::FM_Semiauto); +} + +void SwitchToBurst() +{ + Barrel->SwitchFireMode(EFireMode::FM_Burst); +} +``` + +### Ammunition Types + +```cpp +// Set up different ammunition types +TArray> MixedAmmo; +MixedAmmo.Add(FMJBulletClass); // Full Metal Jacket +MixedAmmo.Add(APBulletClass); // Armor Piercing +MixedAmmo.Add(HPBulletClass); // Hollow Point + +Barrel->SetAmmo(90, false, false, true, MixedAmmo); +``` + +### Weapon Jamming + +```cpp +// Simulate weapon jamming +void SimulateJam() +{ + Barrel->ShootingBlocked = true; + + // Clear jam after delay + FTimerHandle JamTimer; + GetWorld()->GetTimerManager().SetTimer(JamTimer, [this]() + { + Barrel->ShootingBlocked = false; + }, 2.0f, false); +} +``` + +## Best Practices + +1. **Component Positioning**: Always place the barrel component at the muzzle location +2. **Fire Rate Balance**: Use realistic fire rates for your weapon type +3. **Ammunition Management**: Implement proper ammunition tracking and reload systems +4. **Network Optimization**: Use appropriate replication settings for your game type +5. **Debug During Development**: Enable debug features during development, disable for shipping +6. **Performance Monitoring**: Monitor performance with high-rate fire weapons +7. **Event Binding**: Use barrel events for weapon effects and feedback + +## Troubleshooting + +### Common Issues + +#### Weapon Not Firing +- Check if `ShootingBlocked` is false +- Verify ammunition is loaded (`GetAmmoCount() > 0`) +- Ensure fire mode is appropriate for input method + +#### Networking Issues +- Enable `ReplicateVariables` for multiplayer +- Check authority when calling `Shoot()` +- Verify client prediction settings + +#### Performance Problems +- Reduce fire rate for high-rate weapons +- Enable object pooling +- Limit simultaneous active bullets + +This comprehensive barrel component system provides the foundation for creating sophisticated weapon systems in EasyBallistics. \ No newline at end of file diff --git a/docs/docs/core-concepts/unit-conversions.md b/docs/docs/core-concepts/unit-conversions.md new file mode 100644 index 0000000..015a4e3 --- /dev/null +++ b/docs/docs/core-concepts/unit-conversions.md @@ -0,0 +1,310 @@ +# Unit Conversions Reference + +EasyBallistics uses a mix of metric and imperial units to accommodate different regional preferences and industry standards. This guide covers all unit conversions and provides reference tables for common ballistic calculations. + +## Units Overview + +### Primary Units in EasyBallistics + +| Property | Default Unit | Alternative Units | +|----------|-------------|-------------------| +| **Distance** | Centimeters (cm) | Meters (m), Inches (in), Feet (ft) | +| **Velocity** | Centimeters per second (cm/s) | Meters per second (m/s), Feet per second (fps), Miles per hour (mph) | +| **Mass** | Kilograms (kg) | Grams (g), Grains (gr), Pounds (lb) | +| **Energy** | Joules (J) | Foot-pounds (ft-lbs), Calories (cal) | +| **Pressure** | Millibars (mbar) | Pascals (Pa), PSI (psi), Atmospheres (atm) | +| **Temperature** | Celsius (°C) | Fahrenheit (°F), Kelvin (K) | +| **Angle** | Radians (rad) | Degrees (°), Mils (mil) | + +### Unreal Engine Units + +- **Unreal Units**: 1 Unreal Unit = 1 centimeter +- **World Scale**: EasyBallistics uses `WorldScale` parameter for unit scaling + +## Conversion Functions + +### Built-in Mathematical Ballistics Conversions + +```cpp +// Velocity conversions +float MPS = UEBMathematicalBallistics::ConvertFPStoMPS(3000.0f); // 3000 fps to m/s +float FPS = UEBMathematicalBallistics::ConvertMPStoFPS(914.4f); // 914.4 m/s to fps + +// Energy conversions +float Joules = UEBMathematicalBallistics::ConvertFtLbsToJoules(2500.0f); // 2500 ft-lbs to J +float FtLbs = UEBMathematicalBallistics::ConvertJoulesToFtLbs(3390.0f); // 3390 J to ft-lbs +``` + +### Custom Conversion Functions + +```cpp +// Distance conversions +float ConvertInchesToCM(float Inches) { return Inches * 2.54f; } +float ConvertCMToInches(float CM) { return CM / 2.54f; } +float ConvertMetersToCM(float Meters) { return Meters * 100.0f; } +float ConvertCMToMeters(float CM) { return CM / 100.0f; } +float ConvertFeetToCM(float Feet) { return Feet * 30.48f; } +float ConvertCMToFeet(float CM) { return CM / 30.48f; } + +// Velocity conversions +float ConvertFPStoCMPS(float FPS) { return FPS * 30.48f; } +float ConvertCMPStoFPS(float CMPS) { return CMPS / 30.48f; } +float ConvertMPStoCMPS(float MPS) { return MPS * 100.0f; } +float ConvertCMPStoMPS(float CMPS) { return CMPS / 100.0f; } +float ConvertMPHtoCMPS(float MPH) { return MPH * 44.704f; } +float ConvertCMPStoMPH(float CMPS) { return CMPS / 44.704f; } + +// Mass conversions +float ConvertGrainsToKg(float Grains) { return Grains * 0.0000647989f; } +float ConvertKgToGrains(float Kg) { return Kg / 0.0000647989f; } +float ConvertGramsToBg(float Grams) { return Grams / 1000.0f; } +float ConvertKgToGrams(float Kg) { return Kg * 1000.0f; } +float ConvertPoundsToKg(float Pounds) { return Pounds * 0.453592f; } +float ConvertKgToPounds(float Kg) { return Kg / 0.453592f; } + +// Angle conversions +float ConvertDegreesToRadians(float Degrees) { return Degrees * (PI / 180.0f); } +float ConvertRadiansToDegrees(float Radians) { return Radians * (180.0f / PI); } +float ConvertMilsToRadians(float Mils) { return Mils * 0.000981747f; } +float ConvertRadiansToMils(float Radians) { return Radians / 0.000981747f; } +``` + +## Conversion Tables + +### Velocity Conversions + +| From | To | Multiply by | +|------|-----|-------------| +| fps | m/s | 0.3048 | +| fps | cm/s | 30.48 | +| fps | mph | 0.681818 | +| m/s | fps | 3.28084 | +| m/s | cm/s | 100.0 | +| m/s | mph | 2.23694 | +| cm/s | fps | 0.0328084 | +| cm/s | m/s | 0.01 | +| cm/s | mph | 0.0223694 | + +### Common Velocity Reference Values + +| Velocity | fps | m/s | cm/s | +|----------|-----|-----|------| +| **Subsonic** | < 1,125 | < 343 | < 34,300 | +| **Transonic** | 1,125-1,440 | 343-439 | 34,300-43,900 | +| **Supersonic** | > 1,440 | > 439 | > 43,900 | +| **Typical .22 LR** | 1,200 | 366 | 36,600 | +| **Typical 5.56mm** | 3,000 | 914 | 91,400 | +| **Typical .308 Win** | 2,700 | 823 | 82,300 | +| **Typical .50 BMG** | 2,900 | 884 | 88,400 | + +### Mass Conversions + +| From | To | Multiply by | +|------|-----|-------------| +| grains | kg | 0.0000647989 | +| grains | g | 0.0647989 | +| kg | grains | 15,432.36 | +| kg | g | 1,000.0 | +| g | grains | 15.43236 | +| g | kg | 0.001 | +| lb | kg | 0.453592 | +| lb | g | 453.592 | + +### Common Bullet Weight Reference + +| Bullet Weight | Grains | Grams | Kg | +|---------------|--------|-------|-----| +| **Light .22 LR** | 40 | 2.59 | 0.00259 | +| **Heavy .22 LR** | 60 | 3.89 | 0.00389 | +| **Light 5.56mm** | 55 | 3.56 | 0.00356 | +| **Heavy 5.56mm** | 77 | 4.99 | 0.00499 | +| **Light .308** | 150 | 9.72 | 0.00972 | +| **Heavy .308** | 180 | 11.66 | 0.01166 | +| **Light .50 BMG** | 650 | 42.12 | 0.04212 | +| **Heavy .50 BMG** | 800 | 51.84 | 0.05184 | + +### Energy Conversions + +| From | To | Multiply by | +|------|-----|-------------| +| ft-lbs | J | 1.35582 | +| ft-lbs | cal | 0.323832 | +| J | ft-lbs | 0.737562 | +| J | cal | 0.238846 | +| cal | ft-lbs | 3.08596 | +| cal | J | 4.184 | + +### Common Energy Reference Values + +| Energy Level | ft-lbs | Joules | +|--------------|--------|--------| +| **Minimum for hunting** | 1,000 | 1,356 | +| **Typical .22 LR** | 140 | 190 | +| **Typical 5.56mm** | 1,300 | 1,763 | +| **Typical .308 Win** | 2,500 | 3,390 | +| **Typical .50 BMG** | 13,000 | 17,626 | + +### Distance Conversions + +| From | To | Multiply by | +|------|-----|-------------| +| inches | cm | 2.54 | +| inches | m | 0.0254 | +| feet | cm | 30.48 | +| feet | m | 0.3048 | +| cm | inches | 0.393701 | +| cm | feet | 0.0328084 | +| m | cm | 100.0 | +| m | feet | 3.28084 | + +### Pressure Conversions + +| From | To | Multiply by | +|------|-----|-------------| +| mbar | Pa | 100.0 | +| mbar | psi | 0.0145038 | +| mbar | atm | 0.000986923 | +| Pa | mbar | 0.01 | +| Pa | psi | 0.000145038 | +| psi | mbar | 68.9476 | +| psi | Pa | 6,894.76 | +| atm | mbar | 1,013.25 | +| atm | psi | 14.6959 | + +### Temperature Conversions + +| From | To | Formula | +|------|-----|---------| +| °C | °F | (°C × 9/5) + 32 | +| °C | K | °C + 273.15 | +| °F | °C | (°F - 32) × 5/9 | +| °F | K | (°F - 32) × 5/9 + 273.15 | +| K | °C | K - 273.15 | +| K | °F | (K - 273.15) × 9/5 + 32 | + +## Practical Examples + +### Setting Up Bullet Properties + +```cpp +// Creating a 5.56mm NATO bullet +FMathematicalBulletProperties BulletProps; + +// Using imperial measurements (common in US) +BulletProps.GrainWeight = 62.0f; // 62 grains +BulletProps.DiameterInches = 0.224f; // 0.224 inches +BulletProps.LengthInches = 0.825f; // 0.825 inches +BulletProps.BallisticCoefficientG1 = 0.310f; // G1 BC + +// EasyBallistics handles internal conversions automatically +float MassKg = BulletProps.GetMassKg(); // Converts to kg +float DiameterCm = BulletProps.GetDiameterCm(); // Converts to cm +``` + +### Atmospheric Conditions + +```cpp +// Setting up atmospheric conditions +Bullet->SeaLevelAirDensity = 1.225f; // kg/m³ (standard) +Bullet->SeaLevelAirPressure = 1013.25f; // mbar (standard) +Bullet->SeaLevelAirTemperature = 15.0f; // °C (standard) + +// Converting from imperial if needed +float TempF = 59.0f; // 59°F +float TempC = (TempF - 32.0f) * 5.0f / 9.0f; // Convert to Celsius +Bullet->SeaLevelAirTemperature = TempC; +``` + +### Ballistic Calculations + +```cpp +// Calculate penetration using metric inputs +float VelocityMPS = 800.0f; // 800 m/s +float ImpactAngle = 15.0f; // 15 degrees + +// Convert to EasyBallistics units +float VelocityCMPS = VelocityMPS * 100.0f; // Convert to cm/s +float ImpactAngleRad = ImpactAngle * PI / 180.0f; // Convert to radians + +// Use in calculations +float PenetrationDepth = UEBMathematicalBallistics::CalculatePenetrationDepth( + BulletProps, MaterialProps, VelocityCMPS, ImpactAngle +); +``` + +## Best Practices + +### Consistent Units + +1. **Internal Consistency**: Use EasyBallistics' native units (cm, cm/s, kg) for all internal calculations +2. **User Interface**: Convert to user-preferred units only at the UI level +3. **Documentation**: Always specify units in comments and documentation + +### Conversion Patterns + +```cpp +// Good: Clear conversion with comments +float VelocityFPS = 2800.0f; // User input in fps +float VelocityCMPS = VelocityFPS * 30.48f; // Convert to cm/s for EasyBallistics + +// Good: Use built-in conversion functions +float VelocityMPS = UEBMathematicalBallistics::ConvertFPStoMPS(VelocityFPS); +float VelocityCMPS = VelocityMPS * 100.0f; + +// Avoid: Mixed units without clear conversion +float BadVelocity = 2800.0f; // Unclear units +``` + +### Regional Considerations + +```cpp +// Create region-specific presets +struct FAmericanUnits +{ + static float ConvertVelocity(float FPS) { return FPS * 30.48f; } + static float ConvertMass(float Grains) { return Grains * 0.0000647989f; } + static float ConvertDistance(float Inches) { return Inches * 2.54f; } +}; + +struct FMetricUnits +{ + static float ConvertVelocity(float MPS) { return MPS * 100.0f; } + static float ConvertMass(float Grams) { return Grams / 1000.0f; } + static float ConvertDistance(float CM) { return CM; } +}; +``` + +## Troubleshooting Unit Issues + +### Common Problems + +1. **Incorrect Velocities**: Check if you're using fps vs m/s vs cm/s +2. **Wrong Penetration**: Verify angle units (degrees vs radians) +3. **Mass Confusion**: Ensure grains vs grams vs kilograms consistency +4. **Distance Errors**: Confirm centimeters vs meters vs inches + +### Debug Unit Verification + +```cpp +// Add unit verification in debug builds +#if WITH_EDITOR +void VerifyUnits() +{ + // Test velocity conversion + float TestFPS = 3000.0f; + float TestMPS = UEBMathematicalBallistics::ConvertFPStoMPS(TestFPS); + float TestCMPS = TestMPS * 100.0f; + + UE_LOG(LogTemp, Warning, TEXT("Velocity: %f fps = %f m/s = %f cm/s"), + TestFPS, TestMPS, TestCMPS); + + // Test mass conversion + float TestGrains = 55.0f; + float TestKg = TestGrains * 0.0000647989f; + + UE_LOG(LogTemp, Warning, TEXT("Mass: %f grains = %f kg"), TestGrains, TestKg); +} +#endif +``` + +This comprehensive unit conversion system ensures accurate ballistic calculations while accommodating different regional preferences and industry standards. \ No newline at end of file diff --git a/docs/docs/tutorials/blueprint-integration.md b/docs/docs/tutorials/blueprint-integration.md new file mode 100644 index 0000000..018d156 --- /dev/null +++ b/docs/docs/tutorials/blueprint-integration.md @@ -0,0 +1,441 @@ +# Blueprint Integration Guide + +EasyBallistics provides extensive Blueprint support, allowing developers to create sophisticated ballistic systems without writing C++ code. This guide covers everything from basic setup to advanced Blueprint patterns. + +## Prerequisites + +- Basic knowledge of Unreal Engine Blueprints +- Understanding of Actor Components and Data Assets +- Familiarity with Unreal's event system + +## Quick Start: Creating Your First Ballistic Weapon + +### Step 1: Create Weapon Blueprint + +1. **Create New Blueprint**: Right-click in Content Browser → Blueprint Class → Actor +2. **Name**: `BP_MyWeapon` +3. **Add Components**: + - `Static Mesh Component` (for weapon visual) + - `EBBarrel Component` (for ballistic system) + +### Step 2: Configure Barrel Component + +In your weapon Blueprint's Event Graph: + +```blueprintscript +Event BeginPlay +├── Get EBBarrel Component +├── Set Fire Mode (Full Auto) +├── Set Fire Rate Min (600.0) +├── Set Fire Rate Max (650.0) +└── Set Ammo (Count: 100, Bullet Class: BP_MyBullet) +``` + +### Step 3: Create Bullet Properties + +1. **Right-click** in Content Browser +2. **Ballistics** → **Bullet Properties** +3. **Name**: `BP_MyBulletProperties` +4. **Configure**: Set grain weight, diameter, ballistic coefficient + +### Step 4: Create Bullet Blueprint + +1. **Create New Blueprint**: Blueprint Class → EBBullet +2. **Name**: `BP_MyBullet` +3. **Set Bullet Properties Asset**: Reference your `BP_MyBulletProperties` + +### Step 5: Add Shooting Logic + +In your weapon Blueprint: + +```blueprintscript +Input Action Fire +├── Branch (Is Valid: EBBarrel) +│ └── True: EBBarrel → Shoot (Trigger: True) +└── False: Print String "No barrel component!" +``` + +## Advanced Blueprint Patterns + +### Smart Weapon System + +Create a flexible weapon system that adapts to different situations: + +```blueprintscript +// Weapon Controller Blueprint +Event BeginPlay +├── Create Dynamic Material Instance +├── Bind Events +│ ├── Barrel → On Shot Fired → Play Muzzle Flash +│ ├── Barrel → On Ammo Depleted → Show Reload UI +│ └── Barrel → On Ready To Shoot → Hide Reload UI +└── Initialize Weapon Stats +``` + +### Ammunition Management + +```blueprintscript +// Reload System +Custom Event: Reload Weapon +├── Branch (Has Ammo in Inventory) +│ ├── True: +│ │ ├── EBBarrel → Set Ammo (New Ammo Array) +│ │ ├── Play Reload Animation +│ │ └── Update UI +│ └── False: +│ ├── Play Empty Sound +│ └── Show "No Ammo" Message +``` + +### Fire Mode Switching + +```blueprintscript +// Fire Mode System +Input Action: Switch Fire Mode +├── Switch on Enum (Current Fire Mode) +│ ├── Auto: EBBarrel → Switch Fire Mode (Semi Auto) +│ ├── Semi: EBBarrel → Switch Fire Mode (Burst) +│ └── Burst: EBBarrel → Switch Fire Mode (Auto) +├── Update Fire Mode UI +└── Play Switch Sound +``` + +## Blueprint Events and Delegates + +### Barrel Component Events + +```blueprintscript +// Bind to Barrel Events +Event BeginPlay +├── EBBarrel → Bind Event to On Before Shot Fired +├── EBBarrel → Bind Event to On Shot Fired +├── EBBarrel → Bind Event to On Ammo Depleted +└── EBBarrel → Bind Event to On Ready To Shoot + +// Event Handlers +On Before Shot Fired +├── Spawn Muzzle Flash +├── Apply Screen Shake +└── Play Fire Sound + +On Shot Fired +├── Add Recoil to Camera +├── Update Ammo Counter +└── Spawn Shell Casing + +On Ammo Depleted +├── Show Reload Prompt +├── Play Empty Sound +└── Block Further Shooting + +On Ready To Shoot +├── Hide Reload Prompt +└── Enable Shooting Input +``` + +### Bullet Impact Events + +```blueprintscript +// Bullet Impact System +Event BeginPlay (in Bullet Blueprint) +├── Use New Impact System = True +├── Enable Spalling = True +└── Bind Impact Events + +// Impact Event Handler +On Impact +├── Branch (Did Penetrate) +│ ├── True: Spawn Penetration Effect +│ └── False: Spawn Impact Effect +├── Branch (Is Ricochet) +│ ├── True: Spawn Ricochet Effect +│ └── False: Continue Normal Impact +└── Apply Damage to Hit Actor +``` + +## Material Response System + +### Creating Material Response Maps + +```blueprintscript +// Material Response Setup +Event BeginPlay +├── Create Material Response Map +├── Add Entry (Steel) +│ ├── Penetration Depth Multiplier: 0.5 +│ ├── Ricochet Probability: 0.3 +│ └── Enable Spalling: True +├── Add Entry (Wood) +│ ├── Penetration Depth Multiplier: 2.0 +│ ├── Ricochet Probability: 0.1 +│ └── Enable Spalling: False +└── Assign to Bullets +``` + +### Dynamic Material Properties + +```blueprintscript +// Context-Sensitive Material Response +Function: Get Material Properties +├── Input: Physical Material +├── Switch on Physical Material +│ ├── Steel: Return Steel Properties +│ ├── Wood: Return Wood Properties +│ ├── Concrete: Return Concrete Properties +│ └── Default: Return Default Properties +└── Output: Material Properties Structure +``` + +## Advanced Ballistic Features + +### Atmospheric Effects + +```blueprintscript +// Dynamic Weather System +Event BeginPlay +├── Get Weather Manager +├── Bind to Weather Changed Event +└── Update Bullet Atmospheric Properties + +On Weather Changed +├── Get Current Weather Data +├── Calculate Air Density +├── Calculate Temperature +├── Calculate Pressure +└── Update All Active Bullets +``` + +### Predictive Targeting + +```blueprintscript +// Lead Target Calculation +Function: Calculate Lead Target +├── Input: Target Actor, Target Velocity +├── EBBarrel → Calculate Aim Direction +│ ├── Bullet Class: Current Bullet +│ ├── Target Location: Target Position +│ ├── Target Velocity: Target Movement +│ └── Max Time: 10.0 +├── Output: Aim Direction, Predicted Hit Location +└── Update Crosshair Position +``` + +### Ballistic Trajectory Visualization + +```blueprintscript +// Trajectory Preview +Function: Show Trajectory +├── Input: Start Location, Aim Direction +├── EBBarrel → Predict Hit +│ ├── Bullet Class: Current Bullet +│ ├── Max Time: 5.0 +│ └── Step Size: 0.1 +├── For Each (Trajectory Point) +│ └── Spawn Trajectory Marker +└── Output: Trajectory Spline +``` + +## Debugging in Blueprints + +### Debug Visualization + +```blueprintscript +// Debug Controls +Input Action: Toggle Debug +├── Branch (Debug Enabled) +│ ├── True: +│ │ ├── EBBarrel → Set All Bullet Debug Categories (False) +│ │ └── Set Debug Enabled (False) +│ └── False: +│ ├── EBBarrel → Set Bullet Debug Category ("Trajectory", True) +│ ├── EBBarrel → Set Bullet Debug Category ("Impact", True) +│ └── Set Debug Enabled (True) +``` + +### Performance Monitoring + +```blueprintscript +// Performance Tracker +Event Tick +├── Branch (Debug Mode) +│ └── True: +│ ├── Get Active Bullet Count +│ ├── Get Frame Rate +│ ├── Update Performance UI +│ └── Check Performance Warnings +``` + +## UI Integration + +### Weapon Status Display + +```blueprintscript +// Weapon UI Controller +Event BeginPlay +├── Get Weapon Reference +├── Bind to Weapon Events +└── Initialize UI Elements + +Update Ammo Display +├── Get Ammo Count (Include Chambered) +├── Get Total Ammo Available +├── Update Ammo Text +└── Update Ammo Bar + +Update Fire Mode Display +├── Get Current Fire Mode +├── Switch on Fire Mode +│ ├── Auto: Set Icon (Auto) +│ ├── Semi: Set Icon (Semi) +│ └── Burst: Set Icon (Burst) +└── Update Fire Mode Text +``` + +### Crosshair System + +```blueprintscript +// Dynamic Crosshair +Event Tick +├── Get Weapon Spread +├── Calculate Crosshair Size +├── Get Predicted Hit Location +├── Update Crosshair Position +└── Update Crosshair Color (Hit/Miss) +``` + +## Multiplayer Considerations + +### Network Replication + +```blueprintscript +// Multiplayer Weapon Setup +Event BeginPlay +├── Branch (Has Authority) +│ ├── True: Setup Server Logic +│ └── False: Setup Client Logic +├── EBBarrel → Set Replicate Variables (True) +├── EBBarrel → Set Replicate Shot Fired Events (True) +└── EBBarrel → Set Client Side Aim (True) +``` + +### Client Prediction + +```blueprintscript +// Client-Side Prediction +Custom Event: Client Fire Weapon +├── Branch (Has Authority) +│ ├── True: Fire Immediately +│ └── False: +│ ├── Fire Predicted Shot +│ └── Send Fire Request to Server +``` + +## Performance Optimization + +### Object Pooling + +```blueprintscript +// Bullet Pool Manager +Event BeginPlay +├── Create Bullet Pool Array +├── For Loop (Pool Size) +│ └── Add Bullet to Pool +└── Initialize Pool Manager + +Get Pooled Bullet +├── Branch (Pool Has Available) +│ ├── True: Return Pooled Bullet +│ └── False: Create New Bullet +└── Configure and Return Bullet +``` + +### LOD System + +```blueprintscript +// Distance-Based LOD +Event Tick +├── Get Distance to Player +├── Switch on Distance Range +│ ├── Close: Full Detail +│ ├── Medium: Reduced Detail +│ └── Far: Minimal Detail +└── Apply LOD Settings +``` + +## Common Blueprint Patterns + +### Weapon Pickup System + +```blueprintscript +// Weapon Pickup +On Component Begin Overlap +├── Cast to Player Character +├── Branch (Cast Success) +│ └── True: +│ ├── Add Weapon to Inventory +│ ├── Play Pickup Sound +│ ├── Spawn Pickup Effect +│ └── Destroy Pickup Actor +``` + +### Weapon Customization + +```blueprintscript +// Attachment System +Function: Attach Weapon Mod +├── Input: Mod Type, Mod Data +├── Switch on Mod Type +│ ├── Barrel: Modify Barrel Properties +│ ├── Scope: Modify Accuracy +│ └── Magazine: Modify Ammo Capacity +├── Update Weapon Stats +└── Refresh Weapon Model +``` + +### Damage System Integration + +```blueprintscript +// Damage Application +On Impact (Bullet) +├── Get Hit Actor +├── Calculate Damage +│ ├── Base Damage +│ ├── Velocity Modifier +│ └── Penetration Modifier +├── Apply Damage to Actor +└── Spawn Damage Effects +``` + +## Best Practices + +### Blueprint Organization + +1. **Separate Concerns**: Keep weapon logic, UI, and effects in separate Blueprints +2. **Use Interfaces**: Create weapon interfaces for consistent behavior +3. **Event-Driven**: Use events instead of tick-based updates where possible +4. **Performance**: Minimize Blueprint tick functions, use timers instead + +### Error Handling + +```blueprintscript +// Robust Error Handling +Function: Safe Fire Weapon +├── Branch (Is Valid: Barrel Component) +│ └── True: Continue +│ └── False: Print Error and Return +├── Branch (Has Ammo) +│ └── True: Continue +│ └── False: Play Empty Sound and Return +├── Branch (Not Shooting Blocked) +│ └── True: Fire Weapon +│ └── False: Print "Weapon Blocked" and Return +``` + +### Testing and Debugging + +1. **Use Debug Nodes**: Add debug prints and visualization +2. **Test Multiplayer**: Always test networking features +3. **Profile Performance**: Monitor frame rate with complex setups +4. **Validate Inputs**: Check for null references and invalid data + +This comprehensive Blueprint integration guide provides the foundation for creating sophisticated ballistic systems entirely through Blueprints, leveraging the full power of EasyBallistics' Blueprint-friendly architecture. \ No newline at end of file diff --git a/docs/sidebars.js b/docs/sidebars.js index eda55f4..bc16ea6 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -29,17 +29,20 @@ const sidebars = { label: 'Core Concepts', items: [ 'core-concepts/overview', + 'core-concepts/barrel-component', 'core-concepts/gatling-mechanics', 'core-concepts/trajectory-prediction', 'core-concepts/mathematical-ballistics', 'core-concepts/ballistic-impact-component', 'core-concepts/spalling-system', + 'core-concepts/unit-conversions', ], }, { type: 'category', label: 'Tutorials', items: [ + 'tutorials/blueprint-integration', 'tutorials/material-setup-guide', 'tutorials/advanced-weapon-systems', ], @@ -63,6 +66,7 @@ const sidebars = { 'api/overview', 'api/bullet-reference', 'api/material-properties-reference', + 'api/debug-commands-reference', ], }, 'troubleshooting',