The Music Update - Alpha Version 9

· itch.io ·

6 min read Original article ↗

Commissioning Music

attention grabbing image

Because I have invested so much time into this project, I wanted to add music that would reflect the love and care that I put into it. Also, music and sound in general is more important than visuals. Bad music = Bad game.

This also presented an interesting opportunity to commission someone to make music for my game. Up to this point, I have created every asset for 'Let's Learn!'. I also wanted to challenge myself to collaborate with someone else. I really need to work on my leadership skills if I ever want to make anything interesting and fun.

Finding a Composer

You might ask, "Landen, how did you find a composer?" And the answer is quite simple: I didn't. I asked one of my friends if they knew any composers and they immediately gave me a great recommendation. I commissioned a single song with multiple tracks from Robert Mai. I listened to some of his works on his website and thought they sounded very nice and professional. I sent him an email asking if he would be interested and requested a quote. 

I don't want to give an exact amount, but let's say it was less than $1,000 dollars for the single song. There were multiple tracks included, which allowed for enough variation between replays. Also, Robert was able to do some live recordings for this composition because it lined up with another project he was working on.

Please give 'Let's Learn!' a quick play-through to hear all the song variations!

Adding Music in Godot

As with any new feature I add to my game, this turned out to be way easier than I expected. This is thanks to how easy-to-use and flexible Godot is.

The current implementation is as follows:

  • A new global scene called "BackgroundMusic" handles the current track and transitions.
  • The piano track is looped on the main menu.
  • Each level has a variable called 'Desired Song' which will loop after the current song.
  • Each level has an option to force a transition to the 'Desired Song' after the level finishes loading.

That's about it. Its a very simple implementation that offers flexible track selection.

Fading Between Audio Tracks

I used enums for track selection and a class_name for enum type support.

I borrowed the code for fading audio from a YouTube tutorial. It's very simple.

func _process(delta: float) -> void:
    if _fading:
        volume_db -= fade_speed * delta
        fade_in_stream.volume_db += fade_speed * delta
        if fade_in_stream.volume_db >= 0:
            volume_db = 0
            fade_in_stream.volume_db = -60
            stream = fade_in_stream.stream
            play(fade_in_stream.get_playback_position())
            fade_in_stream.stop()
            _fading = false

I also added a 15 second timer between track loops to prevent a single track from being too repetitive.

I have not had the chance to playtest this setup yet. I estimate it takes a new player around 20 minutes to complete all the current puzzles in the game. I decided to change audio tracks every 2 or 3 levels based on their difficulty. I also tried to place the most interesting audio tracks on the most difficult puzzles, because I wanted players to hear the whole composition while they are stuck solving the puzzle.

Audio Compression with FFmpeg

To minimize the game's file size with all the new music tracks, I used FFmpeg to transcode the original wav files to variable bitrate MP3s. Apparently, you can do this with FFmpeg.

ffmpeg.exe -i input.wav -c:a libmp3lame -q:a 2 output.mp3

This resulted in a single .wav file going from a hefty 35MB to only 2.5MB without any noticeable quality loss.

An audio spectrogram view in Audacity.

Adding a Settings Menu

Now that our game has music, I wanted to add the ability to control the volume of music and sound effects separately. This can be done with dedicated audio buses.

With every AudioStreamPlayer set to the correct bus, I can now control the audio levels via a horizontal slider on my settings menu.

@onready var sfx_bus := AudioServer.get_bus_index("SFX")
@onready var music_bus := AudioServer.get_bus_index("Music")

func _ready() -> void:
    %SFXSlider.value = AudioServer.get_bus_volume_linear(sfx_bus)
    %MusicSlider.value = AudioServer.get_bus_volume_linear(music_bus)

func _on_sfx_slider_value_changed(value: float) -> void:
    AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(value))

func _on_music_slider_value_changed(value: float) -> void:
    AudioServer.set_bus_volume_db(music_bus, linear_to_db(value))


Making a New Campaign

New buttons: 'WIP Start' and 'Settings'

Astute observers might have noticed the new "WIP Start" button on the main menu. This button will let you play the new Work-In-Progress campaign.

I want to remake the campaign for the following reasons

  • To add more elementary school level grammar points
  • To add a basic story with more mascots from neighboring towns
  • To give players a tangible story goal to work towards

Because these new levels are unfinished, I separated them from the more polished levels with the WIP button. Please give them a try and leave feedback via the form in the game's description.

More Intuitive UI

Previously, when holding physics objects, it was sometimes unclear which object you were holding. Because of the orthographic view, players sometimes didn't even realize that they were holding an object. To make this easier to understand, I added a small hand that will follow your held object.

Isn't the new UI adorable?!

I also implemented custom mouse cursors that were provided in the same cursor pack by Kenny.

You can also see the UI hand through walls.

It is very easy to change the mouse cursor in Godot:

Input.set_custom_mouse_cursor(your_resource_here)

Simulating Physics In-Editor

On one of the new WIP levels, I added a lot more fruit that had to sit on shelves at odd angles.

A bunch of fruit on the first Work-In-Progress level.

Instead of manually positioning and rotating all those objects, I wrote a one-off node that you can run in-editor via the @tool annotation. When I press the 'Enable Physics' button, the physics server will be set to 'Active'. After everything has settled, I click the 'Disable Physics' button and save the scene. It's quite handy!

A demo of in-editor physics.

The script is dead simple. It's basically just 'PhysicsServer3D.set_active(boolean)'

@tool
extends Node

@export_tool_button("Enable Physics", "Play") var enable_physics_button = enable_physics
@export_tool_button("Disable Physics", "Stop") var disable_physics_button = disable_physics

func enable_physics() -> void:
    PhysicsServer3D.set_active(true)
    print()
    print_rich("[color=blue]##################################[/color]")
    print_rich("[color=blue]### PHYISCS ENABLED IN EDITOR! ###[/color]")
    print_rich("[color=blue]##################################[/color]")
    print_rich("[color=red]Don't forget to disable physics when finished.[/color]")


func disable_physics() -> void:
    PhysicsServer3D.set_active(false)
    print_rich("[color=#1DB954]########################[/color]")
    print_rich("[color=#1DB954]### PHYISCS DISABLED ###[/color]")
    print_rich("[color=#1DB954]########################[/color]")

NOTE: You might have to save the script and restart your editor for the tool to work properly. Godot's @tool annotation seems to be very fussy.

Removing Internationalization

Because I plan on doing heavy changes, I removed the internationalization that I mentioned in a previous dev log. I might add it back someday, but likely only for essential elements. I am not sure if anyone actually cared about the puzzles having a Japanese version. But it was fun to watch my friends try the Japanese version of the puzzles.

Additional Info

  • There is a new Settings menu. For now, only for controlling audio.
  • Limited fps to 60
  • Known bug: Objects will no longer have shadows after being picked up. Waiting for Godot 4.8 to hopefully solve this.
  • Fixed various bugs

That's all for now. Thanks for reading!