Tactics RPG Music

It’s been almost a year since the last post, but I finally have a reason to revisit this project. Brennan Anderson wrote some amazing music after following along with the Tactics RPG project and was generous enough to share it with the rest of us. Thanks to him, we will go ahead and add a follow-up post that describes working with music.

Audio Animation

Audio is just data and it has attributes that you can and probably will animate. Primarily you will animate volume. This can allow you to fade a track in or out, or crossfade between two music tracks, etc. We already have a reusable animation system in this project which we can easily extend for this feature. Add a new script called AudioSourceVolumeTweener and copy the following:

using UnityEngine;
using System.Collections;

public class AudioSourceVolumeTweener : Tweener 
{
	public AudioSource source 
	{
		get 
		{
			if (_source == null)
				_source = GetComponent<AudioSource>();
			return _source;
		}
		set
		{
			_source = value;
		}
	}
	protected AudioSource _source;

	protected override void OnUpdate () 
	{
		base.OnUpdate ();
		source.volume = currentValue;
	}
}

Because the Tweener inherits from an EasingControl, it already has startValue, currentValue, and endValue fields. All we need is a float value to animate the volume of an audio source, so we can use these values directly – we simply pass the currentValue of the tweener to the AudioSource’s volume field in the OnUpdate callback and we’re done!

In order to trigger the animation of an AudioSource’s volume, it would be nice to add some more extensions like we have done for animating transforms, etc. Add another script named AudioSourceAnimationExtensions and copy the following:

using UnityEngine;
using System;
using System.Collections;

public static class AudioSourceAnimationExtensions 
{
	public static Tweener VolumeTo (this AudioSource s, float volume)
	{
		return VolumeTo(s, volume, Tweener.DefaultDuration);
	}

	public static Tweener VolumeTo (this AudioSource s, float volume, float duration)
	{
		return VolumeTo(s, volume, duration, Tweener.DefaultEquation);
	}

	public static Tweener VolumeTo (this AudioSource s, float volume, float duration, Func<float, float, float, float> equation)
	{
		AudioSourceVolumeTweener tweener = s.gameObject.AddComponent<AudioSourceVolumeTweener>();
		tweener.source = s;
		tweener.startValue = s.volume;
		tweener.endValue = volume;
		tweener.duration = duration;
		tweener.equation = equation;
		tweener.Play ();
		return tweener;
	}
}

Hopefully this pattern will look familiar, we simply overloaded the VolumeTo method with a few different sets of parameters so you could be increasingly specific about “how” the volume changed. You may only care about the target volume level, but you might also want to choose how long it takes to get there or with what kind of animation curve it animates along. The less specific versions pass default values to the most specific version so that you only really implement the function once.

Cross Fade Demo

For example sake, here is a sample script which cross fades between two audio sources using our new Tweener subclass and extension. This script wont be included in the repository and it is included merely to demonstrate the potential use of our new feature.

using UnityEngine;
using System.Collections;

public class CrossFadeAudioDemo : MonoBehaviour 
{
	[SerializeField] AudioSource fadeInSource;
	[SerializeField] AudioSource fadeOutSource;

	void Start () 
	{
		fadeInSource.volume = 0;
		fadeOutSource.volume = 1;

		fadeInSource.Play();
		fadeOutSource.Play();

		fadeInSource.VolumeTo(1);
		fadeOutSource.VolumeTo(0);
	}
}

If you would like to test this demo, I would recommend you create a new scene. Next, add two audio sources which are preconfigured to use different audio clips. I set both of the audio sources to NOT play on awake so I could configure them first. Don’t forget to hook up the references for them to this script in the inspector. Press play. When the scene starts, it will configure one of the sources to have no volume and fade in, while the other will start at full volume and fade out. If you like you can add additional parameters to the VolumeTo statements such as providing a longer duration so that the effect is more obvious.

Audio Events

One feature I would love to see in Unity is a greater use of event driven programming. For example, it would be great to know when an audio source loops or completes playing. Lacking that, I can accomplish what I need with either a scheduled callback or a polling system. To schedule a callback you can use something like MonoBehaviour.Invoke and or MonoBehaviour.InvokeRepeating as a replacement for the lack of any completion event on the audio source. If you’re curious, those snippets might look something like the following:

float delay = source.clip.length - source.time;
if (source.loop)
	InvokeRepeating("AudioSourceLooped", delay, source.clip.length);
else
	Invoke("AudioSourceCompleted", delay);

Unfortunately I found that this was a pretty fragile approach. One problem is that an Audio Clip’s length in seconds doesn’t necessarily equate to how long an Audio Source will spend playing it. For example, if the pitch of an audio source is modified, then it can play the clip in more or less time depending on the new pitch.

Because I didn’t feel like running a bunch of tests on all of the variety of things which could potentially modify time in one form or another to mess up the timing with the invoke call, I decided to use the polling approach instead. This pattern is achieved through a coroutine. Add a new script called AudioTracker and copy the following:

using UnityEngine;
using System;
using System.Collections;

public class AudioTracker : MonoBehaviour {
	#region Actions
	// Triggers when an audiosource isPlaying changes to true (play or unpause)
	public Action<AudioTracker> onPlay;

	// Triggers when an audiosource isPlaying changes to false without completing (pause)
	public Action<AudioTracker> onPause;

	// Triggers when an audiosource isPlaying changes to false (stop or played to end)
	public Action<AudioTracker> onComplete;

	// Triggers when an audiosource repeats
	public Action<AudioTracker> onLoop;
	#endregion

	#region Fields & Properties
	// If true, will automatically stop tracking an audiosource when it stops playing
	public bool autoStop = false;

	// The source that this component is tracking
	public AudioSource source { get; private set; }

	// The last tracked time of the audiosource
	private float lastTime;

	// The last tracked value for whether or not the audioSource was playing
	private bool lastIsPlaying;

	const string trackingCoroutine = "TrackSequence";
	#endregion

	#region Public
	public void Track(AudioSource source) {
		Cancel();
		this.source = source;
		if (source != null) {
			lastTime = source.time;
			lastIsPlaying = source.isPlaying;
			StartCoroutine(trackingCoroutine);
		}
	}

	public void Cancel() {
		StopCoroutine(trackingCoroutine);
	}
	#endregion

	#region Private
	IEnumerator TrackSequence () {
		while (true) {
			yield return null;
			SetTime(source.time);
			SetIsPlaying(source.isPlaying);
		}
	}

	void AudioSourceBegan () {
		if (onPlay != null) {
			onPlay(this);
		}
	}

	void AudioSourceLooped () {
		if (onLoop != null)
			onLoop(this);
	}

	void AudioSourceCompleted () {
		if (onComplete != null)
			onComplete(this);
	}

	void AudioSourcePaused () {
		if (onPause != null)
			onPause(this);
	}

	void SetIsPlaying (bool isPlaying) {
		if (lastIsPlaying == isPlaying)
			return;
		
		lastIsPlaying = isPlaying;

		if (isPlaying)
			AudioSourceBegan();
		else if (Mathf.Approximately(source.time, 0))
			AudioSourceCompleted();
		else
			AudioSourcePaused();

		if (isPlaying == false && autoStop == true)
			StopCoroutine(trackingCoroutine);
	}

	void SetTime (float time) {
		if (lastTime > time) {
			AudioSourceLooped();
		}
		lastTime = time;
	}
	#endregion
}

When you use this script it will cause a coroutine to track the playback of the audiosource on a frame by frame basis. Note that this means you won’t catch the exact moment that a bit of audio has completed or looped, but it should at least be very close – a game even running at 30 fps would be within a few hundreths of a second in accuracy. I would also point out that even if you could get an event at the exact moment an audio track completes that you would be unlikely to do much anyway since it would occur outside of unity’s execution thread and you wouldn’t be able to interact with any Unity objects.

It is important to note that several of the callbacks can be invoked by more than one audio event. For example, you would get the onPlay callback anytime the audiosource changes the isPlaying flag to true. This can happen either when Playing an audiosource for the first time, or as a result of Unpausing a paused audiosource. If you needed to know for certain how a callback was obtained (such as differentiating between “unpause” and “play”, or between a play to the end and “stop”) then you would need to wrap the relevant AudioSource methods. For example, you could implement a “Stop” method on the tracker, which then tells the tracked source to “Stop”, so that you would now be able to determine you had manually stopped playback instead of letting it play to the end and stopping on its own. I decided not to wrap these calls because it would be too easy to forget to use them and missed expectations might lead to some frustrating logic bugs.

I feel a lot more comfortable with this version over “Invoke”, because it doesn’t make any assumptions about the timing of the audio… well except for looping. You could always set the playback time manually which could cause the script to think it had looped. Otherwise, it should handle all of the use cases I can think of off the top of my head.

Loop Demo

Like the earlier demo, the following script also wont be included in the repository and it is included merely to demonstrate the potential use of audio events for looping and completion. In this demo, I setup a temporary scene with two audio sources. One was configured with the sound of a laser blast, and the other an explosion. Both audiosources were set not to play on awake, and the laser had loop enabled.

If you setup a similar scene and play it, you will see that the laser sound will play some random number of times (based on the loopCount variable) and on each loop, the loopStep variable will increment and I will change the pitch of the laser so that the next play through happens in a different amount of time (but also adds a nice bit of variance – you could do this for a lot of sound fx like footsteps, etc). When the desired number of loops has been achieved we disable the looping and wait for the audio source to complete. When that event is triggered I tell the explosion audio source to play.

using UnityEngine;
using System.Collections;

public class LoopDemo : MonoBehaviour 
{
	[SerializeField] AudioSource laser;
	[SerializeField] AudioSource explosion;

	AudioTracker tracker;
	int loopCount, loopStep;

	void Start () {
		loopCount = Random.Range(4, 10);

		tracker = gameObject.AddComponent<AudioTracker>();
		tracker.onLoop = OnLoop;
		tracker.Track(laser);

		laser.Play();
	}

	void OnLoop (AudioTracker sender) {
		laser.pitch = UnityEngine.Random.Range(0.5f, 1.5f);
		loopStep++;
		if (loopStep >= loopCount) {
			laser.loop = false;
			tracker.onComplete = OnComplete;
		}
	}

	void OnComplete (AudioTracker sender) {
		explosion.Play();
	}
}

Audio Sequence

The music that Brennan provided isn’t a normal music track – what I mean is that he provided two different assets that are meant to be used together. There is an intro music track, followed by a loopable music track. The loopable portion should play when the intro completes, and then continue playing for as long as this scene is active. Unfortunately this creates a particular problem for Unity, because Unity is not event driven and doesn’t allow you to interact with it on a background thread.

You might consider using the AudioTracker to accomplish this task, but it isn’t the ideal solution. The actual playback of the audio can complete in-between frames and in order to continue on with the next track without any noticeable hitches we will have to use another method Unity provides instead – PlayScheduled. This handy method has the benefit of making sure that music can begin even between frames and also that it will already be loaded and ready when the time comes to begin playing. Unfortunately, it isn’t a very smart method and requires a lot of hand holding and assumptions that I had hoped to avoid. To make things trickier, an AudioSource doesn’t provide a field representing its current state, or a variety of other important bits of data (at least not that I am aware of – feel free to correct me). Here are some gotchas I encountered:

  • isPlaying will return true even while it is waiting to play (because it is scheduled) but of course you wont hear anything, nor will the time field be updated
  • isPlaying will return false when it is paused and when it is stopped
  • UnPause will cause a paused audiosource to set isPlaying back to true, but not a stopped audiosource
  • There is no field that indicates the difference between a paused or stopped audiosource
  • There is no field indicating whether an audiosource is currently scheduled to play or not
  • There is nothing to tell you when a scheduled audiosource is scheduled to begin
  • You can pause a scheduled audiosource, but it doesn’t delay the scheduled start time accordingly

In order to help manage all of this I created a few new classes. Create a new script called AudioSequenceData and copy the following:

using UnityEngine;
using System.Collections;

public class AudioSequenceData {

	#region Fields & Properties
	public double startTime { get; private set; }
	public readonly AudioSource source;

	public bool isScheduled { 
		get { 
			return startTime > 0; 
		}
	}

	public double endTime { 
		get { 
			return startTime + source.clip.length;
		}
	}
	#endregion

	#region Constructor
	public AudioSequenceData (AudioSource source) {
		this.source = source;
		startTime = -1;
	}
	#endregion

	#region Public
	public void Schedule (double time) {
		if (isScheduled)
			source.SetScheduledStartTime(time);
		else
			source.PlayScheduled(time);
		startTime = time;
	}

	public void Stop () {
		startTime = -1;
		source.Stop();
	}
	#endregion
}

This class helps to control and track information on a single AudioSource. While Unity provided methods to schedule them, they didn’t provide a way to check when it was scheduled after the fact (again unless I missed it somewhere). Using this class, I can schedule a clip to play at a specific time, but then if I need to reschedule it, it will know it had already been scheduled and use the appropriate method to modify the schedule instead.

Next, we need something that can manage a list of these Data objects, and also manage pausing and resuming the sequence so that future scheduled clips will still play when you expect them to. Create a new script named AudioSequence and copy the following:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class AudioSequence : MonoBehaviour {

	#region Enum
	private enum PlayMode
	{
		Stopped,
		Playing,
		Paused
	}
	#endregion

	#region Fields
	Dictionary<AudioClip, AudioSequenceData> playMap = new Dictionary<AudioClip, AudioSequenceData>();
	PlayMode playMode = PlayMode.Stopped;
	double pauseTime;
	#endregion

	#region Public
	public void Play (params AudioClip[] clips) {
		if (playMode == PlayMode.Stopped)
			playMode = PlayMode.Playing;
		else if (playMode == PlayMode.Paused)
			UnPause();

		double startTime = GetNextStartTime();
		for (int i = 0; i < clips.Length; ++i) {
			AudioClip clip = clips[i];
			AudioSequenceData data = GetData(clip);
			data.Schedule(startTime);
			startTime += clip.length;
		}
	}

	public void Pause () {
		if (playMode != PlayMode.Playing)
			return;
		playMode = PlayMode.Paused;

		pauseTime = AudioSettings.dspTime;
		foreach (AudioSequenceData data in playMap.Values) {
			data.source.Pause();
		}
	}

	public void UnPause () {
		if (playMode != PlayMode.Paused)
			return;
		playMode = PlayMode.Playing;

		double elapsedTime = AudioSettings.dspTime - pauseTime;
		foreach (AudioSequenceData data in playMap.Values) {
			if (data.isScheduled)
				data.Schedule( data.startTime + elapsedTime );
			data.source.UnPause();
		}
	}

	public void Stop () {
		playMode = PlayMode.Stopped;
		foreach (AudioSequenceData data in playMap.Values) {
			data.Stop();
		}
	}

	public AudioSequenceData GetData (AudioClip clip) {
		if (!playMap.ContainsKey(clip)) {
			AudioSource source = gameObject.AddComponent<AudioSource>();
			source.clip = clip;
			playMap[clip] = new AudioSequenceData(source);
		}
		return playMap[clip];
	}
	#endregion

	#region Private
	AudioSequenceData GetLast () {
		double highestEndTime = double.MinValue;
		AudioSequenceData lastData = null;
		foreach (AudioSequenceData data in playMap.Values) {
			if (data.isScheduled && data.endTime > highestEndTime) {
				highestEndTime = data.endTime;
				lastData = data;
			}
		}
		return lastData;
	}

	double GetNextStartTime () {
		AudioSequenceData lastToPlay = GetLast();
		if (lastToPlay != null && lastToPlay.endTime > AudioSettings.dspTime)
			return lastToPlay.endTime;
		else
			return AudioSettings.dspTime;
	}
	#endregion
}

At the top of this script we provided a PlayMode enum that could track the state of the whole sequence – whether Stopped or Playing etc. This helps overcome the lack of state information on AudioSources but also helps because this script manages multiple audiosources, some which may have already completed (and therefore be stopped).

When you want to add one or more AudioClips to the sequence, just call Play and pass them along. It shouldn’t matter if the sequence is already playing or paused, it will still add them to the end of the list and schedule them for playback accordingly.

I also provided Pause and Unpause which provide a convenient way to temporarily stop playback of an audiosource. This wont stop a scheduled playback, but it will reschedule the playback when you resume playing so that each track will play one after the other.

If you want to stop playback, including the scheduling of playback, you can use the Stop method.

You can get the AudioSequenceData for any clip by using the GetData method. This can let you know whether or not a clip is scheduled to play, and when it should start and stop playing. For the most part you probably wont need this, but its there for special cases.

The private method, GetLast returns the audio source that has the latest end time. It will be used to figure out the new start time of a clip which you would want to play at the end of the sequence.

The private method, GetNextStartTime will return the endTime of the last audio clip in the list if there is one – but it is possible that the endTime has completed in the past. To be safe, the method will return only values that are greater than or equal to the current AudioSettings.dspTime value so that new calls to play will start now or in the future.

Music Player

Now that we have a way to seamlessly play two (or more) music tracks together, I wanted to create a simple component that could automatically play music just like Brennan provided it. Using this script, it should be about as easy to setup your music as it would have been if it were a single file. Add a new script called MusicPlayer and copy the following:

using UnityEngine;
using System.Collections;

public class MusicPlayer : MonoBehaviour {
	public AudioClip introClip;
	public AudioClip loopClip;
	public AudioSequence sequence { get; private set; }

	void Start () {
		sequence = gameObject.AddComponent<AudioSequence>();
		sequence.Play(introClip, loopClip);
		AudioSequenceData data = sequence.GetData(loopClip);
		data.source.loop = true;
	}
}

Now we just need to incorporate this script and the music assets into our game:

  1. Import the music into your project.
  2. Set the “Load Type” for both assets to be Streaming. This will help keep memory requirements lower and is a good idea for all music.
  3. Open the Battle scene.
  4. Add a child game object to the Battle Controller called Music.
  5. Add the MusicPlayer component to the Music game object.
  6. In the inspector, assign the Intro Clip to use the Strategy RPG Battle_Intro asset.
  7. In the inspector, assign the Loop Clip to use the Strategy RPG Battle_Loop asset.
  8. Press play and enjoy the new music!

Extra

As a side note, if you use an audio mixer (new in Unity 5), you can globally adjust the volume or audio effects of any audio source that uses it. This setup requires little more than an exposed parameter and a UI script on your canvas to modify it – be sure to check out Unity’s nice video tutorials that show how. This solves most if not all of my other needs for an Audio Controller such as knowing when to mute or change volume for music and or sound fx.

Summary

In this post we provided several reusable components related to audio and music in particular. First we created a new Tweener to allow us to programmatically fade in or out music using any specified volume, duration and animation curve we desire. Then we created a script which tracked the playback of an audio source via a couroutine so that you could get callbacks for audio based events like when it begins playing, stops playing, loops or completes. Finally we created a system that could allow us to play a sequence of audioclips without any gaps – perfect for playing the new music assets that we added to the project.

All of these scripts are “fresh” (read as “not battle tested” or “use at your own risk”) but should provide a helpful starting point at a minimum. If you find any bugs let me know and I’ll attempt to fix it.

Don’t forget that the project repository is available online here. If you ever have any trouble getting something to compile, or need an asset, feel free to use this resource.

Advertisements

2 thoughts on “Tactics RPG Music

  1. Followed everything to the point, and everything seems like it should be working, however, I come across two problems when starting the battle scene. I get two errors. One is

    NullReferenceException: Object reference not set to an instance of an object
    UnitFactory.AddJob (UnityEngine.GameObject obj, System.String name) (at Assets/Scripts/Factory/UnitFactory.cs:65)

    the other error i get is

    No Prefab for name: Jobs/Warrior
    UnityEngine.Debug:LogError(Object)

    Any idea where I went wrong..I am at the end of the tutorial and everything seems like it should be great. but I cant figure this out.

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s