Part 1 | Part 2 | Part 3 | Part 4
Handling Sound in Unity
Default Unity sound control is clumsy. GameObjects can only have one AudioSource as a component. AudioClips are inflexible. In these posts, I describe an audio manager that is simple to use, easy to organize, and adds versatility to the Unity sound tools, building on its existing structure. This includes playing audio clips dynamically with full control, tracking sounds that are currently playing, pausing select sounds, and controlling volume dynamically. This tutorial assumes you have a basic understanding of sound handling in Unity, check out the manual for an overview.Singleton
First off, we need the singleton. For this, I’ve referenced the article 50 tips for working with unity. I recommend reading this in its entirety- you don’t have to agree with everything, and you don’t have to absorb every detail, but it’s good think about these things!The premise of the singleton is that you have a single object which can be accessed at any time, without explicit instantiation, and you know that the instance you are accessing is the sole instance of the singleton object. It works through a static method which calls the stored instance of the object. Inside the method, the singleton checks if the instance exists and returns it. If it does not exist, it creates one and returns this. From the outside, though, it seems as if the single instance is always available.
The great benefit to this for our AudioManager is we do not have to manage a GameObject for it for each scene. We simply have each sound clip to play call the AudioManager, and if the instance doesn’t exist, the singleton creates one, and if it already exists, the singleton uses the existing one, so there is no threat of creating multiple audio manager instances in one scene, and we don’t have to concern ourselves with its existence.
using UnityEngine; using System.Collections; public class Singleton<T> : MonoBehaviour where T : MonoBehaviour { protected static T instance; //Returns the instance of this singleton public static T Instance { get { if (instance == null) { instance = (T)FindObjectOfType(typeof(T)); if (instance == null) { GameObject container = new GameObject(); container.name = typeof(T)+"Container"; instance = (T)container.AddComponent(typeof(T)); } } return instance; } } }The T listed represents a generic type, which allows you to create singletons of various types by inheriting this class. In the context of this post, though, we will only be using it for our AudioManager.
Also note that the Singleton inherits from MonoBehaviour, which allows us to implement any of its subclasses as components on GameObjects.
You can google the Unity command “DontDestroyOnLoad” to see how to make a GameObject persistent across scenes, but I am not putting it in this base class since we may not want all derived classes persistent across scenes. For example, in our AudioManager, we do not need a persistent GameObject.
AudioManager - Initialization
To begin, we will initialize our new AudioManager class using Awake(). We use this instead of Start() because we want to make sure our AudioManager is available first thing in the scene, in case there are any sounds that start playing immediately (like music.)using UnityEngine; using System.Collections; using System.Collections.Generic; public class AudioManager : Singleton<AudioManager> { void Awake() { Debug.Log("AudioManager Initializing"); try { transform.parent = GameObject.FindGameObjectWithTag("MainCamera").transform; transform.localPosition = new Vector3(0, 0, 0); } catch { Debug.Log("Unable to find main camera to put audiomanager"); } } }We start by creating a message logging the AudioManager initialization. Next, we try to attach the AudioManager to the main camera, placed within a try/catch block so that we can log any issue finding the main camera or attaching this object. This may be something worth playing around with if your AudioListener isn’t always on your main camera, or you don’t tag your game camera with the “MainCamera” tag, but for Tumbleweed this works.
Setting the ‘transform.parent’ property of the object makes it appear under the parent object in the scene hierarchy while running the scene. It helps organize your hierarchy, but more importantly, all transforms (movement) of the parent GameObject will also apply to its children (and its children’s children.) This way, the AudioManager will stick to the camera like a cat to a tree. Now we can attach certain sounds (like music) to the AudioManager itself, and know that it will stay on the camera, which means it will not change volume or pan position unless we explicitly instruct it to.
Initializing AudioManager in a scene:
- Any script makes a call to AudioManager.Instance (eg var AM = AudioManager.Instance).
- The Singleton base class checks for an instance of AudioManager.
- Not finding an instance, the Singleton creates a new AudioManager GameObject and attaches the AudioManager component (script.)
- The AudioManager script Awake() method searches for the main camera (with the “MainCamera” tag.)
- Finding it, it attaches itself to the main camera by making the camera its parent.
It's not true that GameObjects can only have one AudioSource as a component. You can add as many as you want through GetComponents (plural). Access each one like this:
ReplyDeleteaudioSource = GetComponents()[audioSourceIndex];
They are indexed by the order in which they appear in the inspector.
The blog software couldn't deal with angle brackets. Let's try again.
DeleteaudioSource = GetComponents < AudioSource > ()[audioSourceIndex];
If that doesn't work, "AudioSource" appears between angle-brackets after "GetComponents" and before the parenthesis.