Day 12: Singleton & Sounds

In today’s class, we discussed strategies for audio, and covered the Singleton pattern – a very powerful tool (but also a potentially tricky/dangerous one) for organizing our game and exposing our “manager” scripts to the objects in our scene. Tomorrow we will create the Game Manager which will handle our game state and flow, and on Friday we will spice up our visuals and learn about the “build” process.

Part 1: Audio Strategy

When we are making our game, there are a number of decisions that will center around how we want to treat the sounds in our game. When should they be playing in our game? What should be emitting the sound? Do I need to have control over the sound, such as starting and stopping playback, or adjusting the speed or pitch? Are my audio clips short or long? How often can I expect to hear each sound? These questions will inform our decisions as to which objects should emit what sounds, and how much control we may need to exert over them through code.

For most of my games, my sounds tend to fall into one of four categories:

  • Omnipresent Sounds – these are sounds that will generally play throughout an experience, such as background music, or ambient noise. The sound is continuous, long, and probably loops. It tends to remain at a steady volume, regardless of position. For these types, I prefer to use an empty object that contains only an AudioSource component dedicated to playing the one sound, with the loop property selected. These may be set to play on awake or respond to a Play( ) command, depending on the start/stop conditions.
  • Object Sounds – these sounds have an obvious point of origin. We perceive them as coming from some object in our scene – perhaps a character, perhaps an inanimate object. These may be lines of dialog, footsteps, music coming from a radio, a cat meowing, or a twinkling sound to indicate a point of interest. We expect these sounds to come from “somewhere” as opposed to “everywhere” so often we will want them to have a spatial quality or directionality. These are the most likely to be controlled by some behavior or react to some condition. In this case, I expect these to be generated from an AudioSource component that is attached to a GameObject or the child of an object in the scene, and managed some script, likely on the object itself. These sources will often have the 3D sound at least partially implemented, and will likely rely on the Play( ) command.
  • Prefab Sounds – I see these as “object sounds on autopilot”. There is no logic expected to control the sound, they simply play when instantiated, and may continue for the duration of the instance object. These are best used for environmental sound effects that are a little longer or loop (such as a motor running, or a crackling fire), or a larger sound effect (such as a door opening, or longer explosion). Generally this involves a prefab object with an AudioSource with a preset AudioClip that will Play On Awake and may or may not Loop. This can be 2D or 3D sound depending on if your noise needs a point of origin. These will play through until the object is destroyed, or the clip is finished (if not looping).
  • One-Shot Sounds – these are useful for quick sounds, especially repeating sounds, or sound that may overlap. PlayOneShot( ) is a play-and-forget function… once it begins you cannot stop it until it has played through. Also, this does not loop. It must be applied to an audio source, so it will respect the settings of that objects component, such as the spatial quality (and thus position of the object). This type is best for quick sound effects, especially those that may overlap from the same source. Footsteps, gunshots, UI noises, beeps, scoring noises, quick audio effects that don’t need to be controlled or shut off.

To start, we will create two simple “prefab sounds” – sound effects that will play for the Player Bullet and the Enemy Bomb prefabs. In this case the implementation is simple. I open my prefab to edit it, and attach the sound to the object either by dragging the clip onto the game object itself, or by adding an AudioSource component and setting the AudioClip to the appropriate source. I chose a short “pew” noise for my bullet, and a longer descending whistle noise for my enemy bomb. Both components were set to Play On Awake, so the audio plays automatically as soon as the element appears, and stops the moment the element is destroyed. (This is particularly helpful for the Bomb, which may collide with something before the clip has finished playing, hence the reason we use this implementation rather than a “one-shot” style sound.

Next, I’m going to add some ambient space noises, by creating an empty game object that will server as our background sound emitter. I will add an AudioSource component and set my Audio Clip to be the appropriate sound, and make sure to select “Loop” so that it keeps playing.

These sources will now play when they are part of the scene, either by being placed there in the editor, or instantiated by code. Next we will look at defining a script to handle more of our sound generation.

Part 2: Sound Manager (Singleton)

Back when we discussed our “train station” metaphor, I mentioned a special method for assigning and calling a single instance of an object, known as the “Singleton” pattern. This is a way of declaring a class so that there is only ever one single instance of it. In other situations, the Singleton instantiates itself. Our code, however, is living inside the confines of our game engine, so we will still need to attach it to an object that exists in our scene, rather than relying on self-instantiation.

Think of the Singleton as being similar to the Presidency. At any given time, there is only one President. A new President may come along and replace the old, and when they do the former ceases to be the President. The responsibilities of the office only ever point to one person at a time… the current President.

So the Singleton pattern is a programming method by which we can define a particular instance of a class within the class declaration itself, so that we never have to worry about “finding” or establishing a relationship or link… we simply call the only instance by calling a property in the class that holds the instance itself.

Sounds confusing, and it kind of is, but just roll with it and you’ll see how this works.

First, we need to create something to generate the sounds. I first create an empty game object by going to Create > Create Empty, and naming it “SoundManager”. I then add a script to this, which I also name “SoundManager”. (These don’t have to share the same name, this is just a personal preference as I only expect to have one of these). I make the Background Sound object a child of this, and create another child object for Explosion Sounds. This Explosion Sound object gets an AudioSource component that will emit our PlayOneShot( ) sounds.

In our Sound Manager script, I create a private variables to hold the AudioSource components of these objects, but I open them to the Editor (despite being private) using the [Serialize Field] attribute.

    [SerializeField]
    private AudioSource backgroundMusic;

    [SerializeField]
    private AudioSource explosionObject;

    public AudioClip enemyExplosionClip;
    public AudioClip playerExplosionClip;

Now it is time to make our SoundManager into a singleton. This way we can adjust sounds or modify them or turn them off as needed. If we emit sounds from the children of the Sound Manager, we only need to look internally to make adjustments, rather than performing costly “find” processes.

The first step to declare a singleton is to define a public static version of the class within itself, like so:

public class SoundManager: MonoBehavior
{
    public static SoundManager S;
...

Weird, right? We just set up a SoundManager type variable inside of our SoundManager type script?

Weirder yet, watch what we do next:

private void Awake()
{
    S = this; // Singleton Definition
}

As our object wakes up, it declares itself to be this value “S”.

What is “public static” anyways? The “public” designation is pretty easy – it is a value that can be accessed from outside of the class. But the “static” designation means that all instances of the class share the same value for that variable. So in theory, you can create as many SoundManagers as you want, but if you access the “S” variable, you will always get a reference to the same object, the last one that woke up and set itself to “this”.

So why is this useful? Because now we no longer have to find our SoundManager. We can simply get directly to the active instance of our SoundManagerscript by writing SoundManager.S.{{whatever public variable or method}}

As a demonstration of this, we create a public function named MakeEnemyExplosion( ) which will play when an enemy is destroyed. The declaration is simple:

    public void MakeEnemyExplosion()
    {
        explosionObject.PlayOneShot(enemyExplosionClip);  // make the enemy explosion noise
    }

Now we can add a command in our Enemy.cs script that calls this particular function to generate the sound by writing:

SoundManager.S.MakeEnemyExplosion();

It all simply works.

NOTE – Often times, the variable name “S” is used as shorthand for Singleton, but you can choose whatever name you want. You could call your SoundManager singleton “Instance” or “Singleton” or even “Frank”, so our script made calls to SoundManager.Frank.MakeExplosion( ) instead. “S” is just a commonly used shorthand because Singleton starts with S.

This SoundManager singleton can be accessed from any script running in our scene. To demonstrate this, we create another function to stop the background music, StopBackgroundMusic( ) and call this from the Mothership coroutine when it no longer has any children.

SoundManager.S.StopBackgroundMusic();


SoundManager.cs
using UnityEngine;

public class SoundManager : MonoBehaviour
{
    public static SoundManager S; // declare the singleton

    [SerializeField]
    private AudioSource backgroundMusic;

    [SerializeField]
    private AudioSource explosionObject;

    public AudioClip enemyExplosionClip;
    public AudioClip playerExplosionClip;

    private void Awake()
    {
        S = this; // Singleton Definition
    }

    public void MakeEnemyExplosion()
    {
        explosionObject.PlayOneShot(enemyExplosionClip);  // make the enemy explosion noise
    }

    public void StopBackgroundMusic()
    {
        backgroundMusic.Stop();
    }

}


EnemyScript.cs
using UnityEngine;

public class EnemyScript : MonoBehaviour
{
    public GameObject enemyBombPrefab;

    public void DropABomb()
    {
        // make a bomb
        GameObject thisBomb = Instantiate(enemyBombPrefab, (transform.position + Vector3.down), Quaternion.identity);
        Destroy(thisBomb, 2f); 
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.B))
        {
            DropABomb();
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.transform.tag == "PlayerBullet")
        {
            // make the explosion noise
            SoundManager.S.MakeEnemyExplosion();

            // destroy myself
            Destroy(this.gameObject);

            // destroy the thing that killed me
            Destroy(collision.gameObject);
        }
    }
}


MotherShipScript.cs
using System.Collections;
using UnityEngine;

public class MotherShipScript : MonoBehaviour
{
    public int stepsToSide;
    public float sideStepUnits;
    public float downStepUnits;
    public float timeBetweenSteps;
    public float timeBetweenBombs;

    // Start is called once before the first execution of Update after the MonoBehaviour is created
    void Start()
    {
        // start the attack
        StartCoroutine(MoveMother());
        StartCoroutine(SendABomb());
    }

    // Update is called once per frame
    void Update()
    {
        
    }

    public IEnumerator SendABomb()
    {
        bool isRunning = true;

        while (isRunning) 
        {
            // how many children are there?
            int enemyCount = transform.childCount;

            // if there are children....
            if (enemyCount > 0)
            {
                // pick one at random  (between 0 and enemyCount)
                int enemyIndex = Random.Range(0, enemyCount);  // remember int Range is maxExclusive

                // get the object
                Transform thisEnemy = transform.GetChild(enemyIndex);

                // make sure it has the EnemyScript component  (just in case)
                EnemyScript enemyInstance = thisEnemy.GetComponent<EnemyScript>();

                if (enemyInstance)
                {
                    // drop the bomb
                    enemyInstance.DropABomb();
                }
            }
            else
            {
                // no more children, stop running
                isRunning = false;
            }

            yield return new WaitForSeconds(timeBetweenBombs);
        }
        Debug.Log("SendABomb has finished");
    }

    public IEnumerator MoveMother()
    {
        // define our vectors
        Vector3 sideVector = Vector3.right * sideStepUnits;
        Vector3 downVector = Vector3.down * downStepUnits;

        // make our movement loop while there are enemies left
        while (transform.childCount > 0)
        {
            // move to the side
            for (int i = 0; i < stepsToSide; i++)
            {
                // move to the side
                transform.position += sideVector;

                // wait for the next move
                yield return new WaitForSeconds(timeBetweenSteps);
            }

            // move down
            transform.position += downVector;
            yield return new WaitForSeconds(timeBetweenSteps);

            // Switch Direction
            sideVector *= -1f;
        }

        Debug.Log("Mothership has no children");

        SoundManager.S.StopBackgroundMusic();
    }
}