Rhythm Quest Devlog 8 — Menu Rework

Before and After

Here’s a video showing roughly what the menu looked like to start with:

New Button Graphics

The old buttons were designed a looooooong time ago and basically referenced the plain blue window style in some of the Final Fantasy games:

Music Reactivity

One of the main goals for this entire rework was to add some sort of music reactivity to the menu system. During the ~1 year period where I had stopped working on Rhythm Quest, I had been doing a lot of thinking about how to make an interesting and appealing menu system without involving a ton of work (i.e. beautiful art which I’m incapable of drawing). The answer ended up being to bring the “music-first” ethos of the gameplay into the rest of the game.

Beat Sync

Fortunately, music synchronization was already more or less problem for me at this point. See Devlog #4 for an explanation of how that works. I haven’t explained how to tie it to the actual beat of the music, but that’s not too hard:

float GetIntensity(float offset, float patternLength) {
// (Gets the current time, then converts from time to beat)
float beatOffset = MusicController.CurrentBeat();

// Wrap based on beat pattern length and take the difference from our target.
// (note: the % operator will give negative values in C#, so I use a wrapper)
beatOffset = Utils.ModPositive(beatOffset - offset, patternLength);

// Normalize to 0-1 based on duration of the pulse.
float pulseProgress = Mathf.Clamp01(beatTime / _pulseDuration);

// Apply some easing (ease out quad):
pulseProgress = 1.0f - (1.0f - progress) * (1.0f - progress);

// Invert so that we go from 1 to 0 instead of 0 to 1.
return 1.0f - pulseProgress;
_beatDuration * beatLength

Music Transitions

Transitions between menu screens are done via a simple slide animation. As I mentioned earlier, different menus can also have different background music loops:

// (Note that this time will never be "exact" since 
// AudioSettings.dspTime runs on a separate timeline)
float currentTime = (float)(AudioSettings.dspTime - _audioDspStartTime);
// (Simple conversion that uses the BPM of the song)
float currentBeat = _song.TimeToBeat(currentTime);
// Find the next downbeat.
float transitionEndBeat = Mathf.CeilToInt(currentBeat);
float transitionEndTime = _song.BeatToTime(transitionEndBeat)
float transitionDuration = transitionEndTime - currentTime;
// We could add the buffer in terms of beats or in terms of seconds.
// Either way is equivalent here since the entire main menu (currently) has constant BPM.
float transitionEndBeat = Mathf.CeilToInt(currentBeat + 0.5f);
  • A transition sweep sfx starts playing immediately at the start of the transition
  • The new music loop needs to be scheduled to kick in at the end of the transition
  • I also schedule a “landing” impact sfx at the end of the transition
  • The old music loop needs to be stopped at the end of the transition
  • The transition sweep sfx fades out quickly during the last sixteenth note of the transition (quarter of a beat)
// Calculate transition "fade start" time, when we want to start
// fading the sweep sfx.
float transitionFadeTime = _song.BeatToTime(transitionEndBeat - 0.25f);
float fadeDuration = _song.BeatToTime(0.25f);
// Play the transition sweep sfx immediately.
// Retain a handle to the AudioSource so we can fade it.
AudioSource sweepAudio = AudioManager.PlaySound(_sweepSfx);
// Schedule landing sfx for end of transition.
AudioManager.PlayScheduled(_transitionEndSfx, _audioDspStartTime + transitionEndTime);
// Schedule new music loop for end of transition.
// We need to queue it up at the appropriate offset first!
_audioSources[newMusicIndex].time = transitionEndTime % _audioSources[newMusicIndex].clip.length;
_audioSources[newMusicIndex].PlayScheduled(_audioDspStartTime + transitionEndTime);
// Loop while transition is happening...
while (AudioSettings.dspTime < _audioDspStartTime + transitionEndTime) {
// How far are we through the fade section?
float timeWithinFade = AudioSettings.dspTime - _audioDspStartTime - transitionFadeTime;
float fadeProgress = Mathf.Clamp01(timeWithinFade / fadeDuration);
// I use an exponent to affect the easing on the fade.
// An exponent of 0.25 makes the fade happen more on
// the tail end (ease in).
sweepSource.volume = Mathf.Pow(1.0f - fadeProgress, 0.25f);
yield return new WaitForEndOfFrame();
// Transition should be done now. Stop the old music loop.

Other Stuff

That’s about all that I’ll cover here, but I want to stress that there is a ton of other miscellaneous work involved here that I haven’t even talked about. Very briefly, this includes things such as:

  • Allowing for menu navigation with keyboard, mouse, gamepad, OR touch input
  • Smartly handling button auto-selection depending on input device (if using keyboard/gamepad, the first option should be highlighted, otherwise not)
  • Supporting localization for all text in the menus, including dynamic text
  • Supporting screen readers so that visually impaired persons can navigate the menu
  • Disallowing menu input while a transition is happening
  • Remembering previous menu selection (returning to a previous menu should preserve the selection state)
  • Allowing for the menu scene to be loaded to a certain state (i.e. when returning from a level, it should have that level preselected)
  • etc…



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store