Sound Kraft

Unity / C#

Unity Audio package

Why Soundkraft™?

Often when working in unity you want have sounds for things like gunshots and footsteps. It is very easy just to slap a audio source component on the object that you want to play the sound and you call the “Play()” function. But this leaves some things to be desired, the same sound over and over again does not sound great so maybe you start to pitch it to add some variation. Later on, you realize you want more than one sound for your footsteps and you add an array of clips. The whole thing gets more and more complex and the simple component you created for just playing some simple footsteps has become a behemoth of a code base. This is where SoundKraft™ comes in, it is made to store all the data regarding the permutations of the sound in a simple to read data asset. Furthermore, it utilizes an object pool to keep the amount of active audio object to an minimum. I this text I will describe how I when about to create this system. If you want to download the source code here is a link to the project on git.



The Structure

As mention earlier the goal of the structure is to make the use of dynamic SFX as easy as possible and as alive as possible by avoiding repetition of sounds. The data controlling the sounds is stored in the audio object and it is the audio object that is the main interfacing point for the user. But if they want more ingame control they can use the controller, that works a bit like a remote to a audio source that is obtained from the objectpool.



Audio Object

The audio object inherits from scriptable object witch is Unity's default data asset it hold the data regarding the random ranges and values for the sounds. All the user needs to do to play a sound is a reference to this object and they can run the Play method. The audio object object takes care of the selecting of random clips and allocating of audio objects. To avoid the same sound repeating two times I a row I implemented a queuing system whereas in all the clips in the list will be played at least one time before they can be played again. To give the user more control of how the sounds play each audio object can be linked to a unity mixer group, this helps when the user wants to add addition effect to the sound. Furthermore, all the clips are subjected to pitch and volume pitching based on the values setup here in the audio object.

Audio Controller

The job of the controller is just that, to control the sounds that are playing. The controller takes care of fading in and out the sounds and returns the audio object to objectpool when the sound is done playing. The controller takes care of the actual setting of the values in the audio source, in many ways the audio controller is the interface for the audio source. The user should never change the actual audio source by them self’s, instead all of that is handled trough the controller. Every time the play function is called from the audio object a instance of the controller is created as a form of remote for the user to access and change sounds on the fly.

public class AudioObject : ScriptableObject
{
    public AudioMixerGroup MixerGroup;
    public List Clips;

    [MinMax(0, 1, ShowEditRange = true)] public MathHelper.FloatMinMax Volume = new MathHelper.FloatMinMax(1, 1);
    [MinMax(0, 3, ShowEditRange = true)] public MathHelper.FloatMinMax Pitch = new MathHelper.FloatMinMax(1, 1);
    [MinMax(1, 1000, ShowEditRange = true)] public Vector2 Distance = new Vector2(1, 1000);
    [Range(0, 1)] public float SpatialBlend = 1;
    public AudioRolloffMode RolloffMode = AudioRolloffMode.Logarithmic;
    public bool IsLooping;
    public bool Sound2D = true;
    [Header("Fade in")]
    public float FadeInTime = 0;
    public AnimationCurve FadeInCurve;
    [Header("Fade out")]
    public float FadeOutTime = 0;
    public AnimationCurve FadeOutCurve;

    [Header("")] public float MinSpawnInterval = 0f;
    private static AudioListener _audioListener;
    private List _indexList;
    private float _lastSpawnTime;
    private AudioController _lastAvailableController;
    private Camera _camera;

    //Play the sound on specified position and transform
    public AudioController Play(Vector3 pos = default(Vector3), Transform parent = default(Transform), float delay = 0)
    {
        if (Time.time - _lastSpawnTime < MinSpawnInterval && _lastAvailableController != null)
            return _lastAvailableController;

        _lastAvailableController = ObjectPool.Instantiate(AudioSceeneObject, pos, Quaternion.identity, parent).GetComponent();

        _lastAvailableController.SetUp(this);
        _lastAvailableController.transform.position = pos;


        if (parent != default(Transform)) _lastAvailableController.transform.parent = parent;
        _lastSpawnTime = Time.time;
        return _lastAvailableController;
    }
    //Play the sound on the camera
    public AudioController Play()
    {
        Transform currentCameraTransform = null;

        if (Camera.current)
            currentCameraTransform = Camera.current.transform;
        else if (Camera.main)
            currentCameraTransform = Camera.main.transform;
        else if (_audioListener)
        {
            currentCameraTransform = _audioListener.transform;
        }
        else
        {
            _audioListener = FindObjectOfType();
            currentCameraTransform = _audioListener.transform;
        }

        if (currentCameraTransform != null)
            return Play(currentCameraTransform.position, currentCameraTransform);

        Debug.LogError("There is no audio listener in the scene");
        return null;

    }
    //Picks random clip form list an exiles it temporarily till all clips are played
    public AudioClip GetClip()
    {
        if(Clips.Count < 3)
        {
            return Clips[Random.Range(0, Clips.Count)];
        }

        if (_indexList == null || _indexList.Count != Clips.Count)
        {
            _indexList = null;
            _indexList = new List();
            for (int i = 0; i < Clips.Count; i++)
            {
                _indexList.Add(i);
            }
        }

        int quarantinedIndex = Mathf.CeilToInt(Clips.Count / 3f);
        int tempIndex = _indexList[Random.Range(quarantinedIndex + 1, _indexList.Count)];

        _indexList.Remove(tempIndex);
        _indexList.Insert(0, tempIndex);

        return Clips[tempIndex];
    }
    //For Editor testing of sound
    public void TestPlay()
    {
        IEnumerable testerList = FindObjectsOfType().Where(audio => audio.IsPlayingAudioObject(this));
        if (IsLooping && testerList.ToArray().Length > 0)
        {
            foreach (var editorAudioTester in testerList)
            {
                DestroyImmediate(editorAudioTester.gameObject);
            }
        }
        else
            new GameObject("EditorAudio(temp)").AddComponent().Initialize(this);
    }
}

                
public class ObjectPool {
    private static Dictionary> _poolAvailable = new Dictionary>();
    private static Dictionary _prefabMapper = new Dictionary();
    private static Dictionary> _typePoolAvailable = new Dictionary>();

    private static int SizeToAddAtFull = 5;
    private static Transform _transformInstance;
    private static Transform transform {
        get {
            if (_transformInstance == null)
                _transformInstance = new GameObject("ObjectPool").transform;

            return _transformInstance;
        }
    }
    private static  GameObject GetObjectFromPool(GameObject objKey)
    {
        if (_poolAvailable[objKey].Count <= 0) return null;
        GameObject obj = _poolAvailable[objKey].Dequeue();
        _prefabMapper[obj] = objKey;
        obj.transform.localScale = objKey.transform.localScale;
        obj.transform.rotation = objKey.transform.rotation;
        obj.transform.position = objKey.transform.position;
        obj.SetActive(true);
        return obj;
    }
    //Public facing methods
    public static void Destroy(GameObject obj)
    {
        if (_prefabMapper.ContainsKey(obj))
        {
            _poolAvailable[_prefabMapper[obj]].Enqueue(obj);
            obj.transform.SetParent(transform);
            obj.SetActive(false);
        }
        else
            UnityEngine.Object.Destroy(obj);
    }
    public static void Destroy(GameObject obj)
    {
        if (_typePoolAvailable.ContainsKey(typeof(T)) && obj.GetComponent() != null)
        {
            _typePoolAvailable[typeof(T)].Enqueue(obj);
            obj.transform.SetParent(transform);
            obj.SetActive(false);
        }
        else
            UnityEngine.Object.Destroy(obj);
    }
    public static GameObject Instantiate(GameObject prefab, Vector3 position, Quaternion rotation, Transform parent = null)
    {
        GameObject temp = Instantiate(prefab);
        temp.transform.position = position;
        temp.transform.rotation = rotation;
        if(parent != null)
            temp.transform.SetParent(transform);
        return temp;
    }
    public static GameObject Instantiate(GameObject prefab)
    {
        if (!_poolAvailable.ContainsKey(prefab)) _poolAvailable[prefab] = new Queue();
        if (_poolAvailable[prefab].Count <= 0)
        {
            //Add object to fill out pool
            for (int i = 0; i < SizeToAddAtFull; i++)
            {
                GameObject temp = GameObject.Instantiate(prefab, transform, true);
                temp.name = prefab.name + "(" + i + ")";
                temp.SetActive(false);
                _poolAvailable[prefab].Enqueue(temp);
            }
        }

        return GetObjectFromPool(prefab);
    }
    public static T Instantiate(Vector3 position, Quaternion rotation, Transform parent = null) where T : Component
    {
        T temp = Instantiate();

        temp.transform.position = position;
        temp.transform.rotation = rotation;
        if(parent != null)
            temp.transform.SetParent(transform);
        return temp;
    }

    public static T Instantiate() where  T : Component
    {
        Type type = typeof(T);
        if (!_typePoolAvailable.ContainsKey(type)) _typePoolAvailable.Add(type, new Queue());

        if (_typePoolAvailable[type].Count == 0)
        {
            //Add object to fill out pool
            for (int i = 0; i < SizeToAddAtFull; i++)
            {
                GameObject temp = new GameObject(type.Name);
                temp.transform.parent = transform;
                temp.AddComponent();
                temp.SetActive(false);
                _typePoolAvailable[type].Enqueue(temp);
            }
        }

        GameObject obj = _typePoolAvailable[type].Dequeue();
        obj.SetActive(true);
        return obj.GetComponent();
    }
}