My approach (so far) on sound effects

First off I have to say I love what’s posted here. Really nice elegant solution! My thought immediately went to Scriptable Objects and the Serializable Dictionary but I opted to not do that for my v1.

In my v1 (below) I wanted to try without it first to get some of the basics worked out first and see what issues arise.

One issue I seem to have run into is that when multiple sound effects are playing simultaneously some sounds effects don’t play at all. When I turn off the foot steps, I notice the action sequences sound so much more like what I expect which gives me a hint that there’s an issue when greater than some threshold of sound effects are playing.

Anyone know if you have to do anything special in Unity when you have multiple AudioSources? Each instance of this class has three AudioSources and there is one instance per character so there easily could be 20-30 AudioSources in my basic test scene.

using RPG.Attributes;
using RPG.Combat;
using RPG.Movement;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Audio
{
    [RequireComponent(typeof(Health))]
    [RequireComponent(typeof(Fighter))]
    [RequireComponent(typeof(Mover))]
    public class CharacterAudioManager : MonoBehaviour
    {
        [SerializeField] private AudioSource mouthAudioSource;
        [SerializeField] private AudioSource bodyAudioSource;
        [SerializeField] private AudioSource feetAudioSource;

        [SerializeField] private AudioClip[] tookDamageAudioClips;
        [SerializeField] private AudioClip[] armorStruckAudioClips;
        [SerializeField] private AudioClip[] heavyEffortAudioClips;
        [SerializeField] private AudioClip[] footstepAudioClips;
        [SerializeField] private AudioClip[] deathAudioClips;

        private Health health;
        private Fighter fighter;
        private Mover mover;

        //TODO - need some null checks and error handling

        private void Awake()
        {
            health = GetComponent<Health>();
            fighter = GetComponent<Fighter>();
            mover = GetComponent<Mover>();
        }

        private void OnEnable()
        {
            health.CharacterDied += Health_CharacterDied;
            health.CharacterTookDamage += Health_CharacterTookDamage;
            mover.TookFootStep += Mover_TookFootStep;
            fighter.Attacked += Fighter_Attacked;
                        
            //future use e.g.
            //armorComponent.ArmorStruck += ArmorComponent_ArmorStruck;
            
        }

        private void OnDisable()
        {
            health.CharacterDied -= Health_CharacterDied;
            health.CharacterTookDamage -= Health_CharacterTookDamage;
            mover.TookFootStep -= Mover_TookFootStep;
            fighter.Attacked -= Fighter_Attacked;

            //future use e.g.
            //armorComponent.ArmorStruck -= ArmorComponent_ArmorStruck;
        }

        private void Health_CharacterDied()
        {
            mouthAudioSource.PlayOneShot(GetRandomClip(SoundContext.Death));
        }

        private void Health_CharacterTookDamage(float damage)
        {
            mouthAudioSource.PlayOneShot(GetRandomClip(SoundContext.TakeDamage));
        }

        private void Mover_TookFootStep(float speed)
        {
            //TODO: Sound volume should be scaled by speed of movement
            //TODO: Should probably also not try to play sounds of too far away characters
            // since it seems to mess with other sounds.
            if (speed > 2f) { 
                feetAudioSource.PlayOneShot(GetRandomClip(SoundContext.FootStep));
            }
        }

        private void Fighter_Attacked()
        {
            mouthAudioSource.PlayOneShot(GetRandomClip(SoundContext.HeavyEffort));
        }


        private AudioClip GetRandomClip(SoundContext context)
        {
            AudioClip clipToReturn = null;
            int randomIndex;

            switch (context)
            {
                case SoundContext.TakeDamage:
                    randomIndex = Random.Range(0, tookDamageAudioClips.Length);
                    clipToReturn = tookDamageAudioClips[randomIndex];
                    break;
                case SoundContext.ArmorStruck:
                    randomIndex = Random.Range(0, armorStruckAudioClips.Length);
                    clipToReturn = armorStruckAudioClips[randomIndex];
                    break;
                case SoundContext.HeavyEffort:
                    randomIndex = Random.Range(0, heavyEffortAudioClips.Length);
                    clipToReturn = heavyEffortAudioClips[randomIndex];
                    break;
                case SoundContext.FootStep:
                    randomIndex = Random.Range(0, footstepAudioClips.Length);
                    clipToReturn = footstepAudioClips[randomIndex];
                    break;
                case SoundContext.Death:
                    randomIndex = Random.Range(0, deathAudioClips.Length);
                    clipToReturn = deathAudioClips[randomIndex];
                    break;
                default:
                    Debug.LogWarning("Something went wrong!");
                    break;
            }

            return clipToReturn;
        }

        [System.Serializable]
        public struct AudioMapping
        {
            public SoundContext context;
            public AudioClip[] audioClips;
        }

        public enum SoundContext
        {
            TakeDamage,
            HeavyEffort,
            ArmorStruck,
            FootStep,
            Death
        }
    }
}

One other thing I’m trying to figure out is how to delay sound effects until the scene has fully loaded. Because the foot steps happen as soon as the enemies are in their patrol loops, you get a cacophony of sounds right at the start.

We’re loading from either the Portal or from a game save or from initial game launch. I’m trying to think of a non-hacky way to notify my sound system to delay playing anything initially. I imagine once we get to loading screens this might be covered. I’m imagining playing some sort of loading music while everything is loading in the background, then playing some post-loading sound effect overruling all other sounds, then slowly dialing up the volume on every other effect that wants to play.

To get it right, it feels like it needs some sort of coordination across multiple components.

Below I’ll share my code for projectiles. I’m using events again. It is more work now, but enables some stuff in the future I want to do. And it was good practice.

Working on this it is also now super clear that ScriptableObjects and SerializeableDictionary are the way to go with regard to creating a mapping and library of effects. It was a useful exercise for me to try to do both *AudioManager scripts without an SO to truly grasp how useful SO’s are.

using RPG.Audio;
using RPG.Combat;
using UnityEngine;

[RequireComponent(typeof(Projectile))]
public class ProjectileAudioManager : MonoBehaviour
{
    [SerializeField] private AudioSource projectileAudioSource;

    [SerializeField] private AudioClip[] fireballLaunchedClips;
    [SerializeField] private AudioClip[] arrowLaunchedClips;
    [SerializeField] private AudioClip[] fireballLandedClips;
    [SerializeField] private AudioClip[] arrowLandedClips;

    private Projectile projectile;

    //TODO: this needs to be redone with scriptable objects
    //and probably a Serializeable Dictionary

    private void Awake()
    {
        projectile = GetComponent<Projectile>();
    }

    private void OnEnable()
    {
        projectile.ProjectileLaunched += Projectile_ProjectileLaunched;
        projectile.ProjectileLanded += Projectile_ProjectileLanded;
    }

    private void OnDisable()
    {
        projectile.ProjectileLaunched -= Projectile_ProjectileLaunched;
        projectile.ProjectileLanded -= Projectile_ProjectileLanded;
    }

    private void Projectile_ProjectileLaunched(object sender,
                                               ProjectileEventArgs e)
    {
        switch (e.type)
        {
            case ProjectileType.Fireball:
                projectileAudioSource.PlayOneShot(
                             GetRandomClip(SoundContext.FireballLaunched));
                break;
            case ProjectileType.Arrow:
                projectileAudioSource.PlayOneShot(
                             GetRandomClip(SoundContext.ArrowLaunched));
                break;
            default:
                Debug.LogWarning("Something went wrong!");
                break;
        }
    }

    private void Projectile_ProjectileLanded(object sender,
                                             ProjectileEventArgs e)
    {
        switch (e.type)
        {
            case ProjectileType.Fireball:
                projectileAudioSource.PlayOneShot(
                         GetRandomClip(SoundContext.FieryExplosion));
                break;
            case ProjectileType.Arrow:
                projectileAudioSource.PlayOneShot(
                        GetRandomClip(SoundContext.ArrowLanded));
                break;
            default:
                Debug.LogWarning("Something went wrong!");
                break;
        }
    }

    private AudioClip GetRandomClip(SoundContext context)
    {
        AudioClip clipToReturn = null;
        int randomIndex;

        switch (context)
        {
            case SoundContext.FireballLaunched:
                randomIndex = Random.Range(0, fireballLaunchedClips.Length);
                clipToReturn = fireballLaunchedClips[randomIndex];
                break;
            case SoundContext.ArrowLaunched:
                randomIndex = Random.Range(0, arrowLaunchedClips.Length);
                clipToReturn = arrowLaunchedClips[randomIndex];
                break;
            case SoundContext.FieryExplosion:
                randomIndex = Random.Range(0, fireballLandedClips.Length);
                clipToReturn = fireballLandedClips[randomIndex];
                break;
            case SoundContext.ArrowLanded:
                randomIndex = Random.Range(0, arrowLandedClips.Length);
                clipToReturn = arrowLandedClips[randomIndex];
                break;
            default:
                Debug.LogWarning("Something went wrong!");
                break;
        }

        return clipToReturn;
    }
}

Privacy & Terms