Modifying ShooterGame-based code for Salvo/Volley of Projectiles? (Help with Timers)

For my next endeavour, I’m attempting to make a weapon (heavily based on the ShooterGame Weapon code) that won’t just fire one projectile, but per-click will fire a volley of projectiles (Salvo) at a given interval (SalvoDelay). I’ve already added a system for randomizing the trajectory of the shot slightly (ShotVariance), but I’m experiencing three issues with the current system.

The first problem that I can’t understand at all, is that whenever SalvoCount is equal to 2, the weapon fires no projectiles. Anything higher and it seems to fire all of them, but for some reason firing two only plays the effects, it doesn’t spawn the projectiles.

The second issue is that the spawned projectiles as part of a salvo do not all have random ShotVariances each, even though the ‘GetAdjustedAim()’ function is called every loop. Are the functions not called repeatedly for each loop?

The thirdissue is, I can’t find a way to actually integrate the SalvoDelay system. I want it to fire a volley of shots per-click at a given interval, and once finished, wait for the ‘ShotDelay’ timer to finish before it can fire again. In some circumstances, there won’t be enough ammo to complete a full volley, at which point both timers need to re-set. Ideally, I want this to be part of the ‘FireWeapon’ PURE_VIRTUAL function, so that sub-classes of BZGame_Weapon.h can override it. A specific FireSalvo() function as part of that sub-class could work fine also! I’m just not sure how to integrate the timer system.

GetAdjustedAim() Function (With Shot Variance Modification)



FVector ABZGame_Weapon::GetAdjustedAim() const
{
	//FVector SpawnRot = FVector(ShootDir.X + FMath::FRandRange(WeaponConfig.ShotVariance, -WeaponConfig.ShotVariance) + WeaponConfig.ShotPitch, ShootDir.Y + FMath::FRandRange(WeaponConfig.ShotVariance, -WeaponConfig.ShotVariance), ShootDir.Z);

	ABZGame_PlayerController* const PlayerController = Instigator ? Cast<ABZGame_PlayerController>(Instigator->Controller) : NULL;
	FRotator AimOffset = FRotator::ZeroRotator;
	FVector FinalAim = FVector::ZeroVector;

	// If we have a player controller use it for the aim
	if (PlayerController)
	{
		FVector CamLoc;
		FRotator CamRot;
		PlayerController->GetPlayerViewPoint(CamLoc, CamRot);
		AimOffset = CamRot;
	}
	else if (Instigator)
	{
		AimOffset = Instigator->GetBaseAimRotation();
	}

	AimOffset = FRotator(AimOffset.Pitch + FMath::FRandRange(WeaponConfig.ShotVariance, -WeaponConfig.ShotVariance) + WeaponConfig.ShotPitch, AimOffset.Yaw + FMath::FRandRange(WeaponConfig.ShotVariance, -WeaponConfig.ShotVariance), AimOffset.Roll);

	FinalAim = AimOffset.Vector();

	return FinalAim;
}


Modified HandleFiring() function from ShooterGame



void ABZGame_Weapon::HandleFiring()
{
	/* Check if we have Enough Ammo To Fire */
	float OrdnanceCost = WeaponConfig.OrdnanceClass->GetDefaultObject<ABZGame_Ordnance>()->GetAmmoCost();
	bool bEnoughAmmo = OwningGameObject->GetCurrentAmmo() >= OrdnanceCost;

	if (bEnoughAmmo)
	{
		if (CanFire())
		{
			if (MyPawn && MyPawn->IsLocallyControlled())
			{
				FireWeapon(OrdnanceCost); //Pure_Virtual for sub-class overrides.
			}
		}
		else if (MyPawn && MyPawn->IsLocallyControlled())
		{
			if (OwningGameObject->IsAmmoDepleted() && !bRefiring)
			{
				// Play Weapon Empty Sounds etc.
			}
			if (BurstCounter > 0)
			{
				OnBurstFinished();
			}
		}
		if (MyPawn && MyPawn->IsLocallyControlled())
		{
			//Local Client Will Notify Server
			if (Role < ROLE_Authority)
			{
				ServerHandleFiring();
			}

			/* Setup Refire Timer */
			bRefiring = (CurrentState == EWeaponState::Firing && WeaponConfig.ShotDelay > 0.0f);
			if (bRefiring)
			{
				GetWorldTimerManager().SetTimer(this, &ABZGame_Weapon::HandleFiring, WeaponConfig.ShotDelay, false);
			}
		}

		LastFireTime = GetWorld()->GetTimeSeconds();
	}
}


ABZGame_Cannon.cpp (Sub-class of BZGame_Weapon that is supposed to have the salvo functionality as part of FireWeapon() (or a function called by FireWeapon)



void ABZGame_Cannon::FireWeapon(float OrdnanceCost)
{
	for (int32 i = 0; i < WeaponConfig.SalvoCount; i++)
	{
		if (OwningGameObject->GetCurrentAmmo() < OrdnanceCost)
		{
			break;
		}
		else
		{
			FVector Origin = GetMuzzleLocation();
			FVector ShootDir = GetAdjustedAim();
			ServerFireProjectile(Origin, ShootDir);
			UseAmmo(OrdnanceCost);
		}
	}
}

bool ABZGame_Cannon::ServerFireProjectile_Validate(FVector Origin, FVector_NetQuantizeNormal ShootDir)
{
	return true;
}

void ABZGame_Cannon::ServerFireProjectile_Implementation(FVector Origin, FVector_NetQuantizeNormal ShootDir)
{

	FTransform SpawmTM(ShootDir.Rotation(), Origin);
	ABZGame_Ordnance* Projectile = Cast<ABZGame_Ordnance>(UGameplayStatics::BeginSpawningActorFromClass(this, WeaponConfig.OrdnanceClass, SpawmTM));
	if (Projectile)
	{
		Projectile->Instigator = Instigator;
		Projectile->SetOwner(this);
		//Projectile->InitVelocity(ShootDir);

		UGameplayStatics::FinishSpawningActor(Projectile, SpawmTM);
	}
}


Here’s the WeaponConfig struct where everything is defined:



	/* Time Between Firing Rounds */
	UPROPERTY(EditDefaultsOnly, Category = "Weapon")
	float ShotDelay;
	/* Variance Between Rounds */
	UPROPERTY(EditDefaultsOnly, Category = "Weapon")
	float ShotVariance;
	/* Additional Pitch of Initial Ordnance */
	UPROPERTY(EditDefaultsOnly, Category = "Weapon")
	float ShotPitch;
	/* Amount of shots per firing volley */
	UPROPERTY(EditDefaultsOnly, Category = "Weapon")
	int32 SalvoCount;
	/* Delay BEtween Salvo Rounds */
	UPROPERTY(EditDefaultsOnly, Category = "Weapon")
	float SalvoDelay;


Sorted it, if anybody wants help with this in future, send me a PM :slight_smile: