Forum begins after the advertisement:
[Part 16.5] How to add sound effects to your weapons
Home › Forums › Video Game Tutorial Series › Creating a Rogue-like Shoot-em Up in Unity › [Part 16.5] How to add sound effects to your weapons
- This topic has 5 replies, 2 voices, and was last updated 2 months, 2 weeks ago by kyle.
-
AuthorPosts
-
June 30, 2024 at 11:33 pm #15156::
I got a question on YouTube asking about how you can go about adding sound effects to your weapons in the new weapon system in Part 16, so here’s how you do it.
1. Add sound effects to the prefab
To add sound effects onto your weapons, you simply need to find the appropriate prefab(s) that are related to your weapons, and:
- Add an Audio Source component to it
- Import the sound into your project, and assign the imported sound (i.e. Audio Clip) to the Audio Source
- Make sure Play on Awake is checked
- Make sure that Spatial Blend is set to 3D, since the sound is localised (i.e. the further you are from the sound, the softer it should be).
- If you want the effect to keep repeating as long as the prefab exists, check Looping
For example, if you want to add a sound effect to the Lightning Ring weapon, you would find the lightning strike VFX that plays whenever the weapon strikes something, and follow the above steps. This will give you something like this:
In the video, I also go through some optimal practices that you want to pay attention to when importing sounds into your project.
Do note that you don’t necessarily have to add the sounds to just the VFXs. Sounds can also be added to your weapon projectiles, or any prefab in the project where you want to add sounds to.
2. Adding background music
If you want to add background music into your game, simply add an Audio Source to any GameObject in your Scene that is persistent (i.e. it will never get destroyed), assign an Audio Clip, and make sure you check Looping. The Spatial Blend has to be 2D as well, or the sound will attenuate (i.e. get softer the further away it is).
3. Sound attenuation and Audio Listener
The reason you have to attach Audio Sources to GameObjects to play sounds is because every sound in Unity needs to be located somewhere. If you set the Spatial Blend on the Audio Source to any non-2D value, the sound will attenuate, which means it will get softer over distance.
Where does it measure the distance with respect to? If you look at the Main Camera in your Scene, you will see an Audio Listener component. How far away an Audio Source is from the Audio Listener will determine how soft the sound is (if it is a non-2D sound).
4. Hit effects on your
WeaponData
Because we are attaching sounds to VFX objects in our projects, remember that in your weapon data files, you are able to assign a Hit Effect to each of your weapons. These allow you to assign a Particle System GameObject that spawns whenever the weapon hits something. Make full use of these to add more sound effects to your weapons.
The Hit Effect currently doesn’t work with Aura weapons though, so let’s fix that by adding this line to the
Aura.cs
script:Aura.cs
using System.Collections.Generic; using UnityEngine;
/// <summary> /// An aura is a damage-over-time effect that applies to a specific area in timed intervals. /// It is used to give the functionality of Garlic, and it can also be used to spawn holy /// water effects as well. /// </summary> public class Aura : WeaponEffect {
Dictionary<EnemyStats, float> affectedTargets = new Dictionary<EnemyStats, float>(); List<EnemyStats> targetsToUnaffect = new List<EnemyStats>(); // Update is called once per frame void Update() { Dictionary<EnemyStats, float> affectedTargsCopy = new Dictionary<EnemyStats, float>(affectedTargets); // Loop through every target affected by the aura, and reduce the cooldown // of the aura for it. If the cooldown reaches 0, deal damage to it. foreach (KeyValuePair<EnemyStats, float> pair in affectedTargsCopy) { affectedTargets[pair.Key] -= Time.deltaTime; if (pair.Value <= 0) { if (targetsToUnaffect.Contains(pair.Key)) { // If the target is marked for removal, remove it. affectedTargets.Remove(pair.Key); targetsToUnaffect.Remove(pair.Key); } else { // Reset the cooldown and deal damage. Weapon.Stats stats = weapon.GetStats(); affectedTargets[pair.Key] = stats.cooldown * Owner.Stats.cooldown; pair.Key.TakeDamage(GetDamage(), transform.position, stats.knockback); <mark class="green">// Play the hit effect if it is assigned. if(stats.hitEffect) { Destroy(Instantiate(stats.hitEffect, pair.Key.transform.position, Quaternion.identity), 5f); }</mark> } } } } void OnTriggerEnter2D(Collider2D other) { if (other.TryGetComponent(out EnemyStats es)) { // If the target is not yet affected by this aura, add it // to our list of affected targets. if (!affectedTargets.ContainsKey(es)) { // Always starts with an interval of 0, so that it will get // damaged in the next Update() tick. affectedTargets.Add(es, 0); } else { if (targetsToUnaffect.Contains(es)) { targetsToUnaffect.Remove(es); } } } } void OnTriggerExit2D(Collider2D other) { if (other.TryGetComponent(out EnemyStats es)) { // Do not directly remove the target upon leaving, // because we still have to track their cooldowns. if (affectedTargets.ContainsKey(es)) { targetsToUnaffect.Add(es); } } }
}
June 30, 2024 at 11:48 pm #15157::5. Adding a proc effect
If you want to make your game feel satisfying to play, having appropriate sound and visual effects are extremely important. This is because sound and visuals give your game a lot of flavour — just think of how much less satisfying it was during the times when you had to play your favourite game without sound.
Or think of your favourite games without any visual effects.
Alternatively, if you feel that the game you are currently creating is missing some oomph, the problem may not lie in your game design or game mechanics. Rather, you may just need to add some sound or visual effects.
To this end, we also want to add a Proc Effect field to our weapon data (look at the variable under Hit Effect in the above image). The Proc Effect allows us to attach an effect to the player character whenever the weapon activates.
To add the proc effect, we need to modify the
Weapon.Stats
class inWeapon.cs
:Weapon.cs
public abstract class Weapon : Item { [System.Serializable] public class Stats : LevelData {
[Header("Visuals")] public Projectile projectilePrefab; // If attached, a projectile will spawn every time the weapon cools down. public Aura auraPrefab; // If attached, an aura will spawn when weapon is equipped. public ParticleSystem hitEffect<mark class="green">, procEffect</mark>; public Rect spawnVariance; [Header("Values")] public float lifespan; // If 0, it will last forever. public float damage, damageVariance, area, speed, cooldown, projectileInterval, knockback; public int number, piercing, maxInstances; // Allows us to use the + operator to add 2 Stats together. // Very important later when we want to increase our weapon stats. public static Stats operator +(Stats s1, Stats s2) { Stats result = new Stats(); result.name = s2.name ?? s1.name; result.description = s2.description ?? s1.description; result.projectilePrefab = s2.projectilePrefab ?? s1.projectilePrefab; result.auraPrefab = s2.auraPrefab ?? s1.auraPrefab; result.hitEffect = s2.hitEffect == null ? s1.hitEffect : s2.hitEffect; <mark class="green">result.procEffect = s2.procEffect == null ? s1.procEffect : s2.procEffect;</mark> result.spawnVariance = s2.spawnVariance; result.lifespan = s1.lifespan + s2.lifespan; result.damage = s1.damage + s2.damage; result.damageVariance = s1.damageVariance + s2.damageVariance; result.area = s1.area + s2.area; result.speed = s1.speed + s2.speed; result.cooldown = s1.cooldown + s2.cooldown; result.number = s1.number + s2.number; result.piercing = s1.piercing + s2.piercing; result.projectileInterval = s1.projectileInterval + s2.projectileInterval; result.knockback = s1.knockback + s2.knockback; return result; } // Get damage dealt. public float GetDamage() { return damage + Random.Range(0, damageVariance); } } ...
And we also need to update all of the scripts that are
ProjectileWeapon
or subclass its attack function. Currently, since weapons that are notProjectileWeapon
do not proc, we don’t have to worry about classes aboveProjectileWeapon
:ProjectileWeapon.cs
public class ProjectileWeapon : Weapon {
... protected override bool Attack(int attackCount = 1) { // If no projectile prefab is assigned, leave a warning message. if (!currentStats.projectilePrefab) { Debug.LogWarning(string.Format("Projectile prefab has not been set for {0}", name)); ActivateCooldown(true); return false; } // Can we attack? if (!CanAttack()) return false; // Otherwise, calculate the angle and offset of our spawned projectile. float spawnAngle = GetSpawnAngle();
// If there is a proc effect, play it on the player.
if(currentStats.procEffect)
{
Destroy(Instantiate(currentStats.procEffect, owner.transform), 5f);
}// And spawn a copy of the projectile. Projectile prefab = Instantiate( currentStats.projectilePrefab, owner.transform.position + (Vector3)GetSpawnOffset(spawnAngle), Quaternion.Euler(0, 0, spawnAngle) ); prefab.weapon = this; prefab.owner = owner; ActivateCooldown(true); attackCount--; // Do we perform another attack? if (attackCount > 0) { currentAttackCount = attackCount; currentAttackInterval = ((WeaponData)data).baseStats.projectileInterval; } return true; } ...
}
Note that any weapon that subclasses
ProjectileWeapon
and overrides itsAttack()
function needs to add the highlighted line above to its ownAttack()
function. If you have followed my tutorial series completely, the scripts where you need to add them areLightningRingWeapon
andWhipWeapon
. Make sure you only add them AFTER the cooldown check, so they only fire if the weapon procs successfully.LightningRingWeapon.cs
// Damage does not scale with Might stat currently. public class LightningRingWeapon : ProjectileWeapon {
... protected override bool Attack(int attackCount = 1) { // If no projectile prefab is assigned, leave a warning message. if (!currentStats.hitEffect) { Debug.LogWarning(string.Format("Hit effect prefab has not been set for {0}", name)); ActivateCooldown(true); return false; } // If there is no projectile assigned, set the weapon on cooldown. if (!CanAttack()) return false; // If the cooldown is less than 0, this is the first firing of the weapon. // Refresh the array of selected enemies. if (currentCooldown <= 0) { allSelectedEnemies = new List<EnemyStats>(FindObjectsOfType<EnemyStats>()); ActivateCooldown(); currentAttackCount = attackCount; } // Find an enemy in the map to strike with lightning. EnemyStats target = PickEnemy(); if (target) { DamageArea(target.transform.position, GetArea(), GetDamage()); Instantiate(currentStats.hitEffect, target.transform.position, Quaternion.identity); }
// If there is a proc effect, play it on the player.
if (currentStats.procEffect)
{
Destroy(Instantiate(currentStats.procEffect, owner.transform), 5f);
}// If we have more than 1 attack count. if (attackCount > 0) { currentAttackCount = attackCount - 1; currentAttackInterval = currentStats.projectileInterval; } return true; } ...
}
WhipWeapon.cs
public class WhipWeapon : ProjectileWeapon { ... protected override bool Attack(int attackCount = 1) { // If no projectile prefab is assigned, leave a warning message. if (!currentStats.projectilePrefab) { Debug.LogWarning(string.Format("Projectile prefab has not been set for {0}", name)); ActivateCooldown(true); return false; }
// If there is no projectile assigned, set the weapon on cooldown. if (!CanAttack()) return false; // If this is the first time the attack has been fired, // we reset the currentSpawnCount. if (currentCooldown <= 0) { currentSpawnCount = 0; currentSpawnYOffset = 0f; } // Otherwise, calculate the angle and offset of our spawned projectile. // Then, if <currentSpawnCount> is even (i.e. more than 1 projectile), // we flip the direction of the spawn. float spawnDir = Mathf.Sign(movement.lastMovedVector.x) * (currentSpawnCount % 2 != 0 ? -1 : 1); Vector2 spawnOffset = new Vector2( spawnDir * Random.Range(currentStats.spawnVariance.xMin, currentStats.spawnVariance.xMax), currentSpawnYOffset );
// If there is a proc effect, play it on the player.
if (currentStats.procEffect)
{
Destroy(Instantiate(currentStats.procEffect, owner.transform), 5f);
}// And spawn a copy of the projectile. Projectile prefab = Instantiate( currentStats.projectilePrefab, owner.transform.position + (Vector3)spawnOffset, Quaternion.identity ); prefab.owner = owner; // Set ourselves to be the owner. // Flip the projectile's sprite. if(spawnDir < 0) { prefab.transform.localScale = new Vector3( -Mathf.Abs(prefab.transform.localScale.x), prefab.transform.localScale.y, prefab.transform.localScale.z ); } // Assign the stats. prefab.weapon = this; ActivateCooldown(true); attackCount--; // Determine where the next projectile should spawn. currentSpawnCount++; if (currentSpawnCount > 1 && currentSpawnCount % 2 == 0) currentSpawnYOffset += 1; // Do we perform another attack? if (attackCount > 0) { currentAttackCount = attackCount; currentAttackInterval = ((WeaponData)data).baseStats.projectileInterval; } return true; }
}
June 30, 2024 at 11:51 pm #15158::6. Fixing an
AuraWeapon
level up bugFinally, there is also a small issue with the
AuraWeapon.cs
script. If you change the Aura Prefab of anyAuraWeapon
that you have on level up (e.g. let’s say you make the Garlic turn into the Soul Eater aura at Max Level without evolution), the Aura will actually not update. This is because we do not callOnEquip()
to update the aura in theDoLevelUp()
function.Hence, simply add that to your
AuraWeapon
script and this bug will be fixed.AuraWeapon.cs
using UnityEngine;
public class AuraWeapon : Weapon {
protected Aura currentAura; // Update is called once per frame protected override void Update() { } public override void OnEquip() { // Try to replace the aura the weapon has with a new one. if (currentStats.auraPrefab) { if (currentAura) Destroy(currentAura); currentAura = Instantiate(currentStats.auraPrefab, transform); currentAura.weapon = this; currentAura.owner = owner; float area = GetArea(); currentAura.transform.localScale = new Vector3(area, area, area); } } public override void OnUnequip() { if (currentAura) Destroy(currentAura); } public override bool DoLevelUp() { if (!base.DoLevelUp()) return false;
// Ensure that the aura is refreshed if a different aura is assigned for a higher level.
OnEquip();// If there is an aura attached to this weapon, we update the aura. if (currentAura) { currentAura.transform.localScale = new Vector3(currentStats.area, currentStats.area, currentStats.area); } return true; }
}
June 30, 2024 at 11:56 pm #15159::Here is the stream video for reference. The contents above are covered in the 1st hour of the stream. We are currently editing the video to make it a lot shorter and easier to watch.
September 18, 2024 at 9:27 am #15844::7. Fixing
AuraWeapon
not destroying previous AurasIf you’ve implemented the code to change the Aura Prefab on level up, you will notice that if you assign a new Aura Prefab on the next level, the previous level’s Aura remains and the Auras stack. This is due to the
Destroy(currentAura)
function not destroying the Game Object that the particle system is attached to.This is fixed by getting the
currentAura
‘s Game Object in theDestroy()
function.AuraWeapon.cs
using UnityEngine;
public class AuraWeapon : Weapon {
protected Aura currentAura; // Update is called once per frame protected override void Update() { } public override void OnEquip() { // Try to replace the aura the weapon has with a new one. if (currentStats.auraPrefab) { <mark class="green">if (currentAura) Destroy(currentAura.gameObject);</mark> currentAura = Instantiate(currentStats.auraPrefab, transform); currentAura.weapon = this; currentAura.owner = owner; float area = GetArea(); currentAura.transform.localScale = new Vector3(area, area, area); } } public override void OnUnequip() { if (currentAura) Destroy(currentAura); } public override bool DoLevelUp() { if (!base.DoLevelUp()) return false; // Ensure that the aura is refreshed if a different aura is assigned for a higher level. OnEquip(); // If there is an aura attached to this weapon, we update the aura. if (currentAura) { currentAura.transform.localScale = new Vector3(currentStats.area, currentStats.area, currentStats.area); } return true; }
}
- 1 anonymous person
September 18, 2024 at 11:07 am #15846::8. Fixing ArgumentNullException when an enemy inside an Aura is killed by another weapon
When an enemy inside an Aura is killed by another weapon, an ArgumentNullException occurs. This happens in
Aura
‘sUpdate()
function as the script is trying to get theEnemyStats
Key of an enemy which has already been destroyed, such as ataffectedTargets[pair.Key] -= Time.deltaTime;
. Since the Key no longer exists (i.e. Key isnull
), it is unable to retrieve it fromaffectedTargets
and throws the error.To fix this, we need to perform a check that the current
EnemyStats
Key in the foreach loop still exists in-game before trying to access it, and if it’s null we remove it from bothtargetsToUnaffect
andaffectedTargets
.The updated
Update()
function of theAura
script is shown below.void Update() { Dictionary affectedTargsCopy = new Dictionary(affectedTargets);
// Loop through every target affected by the aura, and reduce the cooldown // of the aura for it. If the cooldown reaches 0, deal damage to it. foreach (KeyValuePair pair in affectedTargsCopy) {
// It's possible for enemies in our list to get killed by other weapons.
// If they do, pair.Key will become null, so we need to remove it.
if (!pair.Key)
{
targetsToUnaffect.Remove(pair.Key);
affectedTargets.Remove(pair.Key);
continue;
}
affectedTargets[pair.Key] -= Time.deltaTime;
if (pair.Value <= 0)
{
if (targetsToUnaffect.Contains(pair.Key))
{
// If the target is marked for removal, remove it.
affectedTargets.Remove(pair.Key);
targetsToUnaffect.Remove(pair.Key);
}
else
{
// Reset the cooldown and deal damage.
Weapon.Stats stats = weapon.GetStats();
affectedTargets[pair.Key] = stats.cooldown * weapon.Owner.Stats.cooldown;
pair.Key.TakeDamage(GetDamage(), transform.position, stats.knockback);// Play the hit effect if it is assigned. if (stats.hitEffect) { Destroy(Instantiate(stats.hitEffect, pair.Key.transform.position, Quaternion.identity), 5f); } } } }
}
Thanks to Anthony for bringing these 2 bugs up!- 1 anonymous person
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: