PC Games

Orb
Lasagne Monsters
Three Guys Apocalypse
Water Closet
Blob Wars : Attrition
The Legend of Edgar
TBFTSS: The Pandoran War
Three Guys
Blob Wars : Blob and Conquer
Blob Wars : Metal Blob Solid
Project: Starfighter
TANX Squadron

Android Games

DDDDD
Number Blocks
Match 3 Warriors

Tutorials

2D shoot 'em up
2D top-down shooter
2D platform game
Sprite atlas tutorial
Working with TTF fonts
2D adventure game
Widget tutorial
2D shoot 'em up sequel
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

New tutorials
Tue, 11th May 2021

Orb source code
Sun, 25th April 2021

All Updates »

Tags

android (3)
battle-for-the-solar-system (9)
blob-wars (9)
brexit (1)
code (6)
edgar (6)
games (37)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (6)
water-closet (3)

Books


Project Starfighter

In his fight back against the ruthless Wade-Ellen Asset Protection Corporation, pilot Chris Bainfield finds himself teaming up with the most unlikely of allies - a sentient starfighter known as Athena.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Shoot 'Em Up Tutorial —
Part 10: Sound and music

Introduction

Note: this tutorial builds upon the ones that came before it. If you aren't familiar with the previous tutorials in this series you should read those first.

We can shoot the enemies to destroy them, but right now they just vanish. That's no fun. How about we make them explode, instead? Unpack the code and then type make to build. Once compiling is finished type ./shooter10 to run the code.

A 1280 x 720 window will open, with a colorful background. A spaceship sprite will also be shown, as in the screenshot above. The ship can now be moved using the arrow keys. Up, down, left, and right will move the ship in the respective directions. You can also fire by holding down the left control key. Enemies (basically red versions of the player's ship) will spawn from the right and move to the left. Shoot enemies to destroy them. Enemies can fire back, so you should avoid their shots. Close the window by clicking on the window's close button.

Inspecting the code

Adding sound and music to the game hasn't resulted in a huge number of changes, thankfully. This tutorial will therefore be quite short (especially when compared to the last one). Let's start with the updates we've made to defs.h.


#define MAX_SND_CHANNELS 8

enum
{
	CH_ANY = -1,
	CH_PLAYER,
	CH_ALIEN_FIRE
};

enum
{
	SND_PLAYER_FIRE,
	SND_ALIEN_FIRE,
	SND_PLAYER_DIE,
	SND_ALIEN_DIE,
	SND_MAX
};


We've added a define for the maximum number of channels that we'll have access to. In essence, this is the number of sound effects that can play at the same time. This number can be set to almost anything we wish, but the SDL docs warn that it make crash if set too high. The best advice is to allocate as many as you think you'll need. For us, 8 is just fine. Next we have two enums, one starting with CH_ and the other with SND_. The CH_ enum will specify the channel through which a sound will play, while SND_ will be used to identify a sound effect; both of these are just numbers, but will make our code more useful. You'll notice that while we're planning to allocate 8 channels, we've only declared three CH_ values. More on this later.

Let's move onto init.c where our initSDL function has seen an update:


void initSDL(void)
{
	...
	if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024) == -1)
	{
		printf("Couldn't initialize SDL Mixer\n");
		exit(1);
	}

	Mix_AllocateChannels(MAX_SND_CHANNELS);

What we're doing here is making a call to SDL to ask it to initialise the audio for us. The Mix_OpenAudio function takes four arguments: the frequency, format, channels, and chunkSize. These settings can be important when it comes to performance (for example, an older machine might want to use 22050 for frequency, to speed things up), so it might be worth reading the SDL docs to understand all the settings - https://www.libsdl.org/projects/SDL_mixer/docs/SDL_mixer_11.html. For now, we'll open the audio with a frequency of 44100 (CD quality), the default format, 2 channels (stereo) (note: this is NOT the same as the channels in defs.h), and a chunkSize of 1024. Again, if you experience issues, check out the link and adjust these settings.

With our audio open, we then call Mix_AllocateChannels, passing over MAX_SND_CHANNELS. We now play 8 sound effects at the same time.

Let's move on to a new file: sound.c. This is where all our sound and music functions will reside. Starting with the initSounds function:


void initSounds(void)
{
	memset(sounds, 0, sizeof(Mix_Chunk*) * SND_MAX);

	music = NULL;

	loadSounds();
}

An SDL sound is held in a struct called Mix_Chunk. We've allocated an array of SND_MAX pointers of these (as a static variable) and start by memsetting them. Music in SDL is help in a struct called Mix_Music (also as a pointer). We have one of these, simply called music that we also NULL; this isn't strictly required but it's good practice to zero pointers. Next, we load our sound using loadSounds:


static void loadSounds(void)
{
	sounds[SND_PLAYER_FIRE] = Mix_LoadWAV("sound/334227__jradcoolness__laser.ogg");
	sounds[SND_ALIEN_FIRE] = Mix_LoadWAV("sound/196914__dpoggioli__laser-gun.ogg");
	sounds[SND_PLAYER_DIE] = Mix_LoadWAV("sound/245372__quaker540__hq-explosion.ogg");
	sounds[SND_ALIEN_DIE] = Mix_LoadWAV("sound/10 Guage Shotgun-SoundBible.com-74120584.ogg");
}

Load sounds is a simple function: it simply calls an SDL function called Mix_LoadWAV and stores the returned Mix_Chunk in the appropriate array index. Mix_LoadWAV can load a variety of sounds, not just WAV as the name suggests. Above, we're loading OGGs, but can also load MP3s, for example.

There are three other functions in this compilation unit that we should consider, all quite simple. Considering loadMusic first:


void loadMusic(char *filename)
{
	if (music != NULL)
	{
		Mix_HaltMusic();
		Mix_FreeMusic(music);
		music = NULL;
	}

	music = Mix_LoadMUS(filename);
}

The function takes one argument, the filename of the music file we want to use. We first check to see if we already have music loaded (basically, if music is not NULL), and then stop and free the music data, calling Mix_HaltMusic and Mix_FreeMusic (passing over the pointer to our music data), and then setting music to NULL. Mix_HaltMusic does as one might expect - it causes the music currently playing to stop. Mix_FreeMusic will release all the resources associated with this music data. With that out of the way, we can load our music. This once again is a simple case of calling an SDL function - Mix_LoadMUS. This call returns a pointer to the music data. Note that this doesn't start playing the music; it only loads it into memory for us to use later. To play music, we have to call a different function which we'll cover next:


void playMusic(int loop)
{
	Mix_PlayMusic(music, (loop) ? -1 : 0);
}

The playMusic function we've created does just that: it calls the SDL function Mix_PlayMusic. This function takes just two arguments - the Mix_Music pointer (music) and the number of times the music should loop. If we want it to loop forever, we pass -1. Otherwise, we pass over the number of times want to loop. Our playMusic function only supports looping forever or playing once, but it's sufficient. Finally, let's look at the playSound function:


void playSound(int id, int channel)
{
	Mix_PlayChannel(channel, sounds[id], 0);
}

This function calls the SDL function Mix_PlayChannel, that takes three arguments - the channel we want to play the sound through, the sound itself, and the number of times the sound should loop. As we only want our sound to play once, our playSound function takes just two arguments: the id of the sound and the channel to play it through. All very easy. Finally, let's investigate the updates to stage.c:


static void doPlayer(void)
{
	...
	if (app.keyboard[SDL_SCANCODE_LCTRL] && player->reload <= 0)
	{
		playSound(SND_PLAYER_FIRE, CH_PLAYER);
	...
}

static void doEnemies(void)
{
	...
	fireAlienBullet(e);

	playSound(SND_ALIEN_FIRE, CH_ALIEN_FIRE);
	...
}

static int bulletHitFighter(Entity *b)
{
	...
	if (e == player)
	{
		playSound(SND_PLAYER_DIE, CH_PLAYER);
	}
	else
	{
		playSound(SND_ALIEN_DIE, CH_ANY);
	}
	...

We're basically calling our playSound function at appropriate times in our code - whenever the player fires, whenever an enemy fires, and when the player or an alien is killed. Note how the use of the enums makes the playSound function very easy to read. One thing to take note is that when an alien is killed we play the sound through CH_ANY. If you remember, CH_ANY was declared as -1. What this means is that SDL will play the sound effect using any channel that is currently not in use. This will let us play multiple explosion sounds at the same time. If there are no free channels, SDL will select the oldest playing channel, stopping the sound effect that is playing there. While this might be undesirable at times, this isn't a problem for our game given how simple it is.

The final update to the code comes in main.c:


int main(int argc, char *argv[])
{
	...
	initSounds();

We want to initialize and load our sounds just once, so we do this in main before the start of the game loop. And that's it! Our game has sound and music! In the next tutorial we'll look at drawing text (using bitmap fonts) and adding scoring to the game.

Exercises

  • Experiment with your own sound effects and music. Where else could you add sounds?

Purchase

The source code for all parts of this tutorial (including assets) is available here:

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

Comments

Mobile site