Audio Manager – Code Introduction

When taking on the task of implementing the sounds of Safety Protocol, while I had implemented sounds in previous games as well, I realized that for this project the normal basic implementation I had done until then wouldn’t cut it; We needed an audio manager.

The available plugins for sound implementation felt a bit like overkill, so I took this opportunity to learn more about sound implementation and how to do it more properly. I was really excited to learn more about this!

I took to YouTube to try to find tutorials about how to make audio managers. The one that made the most sense to me and had the most potential was (to noone’s surprise) Brackey’s Introduction to AUDIO in Unity video.

This was to become the perfect base upon which to make my first audio manager.

You can view my general coding style implemented in the audio manager through the image preview seen here. To view the actual code, please see the ‘Audio Manager – Code Preview’ section below.

As a bonus (mainly because it doesn’t warrant its own section):

The second code preview below is an example of how I used an Interface to handle the character movement sounds, to simplify the messy code that can come when handling movement sounds in update functions.

Is it doable without the Interface? Yes.

Is it better with the Interface? Yes. 

//This preview consists of two scripts merged into one for the purpose of this portfolio

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Audio;

[System.Serializable]
public class Sound
{
	public string name;
	
	public AudioClip clip;

	public AudioMixerGroup output;

	[Range(0f, 1f)]
	public float volume = 1;
	[Range(0.1f, 3f)]
	public float pitch = 1;

	[HideInInspector]
	public AudioSource source;
	
	public bool loop;
}

public class AudioManager : MonoBehaviour
{
	public static AudioManager instance;
	public AudioScriptableObject audioScriptableObject = default;
	public Sound[] soundArray = default;
	public List soundList = default;
	private Sound _clip = default;

	void Awake()
	{
		HandleInstances();
	}

	private void Start()
	{
		ClearSoundArray();

		soundArray = GetSoundArray();

		SetupSoundSettings();
	}

	public Sound[] GetSoundArray()
	{
		soundList.AddRange(audioScriptableObject.playerSFX.Select(sound => sound));
		soundList.AddRange(audioScriptableObject.consoleSFX.Select(sound => sound));
		soundList.AddRange(audioScriptableObject.interactSFX.Select(sound => sound));
		soundList.AddRange(audioScriptableObject.environmentSFX.Select(sound => sound));
		soundList.AddRange(audioScriptableObject.MenuSFX.Select(sound => sound));
		soundList.AddRange(audioScriptableObject.generalSFX.Select(sound => sound));
		soundList.AddRange(audioScriptableObject.ambianceBGM.Select(sound => sound));

		return soundList.ToArray();
	}

	public void Play(string name)
	{
		FindClipByName(name);

		if (_clip == null)
		{
			Debug.LogWarning("Sound: " + name + "not found!");
			return;
		}

		_clip.source.Play();
	}

	public bool IsPlaying(string name)
	{
		FindClipByName(name);

		if (_clip == null)
		{
			Debug.LogWarning("Sound: " + name + "not found!");
			return false;
		}

		return _clip.source.isPlaying;
	}

	public void Stop(string name)
	{
		FindClipByName(name);

		if (_clip == null)
		{
			Debug.LogWarning("Sound: " + name + "not found!");
			return;
		}

		_clip.source.Stop();
	}

	public void PauseSounds()
	{
		foreach (Sound sound in soundArray)
		{
			if (IsPlaying(sound.name) == true)
			{
				sound.source.Pause();
			}
		}
	}

	public void UnPauseSounds()
	{
		foreach (Sound sound in soundArray)
		{
			if (IsPlaying(sound.name) == true)
			{
				sound.source.Pause();
			}
		}
	}

	public void StopAllSound()
	{
		foreach (Sound sound in soundArray)
		{
			if (IsPlaying(sound.name) == true)
			{
				sound.source.Stop();
			}
		}
	}

	private void HandleInstances()
	{
		if (instance == null)
		{
			DontDestroyOnLoad(gameObject);
			instance = this;
		}
		else
		{
			if (instance != this)
			{
				Destroy(gameObject);
			}
		}
	}

	private void SetupSoundSettings()
	{
		foreach (Sound sound in soundArray)
		{
			sound.source = gameObject.AddComponent();
			sound.source.clip = sound.clip;
			sound.source.volume = sound.volume;
			sound.source.pitch = sound.pitch;
			sound.source.loop = sound.loop;
			sound.source.outputAudioMixerGroup = sound.output;
		}
	}

	private void ClearSoundArray()
	{
		soundList.Clear();
	}

	private void FindClipByName(string name)
	{
		_clip = Array.Find(soundArray, sound => sound.name == name);
	}
}
//This preview consists of three scripts merged into one for the purpose of this portfolio

using System.Collections;
using UnityEngine;

public interface IMovingSound
{
    void Stop(string clipName);
    void Play(string clipName);
}

public class MovingSound : IMovingSound
{
    void IMovingSound.Play(string clipName)
    {
        if (AudioManager.instance.IsPlaying(clipName) == false)
        {
            AudioManager.instance.Play(clipName);
        }
    }

    void IMovingSound.Stop(string clipName)
    {
        AudioManager.instance.Stop(clipName);
    }
}

public class PlayerWalkState : PlayerState
{
    private IMovingSound walkingSound = new MovingSound();

    /*some code removed from portfolio preview*/

	private void HandleWalkSound()
	{
		if (_playerController.Rigidbody.velocity != Vector3.zero)
		{
			if (_playerController.GroundHit.transform != null && _playerController.GroundHit.transform.gameObject != null)
			{
				if (_playerController.GroundHit.transform.gameObject.layer == LayerMask.NameToLayer("Metal"))
				{
					walkingSound.Stop("containerWalk");
					walkingSound.Stop("floorWalk");
					walkingSound.Play("metalWalk");
				}
				else if(_playerController.GroundHit.transform.gameObject.layer == LayerMask.NameToLayer("Container"))
				{
					walkingSound.Stop("metalWalk");
					walkingSound.Stop("floorWalk");
					walkingSound.Play("containerWalk");
				}
				else
				{
					walkingSound.Stop("metalWalk");
					walkingSound.Stop("containerWalk");
					walkingSound.Play("floorWalk");
				}
			}
		}
		else
		{
			walkingSound.Stop("metalWalk");
			walkingSound.Stop("containerWalk");
			walkingSound.Stop("floorWalk");
		}
	}

	public override void OnDisable()
	{
		walkingSound.Stop("metalWalk");
		walkingSound.Stop("containerWalk");
		walkingSound.Stop("floorWalk");
	}
}

Categories and Scriptable Objects

One main difference from the original Audio Manager in the tutorial I followed is the categories into which I wanted to organize the sounds.

It was a bit trickier than expected to implement this function since using a list of lists doesn’t seem to be properly supported.

To get around this unforeseen issue, after a lot of trial and error and some assistance from Gwangyeong Yun (my programmer team member and also husband), I ended up with an array of lists converted into an array, basically.

Manually Added Audio Sources

Not all of the sounds in the game derrive from the audio manager; for sounds that should get stronger and weaker depending on their distance to the player, I opted for simply applying audio sources directly on the prefabs.
  
There, I adjusted all volumes and pitches manually as well as experimented with the 3D custom volume rolloff settings to get the most out of each sound that the player experiences.
 
Other examples of where I manually added audio sources to the prefabs:
AI Units
Fuseboxes
Computer Servers
VFX areas (Steam, mist, lightning sparks…)

Using Audio Mixer

I also made sure to hook up all of the game sounds to the Unity engine’s own audio mixer so that my fellow team member and programmer, Elliot Rosing, could implement a nice volume control for each sound type in the game’s settings menu.