ToonTanks over-the-shoulder grenades with lots of sounds

I haven’t got the health progress bar widget updating correctly yet, but here’s what I’ve ended up with so far.

I made all the C++ classes available here if anyone wants to poke around, but I’ll explain the differences.

  • Sound cues were used to play random sounds. Each impact/explosion/launcher sound effect has about 3-4 variations each, as well as modulated for slightly random pitch.
    image

  • Attenuation is used for binaural 3d audio spatialization as well as dampening through the air with distance, which is a big reason why it sounds cool I think. This takes a lot tweaking since you don’t have visualizations for audio and frequencies like you do in something like Ableton.
    image

  • With so many sound sources, I had to try and arrange priority through Attenuation as well. Bigger explosions, and things closer to you generally given higher priority. Sometimes sounds randomly don’t play still and I haven’t figured it out yet.

  • Since the grenades roll and bounce on the ground, constant contact while rolling would cause a mess of sound cue spam, so I tried to use game time to set cooldowns to avoid this, as well as choose sounds that included rolling sounds for tails to help fill it out a bit.

/// Play sound effect at location, with a cooldown.
void AProjectileBase::PlaySoundNoSpam(USoundBase* SoundToPlay)
{
	// This is so the hit sound doesn't spam when rolling against the ground.
	WorldTime = UGameplayStatics::GetTimeSeconds(GetWorld());
	float Cooldown = WorldTime - TimeHitSoundPlayed;
	if (Cooldown >= 1) {
		TimeHitSoundPlayed = WorldTime;
		UGameplayStatics::PlaySoundAtLocation(this, SoundToPlay, GetActorLocation());
	}
}
  • Early on I wanted a more over-the-shoulder view, so that required input changes, camera springarm hiearchy changes, and adding a counter-rotation to allow the view and turret to decouple from tank.
// -------------------------------------------------------------------------------------------
/// Rotate turret with mouse (alternative to aiming at cursor).
void APawnTank::RotateView(float Input)
{
	FRotator Rotation = TurretMesh->GetRelativeRotation();
	Rotation.Yaw += Input * MouseSensitivity * GetWorld()->DeltaTimeSeconds;
	TurretMesh->SetRelativeRotation(Rotation);
}

// -------------------------------------------------------------------------------------------
/// Determine rotation speed and direction on y-axis (spinning/yaw). Update RotationDirection.
void APawnTank::RotateTank(float Input)
{
	float Pitch = 0;
	float Yaw = Input * TurnSpeed * GetWorld()->DeltaTimeSeconds;
	float Roll = 0;

	FRotator Rotation = FRotator(Pitch, Yaw, Roll);
	FRotator CounterRotation = FRotator(Pitch, -Yaw, Roll);

	RotationDirection = FQuat(Rotation);
	AddActorLocalRotation(RotationDirection, true);

	// Counter rotate the turret so the view stays the same.
	TurretMesh->AddLocalRotation(CounterRotation, false);
}
  • I wanted to be able to hold down LMB to continuously fire, so I had to setup a little toggle function that’s called by both Pressed and Released.
// -------------------------------------------------------------------------------------------
void APawnTank::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	PlayerInputComponent->BindAxis("MoveForwardAndBack", this, &APawnTank::MoveTank);
	PlayerInputComponent->BindAxis("TurnRightAndLeft", this, &APawnTank::RotateTank);
	PlayerInputComponent->BindAxis("RotateTurret", this, &APawnTank::RotateView);
	PlayerInputComponent->BindAction("Fire", IE_Pressed, this, &APawnTank::FireToggle);
	PlayerInputComponent->BindAction("Fire", IE_Released, this, &APawnTank::FireToggle);
}

// -------------------------------------------------------------------------------------------
/// If IsFiring is set to true then we'll keep shooting as fast as the FireRateTimer wants us to.
void APawnTank::CheckFireCondition()
{
	if (IsFiring && PlayerAlive)
		Fire();
}

// -------------------------------------------------------------------------------------------
/// Toggle IsFiring bool. Only ever called on press and on release from "Fire" input.
void APawnTank::FireToggle()
{
	if (!IsFiring) {
		IsFiring = true;
	}
	else if (IsFiring) {
		IsFiring = false;
	}
}
  • I wanted the grenades to bounce around for awhile if they missed, so I used timers to delay explosions if they didn’t hit any valid targets by the time their life span expires. This also meant I needed to be sure what the grenades were hitting were specifically a turret or the player before firing off all particles, or destruction, or sounds or anything else.
/// When this actor hits another actor. Handles damage, type, etc.
void AProjectileBase::OnHit(UPrimitiveComponent* HitComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp,
	FVector NormalImpulse, const FHitResult& Hit)
{
	AActor* MyOwner = GetOwner();

	if (!MyOwner) {
		return;
	}

	// If we hit another actor:
	if (OtherActor) {
		// We can first check if the other actor is a PawnTurret or PawnTank, and save it as a bool.
		IsTurret = OtherActor->GetClass()->IsChildOf(APawnTurret::StaticClass());
		IsTank = OtherActor->GetClass()->IsChildOf(APawnTank::StaticClass());
	}

	// So if we hit a turret or the player, but not ourselves.
	if (IsTurret || IsTank && OtherActor != GetOwner()) {
		// Play hit particle.
		UGameplayStatics::SpawnEmitterAtLocation(this, HitParticle, GetActorLocation());
		// PLay metal impact sound when hit directly.
		PlaySoundNoSpam(DirectImpactSound);

		// Generate and apply the damage.
		UGameplayStatics::ApplyDamage(
			OtherActor,							// Actor that will be damaged.
			Damage,								// Damage amount.
			MyOwner->GetInstigatorController(),	// Which player instigated it.
			this,								// What actor caused the damage.
			DamageType							// Type of damage done.
			);
		DestroyProjectile();
	}

	// Shake Camera if it's the player hit.
	if (IsTank) {
		GetWorld()->GetFirstPlayerController()->ClientStartCameraShake(HitShake, HitShakeScale);

		DestroyProjectile();
	}

	// Play this sound whenever we bounce off anything.
	PlaySoundNoSpam(ImpactSound);

	// Explode the grenade after ExplosionTimer seconds if we never hit anything.
	GetWorld()->GetTimerManager().SetTimer(
		OUT ExplosionTimerHandle,
		this,
		&AProjectileBase::DestroyProjectile,
		ExplosionTimer,
		false
		);
}
/// Check for actors in radius of explosion and apply impulse.
void AProjectileBase::CreateExplosionImpulse(FVector Location)
{
	// Create basic collision shape!
	FCollisionShape Spherical = FCollisionShape::MakeSphere(ImpulseRadius);

	// Do a sweep check in a radius with SweepMultiChannel().
	bool SweepHit = GetWorld()->SweepMultiByChannel(
		OUT HitResults,						// Our array of results.
		GetActorLocation(),					// Start location.
		GetActorLocation() * 1.01f,			// End location (has to be different than start).
		FQuat::Identity,                    // Rotation (none needed, so blank FQuat).
		ECC_WorldStatic,					// Collision channel.
		Spherical							// Shape.
		);

	if (SweepHit) {
		for (auto& Hit: HitResults) {
			// First, see if the hit actor has a mesh component.
			UStaticMeshComponent* Mesh = Cast<UStaticMeshComponent>(Hit.GetActor()->GetRootComponent());
			// If there is, we'll apply the radial impulse to it.
			if (Mesh) {
				// Grab the mass so we can use more reasonable force numbers.
				float Mass = Mesh->GetMass();

				Mesh->AddRadialImpulse(
					GetActorLocation(),	 // Impulse Location.
					ImpulseRadius,	     // Radius.
					ImpulseForce * Mass, // Force.
					RIF_Constant,	     // RIF (Radial Impact Force) falloff type.
					false
                    );
			}
		}
	}
}
2 Likes

Omg this is what I’ve been trying for months! You mad genius! You did it! How long did it take you to do this? Also where did you get the explosion asset?

2 Likes

Hahaha hell yes. It took me about as long as it did to finish this section. Each step I sometimes went my own direction in certain places, so not long! I just had to give it a shot and try to make it work. Lots of Googling as always.

The explosion particles? It’s already there in the project files! It’s just a slightly modified.

I renamed it after changing it and making two versions, but I think the original is just P_DeathEffect or something like that, in the Assets/Effects folder.

For the pawn death example:

  • On the Spawn details, under the Burst property, I upped the count to 300.

  • Adjusted Lifetime to between 0.2 to 1.0.

  • Changed Initial Size to be 2x on min and max, so they stay spherical.

  • Since I upped the count a lot, under Intial Velocity I upped the velocity a lot, to make sure we even get to see all those additional particles as they spread out:
    image
    Since the explosions were largely happening on the ground, I set Z on minimum to zero so they don’t go underneath the ground and instead send all those 300 pretty particles up ( at least sideways) where we can see it.

  • The Color Over Life and Size By Life is where I did a lot of tweaking to get those pretty blue almost spark-like finish. The In Val I figured out is when the animation moves into that value based on the time the particle is alive. So really you just set each point in time (in seconds I think) where you want that color or size to happen.


    So the orange happens from 0s to 0.8s, red from 0.8s to 1.0s, and the blue at 1.0s and beyond. The particle only lives for 1 second, but since they lerp between, it starts shifting to blue right at the last 0.2s of the particle so you get that pleasant but smooth flicker.
    The size was mostly left the same, where the sphere shrinks to 0.1 scale size (but not 0) so that it’s a tiny blue particle then blinks out of existence, rather than shrinks out of existence (if set to 0). This gave it a sort of spark look at the end I think.

I’ve never played around with the particle system before since all of this is my first time in UE or pretty much any game engine, but once I realized what the arrays were in the settings for each section, it looks almost identical to keyframes in audio and video editing so it’s easy to figure out from there haha.

For the sound, it was just finding nice explosion sounds online, editing them a bit in Ableton Live, then importing them into a Sound Cue, and setting up their Attenuation and stuff like in the OP.

Privacy & Terms