Table of Contents

Chapter 15: Audio Controller

Learn how to create a reusable audio controller class to manage sound effects and music, including volume control, muting/unmuting, and proper resource cleanup.

While playing sounds and music using the simplified sound API is straightforward, a game needs to handle various audio states and resource cleanup including:

  • Track and manage sound effect instances that are created.
  • Dispose of sound effect instances when they are finished.
  • Handle volume control for songs and sound effects.
  • Manage audio states (pause/resume, mute/unmute).

In this chapter you will:

  • Learn how to create a central audio management system.
  • Implement proper resource tracking and cleanup for sound effects.
  • Build methods to control audio state (play/pause, mute/unmute).
  • Add global volume control for different audio types.
  • Integrate the audio controller with your game's core systems.
  • Implement keyboard shortcuts for audio control.

By the end of this chapter, you'll have an audio control system that can be easily reused in future game projects.

The AudioController Class

To get started, in the MonoGameLibrary project:

  1. Create a new directory named Audio.

  2. Add a new class file named AudioController.cs to the Audio directory you just created.

  3. Add the following code as the initial structure for the class

    using System;
    using System.Collections.Generic;
    using Microsoft.Xna.Framework.Audio;
    using Microsoft.Xna.Framework.Media;
    
    namespace MonoGameLibrary.Audio;
    
    public class AudioController : IDisposable
    {
    
    }
    
    Note

    The AudioController class will implement the IDisposable interface, This interface is part of .NET and provides a standardized implementation for an object to release resources. Implementing IDisposable allows other code to properly clean up the resources held by our audio controller when it's no longer needed. For more information on IDisposable, you can read the Implement a Dispose Method article on Microsoft Learn.

AudioController Properties and Fields

The AudioController will need to track sound effect instances created for cleanup and track the state and volume levels of songs and sound effects when toggling between mute states. Add the following fields and properties:

// Tracks sound effect instances created so they can be paused, unpaused, and/or disposed.
private readonly List<SoundEffectInstance> _activeSoundEffectInstances;

// Tracks the volume for song playback when muting and unmuting.
private float _previousSongVolume;

// Tracks the volume for sound effect playback when muting and unmuting.
private float _previousSoundEffectVolume;

/// <summary>
/// Gets a value that indicates if audio is muted.
/// </summary>
public bool IsMuted { get; private set; }

/// <summary>
/// Gets a value that indicates if this audio controller has been disposed.
/// </summary>
public bool IsDisposed {get; private set; }

AudioController Constructor

The constructor just initializes the collection used to track the sound effect instances. Add the following constructor and finalizer:

/// <summary>
/// Creates a new audio controller instance.
/// </summary>
public AudioController()
{
    _activeSoundEffectInstances = new List<SoundEffectInstance>();
}

// Finalizer called when object is collected by the garbage collector
~AudioController() => Dispose(false);
Note

The AudioController class implements a finalizer method ~AudioManager(). This method is called when an instance of the class is collected by the garbage collector and is here as part of the IDisposable implementation.

AudioController Methods

The AudioController will need methods to:

  • Update it to check for resources to clean up.
  • Playing sound effects and songs
  • State management (play/pause, mute/unmute)
  • Volume control
  • Implement the IDisposable interface.

AudioController Update

The Update method will check for existing sound effect instances that have expired and properly dispose of them. Add the following method:

/// <summary>
/// Updates this audio controller
/// </summary>
public void Update()
{
    int index = 0;

    while (index < _activeSoundEffectInstances.Count)
    {
        SoundEffectInstance instance = _activeSoundEffectInstances[index];

        if (instance.State == SoundState.Stopped && !instance.IsDisposed)
        {
            instance.Dispose();
        }

        _activeSoundEffectInstances.RemoveAt(index);
    }
}

AudioController Playback

While the MonoGame simplified audio API allows sound effects to be played in a fire and forget manner, doing it this way doesn't work if you need to pause them because the game paused. Instead, we can add playback methods through the AudioController that can track the sound effect instances and pause them if needed, as well as checking the media player state before playing a song. Add the following methods:

/// <summary>
/// Plays the given sound effect.
/// </summary>
/// <param name="soundEffect">The sound effect to play.</param>
/// <returns>The sound effect instance created by this method.</returns>
public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect)
{
    return PlaySoundEffect(soundEffect, 1.0f, 1.0f, 0.0f, false);
}

/// <summary>
/// Plays the given sound effect with the specified properties.
/// </summary>
/// <param name="soundEffect">The sound effect to play.</param>
/// <param name="volume">The volume, ranging from 0.0 (silence) to 1.0 (full volume).</param>
/// <param name="pitch">The pitch adjustment, ranging from -1.0 (down an octave) to 0.0 (no change) to 1.0 (up an octave).</param>
/// <param name="pan">The panning, ranging from -1.0 (left speaker) to 0.0 (centered), 1.0 (right speaker).</param>
/// <param name="isLooped">Whether the the sound effect should loop after playback.</param>
/// <returns>The sound effect instance created by playing the sound effect.</returns>
/// <returns>The sound effect instance created by this method.</returns>
public SoundEffectInstance PlaySoundEffect(SoundEffect soundEffect, float volume, float pitch, float pan, bool isLooped)
{
    // Create an instance from the sound effect given.
    SoundEffectInstance soundEffectInstance = soundEffect.CreateInstance();

    // Apply the volume, pitch, pan, and loop values specified.
    soundEffectInstance.Volume = volume;
    soundEffectInstance.Pitch = pitch;
    soundEffectInstance.Pan = pan;
    soundEffectInstance.IsLooped = isLooped;

    // Tell the instance to play
    soundEffectInstance.Play();

    // Add it to the active instances for tracking
    _activeSoundEffectInstances.Add(soundEffectInstance);

    return soundEffectInstance;
}

/// <summary>
/// Plays the given song.
/// </summary>
/// <param name="song">The song to play.</param>
/// <param name="isRepeating">Optionally specify if the song should repeat.  Default is true.</param>
public void PlaySong(Song song, bool isRepeating = true)
{
    // Check if the media player is already playing, if so, stop it.
    // If we don't stop it, this could cause issues on some platforms
    if (MediaPlayer.State == MediaState.Playing)
    {
        MediaPlayer.Stop();
    }

    MediaPlayer.Play(song);
    MediaPlayer.IsRepeating = isRepeating;
}

AudioController State Control

The AudioController provides methods to control the state of audio playback including pausing and resuming audio as well as muting and unmuting. Add the following methods:

/// <summary>
/// Pauses all audio.
/// </summary>
public void PauseAudio()
{
    // Pause any active songs playing
    MediaPlayer.Pause();

    // Pause any active sound effects
    foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
    {
        soundEffectInstance.Pause();
    }
}

/// <summary>
/// Resumes play of all previous paused audio.
/// </summary>
public void ResumeAudio()
{
    // Resume paused music
    MediaPlayer.Resume();

    // Resume any active sound effects
    foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
    {
        soundEffectInstance.Resume();
    }
}

/// <summary>
/// Mutes all audio.
/// </summary>
public void MuteAudio()
{
    // Store the volume so they can be restored during ResumeAudio
    _previousSongVolume = MediaPlayer.Volume;
    _previousSoundEffectVolume = SoundEffect.MasterVolume;

    // Set all volumes to 0
    MediaPlayer.Volume = 0.0f;
    SoundEffect.MasterVolume = 0.0f;

    IsMuted = true;
}

/// <summary>
/// Unmutes all audio to the volume level prior to muting.
/// </summary>
public void UnmuteAudio()
{
    // Restore the previous volume values
    MediaPlayer.Volume = _previousSongVolume;
    SoundEffect.MasterVolume = _previousSoundEffectVolume;

    IsMuted = false;
}

/// <summary>
/// Toggles the current audio mute state.
/// </summary>
public void ToggleMute()
{
    if (IsMuted)
    {
        UnmuteAudio();
    }
    else
    {
        MuteAudio();
    }
}

AudioController Volume Control

The AudioController also provides methods to increase and decrease the global volume of songs and sound effects. Add the following methods:

/// <summary>
/// Increases volume of all audio by the specified amount.
/// </summary>
/// <param name="amount">The amount to increase the audio by.</param>
public void IncreaseVolume(float amount)
{
    if (!IsMuted)
    {
        MediaPlayer.Volume = Math.Min(MediaPlayer.Volume + amount, 1.0f);
        SoundEffect.MasterVolume = Math.Min(SoundEffect.MasterVolume + amount, 1.0f);
    }
}

/// <summary>
/// Decreases the volume of all audio by the specified amount.
/// </summary>
/// <param name="amount">The amount to decrease the audio by.</param>
public void DecreaseVolume(float amount)
{
    if (!IsMuted)
    {
        MediaPlayer.Volume = Math.Max(MediaPlayer.Volume - amount, 0.0f);
        SoundEffect.MasterVolume = Math.Max(SoundEffect.MasterVolume - amount, 0.0f);
    }
}

AudioController IDisposable Implementation

Finally, the AudioController implements the IDisposable interface. Add the following methods:

/// <summary>
/// Disposes of this audio controller and cleans up resources.
/// </summary>
public void Dispose()
{
    Dispose(true);
    GC.SuppressFinalize(this);
}

/// <summary>
/// Disposes this audio controller and cleans up resources.
/// </summary>
/// <param name="disposing">Indicates whether managed resources should be disposed.</param>
protected void Dispose(bool disposing)
{
    if(IsDisposed)
    {
        return;
    }

    if (disposing)
    {
        foreach (SoundEffectInstance soundEffectInstance in _activeSoundEffectInstances)
        {
            soundEffectInstance.Dispose();
        }
        _activeSoundEffectInstances.Clear();
    }

    IsDisposed = true;
}

Games often use limited system resources like audio channels. When we're done with these resources, we need to clean them up properly. In .NET, the standard way to handle resource cleanup is through the IDisposable interface.

Think of IDisposable like a cleanup checklist that runs when you're finished with something:

  1. The interface provides a Dispose method that contains all cleanup logic.
  2. When called, Dispose releases any resources the class was using.
  3. Even if you forget to call Dispose, the finalizer acts as a backup cleanup mechanism.

For our AudioController, implementing IDisposable means we can ensure all sound effect instances are properly stopped and disposed when our game ends, preventing resource leaks.

Note

Fore more information on IDisposable and the Dispose method, check out the Implementing a Dispose Method article on Microsoft Learn.

Implementing the AudioController Class

Now that we have the audio controller class complete, let's update the game to use it. We'll do this in two steps:

  1. First, update the Core class to add the AudioController globally.
  2. Update the Game1 class to use the global audio controller from Core.

Updating the Core Class

The Core class serves as our the base game class, so we'll update it first to add and expose the AudioController globally. Open the Core.cs file in the MonoGameLibrary project and update it to the following:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using MonoGameLibrary.Audio;
using MonoGameLibrary.Input;

namespace MonoGameLibrary;

public class Core : Game
{
    internal static Core s_instance;

    /// <summary>
    /// Gets a reference to the Core instance.
    /// </summary>
    public static Core Instance => s_instance;

    /// <summary>
    /// Gets the graphics device manager to control the presentation of graphics.
    /// </summary>
    public static GraphicsDeviceManager Graphics { get; private set; }

    /// <summary>
    /// Gets the graphics device used to create graphical resources and perform primitive rendering.
    /// </summary>
    public static new GraphicsDevice GraphicsDevice { get; private set; }

    /// <summary>
    /// Gets the sprite batch used for all 2D rendering.
    /// </summary>
    public static SpriteBatch SpriteBatch { get; private set; }

    /// <summary>
    /// Gets the content manager used to load global assets.
    /// </summary>
    public static new ContentManager Content { get; private set; }

    /// <summary>
    /// Gets a reference to to the input management system.
    /// </summary>
    public static InputManager Input { get; private set; }

    /// <summary>
    /// Gets or Sets a value that indicates if the game should exit when the esc key on the keyboard is pressed.
    /// </summary>
    public static bool ExitOnEscape { get; set; }

    /// <summary>
    /// Gets a reference to the audio control system.
    /// </summary>
    public static AudioController Audio { get; private set; }

    /// <summary>
    /// Creates a new Core instance.
    /// </summary>
    /// <param name="title">The title to display in the title bar of the game window.</param>
    /// <param name="width">The initial width, in pixels, of the game window.</param>
    /// <param name="height">The initial height, in pixels, of the game window.</param>
    /// <param name="fullScreen">Indicates if the game should start in fullscreen mode.</param>
    public Core(string title, int width, int height, bool fullScreen)
    {
        // Ensure that multiple cores are not created.
        if (s_instance != null)
        {
            throw new InvalidOperationException($"Only a single Core instance can be created");
        }

        // Store reference to engine for global member access.
        s_instance = this;

        // Create a new graphics device manager.
        Graphics = new GraphicsDeviceManager(this);

        // Set the graphics defaults
        Graphics.PreferredBackBufferWidth = width;
        Graphics.PreferredBackBufferHeight = height;
        Graphics.IsFullScreen = fullScreen;

        // Apply the graphic presentation changes
        Graphics.ApplyChanges();

        // Set the window title
        Window.Title = title;

        // Set the core's content manager to a reference of hte base Game's
        // content manager.
        Content = base.Content;

        // Set the root directory for content
        Content.RootDirectory = "Content";

        // Mouse is visible by default
        IsMouseVisible = true;
    }

    protected override void Initialize()
    {
        base.Initialize();

        // Set the core's graphics device to a reference of the base Game's
        // graphics device.
        GraphicsDevice = base.GraphicsDevice;

        // Create the sprite batch instance.
        SpriteBatch = new SpriteBatch(GraphicsDevice);

        // Create a new input manager
        Input = new InputManager();

        // Create a new audio controller.
        Audio = new AudioController();
    }

    protected override void UnloadContent()
    {
        // Dispose of the audio controller.
        Audio.Dispose();

        base.UnloadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        // Update the input manager.
        Input.Update(gameTime);

        // Update the audio controller.
        Audio.Update();

        if (ExitOnEscape && Input.Keyboard.IsKeyDown(Keys.Escape))
        {
            Exit();
        }

        base.Update(gameTime);
    }
}

The key changes made here are:

  1. Added the using MonoGameLibrary.Audio; directive to access the AudioController class.
  2. Added a static Audio property to provide global access to the audio controller.
  3. Created the new audio controller instance in the Initialize method.
  4. Added an override for the UnloadContent method where we dispose of the audio controller.
  5. The audio controller is updated in the Update method.

Updating the Game1 Class

Next, update the Game1 class to use the audio controller for audio playback. Open Game1.cs and make the following updates:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using MonoGameLibrary;
using MonoGameLibrary.Graphics;
using MonoGameLibrary.Input;

namespace DungeonSlime;

public class Game1 : Core
{
    // Defines the slime animated sprite.
    private AnimatedSprite _slime;

    // Defines the bat animated sprite.
    private AnimatedSprite _bat;

    // Tracks the position of the slime.
    private Vector2 _slimePosition;

    // Speed multiplier when moving.
    private const float MOVEMENT_SPEED = 5.0f;

    // Tracks the position of the bat.
    private Vector2 _batPosition;

    // Tracks the velocity of the bat.
    private Vector2 _batVelocity;

    // Defines the tilemap to draw.
    private Tilemap _tilemap;

    // Defines the bounds of the room that the slime and bat are contained within.
    private Rectangle _roomBounds;

    // The sound effect to play when the bat bounces off the edge of the screen.
    private SoundEffect _bounceSoundEffect;

    // The sound effect to play when the slime eats a bat.
    private SoundEffect _collectSoundEffect;

    // The background theme song
    private Song _themeSong;

    public Game1() : base("Dungeon Slime", 1280, 720, false)
    {

    }

    protected override void Initialize()
    {
        base.Initialize();

        Rectangle screenBounds = GraphicsDevice.PresentationParameters.Bounds;

        _roomBounds = new Rectangle(
            _tilemap.TileSize,
            _tilemap.TileSize,
            screenBounds.Width - _tilemap.TileSize * 2,
            screenBounds.Height - _tilemap.TileSize * 2
        );

        // Initial slime position will be the center tile of the tile map.
        int centerRow = _tilemap.Rows / 2;
        int centerColumn = _tilemap.Columns / 2;
        _slimePosition = new Vector2(centerColumn, centerRow) * _tilemap.TileSize;

        // Initial bat position will the in the top left corner of the room
        _batPosition = new Vector2(_roomBounds.Left, _roomBounds.Top);

        // Assign the initial random velocity to the bat.
        AssignRandomBatVelocity();

        // Start playing the background music
        Audio.PlaySong(_themeSong);
    }

    protected override void LoadContent()
    {
        // Create the texture atlas from the XML configuration file
        TextureAtlas atlas = TextureAtlas.FromFile(Content, "images/atlas-definition.xml");

        // Create the slime animated sprite from the atlas.
        _slime = atlas.CreateAnimatedSprite("slime-animation");

        // Create the bat animated sprite from the atlas.
        _bat = atlas.CreateAnimatedSprite("bat-animation");

        // Load the tilemap from the XML configuration file.
        _tilemap = Tilemap.FromFile(Content, "images/tilemap-definition.xml");

        // Load the bounce sound effect
        _bounceSoundEffect = Content.Load<SoundEffect>("audio/bounce");

        // Load the collect sound effect
        _collectSoundEffect = Content.Load<SoundEffect>("audio/collect");

        // Load the background theme music
        _themeSong = Content.Load<Song>("audio/theme");

        base.LoadContent();
    }

    protected override void Update(GameTime gameTime)
    {
        if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
            Exit();

        // Update the slime animated sprite.
        _slime.Update(gameTime);

        // Update the bat animated sprite.
        _bat.Update(gameTime);

        // Check for keyboard input and handle it.
        CheckKeyboardInput();

        // Check for gamepad input and handle it.
        CheckGamePadInput();

        // Creating a bounding circle for the slime
        Circle slimeBounds = new Circle(
            (int)(_slimePosition.X + (_slime.Width * 0.5f)),
            (int)(_slimePosition.Y + (_slime.Height * 0.5f)),
            (int)(_slime.Width * 0.5f)
        );

        // Use distance based checks to determine if the slime is within the
        // bounds of the game screen, and if it's outside that screen edge,
        // move it back inside.
        if (slimeBounds.Left < _roomBounds.Left)
        {
            _slimePosition.X = _roomBounds.Left;
        }
        else if (slimeBounds.Right > _roomBounds.Right)
        {
            _slimePosition.X = _roomBounds.Right - _slime.Width;
        }

        if (slimeBounds.Top < _roomBounds.Top)
        {
            _slimePosition.Y = _roomBounds.Top;
        }
        else if (slimeBounds.Bottom > _roomBounds.Bottom)
        {
            _slimePosition.Y = _roomBounds.Bottom - _slime.Height;
        }

        // Calculate the new position of the bat based on the velocity
        Vector2 newBatPosition = _batPosition + _batVelocity;

        // Create a bounding circle for the bat
        Circle batBounds = new Circle(
            (int)(newBatPosition.X + (_bat.Width * 0.5f)),
            (int)(newBatPosition.Y + (_bat.Height * 0.5f)),
            (int)(_bat.Width * 0.5f)
        );

        Vector2 normal = Vector2.Zero;

        // Use distance based checks to determine if the bat is within the
        // bounds of the game screen, and if it's outside that screen edge,
        // reflect it about the screen edge normal
        if (batBounds.Left < _roomBounds.Left)
        {
            normal.X = Vector2.UnitX.X;
            newBatPosition.X = _roomBounds.Left;
        }
        else if (batBounds.Right > _roomBounds.Right)
        {
            normal.X = -Vector2.UnitX.X;
            newBatPosition.X = _roomBounds.Right - _bat.Width;
        }

        if (batBounds.Top < _roomBounds.Top)
        {
            normal.Y = Vector2.UnitY.Y;
            newBatPosition.Y = _roomBounds.Top;
        }
        else if (batBounds.Bottom > _roomBounds.Bottom)
        {
            normal.Y = -Vector2.UnitY.Y;
            newBatPosition.Y = _roomBounds.Bottom - _bat.Height;
        }

        // If the normal is anything but Vector2.Zero, this means the bat had
        // moved outside the screen edge so we should reflect it about the
        // normal.
        if (normal != Vector2.Zero)
        {
            _batVelocity = Vector2.Reflect(_batVelocity, normal);

            // Play the bounce sound effect
            Audio.PlaySoundEffect(_bounceSoundEffect);
        }

        _batPosition = newBatPosition;

        if (slimeBounds.Intersects(batBounds))
        {
            // Divide the width  and height of the screen into equal columns and
            // rows based on the width and height of the bat.
            int totalColumns = GraphicsDevice.PresentationParameters.BackBufferWidth / (int)_bat.Width;
            int totalRows = GraphicsDevice.PresentationParameters.BackBufferHeight / (int)_bat.Height;

            // Choose a random row and column based on the total number of each
            int column = Random.Shared.Next(0, totalColumns);
            int row = Random.Shared.Next(0, totalRows);

            // Change the bat position by setting the x and y values equal to
            // the column and row multiplied by the width and height.
            _batPosition = new Vector2(column * _bat.Width, row * _bat.Height);

            // Assign a new random velocity to the bat
            AssignRandomBatVelocity();

            // Play the collect sound effect
            Audio.PlaySoundEffect(_collectSoundEffect);
        }

        base.Update(gameTime);
    }

    private void AssignRandomBatVelocity()
    {
        // Generate a random angle
        float angle = (float)(Random.Shared.NextDouble() * Math.PI * 2);

        // Convert angle to a direction vector
        float x = (float)Math.Cos(angle);
        float y = (float)Math.Sin(angle);
        Vector2 direction = new Vector2(x, y);

        // Multiply the direction vector by the movement speed
        _batVelocity = direction * MOVEMENT_SPEED;
    }

    private void CheckKeyboardInput()
    {
        // If the space key is held down, the movement speed increases by 1.5
        float speed = MOVEMENT_SPEED;
        if (Input.Keyboard.IsKeyDown(Keys.Space))
        {
            speed *= 1.5f;
        }

        // If the W or Up keys are down, move the slime up on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.W) || Input.Keyboard.IsKeyDown(Keys.Up))
        {
            _slimePosition.Y -= speed;
        }

        // if the S or Down keys are down, move the slime down on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.S) || Input.Keyboard.IsKeyDown(Keys.Down))
        {
            _slimePosition.Y += speed;
        }

        // If the A or Left keys are down, move the slime left on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.A) || Input.Keyboard.IsKeyDown(Keys.Left))
        {
            _slimePosition.X -= speed;
        }

        // If the D or Right keys are down, move the slime right on the screen.
        if (Input.Keyboard.IsKeyDown(Keys.D) || Input.Keyboard.IsKeyDown(Keys.Right))
        {
            _slimePosition.X += speed;
        }

        // If the M key is pressed, toggle mute state for audio.
        if (Input.Keyboard.WasKeyJustPressed(Keys.M))
        {
            Audio.ToggleMute();
        }

        // If the + button is pressed, increase the volume.
        if (Input.Keyboard.WasKeyJustPressed(Keys.OemPlus))
        {
            Audio.IncreaseVolume(0.1f);
        }

        // If the - button was pressed, decrease the volume.
        if (Input.Keyboard.WasKeyJustPressed(Keys.OemMinus))
        {
            Audio.DecreaseVolume(0.1f);
        }
    }

    private void CheckGamePadInput()
    {
        GamePadInfo gamePadOne = Input.GamePads[(int)PlayerIndex.One];

        // If the A button is held down, the movement speed increases by 1.5
        // and the gamepad vibrates as feedback to the player.
        float speed = MOVEMENT_SPEED;
        if (gamePadOne.IsButtonDown(Buttons.A))
        {
            speed *= 1.5f;
            GamePad.SetVibration(PlayerIndex.One, 1.0f, 1.0f);
        }
        else
        {
            GamePad.SetVibration(PlayerIndex.One, 0.0f, 0.0f);
        }

        // Check thumbstick first since it has priority over which gamepad input
        // is movement.  It has priority since the thumbstick values provide a
        // more granular analog value that can be used for movement.
        if (gamePadOne.LeftThumbStick != Vector2.Zero)
        {
            _slimePosition.X += gamePadOne.LeftThumbStick.X * speed;
            _slimePosition.Y -= gamePadOne.LeftThumbStick.Y * speed;
        }
        else
        {
            // If DPadUp is down, move the slime up on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadUp))
            {
                _slimePosition.Y -= speed;
            }

            // If DPadDown is down, move the slime down on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadDown))
            {
                _slimePosition.Y += speed;
            }

            // If DPapLeft is down, move the slime left on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadLeft))
            {
                _slimePosition.X -= speed;
            }

            // If DPadRight is down, move the slime right on the screen.
            if (gamePadOne.IsButtonDown(Buttons.DPadRight))
            {
                _slimePosition.X += speed;
            }
        }
    }

    protected override void Draw(GameTime gameTime)
    {
        // Clear the back buffer.
        GraphicsDevice.Clear(Color.CornflowerBlue);

        // Begin the sprite batch to prepare for rendering.
        SpriteBatch.Begin(samplerState: SamplerState.PointClamp);

        // Draw the tilemap
        _tilemap.Draw(SpriteBatch);

        // Draw the slime sprite.
        _slime.Draw(SpriteBatch, _slimePosition);

        // Draw the bat sprite.
        _bat.Draw(SpriteBatch, _batPosition);

        // Always end the sprite batch when finished.
        SpriteBatch.End();

        base.Draw(gameTime);
    }
}

The key changes made here are:

  1. The _themeSong field is added to store a reference to the background song to play.
  2. In LoadContent, the background theme song is loaded using hte content manager.
  3. In Initialize, the audio manager is used to play the background theme song.
  4. In Update the audio manager is used to play the bounce and collect sound effects.
  5. In CheckKeyboardInput the following checks were added
    1. If the M key on the keyboard is pressed, it will toggle mute for all audio.
    2. If the + key is pressed, the global volume is increased by 0.1f.
    3. If the - key is pressed, the global volume is decreased by 0.1f.

Running the game now will produce the same result as the previous chapter, only now the lifetime of sound effects and the state management of audio is done through the new audio controller. You can also mute and unumte the audio with the M key and increase and decrease the volume using the + and - keys.

Figure 15-1: Gameplay with audio.

Conclusion

Let's review what you accomplished in this chapter:

  • Created a reusable AudioController class to centralize audio management.
  • Learned about proper resource management for audio using the IDisposable pattern.
  • Implemented tracking and cleanup of sound effect instances.
  • Added global volume control for both sound effects and music.
  • Created methods to toggle audio states (play/pause, mute/unmute).
  • Updated the Core class to provide global access to the audio controller.
  • Added keyboard controls to adjust volume and toggle mute state.

The AudioController class you created is a significant improvement over directly using MonoGame's audio APIs. It handles common audio management tasks that would otherwise need to be implemented repeatedly in different parts of your game. By centralizing these functions, you make your code more maintainable and provide a consistent audio experience across your game.

In the next chapter, we'll start exploring fonts and adding text to the game.