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:
Create a new directory named Audio.
Add a new class file named AudioController.cs to the Audio directory you just created.
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 theIDisposable
interface, This interface is part of .NET and provides a standardized implementation for an object to release resources. ImplementingIDisposable
allows other code to properly clean up the resources held by our audio controller when it's no longer needed. For more information onIDisposable
, 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:
- The interface provides a
Dispose
method that contains all cleanup logic. - When called,
Dispose
releases any resources the class was using. - 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:
- First, update the
Core
class to add theAudioController
globally. - Update the
Game1
class to use the global audio controller fromCore
.
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:
- Added the
using MonoGameLibrary.Audio;
directive to access theAudioController
class. - Added a static
Audio
property to provide global access to the audio controller. - Created the new audio controller instance in the
Initialize
method. - Added an override for the
UnloadContent
method where we dispose of the audio controller. - 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:
- The
_themeSong
field is added to store a reference to the background song to play. - In LoadContent, the background theme song is loaded using hte content manager.
- In Initialize, the audio manager is used to play the background theme song.
- In Update the audio manager is used to play the bounce and collect sound effects.
- In
CheckKeyboardInput
the following checks were added- If the M key on the keyboard is pressed, it will toggle mute for all audio.
- If the + key is pressed, the global volume is increased by
0.1f
. - 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.