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:
- Import the music into your project.
- 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.
- Open the Battle scene.
- Add a child game object to the Battle Controller called Music.
- Add the MusicPlayer component to the Music game object.
- In the inspector, assign the Intro Clip to use the Strategy RPG Battle_Intro asset.
- In the inspector, assign the Loop Clip to use the Strategy RPG Battle_Loop asset.
- 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.
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.
Have you completed the step to use the file menu and choose βPre Production->Parse Jobsβ?
Oh man, I’ve been following all of this project for the past month.
At times it has been hard to digest, since I am not entirely a programmer, things about pattern design and architecture are sometimes hard for me to understand at the beginning why things are being done that way.
But things are okay now, and I have understood in general how things are structured so that I can modify it further.
I want to thank you so much for this, these tutorials have a lot of value for me.
I had wanted to make a tactics game for a long time, and your work helped me have this basis of tools to focus on the design and the particular implementation details of the game I want to make.
Thanks a lot.
Awesome, glad to hear you stuck to it and made it through ok! Good luck on your game!
Thank you so much for this great tutorial! Can I ask a stupid question at this point (this is the first tutorial I learned about Unity)?
Right now, all we learned is in one scene. If we want to create a game with multiple levels, do we create an independent scene for each level? For example, after finish the battle of the first level in one scene, we enter the second scene for the second level. If yes, how do we transfer from one scene to another? Could you please recommend a tutorial?
Thank you again!
Wow, I’m impressed that you made it through my Tactics RPG series as your first tutorial π
You are correct that many games will use scenes to change levels. You will use the SceneManager for this purpose, so you can read the documentation at that link or google search that name and you will find some tutorials pretty quick.
Is there a download link to try out the full game and not just the demo?
I made this project as a hobbyist, for fun, and as a learning exercise for myself and my readers. Making a full Tactics RPG would require more than I can do by myself – the art requirements alone are daunting, not to mention the rest of the programming, music and sound fx, story writing, game design, marketing, etc. I never turned this project into a full game, I was satisfied feeling like I had an “engine” for one. Of course, the project was inspired by “Final Fantasy Tactics”, so feel free to play any of those if you want more!
Ah okay I understand. It was fun following through the tutorial so I just wanted to know if you made it an actual app, etc. If you wanted to change the stats of the jobs for example HP to 1000 from 32 would you just change the JobStats.csv or would I have to call a separate method like SetValue each time I wanted to change the stats.
I’m glad you enjoyed it! Regarding the stats, it depends on what you are trying to accomplish. Setting stats via the csv was meant to provide an easy way to design/balance the game – you update your stats here because you can see them all in a table so you can easily compare and tweak to your hearts content. If you wanted to design the base values of characters to have a much larger HP range then you could do that here.
After the game has been configured, I might use in-game code for extra variance, special conditions, or to modify stats as gameplay progresses. Does that help?
I just wanted to say that I stumbled on this tutorial several years ago when it first started and I was a complete novice programmer. I was completely lost and didn’t understand a wink of it haha.
Now I work as a programmer professionally and I thought I’d come back to it since Final Fantasy Tactics is my favorite game of all time and while I didn’t make a tactics style game, I did adapt all the lessons to a traditional JRPG turn based game and it would not have been possible without this amazing blog.
Thanks so much for sharing all of your knowledge and insights!
Going from not understanding any of it, to understanding it well enough to adapt it to your own project is huge. Glad you kept at it, and you’re very welcome!
I wonder when you will fully complete this project I would love to see this completed
The Tactics RPG project was always only meant to be for learning purposes. I don’t have the time or budget to flesh out a full RPG.
I am wondering since the music is implemented would there be a way to implement some simple sound effects such as walking and attacks?
Adding sound effects is a simple enough process, though you need permission or license for the assets, which is one reason my tutorials are kept so simple. I have only provided what I could make myself or which has been offered freely. Unity has a bunch of great training, including assets to learn with:
learn.unity.com/tutorial/sound-effects-scripting