// Copyright 2018 Mookie. All Rights Reserved. #include "EBBarrel.h" #include "EBBullet.h" #include "EngineUtils.h" #include "EBGun.h" #include "EBMagazine.h" #include "EBWeaponConfiguration.h" #include "EBBulletProperties.h" UEBBarrel::UEBBarrel() { PrimaryComponentTick.bCanEverTick = true; bHiddenInGame = true; bAutoActivate = true; SetIsReplicatedByDefault(ReplicateVariables); RandomStream.GenerateNewSeed(); GatlingRPS = FireRateMin; } void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (ClientSideAim){ if (GetOwner()->GetRemoteRole()==ROLE_Authority){ TimeSinceAimUpdate += DeltaTime; if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) { Aim = GetComponentTransform().GetUnitAxis(EAxis::X); Location = GetComponentTransform().GetLocation(); ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim); TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency); }; }else{ if (!RemoteAimReceived) { Aim = GetComponentTransform().GetUnitAxis(EAxis::X); Location = GetComponentTransform().GetLocation(); } else { FVector LocOffset = (Location - GetComponentLocation()); if (LocOffset.Size() > ClientAimDistanceLimit) { //lag or cheater??? Location = GetComponentLocation() + LocOffset.GetSafeNormal()*ClientAimDistanceLimit; } } } } else { Aim = GetComponentTransform().GetUnitAxis(EAxis::X); Location = GetComponentTransform().GetLocation(); } //Only server can tick if (GetOwner()->GetLocalRole() == ROLE_Authority){ float RemainingDelta; if (FireMode == EFireMode::FM_Gatling) { if (Spooling || (GatlingAutoSpool && Shooting)) { GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMax, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f)); } else { GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMin, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f)); } GatlingPhase += GatlingRPS*DeltaTime; for (int i = 1; i <= GatlingPhase; i++) { if (Cooldown <= 0.0f && LoadNext) { NextBullet(); } if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) { SpawnBullet(GetOwner(), Location, Aim); } } GatlingPhase = FMath::Fmod(GatlingPhase, 1.0f); } else { RemainingDelta = DeltaTime; do { float step = FMath::Min(Cooldown, RemainingDelta); Cooldown -= step; RemainingDelta -= step; if (Cooldown <= 0.0f && LoadNext) { NextBullet(); } //shoot when ready if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) { if (BurstRemaining > 0 || (FireMode != EFireMode::FM_Burst && FireMode != EFireMode::FM_InterBurst)) { SpawnBullet(GetOwner(), Location, Aim); } else { Shooting = false; } } } while (RemainingDelta > 0 && Cooldown > 0); } } } void UEBBarrel::NextBullet() { if (ChamberedBullet == nullptr) { if (Ammo.Num() > 0 && (CycleAmmoCount > 0 || CycleAmmoUnlimited || (!CycleAmmo))) { //cycle ammo if (CycleAmmo) { if (CycleAmmoPos >= Ammo.Num()) { CycleAmmoPos = 0; } ChamberedBullet = Ammo[CycleAmmoPos]; CycleAmmoPos++; if (!CycleAmmoUnlimited) { CycleAmmoCount--; } } else { ChamberedBullet = Ammo[0]; Ammo.RemoveAt(0, 1, EAllowShrinking::Yes); } ReadyToShoot.Broadcast(); } else { AmmoDepleted.Broadcast(); } } } void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim) { TSubclassOf BulletClass = ChamberedBullet; if (BulletClass != nullptr) { FVector OutLocation; FVector OutAim; InitialBulletTransform(InLocation, InAim, OutLocation, OutAim); AEBBullet* Default = Cast(BulletClass->GetDefaultObject()); float BulletSpread = Default->Spread; if (Default->SpreadBias > 0.0f) { float SpreadMult = FMath::Pow(FMath::FRand(), Default->SpreadBias); BulletSpread *= SpreadMult; } float BarrelSpread = Spread; if (SpreadBias > 0.0f) { float SpreadMult = FMath::Pow(FMath::FRand(), SpreadBias); BarrelSpread *= SpreadMult; } float TotalSpread = BulletSpread+BarrelSpread; OutAim = RandomStream.VRandCone(OutAim,TotalSpread); float BulletVelocity = FMath::Lerp(MuzzleVelocityMultiplierMin* Default->MuzzleVelocityMin, MuzzleVelocityMultiplierMax*Default->MuzzleVelocityMax, RandomStream.FRand()); FVector Velocity = OutAim*BulletVelocity; //get parent physics body UPrimitiveComponent* parent = Cast(GetAttachParent()); Velocity += AdditionalVelocity; if (parent != nullptr) { if (parent->IsSimulatingPhysics()) { Velocity += parent->GetPhysicsLinearVelocityAtPoint(OutLocation)*InheritVelocity; } if (Default->Shotgun) { ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier*Default->ShotCount); } else{ ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier); } } BeforeShotFired.Broadcast(); AEBBullet::SpawnWithExactVelocityFromBarrel(BulletClass, Owner, Owner->GetInstigator(), OutLocation, Velocity, this); //spend ammo ChamberedBullet = nullptr; if (FireMode != EFireMode::FM_Gatling) { Cooldown = 1.0f / FMath::Lerp(FireRateMin, FireRateMax, RandomStream.FRand()); } //fire modes switch (FireMode) { case EFireMode::FM_Auto: LoadNext = true; break; case EFireMode::FM_Burst: LoadNext = true; break; case EFireMode::FM_InterBurst: LoadNext = true; break; case EFireMode::FM_Semiauto: Shooting = false; LoadNext = true; break; case EFireMode::FM_Manual: Shooting = false; LoadNext = false; break; case EFireMode::FM_Slamfire: LoadNext = false; break; case EFireMode::FM_Gatling: LoadNext = true; break; }; if (BurstRemaining > 0) { BurstRemaining--; } else { if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) { Cooldown = FMath::Max(Cooldown, BurstCooldown); } } if (ReplicateShotFiredEvents) { ShotFiredMulticast(); } else { ShotFired.Broadcast(); } } } void UEBBarrel::InitialBulletTransform_Implementation(FVector InLocation, FVector InDirection, FVector& OutLocation, FVector& OutDirection) { OutLocation = InLocation; OutDirection = InDirection; } void UEBBarrel::ApplyRecoil_Implementation(UPrimitiveComponent* Component, FVector InLocation, FVector Impulse){ if (Component->IsSimulatingPhysics()) { Component->AddImpulseAtLocation(Impulse, InLocation); } } // Enhanced Debug Control Functions void UEBBarrel::SetBulletDebugCategory(const FString& CategoryName, bool bEnabled) { // Find all bullets fired from this barrel and update their debug settings UWorld* World = GetWorld(); if (!World) { return; } for (TActorIterator ActorItr(World); ActorItr; ++ActorItr) { AEBBullet* Bullet = *ActorItr; if (Bullet && Bullet->GetFiringBarrel() == this) { Bullet->ToggleDebugCategory(CategoryName, bEnabled); } } } void UEBBarrel::SetAllBulletDebugCategories(bool bEnabled) { // Update all debug categories for bullets from this barrel SetBulletDebugCategory(TEXT("Trajectory"), bEnabled); SetBulletDebugCategory(TEXT("Impact"), bEnabled); SetBulletDebugCategory(TEXT("Physics"), bEnabled); SetBulletDebugCategory(TEXT("Performance"), bEnabled); SetBulletDebugCategory(TEXT("Ballistics"), bEnabled); SetBulletDebugCategory(TEXT("Spalling"), bEnabled); SetBulletDebugCategory(TEXT("Pooling"), bEnabled); } void UEBBarrel::ClearAllBulletDebugDisplay() { // Clear debug display for all bullets from this barrel UWorld* World = GetWorld(); if (!World) { return; } for (TActorIterator ActorItr(World); ActorItr; ++ActorItr) { AEBBullet* Bullet = *ActorItr; if (Bullet && Bullet->GetFiringBarrel() == this) { Bullet->ClearDebugDisplay(); } } } FString UEBBarrel::GetBulletDebugInfo() const { // Collect debug information from all bullets fired from this barrel UWorld* World = GetWorld(); if (!World) { return TEXT("No world available"); } int32 BulletCount = 0; int32 DebugEnabledCount = 0; FString DebugInfo = TEXT("Barrel Debug Info:\n"); for (TActorIterator ActorItr(World); ActorItr; ++ActorItr) { AEBBullet* Bullet = *ActorItr; if (Bullet && Bullet->GetFiringBarrel() == this) { BulletCount++; if (Bullet->DebugEnabled) { DebugEnabledCount++; } } } DebugInfo += FString::Printf(TEXT("Total Bullets: %d\n"), BulletCount); DebugInfo += FString::Printf(TEXT("Debug Enabled: %d\n"), DebugEnabledCount); DebugInfo += FString::Printf(TEXT("Impact Debug: %s\n"), DebugImpactInfo ? TEXT("ON") : TEXT("OFF")); return DebugInfo; } void UEBBarrel::SetParentGun(AEBGun* Gun) { ParentGun = Gun; } void UEBBarrel::ApplyBarrelConfiguration(const FBarrelConfiguration& BarrelConfig) { // Apply barrel physical properties BarrelLength = BarrelConfig.BarrelLength; RiflingTwist = BarrelConfig.RiflingTwist; BoreRadius = BarrelConfig.BoreRadius; MuzzleVelocityMin = BarrelConfig.MuzzleVelocityMin; MuzzleVelocityMax = BarrelConfig.MuzzleVelocityMax; // Apply ballistic properties Spread = BarrelConfig.SpreadMax; SpreadBias = BarrelConfig.SpreadBias; MuzzleVelocityMultiplierMin = 1.0f - BarrelConfig.MuzzleVelocityVariation; MuzzleVelocityMultiplierMax = 1.0f + BarrelConfig.MuzzleVelocityVariation; // Apply recoil properties RecoilMultiplier = BarrelConfig.RecoilMultiplier; UE_LOG(LogTemp, Log, TEXT("Barrel Configuration Applied - Length: %.1f, Twist: 1:%.1f, Velocity: %.0f-%.0f cm/s"), BarrelLength, RiflingTwist, MuzzleVelocityMin, MuzzleVelocityMax); } UEBMagazine* UEBBarrel::GetConnectedMagazine() const { if (ParentGun) { return ParentGun->Magazine; } return nullptr; } UEBBulletPropertiesAsset* UEBBarrel::GetChamberedBulletType() const { if (ParentGun) { return ParentGun->GetChamberedBulletType(); } return nullptr; } float UEBBarrel::CalculateEffectiveMuzzleVelocity(UEBBulletPropertiesAsset* BulletType) const { if (!BulletType) { return MuzzleVelocityMin; } // Base velocity from barrel configuration float BaseVelocity = FMath::RandRange(MuzzleVelocityMin, MuzzleVelocityMax); // Apply velocity multiplier variation float VelocityMultiplier = FMath::RandRange(MuzzleVelocityMultiplierMin, MuzzleVelocityMultiplierMax); BaseVelocity *= VelocityMultiplier; // Barrel length affects velocity (longer barrels = higher velocity, up to a point) float LengthFactor = FMath::Clamp((BarrelLength - 10.0f) / 20.0f, 0.8f, 1.2f); BaseVelocity *= LengthFactor; // Bullet weight affects velocity (heavier bullets = lower velocity) float BulletMass = BulletType->BulletProperties.GetMassKg(); float MassFactor = FMath::Clamp(0.004f / BulletMass, 0.7f, 1.3f); // Normalized around 4g bullet BaseVelocity *= MassFactor; return BaseVelocity; } float UEBBarrel::CalculateBarrelAccuracy() const { // Base accuracy from spread float BaseAccuracy = Spread; // Barrel length improves accuracy float LengthFactor = FMath::Clamp(BarrelLength / 20.0f, 0.5f, 1.5f); BaseAccuracy /= LengthFactor; // Rifling twist affects accuracy (optimal twist varies by bullet) // For now, assume 1:7 to 1:9 is optimal for 5.56mm float TwistOptimal = 8.0f; float TwistFactor = 1.0f - FMath::Abs(RiflingTwist - TwistOptimal) * 0.01f; TwistFactor = FMath::Clamp(TwistFactor, 0.8f, 1.2f); BaseAccuracy /= TwistFactor; return BaseAccuracy; } FVector UEBBarrel::CalculateRecoilImpulse(UEBBulletPropertiesAsset* BulletType) const { if (!BulletType) { return FVector::ZeroVector; } // Calculate recoil based on bullet momentum float BulletMass = BulletType->BulletProperties.GetMassKg(); float MuzzleVelocity = CalculateEffectiveMuzzleVelocity(BulletType); float Momentum = BulletMass * (MuzzleVelocity / 100.0f); // Convert cm/s to m/s // Base recoil impulse float RecoilForce = Momentum * RecoilMultiplier; // Barrel length affects recoil (longer barrels spread recoil over more time, but same total impulse) float LengthFactor = FMath::Clamp(20.0f / BarrelLength, 0.8f, 1.5f); RecoilForce *= LengthFactor; // Direction from weapon config (if parent gun has config) FVector RecoilDirection = FVector(0, 0, 1); // Default upward if (ParentGun && ParentGun->WeaponConfig) { RecoilDirection = ParentGun->WeaponConfig->BarrelConfig.RecoilDirection; } return RecoilDirection * RecoilForce; } int UEBBarrel::GetMagazineRoundCount() const { UEBMagazine* Magazine = GetConnectedMagazine(); if (Magazine) { return Magazine->GetCurrentRoundCount(); } return 0; } bool UEBBarrel::HasChamberedRound() const { if (ParentGun) { return ParentGun->IsLoaded(); } return false; } bool UEBBarrel::CanFireFromGun() const { if (ParentGun) { return ParentGun->CanFire(); } return false; }