Play Random Audio help

In this lecture Sam said he will not be covering Random Audio Playing. If you want to learn that go take the 3D Game Kit course on the Unity website. This course is suggested to take 4h and 40mins.
[LINK HERE] 3D Game Kit | Tutorials | Unity Asset Store
So I did that, and they don’t talk about how to add a Random Audio feature at all in that course. lol. So I came back here. Might want to revise the video here or update the content of the course to reflect that information.

So I thought I would try and figure it out on my own. I copied the Random Audio Player script they had in there course. I thought it was quite good, as it allowed you to

  • Randomize Pitch, Have a delay if you wanted, Add in your sounds to randomly play, and then Allow you to add in an override. (I saw they had used the override for when the player character walked on rock floor, grassy field, through mud, ect. They would have an override for each scenario and fill the clips again with footsteps sounds for that particular terrain)

Here is the full code for their Random Audio Player

  • THE only thing I changed was adding the namespace RPG.Audio to the code, when I placed it in our project. Which I mention below.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;


namespace RPG.Audio
{
    [RequireComponent(typeof(AudioSource))]
    public class RandomAudioPlayer : MonoBehaviour
    {
        [Serializable]
        public class MaterialAudioOverride
        {
            public Material[] materials;
            public SoundBank[] banks;
        }

        [Serializable]
        public class SoundBank
        {
            public string name;
            public AudioClip[] clips;
        }

        public bool randomizePitch = true;
        public float pitchRandomRange = 0.2f;
        public float playDelay = 0;
        public SoundBank defaultBank = new SoundBank();
        public MaterialAudioOverride[] overrides;

        [HideInInspector]
        public bool playing;
        [HideInInspector]
        public bool canPlay;

        protected AudioSource m_Audiosource;
        protected Dictionary<Material, SoundBank[]> m_Lookup = new Dictionary<Material, SoundBank[]>();

        public AudioSource audioSource { get { return m_Audiosource; } }

        public AudioClip clip { get; private set; }

        void Awake()
        {
            m_Audiosource = GetComponent<AudioSource>();
            for (int i = 0; i < overrides.Length; i++)
            {
                foreach (var material in overrides[i].materials)
                    m_Lookup[material] = overrides[i].banks;
            }
        }

        /// <summary>
        /// Will pick a random clip to play in the assigned list. If you pass a material, it will try
        /// to find an override for that materials or play the default clip if none can ben found.
        /// </summary>
        /// <param name="overrideMaterial"></param>
        /// <returns> Return the choosen audio clip, null if none </returns>
        public AudioClip PlayRandomClip(Material overrideMaterial, int bankId = 0)
        {
#if UNITY_EDITOR
            //UnityEditor.EditorGUIUtility.PingObject(overrideMaterial);
#endif
            if (overrideMaterial == null) return null;
            return InternalPlayRandomClip(overrideMaterial, bankId);
        }

        /// <summary>
        /// Will pick a random clip to play in the assigned list.
        /// </summary>
        public void PlayRandomClip()
        {
            clip = InternalPlayRandomClip(null, bankId: 0);
        }

        AudioClip InternalPlayRandomClip(Material overrideMaterial, int bankId)
        {
            SoundBank[] banks = null;
            var bank = defaultBank;
            if (overrideMaterial != null)
                if (m_Lookup.TryGetValue(overrideMaterial, out banks))
                    if (bankId < banks.Length)
                        bank = banks[bankId];
            if (bank.clips == null || bank.clips.Length == 0)
                return null;
            var clip = bank.clips[Random.Range(0, bank.clips.Length)];

            if (clip == null)
                return null;

            m_Audiosource.pitch = randomizePitch ? Random.Range(1.0f - pitchRandomRange, 1.0f + pitchRandomRange) : 1.0f;
            m_Audiosource.clip = clip;
            m_Audiosource.PlayDelayed(playDelay);

            return clip;
        }

    }
}

So now onto getting it to work in our project. * We already added this in our course here *, we are just changing the Single Sound to a Random Sound
Lets add it to our regular arrow. I want to play two things here.

  • Launch Sound (random)
  • Hit Sound (random)
    image

So on the Launch Sound we had

And on the Hit Sound we had

The main takeaways here are the one sound (in the Audio Clip) and the “Play On Awake” on the Launch Sound ONLY is checked off.

And now back at the top the the Hierarchy (under the Main “ArrowProjectile White” in this case) we had the Projectile script attached and in there we had the Hit Sound dragged and dropped into the On Hit() section.

Now we are going to change it to play a random sound.
I went back to the Unity 3D Game Kit course and tried to follow what they were doing.
They had on their sound the same type of setup.
In the Hierarchy (it looked just like ours - an AudioSource and then sounds under it)
image
And in the sounds themselves they were the same as ours.


They had the Audio Source, but they also had the Random Audio Player script (which I linked above)

The main takeaway here was that NO Audio Clip was selected in the Audio Source top section

First thing to do was get their script into our project. I copied the Random Audio Player script and pasted it in our scripts section, under Audio.
I then went into the script and changed the namespace to RPG.Audio

Then back to the prefab “ArrowProjectile White” and I added the Random Audio Player script to our Launch Sound and Hit Sound.
Then added the random sound files to each
AND remembered to clear the Audio lip in the Main Audio Source to nothing on both.
Here is the Launch Sound


Here is the Hit Sound

Now back to the Hierarchy at the top again under “ArrowProjectile White” I changed the hit sound (drag and dropped the new hit Sound) to the Projectile script location On Hit area at the bottom. I also changed the Pull down beside it to Play Random Clip.


You will notice it changed the icon beside the Hit Sound from a sound speaker to a script icon.

After this I did a quick play test and the Hit Sound works! :slight_smile: Every time the enemy gets hit it plays a random sound. But also now… there is NO sound being played at all when you Launch the arrow.
So what is the issue there? I tried many different things but will only illustrate one here as to not confuse the issue and/or solution. I thought “where?” should the sound be played, and came to the conclusion it should be played under the Fighter.cs script. I thought this was a good spot as this is where we determine if we are in range of the target, AND if we have a projectile, then we should launch the projectile at the target. So I added this line of code

private RandomAudioPlayer launchAudio;

I added it to the top under the LazyValue

    public class Fighter : MonoBehaviour, IAction, ISaveable, IModifierProvider
    {
        [SerializeField] float timeBetweenAttacks = 1f;
        [SerializeField] Transform rightHandTransform = null;
        [SerializeField] Transform leftHandTransform = null;
        [SerializeField] Weapon defaultWeapon = null;

        Health target;
        float timeSinceLastAttack = Mathf.Infinity;
        LazyValue<Weapon> currentWeapon;

        private RandomAudioPlayer launchAudio;

Now doing so meant that I would also have to call namespace RPG.Audio at the top to get the RandomAudioPLayer script to work. Also the LaunchAudio portion I would paste under the Hit area in the code near the bottom in this Fighter.cs script.

        void Hit()
        {
            if (target == null) { return; }
            if (!GetIsInRange(target.transform)) return;

            float damage = GetComponent<BaseStats>().GetStat(Stat.Damage);
            if (currentWeapon.value.HasProjectile())
            {
                currentWeapon.value.LaunchProjectile(rightHandTransform, leftHandTransform, target, gameObject, damage);
                launchAudio.PlayRandomClip();
            }
            else
            {
                target.TakeDamage(gameObject, damage);
            }
        }

Now we need to associate the sound with the Projectile.cs script itself so that when the Fighter.sc script called the LaunchProjectile or HasProjectile function it would play the sound I added to the “ArrowProjectile White” prefab. So load up the Projectile.cs script and add this line to the top under the SerializeField section. “Public RandomAudioPlayer launchAudio & and the title [Header(“Audio”)]” so you could see it.

    public class Projectile : MonoBehaviour
    {
        [SerializeField] float speed = 1;
        [SerializeField] float damage = 1f;
        [SerializeField] bool isHoming = true;
        [SerializeField] GameObject hitEffect = null;
        [SerializeField] float maxLifeTime = 10;
        [SerializeField] GameObject[] destroyOnHit = null;
        [SerializeField] float lifeAfterImpact = 2;
        [SerializeField] UnityEvent onHit;

        Health target = null;
        GameObject instigator = null;

        [Header("Audio")]
        public RandomAudioPlayer launchAudio;

Remember to call the namespace RPG.Audio at the very top to get the RandomAudioPlayer to work.

And the last thing to do was to add the sound to the prefab. Load up the “ArrowProjectile White” again. And in the top “ArrowProjectle White” under the Hierarchy go to the Inspector and you will see in the Projectile script attached at the bottom a new Launch Audio field. Drag and Drop your Launch Sound under the Hierarchy to this.


And we should be done. I checked the Unity 3D Game Kit course again to see if it was the same as how they added it.

Yup. Looked the same.

Now back to the game to playtest, and I unfortunately get this error now


Line 123 in the Fighter.cs script is you guessed it…

                launchAudio.PlayRandomClip();

Now this is how they called in in the Unity 3D Game Kit course, so I’m not sure what I’m missing here. I’ve tried pasting this line into many different locations in the code for this section (at the beginning of this section, at the end, ect) , and always get the same error. So I’m sure it is something simple that I’m overlooking.

[EDIT]
I’ve been thinking about this some more and I think this might be the issue. In the Projectile.cs script I’m stating at the top Public RandomAudioPlayer launchAudio, and in the Fighter script at the top I’m also stating private RandomAudioPLayer launchAudio. I think I should be calling the launchAudio I assign from the projectile.cs INTO the fighter.cs script for this to work right. But I don’t know how to do this.
[End EDIT]

I’m very sorry this was so long, but I wanted to have a good document for new users to follow on
"How to add this to your game", or “How to fix this if it doesn’t work in your game”.
If anyone has anything to try or if anyone can help I would be extremely happy for the guidance. And I’m sure new users would also be very thankful for the assistance. Cheers Everyone! :rainbow: :rainbow: :star:

1 Like

Hi,

So where do you actually set the random audio player?
i see you made a private non serialized variable for it.

private RandomAudioPlayer launchAudio;

i see where you call it.

launchAudio.PlayRandomClip();

I guess easiest would be to make it into a serializedfield and drag the random audioplayer you want to play in?

If this is not the case, ill read over it again later, after i had some coffee lol!
Nice and detailed description btw!

Thank you RustyNail. I forgot to add that part. :frowning:
I call it (Launch Sound) back on the Projectile script. I added (edited) that part to the first post.
It was tough to remember everything. I tried to get this to work for hours.

Also added the [EDIT] section to the first post, as I think in there “that” is the issue.

1 Like

Actually, at the time of the recording, we didn’t have a course on the 3d Game Kit, but the kit itself had been introduced by Unity. Rick made the 3d Game Kit course later. Sam was just saying to look at the kit itself and see what they were doing, which, by the way, you did. Well done!

Yeah. I learned about moving platforms which was great, and also about doors (pressure plate or lever activated) and how to use/make my own. Super good material on game stuff, with unfortunately nothing on actual coding though. Also… there was nothing on the Random Sound implementation, which was the whole reason I went there. :wink:

So I still need some help with that, do you have any recommendations Brian?

Oh i thought you meant you forgot to do that, but you forgot to link it, and its not solved.
So youre trying to make the random audio player play when you fire the projectile right?
The audio source and randomAudioPlayer are on your projectile?

When you fire it gets instantiated right? you could simply in the projectile call;
PlayRandomClip on Awake then? or do i not understand it right?

I mean if you make a reference to the projectile like

Gameobject projectile = instantiate(etc)

Then under that you could do

projectile.getcomponent<randomaudioplayer> ().playrandomclip;

Then you could call it where the projectile is instantiated.

Unity’s intention with the Game Kit was to get peoples feet wet in Unity, and making it accessible to folks who don’t know (or want to know) C#. Before they aquired Bolt, it was their answer (sort of) to Unreal’s Blueprint only setups.

I’ll paste in my AudioRandomizer (much simpler implemention using ScriptableObjects to represent banks of sounds) when I get home from work.

Hey there @RustyNail

Yes, I forgot to tell you (aka… everyone) that I actually DID attach it to the projectile script inside the prefab.

And yes. I’m trying to get a random sound to play every time you fire the bow. We originally only had it play one sound, and I thought after you fire the bow more than 3 times to kill one enemy it might be nice to randomize that sound as well.

The problem is that now it doesn’t play anything. And I think you have to assign the random sound in the Projectile script (which we did do above), and then call that sound in the Fighter script.

And I think this is where the problem is.
I added “Public RandomAudioPlayer laucnhAudio;” in the projectile script
and then added almost the same thing
“private RandomAudioPlayer launchAudio;” in the Fighter script.

I think in the Fighter script there should be something like maybe

        public void Grunt ()
        {
            if (gruntAudio != null)
                gruntAudio.PlayRandomClip ();
        }

or maybe something like

        void PlayStep(int frontFoot)
        {
            if (frontStepAudio != null && frontFoot == 1)
                frontStepAudio.PlayRandomClip();
            else if (backStepAudio != null && frontFoot == 0)
                backStepAudio.PlayRandomClip ();
        }

Where you see the Name of the audio and the PlayRandomClip. I got these two examples from the 3D Game Kit course. So I’m assuming I need to call it (the random sound) in some way.

Thanks for your help my friend.

@Brian_Trotter If you could send your code that would be fantastic. I love (and am impressed) by everyone’s different approaches to solve the same problem. I think that’s one of the joys of making stuff. There isn’t only one way to do it. It’s hard for me to learn mind you, but it’s keeping me on my toes. :stuck_out_tongue: Thank you again Brian. :+1:

If you want the RandomAudioPlayer on your projectile to play, you need to call that one,
you could do it in the projectile script, onAwake or Start. Or where its instantiated,
but you need a reference to that specific player, else it will not work.

If you want to set one per weapon, you can also put the randomaudioplayer and audiosource on your weapon, and call it from eh, Hit() i believe it is? or Shoot()? I dont remember, the method each weapon has anyways.

In different classes, even if you call it the same, its not the same instance.

[Header("Audio")]
        [SerializeField] RandomAudioPlayer launchAudio;

        public void Start()
        {
            if (launchAudio != null)
                launchAudio.PlayRandomClip ();
        }

if you put that on your projectile script and drag in the randomaudioplayer you have on your projectile,
each time one it created, it will play a random sound.

The downside of this is, when the projectile gets destroyed, the sound stops.
Thats why i put mine on the weapon, but just for testing, try this, it should work.

Here’s my solution. We start with a ScriptableObject to hold an array of sounds to play randomly. Why a ScriptableObject? Because I want to be able to put the same bank of sounds on several other characters and weapons… i.e. you might have 4 swords, which all use sword sounds, so rather than dragging them all into each one, you have this ScriptableAudioArray

using System.Collections.Generic;
using UnityEngine;

#pragma warning disable CS0649
namespace TkrainDesigns.ScriptableObjects
{
    [CreateAssetMenu(fileName = "AudioArray", menuName = "Shared/AudioArray")]
    public class ScriptableAudioArray : ScriptableObject
    {
        [SerializeField] List<AudioClip> clips = new List<AudioClip>();

        public int Count
        {
            get { return clips.Count; }
        }

        public AudioClip GetClip(int clip)
        {
            if (clips.Count == 0) return null;
            return clips[Mathf.Clamp(clip, 0, clips.Count - 1)];
        }

        public AudioClip GetRandomClip()
        {
            if (clips.Count > 0) return clips.RandomElement();
            return null;
        }
    }
}

Then, (and I can’t find this script at the moment from my repo, so I’ll do this part from memory), I have an AudioRandomizer class that Plays() a random clip from the collection.

public class AudioRandomizer
{ 
     [Serializefield] ScriptableAudioArray sounds;

     public void Play()
     {
           GetComponent<AudioSource>().PlayOneShot(souns.GetRandomClip();
     }
}

I have other features to apply some random values to the pitch and speed but the real meat of the solution is just what you see above, call AudioRandomizer.Play() instead of AudioSource.Play() and you’re good go go.

1 Like

Here’s the complete AudioRandomizer script, which is a bit more robust than the one I typed from memory:

using TkrainDesigns.ScriptableObjects;
using UnityEngine;

namespace TkrainDesigns
{
    [RequireComponent(typeof(AudioSource))]
    public class AudioRandomizer : MonoBehaviour
    {
        new AudioSource audio;
        public ScriptableAudioArray audioArray;
        public float maxPitchRange = 1.4f;
        public float minPitchRange = .6f;
        public bool playOnAwake;

        void Awake()
        {
            audio = GetComponent<AudioSource>();
            audio.playOnAwake = false;
            audio.spatialBlend = .95f;
        }

        void Start()
        {
            if (playOnAwake)
            {
                Play();
            }
        }

        public void Play()
        {
            audio.pitch = Random.Range(minPitchRange, maxPitchRange);
            if (!audioArray)
            {
                audio.Play();
                return;
            }

            if (audioArray.Count == 0)
            {
                audio.Play();
                return;
            }

            audio.PlayOneShot(audioArray.GetRandomClip());
        }
    }
}

Hey there Brian:

Yeah I think that would be the best option to create a Scriptable Object to hold an array of sounds. Your example of 4 swords, which all use sword sounds, would be a good solution to just attach this Scriptable Audio Array rather than each sound to each sword.

I made a AudioRandomizer.cs script and then a AudioArray.cs script with your code and I might have been away too long from this, as I don’t know how to fix the error it’s telling me. It says in the Audio Array script (you have this as your Scriptable Audio Array, and under your GetRandomClip you have the “return clips.RandomElement();” method)


Shouldn’t the definition to this be defined in the AudioRandomizer script?

It also looks like you are referencing your ScriptableObjects script in your AudioRandomizer script? I didn’t do that and I’m not getting an error. I’m I reading your code right?

Thank you so much for your help Brian. Cheers and have a great day!

Oops, I forgot that I was using some custom code in there…

In this case, it’s a handy library I wrote that lets you get a random element from a List using extension methods…

NewRandom.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace TkrainDesigns
{
    [System.Serializable]
    public class WeightedListElement<T>
    {
        public int probability = 1;
        public T element;
    }

    public static class RandomHelpers
    {
        /// <summary>
        /// Returns a random element from this list of items.
        /// </summary>
        /// <param name="list">This list</param>
        /// <typeparam name="T">The element of this list's type</typeparam>
        /// <returns>One random element, or default if list is empty</returns>
        public static T RandomElement<T>(this List<T> list, T fallback = default)
        {
            if (list.Count == 0) return fallback;
            return list[Random.Range(0, list.Count)];
        }

        /// <summary>
        /// Returns a random element from this array.
        /// </summary>
        /// <param name="array">this array</param>
        /// <typeparam name="T">An element type</typeparam>
        /// <returns></returns>
        public static T RandomElement<T>(this T[] array, T fallback = default)
        {
            return RandomElement(array.ToList(), fallback);
        }

        public static T RandomElement<T>(this IEnumerable<T> collection, T fallback = default)
        {
            return RandomElement(collection.ToList(), fallback);
        }

        public static T RandomWeightedElement<T>(this List<WeightedListElement<T>> list)
        {
            if (list.Count == 0) return default;
            int totalprobablity = list.Sum(element => element.probability);
            int selection = Random.Range(0, totalprobablity);
            foreach (var element in list)
            {
                selection -= element.probability;
                if (selection <= 0) return element.element;
            }

            return default;
        }

        public static T RandomWeightedElement<T>(this WeightedListElement<T>[] array)
        {
            return RandomWeightedElement(array.ToList());
        }

        public static T RandomWeightedElement<T>(this IEnumerable<WeightedListElement<T>> collection)
        {
            return RandomWeightedElement(collection.ToList());
        }

        public static GameObject RandomElement(this List<GameObject> list)
        {
            if (list.Count == 0) return null;
            return list[Random.Range(0, list.Count)];
        }

        /// <summary>
        /// Returns a float between min and max with the probability weighted towards the center of the spread.
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <returns></returns>
        public static float RangeWeighted(float min, float max)
        {
            if (min > max)
            {
                (min, max) = (max, min);
            }

            float center = (min + max) / 2.0f;
            AnimationCurve minCurve = AnimationCurve.EaseInOut(0, min, 1, center);
            AnimationCurve maxCurve = AnimationCurve.EaseInOut(0, center, 1, min);
            float random = Random.Range(0f, 1f);
            if (random < .50f)
            {
                return minCurve.Evaluate(random * 2.0f);
            }

            return max - maxCurve.Evaluate(random * 2.0f);
        }

        /// <summary>
        /// Returns an int between min and max with the probability weighted towards the center of the spread.
        /// </summary>
        /// <param name="min"></param>
        /// <param name="max"></param>
        /// <returns></returns>
        public static int RangeWeighted(int min, int max)
        {
            return (int)RangeWeighted((float)min, (float)max);
        }

        public static float RangeAround(float value, float range = .2f)
        {
            return RangeWeighted(value * (1 - range), value * (1 + range));
        }

        public static int RangeAround(int value, float range = .2f)
        {
            return (int)RangeAround((float)value, range);
        }
    }
}

This extension class is a bit in the weeds, but the upshot is that you can take any List<T> and use the extension method RandomElement() on it and it will automagically select one Random element. There are a lot of other methods in there that I didn’t document so well. I should probably write up a tutorial on using Extenion Methods to extend classes that would otherwise be close to modifications.

Hey there Brian. How are you saving the list? I attached the Audio Randomizer, then I made a ListExample.cs script, then attached the List script to the GameObject and added the audio files. But I can t seem to attach it to the Audio Array as I think I need to be able to save the List script somehow.

This is my ListExample script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace RPG.Audio
{
    public class ListExample : MonoBehaviour
    {
        public List<AudioClip> Clips = new List<AudioClip>();
        
    }

}

And this is from the Inspector