Friday, March 22, 2013

Unity C# AudioManager Tutorial - Part 3 (Control)

This is part 3 of a 4 part tutorial to create an audio manager for unity. This tutorial builds on the structure developed in part 1, expanding the functionality described in part 2. It describes new Play options, and how to manage tracked audio clips.

Part 1 | Part 2 | Part 3 | Part 4

Overloading Play

The next step is to create a play method that plays a sound on top of an existing object, so when the object moves, the sound moves with it. This is useful for things such as a car with a siren, or a character who is walking while they talk. This is another core method, and similar to our previous play method. Again, the concept is referenced from Daniel Rodriguez's helpful post. Except where specified otherwise, all methods in this section of the tutorial are within our AudioManager class.
    
public AudioSource Play(AudioClip clip, Transform emitter, float volume) {
    var source = Play(clip, emitter.position, volume);
    source.transform.parent = emitter;
    return source;
}
Note that instead of a Vector3 parameter, we pass in the GameObject Transform that is emitting the moving sound. This method calls the default play method, creating an object at the emitter position and playing the sound there, but it then attaches the transform of the AudioSource (which is shared by its GameObject) to the emitter object by making the emitter its parent. Now the sound will follow the emitter where it goes. Remember that the original Play method registers the new sound object with the m_activeSounds collection, and destroys the GameObject after the length of the sound has played.

Looping Sound

Since the original play method destroys the sound object after the length of the clip, to loop we need a new method that won't destroy it. It also has to set the audiosource "loop" property to true. I assumed that any sound that loops will have an emitter, but if you needed a looping sound that played in one position in space, you could just remove the line that sets the transform.parent. This method is almost identical to the original except for these changes.
public AudioSource PlayLoop(AudioClip loop, Transform emitter, float volume) {
    //Create an empty game object
    GameObject movingSoundLoc = new GameObject("Audio: " + loop.name);
    movingSoundLoc.transform.position = emitter.position;
    movingSoundLoc.transform.parent = emitter;
    //Create the source
    AudioSource source = movingSoundLoc.AddComponent<AudioSource>();
    setSource(ref source, loop, volume);
    source.loop = true;
    source.Play();
    //Set the source as active
    m_activeAudio.Add(new ClipInfo{source = source, defaultVolume = volume});
    return source;
}
The setSource method becomes valuable now, because if we want to change any default settings of all the AudioSources that are created through the AudioManager, we only have to change it in the setSource method, and it will updated both this and the original Play method.

Stopping the Looping Sound

Now we've got a looping sound going on endlessly, we need to be able to stop it! This is a simple method that requires you to track the sound you'll want to stop at start time, and pass it in as a parameter when you're ready to stop it.
public void stopSound(AudioSource toStop) {
    try {
        Destroy(m_activeAudio.Find(s => s.source == toStop).source.gameObject);
    } catch {
        Debug.Log("Error trying to stop audio source "+toStop);
    }
}
This highlights why it is useful to have all the Play methods return the AudioSource created. Here is an example of a new Init class using the loop and stop methods:

public class Init : MonoBehaviour {
    //testSound is public in order to set in the inspector
    public AudioClip testSound; 
    private AudioSource playingSound;

    void Start(){
        playingSound = AudioManager.Instance.PlayLoop(testSound, m_enemy.transform, 1)
    }

    void Update(){
        if(Input.GetKeyDown(KeyCode.Space)){
            AudioManager.Instance.stopSound(playingSound);
        }
    }
}
Note that since loop sounds are assigned to a parent on creation, if that parent is destroyed for any reason, the sound will be destroyed with it automatically.

Cleaning Up the Audio List

For the final section of this tutorial, I will describe the mechanism for cleaning up inactive ClipInfo objects from our m_activeAudio list. This method will also be key later to control the sounds dynamically.
private void updateActiveAudio() { 
    var toRemove = new List<ClipInfo>();
    try {
        foreach (var audioClip in m_activeAudio) {
            if (!audioClip.source) {
                toRemove.Add(audioClip);
            } 
        }
    } catch {
        Debug.Log("Error updating active audio clips");
        return;
    }
    //cleanup
    foreach (var audioClip in toRemove) {
        m_activeAudio.Remove(audioClip);
    }
}
The method creates an empty list of ClipInfo to remove. We can't remove them in the foreach loops since it would illegally modify the iterator. The method then goes through all the ClipInfos in m_activeAudio and checks each one's AudioSource. In the Play method the AudioSource was set to destroy after playing, so after the sound is finished, the AudioSource source of the ClipInfo will be null. If it is null, we add it to the toRemove list to be removed. After going through all active sounds, we go through all the toRemove ClipInfos and remove them from the m_activeAudio list. Since the ClipInfo list is not directly tied to the AudioSource it holds, this will work no matter how the AudioSource is destroyed (time or parent death.)

Lastly we have to call this in the AudioManager Update() method so that it will be called continuously.
void Update() {
    updateActiveAudio();
}

Next time...

We now have a working AudioManager that will play a sound at a location, play a sound at an emitter, play a looping sound, stop a looping sound, track all active sounds, and clean them up as they finish. In part 4, I will show how this setup allows for dynamic centralized control, including implementing music, pausing non-music sounds, global volume control, and select volume control (for things like voice overs.)

No comments:

Post a Comment