00:00
00:00
3p0ch
If you like hard games try my Daxolissian System series

plasmid @3p0ch

Cat

Scientist

Read the manual & try stuff

So Cal

Joined on 2/13/10

Level:
13
Exp Points:
1,650 / 1,880
Exp Rank:
38,330
Vote Power:
5.48 votes
Audio Scouts
1
Rank:
Portal Security
Global Rank:
23,500
Blams:
50
Saves:
375
B/P Bonus:
8%
Whistle:
Normal
Medals:
4,624
Supporter:
3y 3m 14d

Godot tutorial - All the King's Men - Sound and finishing touches

Posted by 3p0ch - September 8th, 2022


How I handle sound (not with nodes)

While for the most part I like Godot, the one thing that's still an issue is the quality of music playback in HTML5 exports. I found a way to improve music playback but it's complicated and you might not be comfortable doing it as a beginner, so I'll show you the normal way first and then show you how I do it.


Usually you'll probably want your background music to keep playing continuously when the scene changes, but if you play your music through an AudioStreamPlayer node that's in a scene then the music player will be wiped out whenever the scene changes and the new scene will start playing music from the beginning. That's sometimes also true with sound effects: for example if your Main Menu plays a nice little sound when the player clicks buttons, then the Start New Game button will run into issues because the scene will change right afterward and the sound effect will be cut short while you would probably rather have it persist and finish playing even if that makes it stretch across the scene change. So we're going to use the same trick we used to keep data when the scene changes: we're going to make an Autoload that plays sound.


Making a SoundPlayer autoload

Here's code that you can put in a new script called res://scripts/sound_player.gd and then add to the list of Autoloads in Project – Project Settings – Autoload tab.

extends Node

var new_asp:AudioStreamPlayer

func _ready():
	pause_mode = Node.PAUSE_MODE_PROCESS

func play(stream:AudioStream):
	new_asp = AudioStreamPlayer.new()
	add_child(new_asp)
	new_asp.pause_mode = Node.PAUSE_MODE_PROCESS
	new_asp.stream = stream
	new_asp.connect("finished", new_asp, "queue_free")
	new_asp.play()

It creates a variable called new_asp which will act as a reference to AudioStreamPlayer nodes that you generate from code. Before diving into it too much, you could go to your Main scene and add an AudioStreamPlayer node to its hierarchy like usual and play around with it a little bit to see how it works and what properties it has available to mess with. Drag the file for the background music, good_guys_army.ogg, into the res://main folder and add that to the AudioStreamPlayer's Stream property to test it out. It may also help you understand what this code is doing.


This is pretty much the simplest possible implementation of a player that just has one function, play(). It takes an AudioStream as a parameter. An AudioStream is just the data from a sound or music file, like the data from good_guys_army.ogg when you put it in the AudioStreamPlayer node's Stream property earlier, so when you call this from your own code you could do it like

SoundPlayer.play(preload("res://main/good_guys_army.ogg"))

The preload() function converts the file into the data format that Godot expects, in this case converting the sound file into an AudioStream.


When you call the play() function, the first line creates a new AudioStreamPlayer node and makes the new_asp variable refer to it. The next line adds it as a child to the Autoload. Next line is so music keeps playing even if the game is paused. The next line takes the variable named stream (the variable holding the AudioStream data that we'll use as a parameter when we call the function) and puts it in the new AudioStreamPlayer's Stream property, just like dragging the .ogg file into the AudioStreamPlayer node's Stream property earlier. The next line connects the new AudioStreamPlayer's signal called "finished", which is triggered when the audio is done playing (unless it's set to loop in which case it's never triggered), and connects that to the queue_free() function in the new_asp node. That's how you can force the child AudioStreamPlayer to queue_free() itself when it finishes playing instead of becoming a memory leak. And the last line of course makes the node start playing.


Now if you want to make the background music play continuously throughout the game like I did, you can get rid of that AudioStreamPlayer node you added to the Main scene to test stuff out, and do one of two things.

Option 1: At the end of the main scene's _ready() function add the line

SoundPlayer.play(preload("res://main/good_guys_army.ogg"))

Option 2: Add this variable declaration at the beginning of the Main script

export var bgm:AudioStream

And add this line at the end of the _ready() function

SoundPlayer.play(bgm)

Where the advantage of option 2 is that if you have an exported variable with the AudioStream and you move the file around or rename it within Godot then it will keep track of things, so I prefer option 2 myself. Just remember to drag the sound file into the exported variable for the Main scene.


That's fine if you only want to play one background music track continuously throughout the game, but if you want to change background music then that script won't cut it. The most practical way to do it is probably to take a slightly different approach. You can make an Autoload itself be an AudioStreamPlayer – just start the script with

extends AudioStreamPlayer

And it will work. So you could make a new Autoload script named music_player.gd (this Autoload would be specifically for music, you could use the Autoload we made earlier for sound effects and have both Autoloads running in your game) with code like this, which is a slight adaptation of what I did in Daxolissian System: Quad. We're not going to be doing it in this game, but I'll show you how it works.

extends AudioStreamPlayer

var playing_intro:bool = false
var playing_levels:bool = false
var intro_track:AudioStream
var track_list:Array = []

func _ready():
	pause_mode = Node.PAUSE_MODE_PROCESS
	intro_track = preload("res://music/rip.ogg")
	track_list = [
		preload("res://music/electric_shock_therapy.ogg"),
		preload("res://music/hydrozoa.ogg"),
		preload("res://music/metamorphosis.ogg"),
		preload("res://music/paranormal_pumpkins.ogg"),
		preload("res://music/science_fiction.ogg"),
		preload("res://music/tempest.ogg"),
		preload("res://music/vanish.ogg"),
	]

func play_intro():
	if playing_levels:
		stop()
	playing_intro = true
	playing_levels = false

func play_levels():
	if playing_intro:
		stop()
	playing_intro = false
	playing_levels = true

func stop_playing():
	playing_intro = false
	playing_levels = false
	stop()

func _physics_process(delta):
	if playing_intro and not playing:
		stream = intro_track
		play()
	if playing_levels and not playing:
		stream = track_list[randi() % track_list.size()]
		play()

It declares the variables playing_intro and playing_levels, and sets up the variable intro_track to have the song that plays in the intro (Main menu etc) and the variable track_list to have an array of songs that will randomly play during actual gameplay. I wanted to set things up so that whenever the scene changes, it will let this Autoload know whether it should be playing the intro music or some level music, but if the game is already playing the correct type of music when the scene changes (for example if you finish a level and move onto the next level when it's in the middle of a song) then I wanted Godot to just keep playing that song uninterrupted. It should only interrupt the currently playing song if you transition from a scene that's playing the intro music to a scene that plays level music.


To make that happen: the play_intro() and play_levels() functions will be run whenever the scene changes, and the new scene will execute one or the other depending on what type of music it wants to have playing. If Godot has the wrong type of music playing then it will stop the currently playing music, but if isn't not playing the wrong type of music then we won't stop the AudioStreamPlayer Autoload from continuing to play whatever it's playing. The _physics_process() function does the work of starting new tracks. If playing_intro is true but we're not playing the intro music (either because we just switched to the Main menu, or because we reached the end of the intro track) then we start playing the intro music from the beginning. If playing_levels is true and we're not playing music (either because we just switched to a Level scene, or because we reached the end of a level track and it's time to pick a new one), then it randomly picks a song from the array of level tracks and starts playing it.


Hopefully that was understandable, but if not then don't worry about it because we won't be doing things that complicated in this game. Now on to the rest of the sound for All the King's Men.


Making the game's scenes actually play sound

The Player scene has the lion's share of sound effects, so drag the burn, footstep, nom, splat, and sword wav files into res://player and make the following changes to the script. At the very beginning we're going to add some more variables, including exported variables where you should drag the sound files, and some to keep track of the sound of footsteps and the hound munching.

extends KinematicBody2D

export var sword_audio:AudioStream
export var splat:AudioStream
export var burn:AudioStream
export var nom:AudioStream
export var footstep:AudioStream
signal died

var speed:float = 400.0
var swipe_cooldown_max:float = 0.3
var swipe_display_time:float = 0.05
var strike_knockback_force:float = 200.0
var strike_knockback_cooldown_max:float = 0.5
var hound_chomp_time_max:float = 0.4
var footstep_cooldown_max:float = 0.15

var is_alive:bool = true
var move_vector:Vector2
var swipe_cooldown:float = 0.0
var swipe_vector:Vector2
var swipe_angle:float
var strike_knockback_normalized:Vector2 = Vector2.ZERO
var strike_knockback_cooldown:float = 0.0
var hound_chomp_time:float = 0.0
var footstep_cooldown:float = 0.0

We'll add some code to handle the cooldown for the footstep sound at the beginning of _physics_process()

func _physics_process(delta):
	if swipe_cooldown > 0.0:
		swipe_cooldown -= delta
		if not swipe_cooldown > swipe_cooldown_max - swipe_display_time:
			swipe_pivot.visible = false
			sprite_node.rotation = 0.0
	
	if strike_knockback_cooldown > 0.0:
		strike_knockback_cooldown -= delta
	
	if footstep_cooldown > 0.0:
		footstep_cooldown -= delta

To play the footsteps

		# Movement
		move_vector = speed * Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
		if move_vector.length() > 0.1:
			sprite_node.animation = "run"
			if footstep_cooldown <= 0.0:
				footstep_cooldown = footstep_cooldown_max
				SoundPlayer.play(footstep)

To play the sword swipe sound

		# Swipe
		if mouse_is_clicked and swipe_cooldown <= 0.0:
			swipe_cooldown = swipe_cooldown_max
			swipe_sprite.flip_v = not swipe_sprite.flip_v
			swipe_pivot.visible = true
			SoundPlayer.play(sword_audio)

And at the very end of the _physics_process() code we're going to add an elif that can execute if the player is not still alive (because he's being munched by a hound)

	elif sprite_node.animation == "hounded":
		hound_chomp_time -= delta
		if hound_chomp_time < 0.0:
			hound_chomp_time = hound_chomp_time_max
			SoundPlayer.play(nom)

The arrow splat and the fireball burning sounds are relatively easy

func take_arrow(arrow):
	if is_alive:
		arrow.queue_free()
		sprite_node.animation = "arrowed"
		SoundPlayer.play(splat)
		die()

func take_fireball():
	if is_alive:
		sprite_node.animation = "burned"
		SoundPlayer.play(burn)
		die()

And all of that code should be pretty straightforward. Most of the rest of the sounds are too, the tough part is getting an Autoload player set up in the first place and understanding how it works. So I'll leave it as an exercise for the reader to add damage.wav to play when an enemy takes damage but doesn't die, kill.wav to play when an enemy does die, arrow.wav to play when an arrow is fired, and fireball.wav to play when a fireball is fired.


Now for some finishing touches.


Show stats in a HUD

Something I haven't talked about yet, which I didn't do in All the King's Men but which you will probably want to do in your own games, is make the HUD show stats like HP or ammo or such. I obviously didn't do it because there's no HP or ammo or such. But for teaching purposes, you can open the Level scene and add a Label node as a child to the Level scene's HUD node and make it show the current level. Note that you will probably want to have the HUD's text fade out when the Overlay becomes opaque at the end of the level - since the HUD node itself isn't a YSort node, its children will be drawn in the order they're shown in the hierarchy where the top child on the list is drawn in the back and the bottom child on the list is drawn in the front, so the Label node should be moved so it's listed before the Overlay node in the hierarchy. Position and set up the Label node how you like, and in the Level scene's code add a variable declaration like

onready var level_node = get_node("HUD/Label")

and in _ready()

level_node.text = "Level: " + str(SaveHandler.level)

Of course if you were showing something like HP that would change while the scene is being played instead of something like Level Number which will be the same value for the entire lifetime of the scene, then you should put it in _physics_process() instead of _ready(). You might have seen the Godot docs talk about doing things like making the player emit a signal whenever they take damage, making the level node which is a parent of both the player node and the HUD node connect that signal from the player to a function in the HUD, and then making the HUD node have a function that updates the player's HP bar in the HUD whenever it receives that signal. Well, you can do that, or you can write one line of code like I just did, so guess which approach I prefer. Not that that makes it the right thing to do. I will also mention that if you're making a more complex game with a bunch of things going on in the HUD and/or if you're making separate scenes for multiple levels, then instead of having the HUD node just be a regular child node in the Level scene like we're doing with this game, you'll want to make a separate scene just for the HUD and add it as a child in each of the Level scenes. And you'll want to do it much earlier in the design of your game instead of adding it as an afterthought like we're doing now.


Enemy splat

Now for some more fun, what's a medieval game without a little gore? Let's add the blood splats for the enemies when they die. Open the Enemy scene and add a CPUParticles2D node (since I suspect that a regular Particles node won't work in a web export, but I might be wrong) and name it Splat. There are a lot of properties for Particle nodes like this, and things are sort of a matter of taste. I'll tell you what I did, but experiment yourself to see how things work

CPUParticles2D-Amount: 20

CPUParticles2D-Time-Lifetime: 2

CPUParticles2D-Time-OneShot: On

CPUParticles2D-Time-Explosiveness: 1

CPUParticles2D-Time-LifetimeRandomness: 0.5

CPUParticles2D-EmissionShape-Shape: Sphere (with radius 8)

CPUParticles2D-Direction: (-1, 0) (with spread 45)

CPUParticles2D-Gravity: (0, 0) (since this is a top-down and not side-view game)

CPUParticles2D-InitialVelocity-Velocity: 800

CPUParticles2D-InitialVelocity-VeloticyRandom: 1

CPUParticles2D-Damping: 5000

CPUParticles2D-Scale-ScaleAmount: 5

CPUParticles2D-Scale-ScaleAmountRandom: 0.5

CPUParticles2D-Color: red


If you turn on the Emitting property at the top, then you should see it in action in the editor's viewport. Then to actually make it play when the enemy dies, add this code to the variable declarations

onready var splat_node = get_node("Splat")

And this to die()

func die(origin:Vector2):
	splat_node.look_at(origin)
	FunctionsAutoload.reparent(splat_node)
	splat_node.emitting = true
	queue_free()

This is unlike the case when the Player dies. The Player node doesn't queue_free() itself immediately, it stays in the scene and the AnimatedSprite of the player's gruesome death keeps playing. But for the enemy we're going to actually queue_free() the enemy's node (which makes things work more easily because the Level node will be counting the number of Enemy nodes still in play and declare Victory when they're all gone). If the Splat node were still a child of the Enemy node when it queue_free()s itself, then the Splat node would also be freed and we wouldn't actually see it play. So we'll use the reparenting function we wrote earlier and reparent the Splat node to the root scene of the scene tree before the Enemy queue_free()s.


Add some scenery

And there's plenty more polish to add, like the player's ghost flying up and fading away when the player dies from any cause, and those flowers and squirrels and such in the background. But since we're nearing the end of the tutorial, those will be left as exercises for the reader to make sure you were paying attention.


The last part of the tutorial will cover the NewGrounds API and some advanced stuff.


1

Comments

Comments ain't a thing here.