How Are Autoloads Providing Something Singletons Are Not?

Godot Version

4.4.dev3

Question

The autoload docs talk about making a class as a singleton so that it can be loaded anywhere. Making a class a singleton can be accomplished by just using the static keyword on all variables and functions.

See my example below for the singleton class I made for managing all sounds in a game. I don’t have to autoload it, it can be called from every other script in the project, and it does not appear to have any performance issues.

To use it, if I want to play an impact sound from a projectile I simply use: Sounds.play_sound_effect(collision_sound). Likewise, if I want to play a song, I just use Sounds.play_music(battle_hymns.pick_random()) which plays a song and sends a signal to my music player display so it can display all the information about the song.

What am I missing? What benefits would I have registering this node as an Autoload?

#sounds.gd
extends Node

class_name Sounds

signal _now_playing(song: Song)

enum CHANNEL {
	MASTER,
	MUSIC,
	SFX,
	DIALOGUE
}


static var audio_stream_player = {
	"Music" = AudioStreamPlayer.new(),
}

static var singleton := Sounds.new()
static var now_playing := Signal(singleton._now_playing)


static func connect_players():
	for player in audio_stream_player:
		Engine.get_main_loop().current_scene.add_child(audio_stream_player[player])
	audio_stream_player["Music"].set_bus("Music")


static func play_sound_effect(sound: AudioStream):
	play(sound, CHANNEL.SFX)


static func play_music(sound: Song):
	if sound.song == null:
		push_error("%s song is empty. No AudioStream assigned." % [sound.resource_path])
	audio_stream_player["Music"].set_stream(sound.song)
	audio_stream_player["Music"].play()
	print_rich("Song Playing: %s\nby %s" % [sound.title, sound.artist])
	print_rich("Album: %s\nAlbum Link %s" % [sound.album, sound.album_link])
	now_playing.emit(sound)


static func pause_music():
	audio_stream_player["Music"].stream_paused = true


static func unpause_music():
	audio_stream_player["Music"].stream_paused = false


static func play(sound: AudioStream, channel: CHANNEL):
	if sound == null:
			return
	var player = AudioStreamPlayer.new()
	Engine.get_main_loop().current_scene.add_child(player)
	player.set_bus(channel_to_string(channel))
	player.set_stream(sound)
	player.play()

	var timer = LifetimeTimer.new(player)
	timer.wait_time = sound.get_length()
	Engine.get_main_loop().current_scene.add_child(timer)
	timer.start()


static func channel_to_string(channel: CHANNEL):
	match channel:
		CHANNEL.MASTER:
			return "Master"
		CHANNEL.MUSIC:
			return "Music"
		CHANNEL.SFX:
			return "SFX"
		CHANNEL.DIALOGUE:
			return "Dialogue"


static func string_to_channel(value: String):
	match value:
		"Master":
			return CHANNEL.MASTER
		"Music":
			return CHANNEL.MUSIC
		"SFX":
			return CHANNEL.SFX
		"Dialogue":
			return CHANNEL.DIALOGUE

Also, for reference, the song class:

@icon("res://assets/ui/icons/music-library.png")
extends Resource

class_name Song

@export var song: AudioStream
@export var title: String
@export var artist: String
@export var album: String
@export var album_link: String

Autoload creates a Node, this will have access to easier scene tree manipulations.

Autoload can be an entire scene, so you do not have to write messy scene creation code like this sample. You can use _ready() on it’s own rather than finding a place to call this sample (though as a scene you will not need this sample at all).

Signals are straight forward, because it’s a real Node. So this sample can be removed.

1 Like

What you’re saying here is that I could just create a scene to load, attach the AudioStreamPlayer directly, and be done?

2 Likes

I’m realizing this is what new developers must feel like when I tell them ternary operators, while elegant, are not easy to read and there are usually better ways. I spent so much time figuring out how to make my singleton work. LOL

1 Like

FWIW, my refactored code:

extends Node

class_name Sounds

signal now_playing(song: Song)

enum CHANNEL {
	MASTER,
	MUSIC,
	SFX,
	UI,
	AMBIENT,
	DIALOGUE,
}


@onready var music_player: AudioStreamPlayer = $MusicPlayer
@onready var dialogue_player: AudioStreamPlayer = $DialoguePlayer


func play_music(sound: Variant):
	if sound == AudioStream:
		var temp_sound = Song.new()
		temp_sound.song = sound
		sound = temp_sound
		sound.title = sound.song.resource_name
		sound.album = "Unknown"
	if sound is not Song:
		push_error("%s not a valid song file or AudioStream" % [sound.name])
	if sound.song == null:
		push_error("%s song is empty. No AudioStream assigned." % [sound.resource_path])
	
	print_rich("Song Playing: %s\nby %s" % [sound.title, sound.artist])
	print_rich("Album: %s\nAlbum Link %s" % [sound.album, sound.album_link])
	music_player.set_stream(sound.song)
	music_player.play()
	now_playing.emit(sound)


func pause_music():
	music_player.stream_paused = true


func unpause_music():
	music_player.stream_paused = false


func play_sound_effect(sound: AudioStream):
	play(sound, CHANNEL.SFX)


func play_ui_sound(sound: AudioStream):
	play(sound, CHANNEL.UI)

func play_ambient_sound(sound: AudioStream):
	play(sound, CHANNEL.AMBIENT)

func play_dialogue(sound: AudioStream):
	dialogue_player.set_stream(sound)
	dialogue_player.play()

func play(sound: AudioStream, channel: CHANNEL):
	if sound == null:
			return
	var player = AudioStreamPlayer.new()
	Engine.get_main_loop().current_scene.add_child(player)
	player.set_bus(channel_to_string(channel))
	player.set_stream(sound)
	player.play()

	var timer = LifetimeTimer.new(player)
	timer.wait_time = sound.get_length()
	Engine.get_main_loop().current_scene.add_child(timer)
	timer.start()


func channel_to_string(channel: CHANNEL):
	return str(channel)
1 Like