Tuesday, March 12, 2013

Unity C# AudioManager Tutorial - Part 1 (Setup)

This is part 1 of a 4 part tutorial to create an audio manager for Unity 3d. This tutorial focuses on the initial setup of the singleton and audiomanager classes.

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:
  1. Any script makes a call to AudioManager.Instance (eg var AM = AudioManager.Instance).
  2. The Singleton base class checks for an instance of AudioManager.
  3. Not finding an instance, the Singleton creates a new AudioManager GameObject and attaches the AudioManager component (script.)
  4. The AudioManager script Awake() method searches for the main camera (with the “MainCamera” tag.)
  5. Finding it, it attaches itself to the main camera by making the camera its parent.
Now we have an AudioManagerContainer GameObject with an AudioManager component, and it is a child of the MainCamera. At this point, if you have set these classes up correctly, you should be able to call this in your scene, though it does not do anything yet. You need a GameObject in the scene with the "MainCamera" tag. If you write "var AM = AudioManager.Instance" on the Start() method of any object in your scene, you should receive a Console message that reads "AudioManager Initializing." If you successfully receive this message, you are ready to move on to part 2.

Next time...

In part 2, I will create a Play method, and will discuss some of the controllable attributes of sounds in Unity.

2 comments:

  1. 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:

    audioSource = GetComponents()[audioSourceIndex];

    They are indexed by the order in which they appear in the inspector.

    ReplyDelete
    Replies
    1. The blog software couldn't deal with angle brackets. Let's try again.

      audioSource = GetComponents < AudioSource > ()[audioSourceIndex];

      If that doesn't work, "AudioSource" appears between angle-brackets after "GetComponents" and before the parenthesis.

      Delete