2
\$\begingroup\$

I am trying to do some proof of concept work using Monogame, mostly for my own knowledge. One thing I am having trouble with, however, is timing sounds effects and background music to my liking.

So in my scenario I would like to play a series of SoundEffect objects when the game first loads, and when those effects have finished, play a Song in the background until the game is finished. When either the player or computer meets victory conditions the game will end, which involves stopping the background Song, playing a win/lose SoundEffect, and loading the end screen.

Here's where I started:

var open0 = Content.Load<SoundEffect>("Open0");
var open1 = Content.Load<SoundEffect>("Open1");
open0.Play();
open1.Play();
bgMusic = Content.Load<Song>("BGTrack");
MediaPlayer.Play(bgMusic);
MediaPlayer.IsRepeating = true;
MediaPlayer.Volume = 0.25f;

This approach ends up just playing all three items simultaneously. Its more noise than good game experience.

I also tried moving the Play() method calls into an async method and using await on the threads, but of course Play() doesn't return Task so that didn't work. Likewise, SoundEffectInstance and DynamicSoundEffectInstance don't seem to offer any kind of awaitable method calls, unless I'm missing them.

I also tried wrapping subsequent calls in a while loop, using the SoundEffectInstance.State property as my logic control:

var open0 = Content.Load<SoundEffect>("Open0");
var open1 = Content.Load<SoundEffect>("Open1");
SoundEffectInstance open0Instance = open0.CreateInstance();
SoundEffectInstance open1Instance = open1.CreateInstance();

open0Instance.Play();
while (open0Instance.State != SoundState.Playing)
    open1Instance.Play();

while (open0Instance.State != SoundState.Playing &&
        open1Instance.State != SoundState.Playing)
{
    bgMusic = Content.Load<Song>("BGTrack");
    MediaPlayer.Play(bgMusic);
    MediaPlayer.IsRepeating = true;
    MediaPlayer.Volume = 0.25f;
}

Ugly code, but I was trying to figure out what would work...and this didn't. The first sound played and nothing else, so I'm guessing the State property doesn't implement INotifyPropertyChanged (although I'm not even sure that's relevant outside of MVVM now that I think about it).

The sounds at the end of the game are not giving me trouble because I can simply stop the MediaPlayer and play my closing Effect:

MediaPlayer.Stop();
gameObjects.PlayerWinEffect.Play(1.0f, 0.0f, 0.0f);

So is there a way to control SoundEffect and MediaPlayer timing more explicitly?

\$\endgroup\$
  • 1
    \$\begingroup\$ Your while loop will run endlessly in the frame. If the sound effect is longer than 1/60th second (assuming target 60fps) this is going to stall the game no? \$\endgroup\$ – lozzajp Feb 27 '17 at 17:09
1
\$\begingroup\$

As far as I can tell, there is no way to explicitly wait on SoundEffect or SoundEffectInstance, aside from pooling.

What you are gonna do is check on each call to Update. You do not need to use a while loop, because the system will call Update for you.

Place these as members of your game class:

SoundEffectInstance open0Instance;
SoundEffectInstance open1Instance;
Song bgMusic;
int state;

Place these lines in LoadContent:

var open0 = Content.Load<SoundEffect>("Open0");
var open1 = Content.Load<SoundEffect>("Open1");
bgMusic = Content.Load<Song>("BGTrack");
open0Instance = open0.CreateInstance();
open1Instance = open1.CreateInstance();
state = 0;

Place these lines in Update:

switch (state)
{
    case 0:
        open0Instance.Play();
        state = 1;
        break;
    case 1:
        if (open0Instance.State == SoundState.Stopped)
        {
            open1Instance.Play();
            state = 2;
        }
        break;
    case 2:
        if (open1Instance.State == SoundState.Stopped)
        {
            MediaPlayer.Play(bgMusic);
            MediaPlayer.IsRepeating = true;
            MediaPlayer.Volume = 0.25f;
            state = 3;
        }
        break;
     default:
        break;      
}

Place these lines in UnloadContent:

open0Instance.Dispose();
open1Instance.Dispose();
bgMusic.Dispose();

Ok, why does that work? First off, Monogame will call Update repeatedly, so you don't have the need to create a loop. Second, what we are doing is an state machine... using the field state to keep track of the progress we are doing we know what to check and to do next. Finally, I'm showing you the appropiate code to unload the resources.

Now, let's go on the switch step by step...

At the start state = 0, that is the default value (and we have also set it in LoadContent, just for convinience). So, the flow of execution enters here:

case 0:
    open0Instance.Play();
    state = 1;
    break;

That will set the first sound effect to play, and update the state to 1. Since state is now 1... the next time MonoGame calls Update, it will enter here:

case 1:
    if (open0Instance.State == SoundState.Stopped)
    {
        open1Instance.Play();
        state = 2;
    }
    break;

Here, we check if the sound effect we set to play have stopped... if it hasn't we do nothing. MonoGame will continue calling Update, until, eventually the condition evaluates to true, meaning that the sound effect has stopped playing. We proceed to play the second sound effect and set the state to 2. Since state is now 2... the next time MonoGame calls Update, it will enter here:

case 2:
    if (open1Instance.State == SoundState.Stopped)
    {
        MediaPlayer.Play(bgMusic);
        MediaPlayer.IsRepeating = true;
        MediaPlayer.Volume = 0.25f;
        state = 3;
    }
    break;

Similarly to the prior case, we check if the sound effect we set to play have stoppd... once it have, the flow of execution enters the conditional, at which point we start playing the background music and set state to 3. Since state is now 3... the next time MonoGame calls Update, it will enter here:

default:
        break;

That does nothing.


Alternatively consider using the sound effects as songs. Btw, dispose your SoundEffectInstance. When using MediaPlayer, you can subscribe to the event MediaStateChanged of it, you should get a notification when the song stoped playing (verify the state). You might want to use a ManualResetEventSlim which you Set in the event handler, so you can Wait for it.

Another thing that may come handy is SpinWait.SpinUntil.

\$\endgroup\$
  • \$\begingroup\$ This first code block did the trick! However, I'm not real excited about maintaining a property just to track the faux-state of the sound system. I think I'm going to dig into the MediaPlayer approach as well; I wasn't aware that it had an event I could subscribe to so that may be the best solution overall. Either way you helped me solve this and I really appreciate your time! \$\endgroup\$ – William Smith Feb 27 '17 at 20:27
  • 1
    \$\begingroup\$ @WilliamSmith Three things to consider going forward: 1) sound effects are usually fire and forget, it isn't common to wait on them. 2) usually it is advised to not mix concerns such as graphics, audio, AI, etc... all in the Game class. You may want to move music logic to an audio subsystem, and have game objects handle their sound effects. 3) There are fancier ways to handle state machines, consider for instance behavior trees. Hopefully you find something that works for you. \$\endgroup\$ – Theraot Feb 28 '17 at 4:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.