Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon


25. July 2015

 

In this chapter we are going to look at using audio in XNA.  Originally XNA supported one way of playing audio, using XACT (Cross Platform Audio Creation Tool ).  Since the initial release they added a much simplified API.  We will be taking a look at both processes.

 

There is an HD video of this chapter available here.

 

When playing audio there is always the challenge of what formats are supported, especially when you are dealing with multiple different platforms, all of which have different requirements.  Fortunately the content pipeline takes care of a great deal of the complications for us.  Simply add your audio files ( mp3, mp4, wma, wav, ogg ) to the content pipeline and it will do the rest of the work for you.   As you will see shortly though, it is also possible to load audio files outside of the content pipeline.  In this situation, be aware that certain platforms do not support certain formats ( for example, no wma support on Android or iOS, while iOS doesn’t support ogg but does support mp3 ).  Unless you have a good reason, I would recommend you stick to the content pipeline for audio whenever possible.

 

The Perils of MP3

Although MP3 is supported by MonoGame, you probably want to stay away from using it. Why?
Patents. If your game has over 5,000 users you could be legally required to purchase a license. From a legal perspective, Ogg Vorbis is superior in every single way. Unfortunately Ogg support is not as ubiquitous as we'd like it to be.

 

Adding Audio Content using the Content Pipeline

This process is virtually identical to adding a graphic file in your content file.

image

 

Simply add the content like you did using right click->Add Existing Items or the Edit menu:

image

 

If it is a supported format you will see the Processor field is filled ( otherwise it will display Unknown ).  The only option here is to configure the mp3 audio quality, a trade off between size and fidelity.

 

Playing a Song

Now let’s look at the code involved in playing the song we just added to our game.

// This example shows playing a song using the simplified audio api

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace Example1
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        Song song;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

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

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            this.song = Content.Load<Song>("prepare");
            MediaPlayer.Play(song);
            //  Uncomment the following line will also loop the song
            //  MediaPlayer.IsRepeating = true;
            MediaPlayer.MediaStateChanged += MediaPlayer_MediaStateChan
                                             ged;
        }

        void MediaPlayer_MediaStateChanged(object sender, System.
                                           EventArgs e)
        {
            // 0.0f is silent, 1.0f is full volume
            MediaPlayer.Volume -= 0.1f;
            MediaPlayer.Play(song);
        }

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

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            base.Draw(gameTime);
        }
    }
}

 

Notice that we added the using statement Microsoft.Xna.Framework.Media.  We depend on this for the MediaPlayer and Song classes.  Our Song is loaded using the ContentManager just like we did earlier with Texture, this time with the type Song.  Once again the content loader does not use the file’s extension.  Our Song can then be played with a call to MediaPlayer.Play().  In this example we wire up a MediaStateChanged event handler that will be called when the song completes, decreasing the volume and playing the song again.

 

Playing Sound Effects

 

This example shows playing sound effects.  Unlike a Song, SoundEffects are designed to support multiple instances being played at once.  Let’s take a look at playing SoundEffect in MonoGame:

// Example showing playing sound effects using the simplified audio 
api
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Audio;
using System.Collections.Generic;

namespace Example2
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        List<SoundEffect> soundEffects;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
            soundEffects = new List<SoundEffect>();
        }

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

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw 
            textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            soundEffects.Add(Content.Load<SoundEffect>("airlockclose"))
                             ;
            soundEffects.Add(Content.Load<SoundEffect>("ak47"));
            soundEffects.Add(Content.Load<SoundEffect>("icecream"));
            soundEffects.Add(Content.Load<SoundEffect>("sneeze"));

            // Fire and forget play
            soundEffects[0].Play();
            
            // Play that can be manipulated after the fact
            var instance = soundEffects[0].CreateInstance();
            instance.IsLooped = true;
            instance.Play();
        }


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

            if (Keyboard.GetState().IsKeyDown(Keys.D1))
                soundEffects[0].CreateInstance().Play();
            if (Keyboard.GetState().IsKeyDown(Keys.D2))
                soundEffects[1].CreateInstance().Play();
            if (Keyboard.GetState().IsKeyDown(Keys.D3))
                soundEffects[2].CreateInstance().Play();
            if (Keyboard.GetState().IsKeyDown(Keys.D4))
                soundEffects[3].CreateInstance().Play();


            if (Keyboard.GetState().IsKeyDown(Keys.Space))
            {
                if (SoundEffect.MasterVolume == 0.0f)
                    SoundEffect.MasterVolume = 1.0f;
                else
                    SoundEffect.MasterVolume = 0.0f;
            }
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            base.Draw(gameTime);
        }
    }
}

 

Note the using Microsoft.Xna.Framework.Audio statement at the beginning.  Once again we added our audio files using the Content Pipeline, in this case I added several WAV files.  They are loaded using Content.Load() this time with the type SoundEffect.  Next it is important to note the two different ways the SoundEffects are played.  You can either call Play() directly on the SoundEffect class.  This creates a fire and forget instance of the class with minimal options for controlling it.  If you have need for greater control ( such as changing the volume, looping or applying effects ) you should instead create a SoundEffectInstance using the SoundEffect.CreateInstance() call.  You should also create a separate instance if you want to have multiple concurrent instances of the same sound effect playing.  It is important to realize that all instances of the same SoundEffect share resources, so memory will not increase massively for each instance created.  The number of simultaneous supported sounds varies from platform to platform, with 64 being the limit on Windows Phone 8, while the Xbox 360 limits it to 300 instances.  There is no hard limit on the PC, although you will obviously hit device limitations quickly enough.

 

In the above example, we create a single looping sound effect right away.  Then each frame we check to see if the user presses 1,2,3 or 4 and play an instance of the corresponding sound effect.  If the user hits the spacebar we either mute or set to full volume the global MasterVolume of the SoundEffect class.  This will affect all playing sound effects.

 

Positional Audio Playback

Sound effects can also be positioned in 3D space easily in XNA. 

// Display positional audio

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Audio;

namespace Example3
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SoundEffect soundEffect;
        SoundEffectInstance instance;
        AudioListener listener;
        AudioEmitter emitter;


        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

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

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            
            soundEffect = this.Content.Load<SoundEffect>("circus");
            instance = soundEffect.CreateInstance();
            instance.IsLooped = true;

            listener = new AudioListener();
            emitter = new AudioEmitter();

            // WARNING!!!!  Apply3D requires sound effect be Mono!  
            Stereo will throw exception
            instance.Apply3D(listener, emitter);
            instance.Play();
        }

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

            if (Keyboard.GetState().IsKeyDown(Keys.Left))
            {
                listener.Position = new Vector3(listener.Position.X-0.
                                    1f, listener.Position.Y, listener.
                                    Position.Z);
                instance.Apply3D(listener, emitter);
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Right))
            {
                listener.Position = new Vector3(listener.Position.X + 
                                    0.1f, listener.Position.Y, 
                                    listener.Position.Z);
                instance.Apply3D(listener, emitter);
            }

            if (Keyboard.GetState().IsKeyDown(Keys.Up))
            {
                listener.Position = new Vector3(listener.Position.X, 
                                    listener.Position.Y +0.1f, 
                                    listener.Position.Z);
                instance.Apply3D(listener, emitter);
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Down))
            {
                listener.Position = new Vector3(listener.Position.X, 
                                    listener.Position.Y -0.1f, 
                                    listener.Position.Z);
                instance.Apply3D(listener, emitter);
            }            
            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            base.Draw(gameTime);
        }
    }
}

 

In this example, we load a single SoundEffect and start it looping infinitely.  We then create an AudioListener and AudioEmitter instance.  The AudioListener represents the location of your ear within the virtual world, while the AudioEmitter represents the position of the sound effect.  The default location of both is a Vector3 at (0,0,0).  You set the position of a SoundEffect by calling Apply3D().  In our Update() call, if the user hits an arrow key we updated the Position of the AudioListener accordingly.  After changing the position of a sound you have to call Apply3D again.  As you hit the arrow keys you will notice the audio pans and changes volume to correspond with the updated position.  It is very important that your source audio file is in Mono ( as opposed to Stereo ) format if you use Apply3D, or an exception will be thrown.

 

Using XACT

As mentioned earlier, XACT used to be the only option when it came to audio programming in XNA.  XACT is still available and it enables your audio designer to have advanced control over the music and sound effects that appear in your game, while the programmer uses a simple programmatic interface.  One big caveat is XACT is part of the XNA installer or part of the Direct X SDK as is not available on Mac OS or Linux.  If you wish to install it but do not have an old version of Visual Studio installed, instructions can be found here ( http://www.gamefromscratch.com/post/2015/07/23/Installing-XNA-Tools-Like-XACT-without-Visual-Studio-2010.aspx ).  If you are on MacOS or Linux, you want to stick to the simplified audio API that we demonstrated earlier.

Xact is installed as part of the XNA Studio install, on 64bit Windows by default the Xact executable will be located in C:\Program Files (x86)\Microsoft XNA\XNA Game Studio\v4.0\Tools.  Start by running AudConsole3.exe:

image

 

The XACT Auditioning Tool needs to be running when you run the Xact tool.

Then launch Xact3.exe

image

First create a new project:

image

 

Next right click Wave Banks and select New Wave Bank

image

 

Drag and drop your source audio files into the Wave Bank window:

image

 

Now create a new Sound Bank by right clicking Sound Bank and selecting New Wave Bank

image

 

Now drag the Wave you wish to use from the Wave Bank to the Sound Bank

a1

 

Now create a Cue by dragging and dropping the Sound Bank to the Cue window.  Multiple files can be added to a cue if desired.

a2

 

You can rename the Cue, set the probability to play if you set several Sounds in the Cue and change the instance properties of the Cue in the properties window to your left:

image

Now Build the results:

image

 

This will then create two directories in the folder you created your project in:

image

 

These files need to be added directly to your project, you do not use the content pipeline tool!  Simply copy all three files to the content folder and set it’s build action to Copy.

image

 

Now let’s look at the code required to use these generated files:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Audio;

namespace Example4
{
    public class Game1 : Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        AudioEngine audioEngine;
        SoundBank soundBank;
        WaveBank waveBank;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        protected override void Initialize()
        {
            base.Initialize();
        }
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw 
            textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            audioEngine = new AudioEngine("Content/test.xgs");
            soundBank = new SoundBank(audioEngine,"Content/Sound Bank.
                        xsb");
            waveBank = new WaveBank(audioEngine,"Content/Wave Bank.
                       xwb");

            soundBank.GetCue("ak47").Play();
        }

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

            // TODO: Add your update logic here

            base.Update(gameTime);
        }

        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // TODO: Add your drawing code here

            base.Draw(gameTime);
        }
    }
}

 

First you create an AudioEngine using the xgs file, then a SoundBank using the xsb and a WaveBank unsing the xwb file.  We then play the Cue we created earlier with a call to SoundBank.GetQue().Play().  This process allows the audio details to be configured outside of the game while the programmer simply uses the created Que.

 

Finally it is possible to play audio files that weren’t added using the content pipeline or using Xact using a Uri. 

        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw 
            textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            // URL MUST be relative in MonoGame
            System.Uri uri = new System.Uri("content/background.mp3",
                             System.UriKind.Relative);
            Song song = Song.FromUri("mySong", uri);
            MediaPlayer.Play(song);
            MediaPlayer.ActiveSongChanged += (s, e) => {
                song.Dispose();
                System.Diagnostics.Debug.WriteLine("Song ended and 
                                                   disposed");
            };
        }

 

First you create a Uri that locates the audio file you want to load.  We then load it using the method FromUri, passing in a name as well as the uri.  One very important thing to be aware of here, on XNA you could use any URI.  In MonoGame it needs to be a relative path.

 

The Video

 

Programming , , ,

blog comments powered by Disqus

Month List

Popular Comments

Game From Scratch C++ Edition Part 9
Subscribe to GameFromScratch on YouTube Support GameFromScratch on Patreon

Game From Scratch C++ Edition Part 9

 

 

Last chapter we wrote some fairly lousy audio code, now it’s time to clean that up a bit.  Previously we showed how you could use the service locator design pattern to make a swappable interface available without using globals, now we are going to throw away all the FMOD bits and re-write all the SFML parts.  One major issue with the previous code is it loaded the file every time the sound is played.  This is not ideal. As you start adding more sounds to your game, it would get slower and slower.

 

 

 

 

Game development is all about trade-offs and this is a classic one.  Simply put, the slowest part of your computer is almost always the hard drive.  Loading something from file is an extremely expensive action and is something you want to avoid whenever you can help it.  This means our earlier code that load the sound file every time they are played was a very bad idea.  The most common solution to this issue is to implement a file cache, which is exactly what we are going to do!

 

 

 

 

 

So what exactly is a cache?  Well generally speaking, it’s the act of storing commonly used information in faster storage.  For example, your computer’s processor has a small cache, which stores the most commonly accessed bits of memory in super fast ( and more expensive ) dedicated memory so the CPU can access it faster.  We are essentially going to do the same thing, but instead we are storing disk based files in memory instead.  The trade-offs come into play when your cache starts taking up all the systems memory, which could cause things to be even slower than if you had loaded from disk!  It is all a balancing act.

 

 

 

 

 

For now though, the cache we are going to implement is extremely simple.  First off, it will only handle SFML sound and music files, although there is nothing from stopping you from implementing a caching solution that works for all kinds of media.  Basically our cache object is going to store in memory every single audio file, since we don’t have that many of them.  In a more advanced caching solution, the cache would start unloading files that haven’t been accessed recently.  The key to our cache is it is completely transparent to the rest of your code, in fact except for the behind the scenes stuff, nothing changes in your code!

 

 

 

 

 

First off, we make a very small fix to IAudioProvider.h.  In the previous chapter I forgot to declare a virtual destructor, which as I mentioned earlier is a bit of a no-no, as it could lead to your destructor not being called.  Also for some reason <String.h> was included for no reason I can discern, I’ll blame gremlins.  Alright, that covered, lets move on.

 

 

 

 

 

First, lets quickly re-visit SFMLSoundProvider.h.

 

#pragma once #include "stdafx.h" #include "IAudioProvider.h" #include "SoundFileCache.h" class SFMLSoundProvider : public IAudioProvider { public: SFMLSoundProvider(); void PlaySound(std::string filename); void PlaySong(std::string filename, bool looping = false); void StopAllSounds(); bool IsSoundPlaying(); bool IsSongPlaying(); private: static const int MAX_SOUND_CHANNELS = 5; SoundFileCache _soundFileCache; sf::Sound _currentSounds[MAX_SOUND_CHANNELS]; std::string _currentSongName; };

Click here to download SFMLSoundProvider.h

 

 

 

 

 

Things haven’t really changed much from the last chapter.  The key things to be aware of is instead of storing a single sf::Sound and sf::Music file, we now store multiple sounds ( defined by MAX_SOUND_CHANNELS ) and simply store the name of the current song.  The biggest reason for these changes is because the earlier implementation could only play one sound at a time, which was an obvious limitation!  We do however only have a single Music file playing at once, and all we really need to manage that is the file name.

 

 

 

 

 

 

These changes obviously had an effect on SFMLSoundProvider.cpp but things have stayed pretty consistent to what we had before.  Let’s take a look.

 

 

 

 

 

#include "stdafx.h" #include "SFMLSoundProvider.h" #include "SoundFileCache.h" SFMLSoundProvider::SFMLSoundProvider() : _currentSongName("") { } void SFMLSoundProvider::PlaySound(std::string filename) { int availChannel = -1; for(int i = 0; i < MAX_SOUND_CHANNELS;i++) { if(_currentSounds[i].GetStatus() != sf::Sound::Playing) { availChannel = i; break; } } // If all sound channels are in use, do nothing for now if(availChannel != -1) { try { _currentSounds[availChannel] = _soundFileCache.GetSound(filename); _currentSounds[availChannel].Play(); } catch(SoundNotFoundExeception& snfe) { // ERROR, file wasnt found, should handle error here // Currently, this will simply mean nothing happens if an error occurs } } } void SFMLSoundProvider::PlaySong(std::string filename, bool looping) { sf::Music * currentSong; try { currentSong = _soundFileCache.GetSong(filename); } catch(SoundNotFoundExeception&) { // This one is dire, means we couldn't find or load the selected song // So, lets exit! return; } // See if prior song is playing still, if so, stop it if(_currentSongName != "") { try { sf::Music* priorSong = _soundFileCache.GetSong(_currentSongName); if(priorSong->GetStatus() != sf::Sound::Stopped) { priorSong->Stop(); } } catch(SoundNotFoundExeception&) { // Do nothing, this exception isn't dire. It simply means the previous sound we were // trying to stop wasn't located. } } _currentSongName = filename; currentSong->SetLoop(looping); currentSong->Play(); } void SFMLSoundProvider::StopAllSounds() { for(int i = 0; i < MAX_SOUND_CHANNELS; i++) { _currentSounds[i].Stop(); } if(_currentSongName != "") { sf::Music * currentSong = _soundFileCache.GetSong(_currentSongName); if(currentSong->GetStatus() == sf::Sound::Playing) { currentSong->Stop(); } } } bool SFMLSoundProvider::IsSoundPlaying() { for(int i = 0; i < MAX_SOUND_CHANNELS; i++) { if(_currentSounds[i].GetStatus() == sf::Sound::Playing) return true; } return false; } bool SFMLSoundProvider::IsSongPlaying() { if(_currentSongName != "") { return _soundFileCache.GetSong(_currentSongName)->GetStatus() == sf::Music::Playing; } return false; }

Click here to download SFMLSoundProvider.cpp

 

 

 

 

One of the first major changes you are going to notice after the constructor is PlaySound now supports multiple concurrent sounds.  This is done simply by storing the sounds in an array MAX_SOUND_CHANNELS in size.  Now, when we play a sound we need to make sure there is a sound available that isn’t currently playing.  This is done by availChannel to –1, then looping through all of the available channels until we find one that isn’t playing.  If we find an idle channel, we assign its offset to availChannel and exit our loop.

 

 

 

 

 

 

At this point, we have one of two possibilities.  We either finished the loop and none of the sound channels were available, or we found an open channel.  If no channels are available ( meaning every channel is currently used playing a song ) we simply do nothing.  Otherwise we assign the available sound object from the _soundFileCache and tell it to play.  We will cover the _soundFileCache in detail later, but you may have just had your first encounter with exception handling!  For more details on exception handling, take a look at the optional information section.

 

 

 

 

 

 

PlaySong works quite similarly, yet instead of having a pool of sounds available, we actually only keep the songs name.  Right off the hop, we try to get our song from the cache.  Again an error finding the sound is handled using exceptions ( although not handled very well… ).  Assuming everything went fine retrieving the song, we check if _currentSongName has been assigned, if it has, there is a possibility that song is still playing.  Therefore we check to see if a song of that name from cache is currently playing and stop it if it is.  In this case, if an exception occurs during this process it is no big deal, so we simply ignore it.  Finally, we assign _currentSongName to our name filename value and tell it to play.

 

 

 

 

 

StopAllSounds(), IsSoundPlaying() and IsSongPlaying() all work very similarly to how they did before, with the exception that we no longer store a pointer to the current song, and that the sounds buffer is now 5 items instead of just one.  Now lets take a look at the definition of SoundFileCache.h.

 

 

 

#pragma once #include "SFML/Audio.hpp" class SoundFileCache { public: SoundFileCache(void); ~SoundFileCache(void); sf::Sound GetSound(std::string) const; sf::Music* GetSong(std::string) const; private: static std::map<std::string, sf::SoundBuffer*> _sounds; static std::map<std::string, sf::Music*> _music; struct SoundFileDeallocator { void operator()(const std::pair<std::string,sf::SoundBuffer*> & p) { delete p.second; } }; struct MusicFileDeallocator { void operator()(const std::pair<std::string,sf::Music*> & p) { delete p.second; } }; template <typename T> struct Deallocator{ void operator()(const std::pair<std::string,T> &p) { delete p.second; } }; }; class SoundNotFoundExeception : public std::runtime_error { public: SoundNotFoundExeception(std::string const& msg): std::runtime_error(msg) {} };

Click here to download SoundFileCache.h

 

 

 

 

SoundFileCache is a very straight forward class.  Nothing is declared as virtual as this is not a class that is intended to be inherited from.  First off we declare a constructor ( ctor) and destructor (dtor) and a pair of public methods we’ve already seen in use, GetSound and GetSong.  You may notice that GetSound returns a sf::Sound object instead of a pointer to one like GetSong does.  This is because the Sound object is actually pretty light weight, it is in fact the SoundBuffer used to create an sf::Sound that is resource intensive so that is the one thing we cache.

 

 

 

 

 

Now we take a look at the private part of the class.  The cache is actually internally maintained as a pair of separate maps, a class we have used many times.  Next up we have a pair of functors like we saw earlier, that are used with for_each in the destruction process.  Both of the structs are actually just for show, as we use the third more generic Deallocator.  The other two are just there for illustration purposes at this point and can be deleted.

 

 

 

 

 

Notice how SoundFileDeallocator and MusicFileDeallocator look almost identical, with the only difference being the type?  Even the methods are the same.  Whenever you find yourself in a situation like this, its time to consider using a template.  When using templates, you basically let the compiler do the work for you.  The compiler basically creates a method for you matching whatever data type you used.  Templates are not a trivial subject, so for more details check here.  Templates are a very fundamental part of modern C++ and are a subject I highly suggest you research further.

 

 

 

 

 

Finally we declare our exception object SoundNotFoundExeception we saw in use earlier.  This exception simply derives from runtime_error, a part of the standard library.  It simply declares a constructor that takes a string and passes that string to it’s base classes constructor.   In this case it is more about creating the “type” of SoundNotFoundException than it is the implementation.  This is so this specific exception and only it if desired, can be caught.  If this is confusing, read on to learn a bit more about exception handling.

 

 

 

 

Optional information

 

Isn’t that exceptional?

 

 

There are a number of ways to handle error condition and problems in C++.  The traditional way was using return codes, either some result code or a NULL return value.  As we saw earlier, there is also the assert which brings your code to a screeching halt, throwing up an error message with the assert details.  One other option is exceptions.

 

 

There are 4 primary aspects involved in exception handling.  They are:
 
 
The Exception object itself.  In our case we declared SoundNotFoundException and inherited it from std::runtime_exception, the standard base class for exception handling.  This is the object that describes the nature of the exception.  We simply take a message string as part of the exceptions constructor that describes the problem that occurred.  The type of an exception is very important, as it is used later on when catching them.  Therefore you often find yourself creating exception objects that seem to do very little other than exist.
 
 
 
Next is the try statement followed by curly braces.  The area wrapped in braces denotes the area we are monitoring to see if an exception occurred.  One important thing to keep in mind, an exception that is embedded within code marked by a try statement ( possibly even code you didn’t write ) will still be caught.  As you saw from our example, the try block was located in SFMLSoundProvider, but it was actually SoundFileCache that threw the exception.
 
 
 
 
Speaking of caught exceptions, this is the job of catch.  The catch statement must go immediately after the try block.  If you put anything in between the closing brace of the try block and the catch statement, you will get a syntax error.  Within the catch statement, you specify what type of exception you catch, in this case SoundNotFoundExeception&.  You can have multiple catch statements for different kinds of exceptions.  The order you specify catchs in is very important, as once an exception is handled it will not be passed into any other catch blocks.  So for example if we had a catch(runtime_exception) ahead of our catch(SoundNotFoundException), that is the catch block that will be used, because SoundNotFOundException IS a runtime_exception.  Finally you can catch all exception of any types by using catch(…), although generally you shouldn’t.  If you don’t know how to recover from or handle an exception, let someone else have a crack at it who may know what to do.
 
 
 
 
The catch statement is where you would handle reporting or logging of the exception that occurred.  You also have the option of re-throwing the exception once you’ve dealt with it, so other parts of your program can see that an exception has occurred.
 
 
 
 
 
Not much sense catching, if there is nothing to catch and that is exactly what throw does.  When you throw you create a new exception object that will be thrown.  If you want to re-throw an exception you are handling (within a catch block ), simply enter “throw;” and the object will be thrown again, allowing multiple exception handlers to manage a single exception.  One very important thing to keep in mind, program execution ends at the throw, so be very careful not to leak memory or resources.  Additionally, using throw outside of a try block will simply terminate execution.
 
 
 
 
 
One nice aspect about exception handling is, if no exceptions occur, there will be little to no performance cost.  Therefore using exceptions can be more efficient than other error handling methods.  Exceptions can also be thrown within a constructor, which is useful as constructors cannot return an error value.
 
 
 
 
 
Exceptions however, should NOT be used for program control.  Never use an exception in place of a return, continue or other control structure.  Additionally, if you have a method that returns an error code you should keep it that way, and have the method that calls the function decide if an exception occurred or not.  Exceptions, especially unhandled exceptions, can make program flow quite confusing, so use them wisely.

 

 

Now lets take a look at SoundFileCache.cpp

#include "StdAfx.h" #include "SoundFileCache.h" SoundFileCache::SoundFileCache(void) {} SoundFileCache::~SoundFileCache(void) { std::for_each(_sounds.begin(),_sounds.end(),Deallocator<sf::SoundBuffer*>()); std::for_each(_music.begin(),_music.end(),Deallocator<sf::Music*>()); } sf::Sound SoundFileCache::GetSound(std::string soundName) const { std::map<std::string,sf::SoundBuffer *>::iterator itr = _sounds.find(soundName); if(itr == _sounds.end()) { sf::SoundBuffer *soundBuffer = new sf::SoundBuffer(); if(!soundBuffer->LoadFromFile(soundName)) { delete soundBuffer; throw SoundNotFoundExeception( soundName + " was not found in call to SoundFileCache::GetSound"); } std::map<std::string,sf::SoundBuffer *>::iterator res = _sounds.insert(std::pair<std::string,sf::SoundBuffer*>(soundName,soundBuffer)).first; sf::Sound sound; sound.SetBuffer(*soundBuffer); return sound; } else { sf::Sound sound; sound.SetBuffer(*itr->second); return sound; } throw SoundNotFoundExeception( soundName + " was not found in call to SoundFileCache::GetSound"); } sf::Music* SoundFileCache::GetSong(std::string soundName) const { std::map<std::string,sf::Music *>::iterator itr = _music.find(soundName); if(itr == _music.end()) { sf::Music * song = new sf::Music(); if(!song->OpenFromFile(soundName)) { delete song; throw SoundNotFoundExeception( soundName + " was not found in call to SoundFileCache::GetSong"); } else { std::map<std::string,sf::Music *>::iterator res = _music.insert(std::pair<std::string,sf::Music*>(soundName,song)).first; return res->second; } } else { return itr->second; } throw SoundNotFoundExeception( soundName + " was not found in call to SoundFileCache::GetSong"); } std::map<std::string, sf::SoundBuffer*> SoundFileCache::_sounds; std::map<std::string, sf::Music*> SoundFileCache::_music;

Click here to download SoundFileCache.cpp

 

 

 

 

First we have an empty constructor for no particular reason.  Next up is our destructor which cleans up our data. We simply iterate through both of our map data objects and deallocate them using the Deallocator functor.  As we saw earlier, this functor is implemented as a template, so you need to specify the type in each call.  Behind the scenes, the compiler creates a Deallocator taking a SoundBuffer* type and a Deallocator taking a Music* type for us.

 

 

 

 

 

 

GetSound() takes a string representing the sound’s filename.  First we check to see if a sound by that name has already been loaded.  If it hasn’t, we attempt to load it.  If the file load fails ( most likely a file not found ), we delete our sound pointer then throw a SoundNotFoundException.  If this happens, our code is effective done executing as if we had hit a return statement.  Assuming everything went ok loading the file, we add the SoundBuffer to our cache.  Now we create a new sf::Sound object, set its buffer to our newly cached SoundBuffer and return it.  Since this is not a pointer being returned, it will actually result in a new copy being created, but since the SoundBuffer is the intensive part, this is ok.  If the file was located in cache, we simply create and return a new sf::Sound object set to that buffer from cache.  The advantage to this setup is, regardless to if a file has been loaded or not, you use GetSound().

 

 

 

 

 

 

GetSong() works very similar, with the biggest difference being sf::Music files do not use a SoundBuffer like sf::Sound does.  This means the sf::Music object is a bit more heavy weight, so we return a pointer instead of creating a new object each call.  For some odd reason too, sf::Music uses OpenFromFile() instead of LoadFromFile(), which is a shame as it prevents me from templating a single GetAudio<T>() style call, since their signatures wouldn’t match.  You may notice that although our game only uses a single song, SoundFileCache supports as many as you want.

 

 

 

 

 

Finally, since both _sounds and _music are static, we need to implement them in the cpp file.  That is basically all that is required for implementing a cache.  With this change, an audio file will only be loaded from disk once, then on subsequent calls will simply be read from cache, greatly speeding things up.  In a more advanced game there are a couple changes that you might implement differently.  First off, it would make sense to implement a common cache, so you could cache music, art, sound FX, etc… all using the same mechanism.  Second, in a long running program with lots of objects, it makes sense to have the cache unload objects over time, either as memory gets lower or as time elapsed since the item was last accessed.  Finally, it is often handy to pre-cache items you know you are going to need, well before they are called in code.

 

 

 

 

 

 

Now, since this chapter just isn’t quite long enough Winking smile  and because frankly it has to go somewhere, let’s create our other paddle.  This is going to basically be a cut, hack and paste job from our PlayerPaddle.  First create a file called AIPaddle.h.  Inside it should look like:

 

 

 

#pragma once #include "visiblegameobject.h" class AIPaddle : public VisibleGameObject { public: AIPaddle(void); ~AIPaddle(void); void Update(float elapsedTime); void Draw(sf::RenderWindow& rw); float GetVelocity() const; private: float _velocity; // -- left ++ right float _maxVelocity; };

Click here to download AIPaddle.h

 

 

 

 

 

As I said, pretty much a cut and paste job.  As this is basically identical to PlayerPaddle.h, there is little to explain.  Now lets go ahead and create AIPaddle.cpp once created it should contain:

 

 

 

#include "StdAfx.h" #include "AIPaddle.h" #include "Game.h" #include "GameBall.h" AIPaddle::AIPaddle() : _velocity(0), _maxVelocity(600.0f) { Load("images/paddle.png"); assert(IsLoaded()); GetSprite().SetCenter(GetSprite().GetSize().x /2, GetSprite().GetSize().y / 2); } AIPaddle::~AIPaddle() { } void AIPaddle::Draw(sf::RenderWindow & rw) { VisibleGameObject::Draw(rw); } float AIPaddle::GetVelocity() const { return _velocity; } void AIPaddle::Update(float elapsedTime) { const GameBall* gameBall = static_cast<GameBall*> (Game::GetGameObjectManager().Get("Ball")); sf::Vector2f ballPosition = gameBall->GetPosition(); if(GetPosition().x -20 < ballPosition.x) _velocity += 15.0f; else if(GetPosition().x +20 > ballPosition.x) _velocity -= 10.0f; else _velocity = 0.0f; if(_velocity > _maxVelocity) _velocity = _maxVelocity; if(_velocity < -_maxVelocity) _velocity = -_maxVelocity; sf::Vector2f pos = this->GetPosition(); if(pos.x <= GetSprite().GetSize().x/2 || pos.x >= (Game::SCREEN_WIDTH - GetSprite().GetSize().x/2)) { _velocity = -_velocity; // Bounce by current velocity in opposite direction } GetSprite().Move(_velocity * elapsedTime, 0); }

Click here to download AIPaddle.cpp

 

 

 

 

This is again virtually identical to PlayerPaddle.cpp, except we stripped out all the user input logic and instead replaced it with some seriously stupid AI.  ( Don’t worry, we will improve this a bit later ).  All of this new logic takes place in the Update() method.  First we get a reference to our game ball object.  ( As an optimization trick for the future, you could eek a small amount of performance gain out of storing the gameBall pointer, instead of retrieving it once per frame ).  Once we have our ball, we check where it is located and if it is more than 30 pixels to the left or right of our center, we speed up in that direction.  If its within 30 pixels either way, we come to a dead stop.  All the remaining logic we have seen before.

 

 

 

 

 

 

Now that we have an AIPaddle, back in Game.cpp in our Start() method, we change it as follows:

 

void Game::Start(void) { if(_gameState != Uninitialized) return; _mainWindow.Create(sf::VideoMode(SCREEN_WIDTH,SCREEN_HEIGHT,32),"Pang!"); SFMLSoundProvider soundProvider; ServiceLocator::RegisterServiceLocator(&soundProvider); soundProvider.PlaySong("audio/Soundtrack.ogg",true); PlayerPaddle *player1 = new PlayerPaddle(); player1->SetPosition((SCREEN_WIDTH/2),700); AIPaddle * player2 = new AIPaddle(); player2->SetPosition((SCREEN_WIDTH/2),40); GameBall *ball = new GameBall(); ball->SetPosition((SCREEN_WIDTH/2),(SCREEN_HEIGHT/2)-15); _gameObjectManager.Add("Paddle1",player1); _gameObjectManager.Add("Paddle2",player2); _gameObjectManager.Add("Ball",ball); _gameState= Game::ShowingSplash; while(!IsExiting()) { GameLoop(); } _mainWindow.Close(); }

Click here to download Game.cpp

 

 

 

 

And presto, we now have a semi-functioning, if a bit twitchy AI.  Here is a screen shot of what our game currently looks.

 

 

 

 

pang9b

 

 

 

 

In the next chapter we will look at improving our AI a great deal.  Additionally we will add some collision detection logic to the AIPaddle so the ball actually bounces when it hits the paddle.  Lastly, we will add a scoreboard.  In essence, we will turn Pang! into a game.  Oh, and we will be doing a bit of cleaning up, including fixing a horrible bug in the game loop.  Stay tuned!

 

 

 

 

Oh, and just like always, you can download the complete project here.

 

EDIT(5/14/2012): Download SFML 2.0RC project here. This is the above tutorial ported to SFML 2.0. Note, this was ported by a reader. The code will vary slightly from the tutorial above.


 

 

 

 

 

Back to Part Eight Coming soon(ish)




blog comments powered by Disqus


Month List

Popular Comments