Game From Scratch C++ Edition Part 8

 

 

 

A bit of a warning upfront, almost all of this entire part is going to be throw away code.  At the same time as I release 8 I am going to release part 9 that implements a much more proper version.  Do not take from this chapter anything as good working form with SFML or FMOD, all the code added related to those topics is just for demonstration purposes.  This part is more of a learning chapter than anything else, so keep that in mind while you read it.  It does however cover a very useful concept that I believe will help you immensely going forward!  If you are only interested strictly in SFML topics, skip forward to the next section but I promise you, you will regret it! Winking smile

 

 

 

Alright, end of warning.

 

 

 

In our code so far, objects that we have needed available across the entire application, such as the GameObjectManager or the RenderWindow have been simply made available as static members in the Game object.  So if you needed the GameObjectManager you simply called Game::GetGameObjectManager().  In a simple game, this setup is sufficient and marginally better than simply making each of those items global.  However as your game grows, so will the dependencies on these items and you are rapidly moving towards creating a god object.  Now, a god object sounds like a really cool thing, but it isn’t.  Just think for a second how much work it would be if I wanted to change from using SFML to another library?  Because I have exposed RenderWindow throughout the code, this change becomes very difficult and error prone.  We will look an alternative method that reduce this dependency.

 

 

 

In this chapter we are looking at adding sound to our game, in a loosely coupled manner.  Coupling refers to the relationship between two or more classes, and generally you want one class to have to know as little as possible about another class in order to function properly. Truth of the matter is, the topic of playing SFML sound is about as simple as it could be:

 

 

sf::SoundBuffer mySound; mySound.LoadFromFile("MySound.wav"); if(mySound) { sf::Sound sound; sound.SetBuffer(mySound); sound.Play(); }

 

 

 

And…

done.  Not much of a chapter eh?

 

 

 

There is a little bit more complexity than this, for example SoundBuffer’s are quite resource intensive, so they should be cached to keep your game performing well, but that isn’t anything too difficult to conquer.  But this wouldn’t be the worlds most over-engineered Pong clone if we took the easy route out, would it?? 

 

 

 

So lets say in your game you want to be able to allow any object to play a song but you didn’t want to make the sound object globally available, how would you do it?  More importantly, lets say you wanted to be able to switch audio libraries or let the user switch libraries, how would you do this?  If you think about it, it happens all the time.  How many games have you played that allowed you to choose between DirectX9 or DirectX11, or between DirectX and OpenGL… ever wondered how they do that?  Well, that is exactly what we are going to do this chapter and while doing so, I am going to introduce you to something, that if you haven’t encountered them already, will probably change the way you program!

 

 

 

 

That thing… design patterns.  Regardless to if you are writing code for a game, the logic for a toaster or creating a word processor, there are certain common enough tasks that occur in almost all programs.  You can consider design patterns as a “best practices” to solving these common problems.  The Gang of Four are probably the best source on the internet for design patterns and I highly recommend you check that site out. You will find it a bit bizarre at first, but in time they will make a ton of sense. When you find yourself with a very difficult to solve object design problem, take a look at these design patterns and there is a good chance there is a solution to your problem there.  Be careful though, just like when you have a hammer all you see is nails, once you start learning design patterns, you start to over-engineer everything you touch!  Be wary of such an approach.

 

 

 

The particular pattern we are going to use is the Service Locator, which ironically isn’t on the GoF website.

 

 

 

This UML diagram that I ripped off from here demonstrates the Service Locator pattern.

 

 

 

 

image

 

 

 

 

 

One thing about UML diagrams, is they make things look a hell of a lot harder than they really are!  Don’t worry, this process isn’t going to be all that painful, trust me.

 

 

 

Let’s get rid of some of the complexity right away… Client simply refers to the code where your class is going to be called/used from, IService2 and Service2 simply illustrated that ServiceLocator can expose multiple services.  Initialzer simply refers to the code that will create your service locator in the first place.

 

 

 

So that leaves 3 classes to explain, ServiceLocator, IService1 and Service1.  Lets start with IService1, because the other two aren’t going to make any sense without it.

 

 

 

imageBy naming convention a class prefixed with “I” represents an interface.  In C# or Java this is an actual thing, while in C++ it’s something generally called an “abstract base class”, which means it is a class with one or more pure virtual methods.  Alright then, what’s a pure virtual method?  When you declare a pure virtual method, you are simply defining a function definition that MUST be implemented in derived class.  You can’t actually create classes that contain a pure virtual method, if you try to new one, you will get a compiler error.  Instead you inherit from that class and create the inherited class instead.  This forced inheritance provides a common interface and thus the name.  Alright, enough talk, let’s see one in action in IAudioProvider.h:

 

 

 

#pragma once #include "stdafx.h" class IAudioProvider { public: virtual ~IAudioProvider() {} virtual void PlaySound(std::string filename) = 0; virtual void PlaySong(std::string filename, bool looping) = 0; virtual void StopAllSounds() = 0; virtual bool IsSoundPlaying() = 0; virtual bool IsSongPlaying() = 0; };

Click here to download IAudioProvider.h

 

 

 

 

Notice how all of the methods end with “ =  0; “?  That is what makes them pure virtual.  The virtual keyword we already covered, meaning that these methods can be overridden in a derived class.  In this case, they are called “pure” virtual because they have to be implemented in the derived class, as there is no implementation for each of these methods. As in the exist online in a “purely virtual” manner.  As you will see, there is no IAudioProvider.cpp file anywhere in our project.  All that you are declaring here is, somewhere down the road, there (might) be classes that implement these 5 methods.  Actually, lets jump on to one such implementation right away!

 

 

 

imageFrom the UML perspective, SFMLSoundProvider is the Service class.  This class “implements” the IAudioProvider interface.  This is the actual code that, for lack of a better expression, does stuff!   Alright, lets take a look at SFMLSoundProvider.h and SFMLSoundProvider.cpp.  A reminder once again, the code we write in this section is actually going to be re-written to a certain degree in the follow up chapter, so don’t pay too much attention to the details of the code itself.  It is more the idea and structure that we are interested in right now.

 

 

 

SFMLSoundProvider.h:

 

#pragma once #include "stdafx.h" #include "IAudioProvider.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: sf::SoundBuffer _soundBuffer; sf::Sound _sound; sf::Music _music; };

Click here to download SFMLSoundProvider.h

 

 

 

 

Obviously the code is going to look a whole lot like IAudioProvider.h because that is the class we are inheriting/overriding from.  The only real difference is, we’ve added 3 member variables, _soundBuffer, _sound and _music.  _soundBuffer is an sf::SoundBuffer, which represents that actual bits and bytes that make up the waveform of a song.  _sound is an sf::Sound object, which represents the currently playing sound while _music is an sf::Music object that represents the currently playing music file.

 

 

 

Now lets take a look at the implementation, create a file called SFMLSoundProvider.cpp:

 

 

#include "stdafx.h" #include "SFMLSoundProvider.h" SFMLSoundProvider::SFMLSoundProvider() { _sound.SetVolume(100.0f); } void SFMLSoundProvider::PlaySound(std::string filename) { if(_soundBuffer.GetDuration() == 0) { _soundBuffer.LoadFromFile(filename); } if(_sound.GetStatus() == sf::Sound::Playing) { _sound.Stop(); } _sound.SetBuffer(_soundBuffer); _sound.Play(); } void SFMLSoundProvider::PlaySong(std::string filename, bool looping) { _music.OpenFromFile(filename); _music.SetLoop(looping); _music.Play(); } void SFMLSoundProvider::StopAllSounds() { if(_sound.GetStatus() == sf::Sound::Playing) _sound.Stop(); if(_music.GetStatus() == sf::Sound::Playing) _music.Stop(); } bool SFMLSoundProvider::IsSoundPlaying() { return _sound.GetStatus() == sf::Sound::Playing; } bool SFMLSoundProvider::IsSongPlaying() { return _music.GetStatus() == sf::Music::Playing; }

Click here to download SFMLSoundProvider.cpp

 

 

 

 

The code is pretty straight forward ( and fairly bad ).  In the constructor we set the _sound’s playback volume to 100%, I did this merely because I had a weird issue where sound on my one machine defaulted to quite quite.  PlaySound is next up.  Basically this function is called whenever we want to play a song, which first checks to see if the current _soundBuffer is of zero length, if it is it loads the SoundBuffer from a file using the provided file name.  Then it checks to see if our sound is currently playing, and if it is, it stops it then it assigns the buffer to the sound and finally plays it.  You may notice a number of bad things here, but the two biggest being a) we load from file every time the sound is played, and b) there can only ever be one sound!  Don’t worry, we will make a much better class later on.

 

 

 

The next method PlaySong() predictably enough, plays a song.  Opens it from file, sets it to loop or not based on the parameters passed in, and finally tells the song to go ahead and play.  Again, you might notice that there can be only one song playing ( which actually makes sense ) and that it loads from file every time ( which does not make sense! ).

 

 

 

Next we have StopAllSounds() which is responsible for stopping all music and sound playback.  This is done by simply checking the _sound and _music status respectively and checking to see if they are currently playing.  If they are, we call their .stop() method.  Last are a pair of simple methods that check to see if a song or sound are playing, using basically the identical logic to StopAllSounds().  That’s it, this is our implementation of IAudioProvider which can now (quite badly ) play sounds and music.  Now we actually have to put it to use!

 

 

 

imageThis is where the ServiceLocator comes in.  Please ignore the word “Singleton” in that UML diagram, we aren’t actually implementing one here, but I really didn’t feel like doing up my own UML diagram… Anyways, a service locator does exactly what it says it does, it locates a service.  In our case we just finished declaring our IAudioProvider service and then implemented one in the form of SFMLSoundProvider, the ServiceLocator is now going to make that service available to the outside world.  In order to do that we are going to implement yet another class, this one is called predictably enough, ServiceLocator.  Lets go ahead and create ServiceLocator.h and ServiceLocator.cpp and take a look at each one’s code.  

 

ServiceLocator.h:

 

#pragma once #include "stdafx.h" #include "IAudioProvider.h" class ServiceLocator { public: static IAudioProvider* GetAudio() { return _audioProvider; } const static void RegisterServiceLocator(IAudioProvider *provider) { _audioProvider = provider; } private: static IAudioProvider * _audioProvider; };

Click here to download ServiceLocator.h

 

 

 

 

As I said, the UML actually makes things look a lot more complicated than they really are.  Our ServiceLocator simply contains two methods, one that allows you to set (or register ) the IAudioProvider and one that allows you to get the IAudioProvider.  Finally, it has a private variable where it stores a pointer to the IAudioProvider.  That’s it really.  One thing to keep in mind, your ServiceLocator class could be used to locate a number of services… renderer, logger, IO,  etc.

 

 

 

Finally, lets take a look at ServiceLocator.cpp:

 

 

#include "stdafx.h" #include "ServiceLocator.h" IAudioProvider* ServiceLocator::_audioProvider = NULL;

Click here to download ServiceLocator.cpp

 

 

 

That’s it.  Basically since _audioProvider is a static, like the static variables we declared earlier, it needs to be provided an implementation, which is what this is.  So, there is our Service Locator Pattern fully implemented.  Let’s see it in action!

 

 

 

Back in our Game.cpp, we add the following code near the top of Game::Start():

 

 

SFMLSoundProvider soundProvider; ServiceLocator::RegisterServiceLocator(&soundProvider);

 

 

 

 

We now officially have an AudioProvider registered, this is the part of the UML diagram referred to as “The Initialiser”.  Lets see how you would use our audio provider.  Anywhere within the code, to play a song or sound, we simply go:

 

 

ServiceLocator::GetAudio()->PlaySound("audio/kaboom.wav"); ServiceLocator::GetAudio()->PlayMusic("audio/song.ogg");

 

 

In fact, lets go ahead and add some sound to our game.  In Game.cpp, right after you register the service locator, add the following line:

 

 

ServiceLocator::GetAudio()->PlaySong("audio/Soundtrack.ogg",true);

 

 

 

This line of code causes the song “Soundtrack.ogg” to play (on loop) as soon as the game starts.  You can see the updated changes to Game.cpp here.  Then later on in our MainMenu class in the GetMenuResponse() method, if there is a mouse click, add the following code to stop the title soundtrack from playing:

 

 

if(ServiceLocator::GetAudio()->IsSongPlaying()) ServiceLocator::GetAudio()->StopAllSounds();

 

 

 

Click here to see the updated MainMenu.cpp.  So now we have a song playing when the game starts, and that song being stopped when a menu item is selected, now lets add a sound effect to the game.  In GameBall.cpp, when we have determined that the ball did in fact hit the players paddle, lets play a sound like this:

 

 

ServiceLocator::GetAudio()->PlaySound("audio/kaboom.wav");

 

 

 

You can click here to see the changed to GameBall.cpp.

 

 

 

And presto, we have the ability to make calls to our Audio subsystem anywhere in the code, without a tight coupling ( ie, GameBall  needs to know nothing about the SFMLSoundProvider class to play sound ).  In the UML diagram, this is represented these calls are represented as “The Client”.  Now you might be thinking to yourself… damn that was a lot of work and complexity for what could have basically been solved by adding a GetAudio() class to Game.cpp and you are right, but the benefits are worth it.  That decoupling of code not only makes your classes more modular, less prone for abuse/feature bloat and easy to maintain, it adds another very nice feature.

 

 

 

Have you ever heard of FMOD?  Fmod is, I believe, the number one commercially used audio libraries available.  It provides audio functionality, given its dedicated nature, that SFML simply can’t touch.  Now what happens if you wanted to implement FMOD at a later date, or more likely, at the same time?  With the Service Locator, this task is trivial.

 

 

 

FModSoundProvider.h:

 

#pragma once #include "IAudioProvider.h" #include "fmod/fmod.h" #include "fmod/fmod.hpp" #include "fmod/fmod_errors.h" class FModSoundProvider : public IAudioProvider { public: FModSoundProvider(); virtual ~FModSoundProvider(); virtual void PlaySound(std::string filename); virtual void PlaySong(std::string filename, bool looping); virtual void StopAllSounds(); virtual bool IsSoundPlaying(); virtual bool IsSongPlaying(); private: void InitFMOD(); FMOD::System * _system; FMOD::Sound * _sound; FMOD::Channel * _soundChannel; FMOD::Sound * _song; FMOD::Channel * _musicChannel; void ERRCHECK(FMOD_RESULT); };

Click here to downlaod FModSoundProvider.h

 

 

 

 

As you can see, just like we did earlier with SFMLSoundProvider, we implement the 5 method inherited from IAudioProvider.  Again like SFMLSoundProvider, there are a number of fields that are specific to the FMod, but since they are all private, it doesn’t matter to the rest of the world.

 

 

 

Now a quick look at the implementation in FModSoundProvider.cpp:

 

 

#include "StdAfx.h" #include "FModSoundProvider.h" FModSoundProvider::FModSoundProvider() : _system(NULL) { InitFMOD(); } FModSoundProvider::~FModSoundProvider() { if(_sound != NULL) { _sound->release(); } if(_song != NULL) { _song->release(); } if(_system != NULL) { _system->release(); } } void FModSoundProvider::PlaySound(std::string filename) { _system->createSound(filename.c_str(),FMOD_DEFAULT,0, &_sound); _system->playSound(FMOD_CHANNEL_REUSE,_sound,false,&_soundChannel); } void FModSoundProvider::PlaySong(std::string filename, bool looping) { if(looping) _system->createSound(filename.c_str(),FMOD_LOOP_NORMAL ,0, &_song); else _system->createSound(filename.c_str(),FMOD_DEFAULT,0, &_song); _system->playSound(FMOD_CHANNEL_REUSE,_song,false,&_musicChannel); } void FModSoundProvider::StopAllSounds() { { int channelIndex; FMOD_RESULT result; FMOD::Channel *nextChannel; for (channelIndex = 0; channelIndex < 100; channelIndex++) { result = _system->getChannel(channelIndex, &nextChannel); if ((result == FMOD_OK) && (nextChannel != NULL)) nextChannel->stop(); } } } bool FModSoundProvider::IsSoundPlaying() { bool result; _soundChannel->isPlaying(&result); return result; } bool FModSoundProvider::IsSongPlaying() { bool result; _musicChannel->isPlaying(&result); return result; } void FModSoundProvider::ERRCHECK(FMOD_RESULT result) { if (result != FMOD_OK) { exit(-1); } } void FModSoundProvider::InitFMOD() { FMOD_RESULT result; unsigned int version; int numdrivers; FMOD_SPEAKERMODE speakermode; FMOD_CAPS caps; char name[256]; /* Create a System object and initialize. */ result = FMOD::System_Create(&_system); ERRCHECK(result); result = _system->getVersion(&version); ERRCHECK(result); if (version < FMOD_VERSION) { printf("Error! You are using an old version of FMOD %08x. This program requires %08x\n", version, FMOD_VERSION); return; } result = _system->getNumDrivers(&numdrivers); ERRCHECK(result); if (numdrivers == 0) { result = _system->setOutput(FMOD_OUTPUTTYPE_NOSOUND); ERRCHECK(result); } else { result = _system->getDriverCaps(0, &caps, 0, &speakermode); ERRCHECK(result); /* Set the user selected speaker mode. */ result = _system->setSpeakerMode(speakermode); ERRCHECK(result); if (caps & FMOD_CAPS_HARDWARE_EMULATED) { /* The user has the 'Acceleration' slider set to off! This is really bad for latency! You might want to warn the user about this. */ result = _system->setDSPBufferSize(1024, 10); ERRCHECK(result); } result = _system->getDriverInfo(0, name, 256, 0); ERRCHECK(result); if (strstr(name, "SigmaTel")) { /* Sigmatel sound devices crackle for some reason if the format is PCM 16bit. PCM floating point output seems to solve it. */ result = _system->setSoftwareFormat(48000, FMOD_SOUND_FORMAT_PCMFLOAT, 0,0, FMOD_DSP_RESAMPLER_LINEAR); ERRCHECK(result); } } result = _system->init(100, FMOD_INIT_NORMAL, 0); if (result == FMOD_ERR_OUTPUT_CREATEBUFFER) { /* Ok, the speaker mode selected isn't supported by this soundcard. Switch it back to stereo... */ result = _system->setSpeakerMode(FMOD_SPEAKERMODE_STEREO); ERRCHECK(result); /* ... and re-init. */ result = _system->init(100, FMOD_INIT_NORMAL, 0); } ERRCHECK(result); }

Click here to download FmodSoundProvider.cpp

 

 

 

 

Just to be clear again, this is not production code, anything but.  This code is purely to demonstrate the Service Locator pattern and nothing more.  I simply downloaded FMod and hacked away until I had a playable sound and music file, do NOT model anything after this code, it truly is awful!  That said, it does run and now your game is capable of supporting two different audio providers.

 

 

 

You might be wondering how many code changes need to be made to now support FMod?  The answer… well none.  All throughout your program, code continues to call PlaySound(), PlaySong(), etc…  and works like nothing has changed.  This is why the pattern is so handy.  You can now make an invasive change like completely swapping the audio library with exactly zero changes to your source code!

 

 

 

Actually, that was a bit of a lie, there is one change.  Back in your Game.cpp file, it should now look like this:

 

 

#ifdef USE_FMOD FModSoundProvider soundProvider; #else SFMLSoundProvider soundProvider; #endif ServiceLocator::RegisterServiceLocator(&soundProvider);

Click here to download Game.cpp

 

 

 

 

That’s it.  Your code now supports two completely separate audio systems that can be changed with a single declaration.  There is also absolutely no reason why your code couldn’t be driven by a config file, so you could allow the user to choose which one to use.  In a more complex example than Pang! I would consider making a number of services available via the ServiceProvider, including the graphics and logging classes.  Even if you are only using a single library for your services, it pays to have this flexibility built in in case you change your mind down the road.

 

 

 

Like all other chapters, all of the code, binaries and supporting audio files are available for download right here.  The soundfx file and ogg soundtrack are available separately here and here. For this chapter only I included all of the DLLs , libs and changes to the project file to get FMod up and working, so if you want to run it, simply define USE_FMOD and it will run the code using FMod instead of SFML.  As these libraries are quite big, starting in Pang9 I will remove FModSoundProvider.h and FModSoundProvider.cpp from the project, along with all the supporting binary files.  These were just for demonstration and make the zip file quite a bit larger.

 

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.

 

 

 

 

I hope you enjoyed this chapter, I know it went a bit “out there”, but I think the topic is extremely valuable.  In the next chapter, we will be back to a much more traditional format, covering how to “properly” implement SFMLSoundProvider among other things.

 

 



Back to Part Seven Forward to Part Nine




blog comments powered by Disqus