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.
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 lightweight, 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 conditions 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 exception’s constructor that describes the problem that occurred. The type of 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 existing.
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 catches 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 type 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 to 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 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.
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) |