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

Viewing 6 posts - 1 through 6 (of 6 total)
  • Author
    Posts
  • #15156
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    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.

    View post on imgur.com

    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:

    1. Add an Audio Source component to it
    2. Import the sound into your project, and assign the imported sound (i.e. Audio Clip) to the Audio Source
    3. Make sure Play on Awake is checked
    4. 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).
    5. 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:

    View post on imgur.com

    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.

    View post on imgur.com

    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);
    
                        // Play the hit effect if it is assigned.
                        if(stats.hitEffect)
                        {
                            Destroy(Instantiate(stats.hitEffect, pair.Key.transform.position, Quaternion.identity), 5f);
                        }
                    }
                }
            }
        }
    
        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);
                }
            }
        }
    }
    #15157
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    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.

    View post on imgur.com

    To add the proc effect, we need to modify the Weapon.Stats class in Weapon.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, procEffect;
            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;
                result.procEffect = s2.procEffect == null ? s1.procEffect : s2.procEffect;
                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 not ProjectileWeapon do not proc, we don’t have to worry about classes above ProjectileWeapon:

    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 its Attack() function needs to add the highlighted line above to its own Attack() function. If you have followed my tutorial series completely, the scripts where you need to add them are LightningRingWeapon and WhipWeapon. 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;
        }
    }
    #15158
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    6. Fixing an AuraWeapon level up bug

    Finally, there is also a small issue with the AuraWeapon.cs script. If you change the Aura Prefab of any AuraWeapon 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 call OnEquip() to update the aura in the DoLevelUp() 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;
        }
    
    }
    #15159
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    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.

    #15844
    kyle
    Level 7
    Moderator
    Helpful?
    Up
    1
    ::

    7. Fixing AuraWeapon not destroying previous Auras

    If 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 the Destroy() 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)
            {
                if (currentAura) Destroy(currentAura.gameObject);
                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
    has upvoted this post.
    #15846
    kyle
    Level 7
    Moderator
    Helpful?
    Up
    1
    ::

    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‘s Update() function as the script is trying to get the EnemyStats Key of an enemy which has already been destroyed, such as at affectedTargets[pair.Key] -= Time.deltaTime;. Since the Key no longer exists (i.e. Key is null), it is unable to retrieve it from affectedTargets 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 both targetsToUnaffect and affectedTargets.

    The updated Update() function of the Aura 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
    has upvoted this post.
Viewing 6 posts - 1 through 6 (of 6 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: