Randomized Sound Sets via Scriptable Objects

Hello there,

I would like to share my solution to give different characters different sound sets (=voices) by using scriptable objects. Here is an example:

Here is the code for the SO:

using UnityEngine;

namespace RPG.Audio
{
    [CreateAssetMenu(fileName = "SoundSet", menuName = "Sounds/New Sound Set", order = 0)]
    public class SoundSet : ScriptableObject
    {
        [Range(0, 1)] public float volume = 1f;
        public Subset[] subsets;

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

The SoundContext is a simple enum (see stats section):

namespace RPG.Audio
{
    public enum SoundContext
    {
        GetHit,
        Footstep,
        Landing,
        Death
    }
}

The scriptable object can then be dragged into a “Character Sounds” component, that I created:

image

In the CharacterSounds I convert the soundSet into a dictionary for performance and readability:

private void Awake() => BuildSoundDictionary();

        private void BuildSoundDictionary()
        {
            foreach (var subset in soundSet.subsets)
            {
                if (subset.audioClips.Length == 0) continue;
                soundDictionary.Add(subset.context, subset.audioClips);
            }
        }

… and do some randomizing magic afterwards using a modified version of this: The best way to randomize sounds in Unity 3D C# - Sonigon

If you have any questions or suggestions, please let me know. :slight_smile:

Soro

4 Likes

Take a look at a video called Overthrowing the tyrany of Monobehaviour. It shows a really good way to add variance to your sound effects through random pitch and volume adjustments. Also live audio test.

I quite like your context idea.

1 Like

This is well done. This allows you to set up a single source Audio Event system, rather than my version which requires a separate gameObject for each extra audio event type you have (I sitll randomize the audio, of course).

Nice I was just thinking that SO are a good solution to this and thinking of coming up with something.

image
I don’t understand in your code where the sound sets are coming from can you please include your code for the character sounds and any other relevant scripts.

Also what is this line of code doing:

I’ve usually seen => when it comes to making something a getter for a private variable, what is it doing in this context?

The => is what’s called a Lambda operator or an “expression” operator. It allows us to replace a code block with other types of expressions…

For methods, it’s great for one liners… since all that is in Awake() is

void Awake()
{
     BuildSoundDictionary();
}

Because Awake is only one line, we can substitute this with the Lambda operator

void Awake() => BuildSoundDictionary();
2 Likes

If you’re familiar with them, I find them great to help improve readability in code. Less lines of code to scroll through.

Hi I’m recreating @Sorokan scripts I’m creating the CharacterSounds which hasn’t been fully shared.

And I can’t find out how the context audio sources have drop downs to select the enum value on the left and then the audio source to the right of it:
image

How is this achieved?

Hi there,

please find my CharacterSounds script below. I still consider it work in progress and the randomizing logic you use really depends on your personal taste. Therefore I would recommend using this as a reference for your own code rather than just copying it. It also uses the custom class SerializableDictionary which can be found here: https://github.com/azixMcAze/Unity-SerializableDictionary. This let’s you… well, serialize dictionaries of primitive types (and lists according to the doc) which should answer your last question.

Cheers!

using System.Collections.Generic;
using UnityEngine;

namespace RPG.Audio
{
    public class CharacterSounds : MonoBehaviour
    {
        [SerializeField] SoundSet soundSet = null;

        [SerializeField] AudioSource defaultAudioSource = null;
        [SerializeField] SerializableDictionary<SoundContext, AudioSource> contextAudioSources = new SerializableDictionary<SoundContext, AudioSource>();

        Dictionary<SoundContext, List<int>> lastPlayed = new Dictionary<SoundContext, List<int>>();
        Dictionary<SoundContext, AudioClip[]> soundDictionary = new Dictionary<SoundContext, AudioClip[]>();

        private void Awake() => BuildSoundDictionary();

        private void BuildSoundDictionary()
        {
            foreach (var subset in soundSet.subsets)
            {
                if (subset.audioClips.Length == 0) continue;
                soundDictionary.Add(subset.context, subset.audioClips);
            }
        }

        public void PlayGetHitSound() => PlayRandomSoundOfType(SoundContext.GetHit);

        public void PlayDeathSound() => PlayRandomSoundOfType(SoundContext.Death);

        public void PlayRandomSoundOfType(SoundContext context)
        {
            if (!contextAudioSources.TryGetValue(context, out AudioSource audioSource))
            {
                if (defaultAudioSource == null) return;
                else audioSource = defaultAudioSource;
            }

            if (!soundDictionary.TryGetValue(context, out AudioClip[] audioClips)) { return; }

            audioSource.PlayOneShot(GetRandomAudioClip(context, audioClips), soundSet.volume);
        }

        private AudioClip GetRandomAudioClip(SoundContext type, AudioClip[] audioClips)
        {
            if (audioClips.Length < 2) { return audioClips[0]; }

            List<int> playedClips;
            if (!lastPlayed.TryGetValue(type, out playedClips))
            {
                playedClips = new List<int>();
                lastPlayed.Add(type, playedClips);
            }

            if (playedClips.Count > audioClips.Length / 2)
            {
                playedClips.RemoveAt(0);
            }

            int audioClipIndex;
            do
            {
                audioClipIndex = UnityEngine.Random.Range(0, audioClips.Length);
            }
            while (playedClips.Contains(audioClipIndex));

            playedClips.Add(audioClipIndex);

            return audioClips[audioClipIndex];
        }
    }
}
1 Like

Thank you I will be modifying it I just wanted to see your code to have an idea of where to start of and what a good random audio script looks like.

btw I’m not sure if your computer auto-corrected but I found that a very odd way to end your message.

Lol, yes this was meant to say cheers :smiley:

Privacy & Terms