Sponsored By

Dynamic music in a hurry - creating an adaptive music system for Oshka

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.

Game Developer, Staff

December 11, 2018

9 Min Read

 

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:

 

Dynamic system on a static budget

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.

 

Lo-fi = ship fast

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!

 

Issues with this method

  • 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.

 

Benefits of this method

  • 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!

 

Final thoughts

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 Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like