Chapter 8

Recipe: Sound Manager

Audio can make or break a video game. Good audio can provide a deeper level of immersion by reinforcing the themes of the game world and filling out environments into living, noise-making places. On the other hand, bad audio can irritate players and turn a good gameplay experience into something repetitive and empty.

Most commercial games have several different layers of audio playing at the same time, transitioning or changing contextually. To achieve commercial levels of audio, Unity’s audio system will need a lot of help. Out of the box, it isn’t exactly an easy process to do some simple tasks, such as fading audio in and out or controlling volume levels. To accomplish even basic functionality, extra audio management code is required.

The audio code in this book provides the following functionality to our framework:

  1. To provide a single audio source for multiple audio clips
  2. To be able to manage audio clips from a single source
  3. To be able to play, pause, or stop an audio clip
  4. To be able to play, pause, or stop music streaming from disc
  5. To provide volume control functions to set volume and fade music in and out
  6. To provide accessibility, by being a static singleton instance, so that all calls to audio are of the same format

There are two scripts in this book related to the playback and management of audio. One is the sound controller, intended for sound effects, BaseSoundController.cs. The second is intended for music control, the script MusicController.cs.

8.1 The Sound Controller

In the example framework for this book, it is assumed that each scene has a sound controller, an empty gameObject with the BaseSoundController.cs script attached to it. The component has an array populated in the Unity editor Inspector window by the sounds required for the game (their AudioClips). When a sound is required by another script, it calls upon the sound manager to play it, passing in the index of the AudioClip as it stands in the array:

BaseSoundController.Instance.PlaySoundByIndex(the index number from the array of sounds);

Centralizing the audio playback avoids dealing with properties in multiple places, such as having to set the volume level on every AudioSource when a user changes it in the options menu. When audio is centralized like this, one volume setting and one volume script can easily change all of the game audio.

Below is the full sound controller script:

using UnityEngine;
using System.Collections;
public class SoundObject
{
	public AudioSource source;
	public GameObject sourceGO;
	public Transform sourceTR;
	
	public AudioClip clip;
	public string name;
			
	public SoundObject(AudioClip aClip, string aName, float aVolume)
	{
		 // in this (the constructor) we create a new audio source // and store the details of the sound itself
		sourceGO= new GameObject("AudioSource_"+aName);
		sourceTR= sourceGO.transform;
		source= sourceGO.AddComponent<AudioSource>();
		source.name= "AudioSource_"+aName;
		source.playOnAwake= false;
		source.clip= aClip;
		source.volume= aVolume;
		clip= aClip;
		name= aName;
	}
	
	public void PlaySound(Vector3 atPosition)
	{
		sourceTR.position= atPosition;
		source.PlayOneShot(clip);
	}
}
public class BaseSoundController : MonoBehavior
{
	public static BaseSoundController Instance;
	
	public AudioClip[] GameSounds;
	
	private int totalSounds;
	private ArrayList soundObjectList;
	private SoundObject tempSoundObj;
	
	public float volume= 1;
	 public string gamePrefsName= "DefaultGame"; // DO NOT FORGET TO SET // THIS IN THE EDITOR!!
	public void Awake()
	{
		Instance= this;
	}
	
	void Start ()
	{
		 // we will grab the volume from PlayerPrefs when this script // first starts
		volume= PlayerPrefs.GetFloat(gamePrefsName+"_SFXVol");
		Debug.Log ("BaseSoundController gets volume from prefs 				"+gamePrefsName+"_SFXVol at "+volume);
		soundObjectList=new ArrayList();
		
		 // make sound objects for all of the sounds in GameSounds 	// array
		foreach(AudioClip theSound in GameSounds)
		{
			 tempSoundObj= new SoundObject(theSound, 	theSound.name, volume);
			soundObjectList.Add(tempSoundObj);
			totalSounds++;
		}
	}
	
	public void PlaySoundByIndex(int anIndexNumber, Vector3 aPosition)
	{
		 // make sure we're not trying to play a sound indexed higher // than exists in the array
		if(anIndexNumber>soundObjectList.Count)
		{
			 Debug.LogWarning("BaseSoundController>Trying to do PlaySoundByIndex with invalid index number. Playing last sound in array, instead.");
			anIndexNumber= soundObjectList.Count-1;
		}
		
		tempSoundObj= (SoundObject)soundObjectList[anIndexNumber];
		tempSoundObj.PlaySound(aPosition);	
	}
}

8.1.1 Script Breakdown

This script should be attached to an empty gameObject somewhere in the scene. For the volume control to work correctly, the gamePrefsName should be set to a name suitable for describing the game. That way, it will use PlayerPrefs to grab volume levels.

AudioClips should be dragged into the GameSounds array via the Inspector window in the Unity editor. Each AudioClip in the array will have its own AudioSource and GameObject instantiated when the BaseSoundController.cs script first runs. Think of each sound as having its own audio channel to avoid overlaps or too many different sounds playing on a single AudioSource. Internally, a class called SoundObject is used to store information about the audio sources and their gameObjects. When the main sound controller script needs to access each one, the references in SoundObject are used to avoid having to repeatedly look for them.

The SoundObject class is declared at the top of the script:

using UnityEngine;
using System.Collections;
public class SoundObject
{
	public AudioSource source;
	public GameObject sourceGO;
	public Transform sourceTR;
	
	public AudioClip clip;
	public string name;
			
	public SoundObject(AudioClip aClip, string aName, float aVolume)
	{
		// in this (the constructor) we create a new audio source 				// and store the details of the sound itself
		sourceGO= new GameObject("AudioSource_"+aName);
		sourceTR= sourceGO.transform;
		source= sourceGO.AddComponent<AudioSource>();
		source.name= "AudioSource_"+aName;
		source.playOnAwake= false;
		source.clip= aClip;
		source.volume= aVolume;
		clip= aClip;
		name= aName;
	}

The SoundObject single function is to PlaySound(). A position is passed in as a parameter, and AudioSource.PlayOneShot() is used to start the AudioClip playback.

PlayOneShot() is a simple method for getting the AudioSource to play the desired clip without having to set its AudioClip property permanently. To get it to play in the correct location in the 3D world, the position of the AudioSource is set just before the call to play it happens:

	public void PlaySound(Vector3 atPosition)
	{
		sourceTR.position= atPosition;
		source.PlayOneShot(clip);
	}
}

BaseSoundController derives from MonoBehavior so that it can use the Awake() and Start() functions called by the Unity engine:

public class BaseSoundController : MonoBehavior
{
	public static BaseSoundController Instance;
	
	public AudioClip[] GameSounds;
	
	private int totalSounds;
	private ArrayList soundObjectList;
	private SoundObject tempSoundObj;
	
	public float volume= 1;
	public string gamePrefsName= "DefaultGame"; // DO NOT FORGET TO SET // THIS IN THE EDITOR!!

The variable named Instance is static typed, accessible from anywhere, which makes playing a sound possible from any other script in the game. When the script’s Awake() function is called, Instance is set to the instance of the script. The assumption is that each scene requiring sound effects will contain one instance of this script in a scene, attached to an empty gameObject or similar. Since this script does not check for multiple instances, care must be taken to ensure that there is only one instance in a scene:

	public void Awake()
	{
		Instance= this;
	}
	
	void Start ()
	{

PlayerPrefs saves a file to the user’s hard drive, which can be accessed at any time for saving trivial data. The Unity documentation has the following information on where PlayerPrefs files are stored:

Editor/Standalone

On Mac OS X PlayerPrefs are stored in ~LibraryPreferences folder, in a file named unity.[company name].[product name].plist, where company and product names are the names set up in Project Settings. The same .plist file is used for both Projects run in the Editor and standalone players.

On Windows, PlayerPrefs are stored in the registry under HKCUSoftware[company name][product name] key, where company and product names are the names set up in Project Settings.

On Linux, PlayerPrefs can be found in ~/.configunity3d[CompanyName]/[ProductName].configunity3d again using the company and product names specified in the Project Settings.

WebPlayer

On Web players, PlayerPrefs are stored in binary files in the following locations:

Mac OS X: ~LibraryPreferencesUnityWebPlayerPrefs

Windows: %APPDATA%UnityWebPlayerPrefs

There is one preference file per Web player URL and the file size is limited to 1 MB. If this limit is exceeded, SetInt, SetFloat, and SetString will not store the value and throw a PlayerPrefsException.

The filename for the preference files this script uses is made up of a combination of the contents of the string named gamePrefsName and a suffix of _SFXVol. The gamePrefsName string should be set in the Unity editor so that each game has a unique preferences file.

PlayerPrefs may be formatted in three different ways: floats, integers, or strings, in this case, a float between 0 and 1 to represent sound volume:

		 // we will grab the volume from PlayerPrefs when this script // first starts
		volume= PlayerPrefs.GetFloat(gamePrefsName+"_SFXVol");

To store SoundObject instances, an ArrayList is used. The ArrayList needs to be initialized before any attempts to access it:

		soundObjectList=new ArrayList();

Next, the script iterates through the array of AudioClips set in the Unity editor and builds the SoundObject instances to hold information about each sound. The soundObjectList ArrayList is populated by the new SoundObject instances.

A foreach loop iterates through each AudioClip in the GameSounds array:

		foreach(AudioClip theSound in GameSounds)
		{

The constructor function of the SoundObject takes three parameters: the AudioClip, the name of the sound, and its intended volume (based on the volume value read earlier from the PlayerPrefs and stored in the variable named volume):

			 tempSoundObj= new SoundObject(theSound, 	theSound.name, volume);
			soundObjectList.Add(tempSoundObj);

The integer variable totalSounds counts how many sounds go into the ArrayList to save having to recount the array each time we need it:

			totalSounds++;
		}
	}

To play a sound, the function PlaySoundByIndex takes two parameters: an index number (integer) and a position. The index number refers to an AudioClip’s position in the original AudioClip array:

	public void PlaySoundByIndex(int anIndexNumber, Vector3 aPosition)
	{

For safety, the index number is checked to make sure that it is valid so that it won’t raise a null reference exception because of a mistake with the call. The index number will be set to the last AudioClip in the array if it is higher than it should be and a warning logged in the console to raise attention to it:

		if(anIndexNumber>soundObjectList.Count)
		{
			 Debug.LogWarning("BaseSoundController>Trying to do PlaySoundByIndex with invalid index number. Playing last sound in array, instead.");
			anIndexNumber= soundObjectList.Count-1;
		}
		

A temporary AudioClip is made to hold the required sound as it is played with AudioSource.PlaySound():

		tempSoundObj= (SoundObject)soundObjectList[anIndexNumber];
		tempSoundObj.PlaySound(aPosition);	
	}
}

There is still some way to go before this sound controller provides a complete solution to game audio, but it should serve as a good, solid start.

8.2 The Music Player

The MusicController.cs script is intended for playing music. It will set the volume according to a PlayerPrefs value and handle volume fading and audio clip looping.

Below is the full script:

using UnityEngine;
using System.Collections;
public class MusicController : MonoBehavior
{
	private float volume;
	 public string gamePrefsName= "DefaultGame"; // DO NOT FORGET TO SET // THIS IN THE EDITOR!!
	public AudioClip music;
	
	public bool loopMusic;
	
	private AudioSource source;
	private GameObject sourceGO;
	private int fadeState;
	private int targetFadeState;
	private float volumeON;
	private float targetVolume;
	public float fadeTime=15f;
	public bool shouldFadeInAtStart= true;
	void Start ()
	{
		 // we will grab the volume from PlayerPrefs when this script // first starts
		volumeON= PlayerPrefs.GetFloat(gamePrefsName+"_MusicVol");
	
		 // create a game object and add an AudioSource to it, to 	// play music on
		sourceGO= new GameObject("Music_AudioSource");
		source= sourceGO.AddComponent<AudioSource>();
		source.name= "MusicAudioSource";
		source.playOnAwake= true;
		source.clip= music;
		source.volume= volume;
		// the script will automatically fade in if this is set
		if(shouldFadeInAtStart)
		{
			fadeState=0;
			volume=0;
		} else {
			fadeState=1;
			volume=volumeON;
		}
		// set up default values
		targetFadeState=1;
		targetVolume=volumeON;
		source.volume=volume;
	}
	
	void Update ()
	{
		 // if the audiosource is not playing and it's supposed to // loop, play it again (Sam?)
		if(!source.isPlaying && loopMusic)
			source.Play();
		// deal with volume fade in/out
		if(fadeState!=targetFadeState)
		{
			if(targetFadeState==1)
			{
				if(volume==volumeON)
					fadeState=1;
			} else {
				if(volume==0)
					fadeState=0;
			}
			 volume=Mathf.Lerp(volume, targetVolume, 		Time.deltaTime * fadeTime);
			source.volume=volume;
		}
	}
	public void FadeIn (float fadeAmount)
	{
		volume=0;
		fadeState=0;
		targetFadeState=1;
		targetVolume=volumeON;
		fadeTime=fadeAmount;
	}
	public void FadeOut (float fadeAmount)
	{
		volume=volumeON;
		fadeState=1;
		targetFadeState=0;
		targetVolume=0;
		fadeTime=fadeAmount;
	}
}

8.2.1 Script Breakdown

The MusicController.cs script should be added to an empty gameObject in a scene. It derives from MonoBehavior to use the Start() and Update() functions called by the engine:

public class MusicController : MonoBehavior
{

The MusicController script can automatically fade music in when it loads, making for a nicer transition into the scene. shouldFadeInAtStart should be set in the Unity editor Inspector window on the component:

	public bool shouldFadeInAtStart= true;
	void Start ()
	{

As with the sound controller, it is important to set the gamePrefsName string to load and save preferences correctly:

		 // we will grab the volume from PlayerPrefs when this script // first starts
		volumeON= PlayerPrefs.GetFloat(gamePrefsName+"_MusicVol");

In the next part of the script, a new gameObject is created and an AudioSource added to it. The AudioSource is set to play on awake so that the music starts automatically at the start of the scene. The music variable should be set in the Unity editor Inspector window, so that the music AudioClip can be carried over to the new source:

		 // create a game object and add an AudioSource to it, to 	// play music on
		sourceGO= new GameObject("Music_AudioSource");
		source= sourceGO.AddComponent<AudioSource>();
		source.name= "MusicAudioSource";
		source.playOnAwake= true;
		source.clip= music;
		source.volume= volume;

The final part of the Start() function deals with setting up the default fade settings. When shouldFadeInAtStart is set to true, the volume will start at 0, and the fadeState will be 0. The fader variables will be explained in full along with the fader code further down within the Update() loop:

		// the script will automatically fade in if this is set
		if(shouldFadeInAtStart)
		{
			fadeState=0;
			volume=0;
		} else {
			fadeState=1;
			volume=volumeON;
		}
		// set up default values
		targetFadeState=1;
		targetVolume=volumeON;

The AudioSource created to play music needs to have its volume set at the beginning for it to start out at the volume set by the preference file:

		source.volume=volume;
	}
	

The Update() function begins by checking to see whether the AudioSource in the variable source is playing by polling its AudioSource.isPlaying property. If this is false and the music is set to loop (by the loopMusic variable), the next line makes a call to Play() on the AudioSource, restarting the music:

	void Update ()
	{
		 // if the audiosource is not playing and it's supposed to 	// loop, play it again (Sam?)
		if(!source.isPlaying && loopMusic)
			source.Play();

The fader works by having a fadeState variable and a targetFadeState. When targetFadeState is different to fadeState, it gets to work on fading the volume in whichever direction it needs to go until it reaches the target volume. When the volume is at the target volume, fadeState and targetFadeState will be the same. The target volume is decided by the state of targetFadeState. When targetFadeState is 0, the target volume will be 0. When targetFadeState is 1, the target volume will be the value of the variable volumeON, which was given its value back in the Start() function when the volume was grabbed from PlayerPrefs:

		// deal with volume fade in/out
		if(fadeState!=targetFadeState)
		{
			if(targetFadeState==1)
			{
				if(volume==volumeON)
					fadeState=1;
			} else {
				if(volume==0)
					fadeState=0;
			}

A quick interpolation between the current volume and the targetVolume, with the time it takes to fade decided by Time.deltaTime multiplied by fadeTime:

			 volume=Mathf.Lerp(volume, targetVolume, 		Time.deltaTime * fadeTime);
			source.volume=volume;
		}
	}

The last two functions in the MusicController class set up the fader variables for either fading in or fading out through code. The single parameter fadeAmount is a value for which to decide how long it should fade in or out:

	public void FadeIn (float fadeAmount)
	{
		volume=0;
		fadeState=0;
		targetFadeState=1;
		targetVolume=volumeON;
		fadeTime=fadeAmount;
	}
	public void FadeOut (float fadeAmount)
	{
		volume=volumeON;
		fadeState=1;
		targetFadeState=0;
		targetVolume=0;
		fadeTime=fadeAmount;
	}
}

8.3 Adding Sound to the Weapons

There are two places that may, at first glance, make good sound trigger points for weapons. One is when the fire button is pressed in the player script, and the other is when the projectile itself is created.

Making a sound when the fire button is pressed may seem like a good idea, but to do that, we have to make sure that the projectile actually makes it into the world. After the fire button is pressed, it may be that there is no ammunition left in the weapon or that the weapon is in its reloading period. Making the sound without firing the projectile would be rather silly.

Making a sound when the projectile is spawned may in fact be a better option, as it will only make a sound when a projectile is successfully made. Also, the sound can be tailored to suit the projectile. For example, perhaps a small laser blast has a less powerful sound effect than a larger blast, or perhaps a green laser sounds different from a red laser.

In the example project for this book, the code to play a firing sound from the projectile will already be in place. The code resides within the ProjectileController.cs script.

Note that placing the call to play the projectile sound in the Awake() or Start() function may cause problems because of the time it takes to initialize versus the time it takes for the script creating the projectile to get around to position it correctly in the 3D world. To ensure that the projectile will have been positioned correctly when the audio is played, this code goes into the Update() loop with a simple Boolean flag to stop the sound getting played more than once.

The variable declarations related to the firing effect, to go with the other declarations at the beginning of the script, are as follows:

	private bool didPlaySound;
	private int whichSoundToPlayOnStart= 0;

The code to play the sound is placed at the top of the Update() function. It checks first to make sure that the sound has not already been played and then makes a call out to the BaseSoundController static variable Instance. The PlaySoundByIndex parameters are the contents of the integer whichSoundToPlayOnStart variable and the projectile transform’s position. After the sound call, didPlaySound is set to true to prevent duplicate calls to play:

		if(!didPlaySound)
		{
	BaseSoundController.Instance.PlaySoundByIndex(whichSoundToPlayOnStart, myTransform.position);
			didPlaySound=true;
		}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.70.185