Friday, March 15, 2013

Unity C# AudioManager Tutorial - Part 2 (Play)

This is part 2 of a 4 part tutorial to create an audio manager for unity. This tutorial focuses on creating the main methods that will play sounds, as will as tracking and managing active sounds.

Part 1 | Part 2 | Part 3 | Part 4

AudioManager - ClipInfo Object

We will first create a ClipInfo object as a new data type to hold all the relevant data about each sound that is handled by the audio manager. Right now it contains only variables for the sound clip to play and its initial volume, but it could be expanded to hold any information you want to keep track of, such as the time the clip started playing, or how many times it has looped. We will add a member variable List of this ClipInfo to our AudioManager to track all audio playing, and we will update our Awake method to initialize this List.
public class AudioManager : Singleton<AudioManager> {
    class ClipInfo
    {
       //ClipInfo used to maintain default audio source info
       public AudioSource source { get; set; }
       public float defaultVolume { get; set; }
    }

    private List<ClipInfo> m_activeAudio;

    void Awake(){
       m_activeAudio = new List<ClipInfo>();
       
       [see previous tutorial]
    }
}
First note that the ClipInfo class is inside the audio manager. This allows it to be accessed only by the audio manager, hiding the data. You can read the rules on nesting classes here.

Second, notice that ClipInfo holds the AudioSource directly, ignoring the GameObject that the source must be attached to.

Lastly, we create a 'defaultVolume' property so that we can change the volume of the audio source dynamically, but remember what volume it was created at. More on this in part 4.

The Play Method

For the core functionality of this AudioManager, I’ve referenced Daniel Rodriguez for the basic idea (I also recommend his post on setting up managers for your unity projects.) The Play method of the AudioManager will:
  1. Create a new GameObject.
  2. Set its AudioSource to be our given sound clip.
  3. Play the AudioSource.
  4. Destroy the object.
The beauty of this is that we can play a given audio clip at any time by writing AudioManager.Instance.Play(parameters). We don’t have to worry about creating the AudioManager since it is a singleton, we don’t have to worry about creating and destroying the accompanying GameObjects because everything is handled within the method.
    public AudioSource Play(AudioClip clip, Vector3 soundOrigin, float volume) {
       //Create an empty game object
       GameObject soundLoc = new GameObject("Audio: " + clip.name);
       soundLoc.transform.position = soundOrigin;

       //Create the source
       AudioSource source = soundLoc.AddComponent<AudioSource>();
       setSource(ref source, clip, volume);
       source.Play();
       Destroy(soundLoc, clip.length);
       
       //Set the source as active
       m_activeAudio.Add(new ClipInfo{source = source, defaultVolume = volume});
       return source;
    }
For sound data, the method takes an audio clip and a volume value as parameters, but could be extended to take other parameters to modify the sound, such as pitch.

The other parameter here is the Vector3 that indicates the position of the sound. This is where the sound is playing from, and uses Unity’s built in sound management to determine the panning and volume of the sound based on the position of the audio listener component in your scene and the rolloff for the audio source (more in rolloff in a little bit.)

This isn’t a long method, and it is the most critical one, so I will step through the code.
   //Create an empty game object
   GameObject soundLoc = new GameObject("Audio: " + clip.name);
   soundLoc.transform.position = soundOrigin;
This creates an empty GameObject in the world. When running the game, this will show up in the inspector as “Audio: [whatever your clip name is as listed in the Unity assets]”. It then moves this newly created object to the Vector3 position passed in.

Next we attach an audio source to the fresh GameObject we just created:
   //Create the source
   AudioSource source = soundLoc.AddComponent<AudioSource>();
   setSource(ref source, clip, volume);
   source.Play();
   Destroy(soundLoc, clip.length);
After creating the AudioSource, we pass a reference of it to ‘setSource’, which I will outline shortly. Basically this method takes the values passed in and applies them to the AudioSource of our newly created GameObject. We start the AudioSource playing, then call the Destroy method on the new GameObject, passing clip.length as the parameter of how long to wait before destroying the object. Since clip.length is the time it takes to play the clip, this will destroy our AudioSource GameObject immediately after finishing the sound. This is essentially like playing a one shot of an AudioClip, but since it is an AudioSource, we can do more things with the clip, such as changing it’s volume while it is playing, or pausing it, which we will take advantage of later.

Lastly, we register this audio source with the AudioManager as a ClipInfo object which we created earlier. This way the AudioManager tracks the sound and can handle it as it is playing.
   //Set the source as active
   m_activeAudio.Add(new ClipInfo{source = source, defaultVolume = volume});
   return source;
Since ‘source’ and ‘defaultVolume’ are public properties of our ClipInfo object, we can assign them using object initializers (using the curly braces), which allows us to avoid the need for an explicit constructor. We add the new ClipInfo object to our AudioManager member variable List m_activeAudio, which maintains references to all the sounds that are playing.

setSource Method

For the final part of this section of the tutorial, we will create the “setSource” method referenced above in the “Play” method. This method sets the properties of our new audio source.
private void setSource(ref AudioSource source, AudioClip clip, float volume) {
   source.rolloffMode = AudioRolloffMode.Logarithmic;
   source.dopplerLevel = 0.2f;
   source.minDistance = 150;
   source.maxDistance = 1500;
   source.clip = clip;
   source.volume = volume;
}
The AudioSource is sent in as a ‘ref’, so that all changes made to the AudioSource parameter passed in will be kept after this method is finished and our ‘Play’ method resumes.

Rolloff mode is the way that Unity makes the sound decrease in volume as its distance from the AudioListener in the scene increases. In the inspector you can see a graph that models this.

The Linear mode means that the sound decreases at an equal rate as you get further from the listener. The Logarithmic mode means that the sound decreases sharply as you begin to move away from the listener, but the farther you get away from the listener, the less the sound decreases. I opted for Logarithmic sound because it more closely matches the way you hear sounds in reality- a noise made at 10ft away will sound much louder than the same noise at 100ft away, but a noise at 500ft away will sound only slightly louder than the same noise at 800ft away.

The dopplerLevel is related to how the sound is affected by movement (like sirens on a passing ambulence.) I set this to .2 because I found that the default value of 1 can create strange sound anomalies when rotating the camera quickly.

minDistance and maxDistance are important, particularly for Logarithmic rolloff.

minDistance is how far the sound must be from the listener before the volume starts to decrease. This is especially important because if this is set to its default 0, the sound will start to drop off sharply at any distance from the listener. This means that a sound made at 1ft away will be significantly louder than a sound made at 2 ft away, which just does not feel right in-game. For Tumbleweed, a distance of 150 seems to be working for now- all sounds less than 150 game ‘units’ away will play at max volume, and after that the sound dropoff will begin.

maxDistance is important because it determines both the maximum distance at which a sound will play, as well as the curve of sound dropoff. With logarithmic rolloff, you can make this number really huge, and sounds that play in the far distance and middle distance will still sound natural.

Finally, we set the sound clip to our clip, and the default volume to our volume.

We covered a lot here. If set up correctly, you should now be able to play any AudioClip by calling “AudioManager.Instance.Play(AudioClip, Vector3, float)” with your desired clip, position, and volume.

Here is a my project setup and in-game hierarchy in Unity so far:

Next time...

In part 3 of the AudioManager tutorial, I will cover placing moving sounds on moving objects, pausing sounds, and changing volumes.

1 comment:

  1. hi,
    this code works perfectly. but trying to set the soundLoc game object not to be destroyed on load causes problems: in the next loaded scene after a soundLoc was set not to be destroyed in the previous some clips played are not audible. the audio manager stil seems to do its work, clips are created and played, but about every 10h clip or so is not audible. any idea what could cause this behaviour?

    ReplyDelete