0 votes

I've been trying to work around this, banging my head, for 2 days now. XD

I know this can be solved via dirty, spaghetti code and shove all audio into their respective scripts/scenes. But i'm too stubborn and i want this to work.

Case:

  • I want to play all my audio from a singleton.
  • I want a scene with a library of all my audio.
  • Ideally, i want to call it from any script like this: audioManager.play("confirmButton") well, ideally.

Situation:

  • I've read the documentation about singletons over and over and over and ...
  • watched all the yt videos about gdscript singletons, inheritance, packaging scenes, scene trees, audio manipulation. But apparently, this isn't enough.
  • BTW, this would be my 3rd time using a singleton.
  • But this is my 1st time:

    • making a scene (audioPlayer.tscn),
    • creating a script for the root node of that scene (audioManager.gd),
    • making THAT script i just made into a singleton.

Now, i made an awesome dirty spaghetti code but with this pattern still, just to make it work. I'm at the stage where at least it's recognizing (i guess?) the children nodes of the root node of audioPlayer.tscn

Printing the properties of the node is telling me that it SHOULD BE WORKING. But I'm here, so it's not. :(

audioPlayer.tscn

  • audioPlayer (Node)

    • gameBgMusic (AudioStreamPlayer)
    • confirmButton (AudioStreamPlayer)
    • spring (AudioStreamPlayer2D)
    • shortJump (AudioStreamPlayer2D)
    • longJump (AudioStreamPlayer2D)

menu.gd (Just to call the function in the singleton)


extends Node func workNowPlsForCryingOutLoud():
$"/root/audioManager".play("confirmButton")

audioManager.gd (Singleton)

extends Node func play(sound = null):
if sound == null:
return
else:
var s = ResourceLoader.load("res://scenes/audioPlayer.tscn")
var scene = s.instance()
###############################################################
# Honestly? IDK if this is getting gameBgMusic or confirmButton
var music = scene.getchild(0)
###############################################################
music.volume
db = 100
music.play() # There is no sound coming out. if music.playing == false: print('not playing') else: print('playing') print('paused: ' + str(music.stream_paused)) print('pitch_scale: ' + str(music.pitch_scale)) print('mix_target: ' + str(music.mix_target)) print('bus: ' + str(music.bus)) print('autoplay: ' + str(music.autoplay)) print('volume_db: ' + music.volume_db)

What's the output of that beautiful pillar of print, you may ask?

  • playing
  • paused: False
  • pitch_scale: 1
  • mix_target: 0
  • bus: Master
  • autoplay: True
  • 100

Just in case someone asks. Yes, i checked my speakers if they were on and my volume if it was high enough. I even cranked it up to 100 while testing it. I also checked if there is actual audio output via YT, Spotify.

Someone save me from insanity. >.<

Godot 3.1.1

asked May 24, 2019 in Engine by jagc (87 points)

2 Answers

0 votes
Best answer

After banging my head some more, i finally figured it out.

For reference, this is the design i came up with:

game loads (root) [(currently using)] (medium sized game)
  load audio script (singleton)
    preload audio library of game
  load changeable scene
    load every other nodes of the scene
      call audio (singleton script) via events/signals
        load audio node [scene tree library] (load all audio of game)
          play necessary audio
            queue_free() after sound plays
              removes loaded audio node

menu.gd (Just to call the function in the singleton)


extends Node func workNowPlsForCryingOutLoud(): var volume_db = 10 var pitch_scale = 3 audioManager.play("confirmButton", {volume_db = volume_db, pitch_scale = pitch_scale})

audioManager.gd (Singleton)


extends Node var audioScene = load("res://scenes/audioPlayer.tscn") as PackedScene sample way to call it: audioManager.play("confirmButton", {volumedb = 100, pitchscale = 9}) func play(audioSoundName, optionals = {}):
var audio = audioScene.instance(); addchild(audio)
var sound = audio.get
node(audioSoundName) _setSoundProperties(sound, optionals) sound.play() return sound

func _setSoundProperties(sound, optionals):
    var volume_db = (100 if not optionals.has('volume_db') else optionals['volume_db'])
    var pitch_scale = (0 if not optionals.has('pitch_scale') else optionals['pitch_scale'])
    var mix_target = (0 if not optionals.has('mix_target') else optionals['mix_target'])
    var bus = ('Master' if not optionals.has('bus') else optionals['bus'])
    var autoplay = (false if not optionals.has('autoplay') else optionals['autoplay'])
    var stream_paused = (false if not optionals.has('stream_paused') else optionals['stream_paused'])

    # MIX_TARGET_STEREO   = 0 The audio will be played only on the first channel.
    # MIX_TARGET_SURROUND = 1 The audio will be played on all surround channels.
    # MIX_TARGET_CENTER   = 2 The audio will be played on the second channel, which is usually the center.
    sound.mix_target = mix_target #int value of any from above comment

    sound.volume_db = volume_db #tested range: 0-100
    sound.pitch_scale = pitch_scale #range from 0-16
    sound.bus = bus #string name of bus channel
    sound.autoplay = autoplay #bool: true or false
    sound.stream_paused = stream_paused #bool: true or false
    
    return sound

What the problem was.
- I was calling the audio scene node from a location it wasn't loaded in to.
- Hence, i keep on getting null based errors.

If you missed it, calling the function can be as simple as: audioManager.play("confirmButton")


Some manifestations of the solution.

Returning the instance after playing the sound is so it's capable of calling built in audio functions.
Implemented like so:

extends Node

var bgSound = audioManager.play("gameBgMusic")

# logic to accept command/signal to change scene

# stop playing the bg sound before changing scenes.
func _changeScene():
  bgSound.stop()

  # change scene code here

I was really banging my head for this to work so i could stick to this kind of design pattern when handling audio - permanently.

In the end,
I realized this wasn't optimal. So i designed a couple of ways to structure a project - keeping the balance of CPU usage and Memory load in mind.

Disclaimer: The perceived size of the game for a design is a personal assessment. This is not in any way backed up by research.

game loads (root) (medium sized game)  [(Used in this solution)]
  load audio script (singleton)
    preload audio library of game
  load changeable scene
    load every other nodes of the scene
      call audio (singleton script) via events/signals
        load audio node [scene tree library] (load all audio of game)
          play necessary audio
            queue_free() after sound plays
              removes loaded audio node

####################################################################

game loads (root) (very small game) [(What i used to do)]
  load changeable scene
    load every other nodes of the scene
      call audio via events/signals
        play necessary audio
    load audio node

####################################################################

game loads (root) (small-medium sized game)
  load changeable scene
    setup scene root for audio stream export vars
    load every other nodes of the scene
    load audio node [empty stream property]
      call audio via events/signals
        set audio stream property
        play necessary audio

####################################################################

game loads (root) (medium sized game)
  load changeable scene
    load audio node [scene tree library] (load all audio of scene)
    load every other nodes of the scene
      call audio via events/signals
        play necessary audio
          queue_free() after sound plays
            removes loaded audio node

####################################################################

game loads (root) (medium-big sized game)
  load audio script (singleton)
    setup audio libraries to preload
    
  load changeable scene
    load every other nodes of the scene
      call audio (singleton script) via events/signals
        load audio node [scene tree library] (load all audio of scene)
          
          play necessary audio
            queue_free() after sound plays
              removes loaded audio node
answered May 28, 2019 by jagc (87 points)
edited May 28, 2019 by jagc

for some reason, the browser can't show all text inside <pre></pre>
It looks like a bug?

So i had to break up the file audioManager.gd into 2 separate preformatted text blocks.

0 votes

How I would play sounds is I'd connect a script to whichever level or character needs a sound.

E.g. if I have a main gamestate scene which holds some global info then I'd play a background sound like so:

extends Node2D

var bgMusic = 'res://SFX/level1.wav'

func _ready():
    if not $BGM.playing:
        $BGM.stream = load(bgMusic)
        $BGM.play()

NB!

$BGM is an AudioStreamplayer type of node selected to interact with.
Also when importing any audio file and doing any changes under import tab (loop, mono etc.) then always click on 'Reimport' button for the changes to take effect.

answered May 24, 2019 by talupoeg (25 points)

Yes, this is very similar to how i did my sounds in my other games.

Thing is, I'm thinking, if i wanna do a large game, this way of doing things won't fly. I wanted a way to make resources - such as sounds, more manageable. That is what I'm trying to practice here. This is also why I'm putting them all in one scene. Then manage it via a singleton.

I don't know if this design pattern is wrong/not possible or it is possible but i'm just doing it wrong.

Though, If there is a totally different way of doing this, as long as i can manage all my audio in one place, i'm all ears...or eyes... yyou know what i'm saying. :)

You can still do a completely separate scene for your audio references and workings.
You'd need to add this "audio scene" as a child to your scene with add_child() method. And then you have a quick and easy access to the node and it's child node's and methods etc.

Another way is to use signals to trigger certain methods elsewhere within your scene tree.

And also your own classes what you were trying to do I think. But I haven't tried it in GDscript yet.

And if looking around godot docs then playing around with singletons and instancing a scene you'd still need to add it as a child.
https://docs.godotengine.org/en/3.1/getting_started/step_by_step/singletons_autoload.html

I'm sorry, where in the documentation are you pointing at?

Your other comment is pointing me at a good direction. I like it!! It's sad though, this is what i was trying to avoid. Instancing the audio scene for every script i need to use it.

In terms of emitting signals to a scene that isn't in the tree preemptively. I guess i have to experiment more on that.

Thanks for the tips!

Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.