Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
In Oshka, an endless Matryoshka doll stacker for iOS devices, we wanted to create a gameplay-adapting music system that matched the Russian folk aesthetic of the game. In this article I’ll describe the design process and code that makes the system tick.
In Oshka, an endless Matryoshka doll stacker with a cute folk-art aesthetic for iOS devices, we wanted to create music that matched the Russian folk aesthetic of the game while also reacting dynamically to gameplay. In this article I’ll describe the process I took to in doing this and walk through the code that makes it tick.
For some quick background, Oshka is the first game developed by Moth Likely - a Melbourne, Australia based micro-game studio comprised of myself and Jair McBain. For this project I handled the code/music/audio and shared game design responsibilities with Jair, who also did all of the art, animation, and UI/UX. The final result and gameplay can be seen below, and if you’d like to try out the game you can get it from the app store here: https://apple.co/2NZ7ArP.
Here’s a short trailer for those that want the TL;DP (too long; didn’t play) version:
In Oshka, players are tasked with launching Matryoshka dolls in an attempt to build the tallest doll tower possible. As the tower of dolls stacks higher, the speed at which the dolls wobble before launch increases, making successful jumps more challenging to achieve. We felt that to have the core loop feel more exciting we would need to increase the tempo of the music to match the increasing speed of the doll wobble.
To create a music system with a dynamic tempo, my initial plan was to implement a sequencer/sampler combo in Unity and sequence all of the music within the editor. As this method involves an internal metronome and manually triggering individual samples, it would allow us to increase the tempo of the music exactly in step with gameplay without any pitch shifting artefacts. (Check out this great tutorial series by Charlie Huguenard on how you can go about implementing a system like this if you’re interested.)
However, due to its complexity and our limited development time of 2 months (the reasoning for which you can read about in Jair’s great postmortem), I decided the sequencer method was probably going to be too complex. It would take a bunch of editor scripting to design an interface that would allow for a streamlined workflow, there would be many moving parts in code meaning a higher chance of bugs, and I also wasn't sure how well iOS would play with Unity's OnAudioFilterRead() method, which the above technique relies on.
With our time constraints in mind, I opted for a more lo-fi, brute force approach. After some testing I discovered that I could achieve a similar result to the sequencer technique by increasing the tempo of the song at either the start or midpoint of the song loop every time the player made 4 successful jumps. I wouldn’t have the accuracy of being able to increase the tempo at any beat as with the sequencer method, but the difference was small enough that testers didn’t notice. So I created the final song loop in Ableton, split it into two halves and then exported these at increasingly faster BPMs. The end result from this process involved exporting each half snippet 11 times at intervals of 10 BPM, for a total of 22 individual sound files ranging from 100 bpm to 200 bpm. You can hear the two snippets at 100 BPM here:
Oshka theme A - 100 BPM:
Oshka theme B - 100 BPM:
To describe how this works in simple terms, we want to loop through Clip A followed by Clip B, then back to Clip A and so on ad infinitum. At the end of any of these clips we want to check if we should be increasing the BPM. If so, the next clip we get will be at a higher BPM. So if we start with a 100 BPM version of Clip A and before this clip finishes the player makes 4 successful jumps, the next clip we play will be a 110BPM version of Clip B. We will then continue using 110BPM clips (back and forth between Clips A and B) until the player jumps successfully another 4 times, at which point we will start using clips at 120BPM.
System Overview
To get this to work in Unity, first I create two AudioClip arrays - one for all of the A Clips, one for all the B Clips - and populated them in the editor in order of BPM from slowest to fastest.
In code I create a coroutine that we’ll loop over while the music is playing. We define our AudioClip variable and then check to see which song section we should be playing - A or B. We check this by iterating over an integer that starts at 0. If we are at 0, we get a clip from song section A and increase the integer by 1. If we are at 1, we get a clip from song section B and set the integer back to 0. We then call a method to get the audio clip at a BPM defined by the current player jump count.
[See the full code here]
IEnumerator PlayGameMusic() { // let's loop 4 eva while (shouldGameMusicBePlaying) { AudioClip clipToPlay = null; // check which section we should play. 0 = A, 1 = B if (currentSongSectionIndex == 0) { // get the clip A at index (and therefore BPM) // relative to current player jumps clipToPlay = songClipsA[GetSongIndexFromJumpCount()]; // increment song section index so it equals 1 // and we play a Clip B next currentSongSectionIndex++; } else { // get the clip B at index (and therefore BPM) // relative to current player jump count clipToPlay = songClipsB[GetSongIndexFromJumpCount()]; // set song section index back to 0 so next time we play a Clip A currentSongSectionIndex = 0; } ...
Finally, we yield on a new coroutine that plays the desired AudioClip. Once this clip has finished playing we repeat the whole loop again.
... // start a coroutine that plays the next clip, // repeat this loop when it's done playing yield return StartCoroutine(PlayClip(clipToPlay)); } }
Deciding which BPM audio clip to play
In our GetSongIndexFromScore() method, we move to a faster set of audio clips every time the player makes 4 successful Jumps. We calculate this simply by dividing the current jump count by 4 and using the result as the index in our current AudioClip array (either for Clip A or Clip B). We check to make sure we haven’t gone out of bounds of our array and then return the result.
int GetSongIndexFromJumpCount() { int currentJumpCount = 0; // get the player's current jump count currentJumpCount = PlayState.Instance.GetJumpCount(); // get an index based on the current jump count divided by 4 int index = currentJumpCount / jumpDivision; // if the result is higher than the length of our audio clip arrays, // just get the last element if (index >= songClipsA.Length) { index = songClipsA.Length - 1; } // return the clip return index; }
And finally, our PlayClip coroutine simply looks like this:
private IEnumerator PlayClip(AudioClip clipToPlay) { // stop the audiosource if it was already playing audioSource.Stop(); // set the next clip to play audioSource.clip = clipToPlay; // playyyyy! audioSource.Play(); // while the clip is playing we yield while (audioSource.isPlaying) { yield return null; } }
And there you have it! Looping music that speeds up as the player progresses through the game without building your own sequencer in Unity!
As we have to end each clip at 0db to prevent clipping, we can get an awkward silence between clips that we wouldn't get using other methods. I was able to mitigate this somewhat with Unity's reverb, letting the tail end of each clip overlap the next.
We don't have the high level of control that we would have had with a sequencer, but it works well enough that the Player doesn't notice.
As we are converting the player’s jump count directly into an array index, we do run the risk of the player jumping ahead in tempo faster than expected, which can sound jarring (e.g. if they manage to make 8 jumps before the current audioclip finishes, the next array index will be two steps ahead of the current, meaning we will move to an audio clip 20 BPM faster than the current instead of the normal 10 BPM increase). I’m sure there’s a simple fix to this, given more time and brain space.
Simplicity - this ended up being a single class comprised of a few coroutines. This alone made it worth it due to the time I ultimately saved.
Allowed me to use the streamlined workflow of a standalone DAW that I wouldn't have had access to with the sequencer method without a heap of Unity editor scripting.
Achieves gameplay adaptivity (e.g. tempo matching gameplay) and has the desired effect for the player - it does the trick!
For me, this is an expression of the “good enough” design philosophy - a good example of how we can save time by looking for simple ways of doing things. It may not be the most impressive solution, but complexity for complexity’s sake often makes finishing a project harder. As cool as it would have been to implement a sequencer/sampler combo in Unity, I saved us a lot of time and achieved a good outcome with a much simpler method - “good enough” is great.
Thanks for reading! If you have any questions, comments, thoughts I'd love to hear them so please post below or hit me up on twitter @AdrianGenerator!
And once more, Oshka is available for download on the App Store now: https://apple.co/2NZ7ArP
Read more about:
Featured BlogsYou May Also Like