Home > Game Development Walkthroughs > Tetris: Adding polish with music, sound effects, backgrounds, game options, an intro sequence and other tweaks

Tetris: Adding polish with music, sound effects, backgrounds, game options, an intro sequence and other tweaks


Our previous version of SimpleTetris is working quite well as a single-player game, but lacks the professional touch. In this article, we’ll look at:

  • how to make pausing and unpausing the game also pause and unpause all of its subsystems
  • how to add music and sound effects and tie them into game events with SimpleFMOD (a library developed in another series on this web site which uses the well-known FMOD SoundSystem to produce audio – see the first part of the FMOD series for more information on how to use FMOD)
  • how to make an event-triggered throw-away animation using Simple2D
  • how to use bitmap images (sprites) as animated backgrounds
  • how to add game options which can be loaded and saved
  • how to add an intro sequence and a credits page

Most of these changes can be slotted in without any changes to the game logic, but there are a few gotchas and some little tweaks we can apply to make things better. I’ll note these below.

Download (SimpleTetris 1.41): Source code | Executable

For comparison, also have on hand the executable for the previous version of the game (SimpleTetris 1.31).

I strongly recommend that you run both versions and do a side-by-side comparison of the behaviour while working through the sections below, then hopefully the changes described will make sense more easily.

Dependencies:

Pausing “everything” when the game is paused

As it stands now, the tracer dots which move around the bucket keep going when the game is paused, as does the bucket fill colour animation. Your game may also contain other fixed time-based animations like this. To pause everything when the game is paused, each sub-system in your game must be notified whenever the user pauses or unpauses so it can take the appropriate action (you can think of this as firing or dispatching a pause or unpause event).

For Tetris:

Add a pause function to TetrisBucket:

// Pause/unpause the bucket when the game is paused/unpaused
void TetrisBucket::Pause(bool paused)
{
	bucketColourAnimation.Pause(paused);
	tracerAnimation.Pause(paused);
}

In the state change code (SimpleTetris::SetGameState(GameStates state)):

int previousState = gameState;

gameState = state;

switch (gameState) {

// ...

case Playing:
  if (previousState == Pause)
  {
    bucket.Pause(false);
  }
  break;

case Pause:
  bucket.Pause();
  break;

Notice that the Playing state can be entered both from the main menu and from the pause state, so we only want to unpause the bucket animations when the player unpauses the game (not when the game starts), hence the check of the previous game state.

If you have other sub-systems which also need to be paused and unpaused, these can be slotted into the case statements above, and we will see further examples of this below.

Adding music and sound effects – The Basics

Setting up

We are going to use FMOD for this, and specifically our library SimpleFMOD which is a wrapper around the official FMOD API that makes things somewhat simpler.

The details of which header and library directories and which linker inputs to use are covered in SimpleFMOD Installation Instructions and Cutting Your Teeth on FMOD Part 1. Once this is setup, there are four steps to being able to use (Simple)FMOD:

  1. include the SimpleFMOD.h header
  2. if desired, import the SimpleFMOD namespace with using namespace SFMOD; (“importing” the namespace just saves you from having to write SFMOD:: before every type name used in the library)
  3. copy fmodex.dll into the same folder as your project output (EXE file)
  4. create an instance of SimpleFMOD which lasts for the lifetime of the application, ie. SimpleFMOD audioEngine;
  5. update the audio engine each frame

Step 3 can be automated into your build process by creating a post-build event. To do this:

  1. right-click on your project in Solution Explorer and choose Properties
  2. navigate in the tree to Configuration Properties -> Build Events -> Post-Build Event
  3. in the Command Line box, type: copy /y "$(ProjectDir)assets\fmodex.dll "$(TargetDir)"

In my SimpleTetris solution, I have all the game assets in a folder called assets in the main project folder, including the DLL supplied with FMOD. Change the path in the command line above as needed. Note that the trailing backslash is not needed if you are using $(ProjectDir) to reference your project folder.

Step 5 must be accomplished by calling SimpleFMOD::Update() on each frame. In SimpleTetris, the following line of code is added as the very last line in SimpleTetris::UpdateObjects():

// Update audio engine
audioEngine.Update();

This ensures that if any songs are fading in or out, that their volumes are updated correctly.

Loading audio

In SimpleFMOD, music is stored in Song objects and effects are stored in SoundEffect objects. The SimpleFMOD object is used to do all interactions with the FMOD sound system.

Let’s start with the two simplest possible examples: music for the main menu, and a sound effect that plays whenever you move a piece.

Loading the audio is very simple. In the Tetris class constructor:

menuMusic = audioEngine.LoadSong("MenuMusic.mp3");
moveEffect = audioEngine.LoadSoundEffect("MoveEffect.mp3");

In the supplied source code, however, we are storing all the audio as embedded resources in the EXE file itself, so the actual code is:

menuMusic = audioEngine.LoadSong(IDMP3_MenuMusic, "MP3");
moveEffect = audioEngine.LoadSoundEffect(IDMP3_MoveEffect, "MP3");

The identifiers used above to reference the resources are defined in resource.h and SimpleTetris.rc. You can change the names of the identifiers in Visual Studio’s Resource Explorer but you should never edit these two files directly as Visual Studio does it for you. See Cutting Your Teeth on FMOD Part 3: Embedded Sounds In Your Application as Resources for more details. If you don’t care about storing the audio as embedded resources, you can just load sound files directly as shown in the first code snippet above.

Linking audio to game events

The menu music should start when the main menu or high score name entry states are entered, stop when the user starts a new game, and be left playing (no change) while the user is navigating the re-define controls page etc. All of these changes can be enacted in SimpleTetris::SetGameState():

case Menu:
  if (previousState != EnterName)
    menuMusic.Start();
  break;

case Playing:
  if (previousState == Menu)
    menuMusic.Fade(2000); // fades out the music over 2 seconds (2000ms)

  if (previousState == Pause)
    bucket.Pause(false);
  break;

case EnterName:
  menuMusic.Start();

When a game ends we might end up in either the EnterName or MainMenu states depending on whether the player set a new high score or not. If the player gets a new high score, the state will change from EnterName to MainMenu after the player enters his or her name, but the menu music will already be playing, and we don’t want to reset it to the start of the song, so we check for this condition in the code above before deciding whether to re-start the menu music when the MainMenu state is entered.

Linking the piece move sound effect is very trivial. We simply add code to fire the sound effect whenever the player presses one of the movement keys. In SimpleTetris::OnKeyDown():

if (currentShape.Active)
{
	if (key == gameKeys[TetrisControls::Left])
	{
		currentShape.MoveLeft();
		moveEffect.Play();
	}

	if (key == gameKeys[TetrisControls::Right])
	{
		currentShape.MoveRight();
		moveEffect.Play();
	}

	if (key == gameKeys[TetrisControls::Down])
	{
		if (currentShape.MoveDown())
			placePiece();

		moveEffect.Play();
	}

	if (key == gameKeys[TetrisControls::FastDrop])
	{
		currentShape.MoveToBottom();
		moveEffect.Play();

		placePiece();
	}

	if (key == gameKeys[TetrisControls::Rotate])
	{
		currentShape.Rotate();
		moveEffect.Play();
	}

	if (key == gameKeys[TetrisControls::Shift] && !held)
		shiftShapes();

	return true;
}

The code essentially remains unchanged from the previous version except for the insertion of moveEffect.Play(); wherever we want the sound effect to be triggered.

A slightly more complicated linking example

From time to time we may want to play a sound effect based on the result of a condition that isn’t currently accessible to the sound code in our application. This is the case with the “line fill” sound effect (lineEffect in the code), which is a sound intended to be played as soon as a piece lands which results in one or more completed lines.

The function TetrisBucket:Add adds a landed piece to the bucket and calculates how many lines have been filled as a result, but this calculation is kept internally and not exposed to the main application. Therefore, the application doesn’t know if a line has been filled and whether or not to play the sound effect. The solution is a simple modification to TetrisBucket::Add:

/* OLD CODE */
void TetrisBucket::Add(TetrisShape &s)

/* BECOMES: */
bool TetrisBucket::Add(TetrisShape &s)
{
  ...
  // Return whether any lines were cleared or not
  return (newFills > 0);
}

And in the main application class:

// Place the current piece into its final place in the bucket
void SimpleTetris::placePiece()
{
	// Put the current shape in the bucket
	if (bucket.Add(currentShape))
		lineEffect.Play();

	// Play landing effect
	landEffect.Play();
}
Other sound effects

The latest version of SimpleTetris defines a slew of other sound effects for events such as a piece landing, leveling up, getting a score combo and the game over state. All of these are implemented in the same way as the examples above, so just search the source code for “.Play()” to find where and how they are hooked into the game code.

In-game music selected based on the current level

If your game has discrete levels (such as a platform game), selecting a music track based on the current level is easy: you just store a reference to the track in the level definition and use it as required as the “current” track.

In Tetris and some other games, the idea of a level is a bit more blurred. In Tetris, the level is nothing more than a number based on how many lines the player has cleared so far, which is used to determine how fast the pieces to drop. Movement through the levels is therefore fluid in that no transition or loading of new data is required. All the levels are essentially the same and the game world is not reset when the level increases.

In this example, I will show code which plays in-game background music and changes it to a new track at levels 1 (the start of the game), 5 and 10. After level 10, the music never changes. The steps are:

  • load all the music when the application starts
  • start the first track playing when a new game starts
  • when the player levels up, determine whether the track needs to be changed. If so, fade out the currently playing track and fade in the new one
  • when the game ends (game over state is reached), fade out the currently playing track
  • ensure the correct track is paused and unpaused when the game is paused or unpaused
Load the music

The tracks are stored in an array of Song and loaded in the application class constructor as follows:

gameMusic[0] = audioEngine.LoadSong(IDMP3_GameMusic1, "MP3");
gameMusic[1] = audioEngine.LoadSong(IDMP3_GameMusic2, "MP3");
gameMusic[2] = audioEngine.LoadSong(IDMP3_GameMusic3, "MP3");

Note there are 3 tracks in total, indexed from 0-2.

Start the first track when a new game starts

Very simple. In SimpleTetris::newGame():

// You are on level 1
level = 1;
gameMusic[0].Start();
Checking if the track should be changed when the player levels up

Previously, no action was taken when the level changed. The entire code for SimpleTetris::UpdateObjects() when the game is in the Playing state looked like this:

// Update the current shape position only if the game is in progress
if (gameState == Playing)
{
	if (currentShape.Update(level))
		placePiece();

	// Update bucket (if during line clear animation) and check if we need to bring in a new piece
	if (bucket.UpdateBucket(score, level, linesCleared))
	{
		newPiece();
		scoreDisplay.UpdateScore(score);
	}
}

Essentially, this code checks if the current piece has just landed, and adds it to the bucket if so. The bucket is updated and the call to UpdateBucket() returns true if a new piece is needed, updating the player’s score, level and total lines cleared. It is here, therefore, that the level can change, and where we need to check if the currently playing track should be changed.

We modify the code as follows:

// Update the current shape position only if the game is in progress
if (gameState == Playing)
{
	if (currentShape.Update(level))
		placePiece();

	// Update bucket (if during line clear animation) and check if we need to bring in a new piece
	int prevLevel = level;

	if (bucket.UpdateBucket(score, level, linesCleared))
	{
		newPiece();
		scoreDisplay.UpdateScore(score);

		// Move to new music if needed
		if (level > prevLevel)
		{
			// Play level up sound
			levelEffect.Play();

			// Work out music tracks
			int trkIndex = min(level / 5, 2);
			int trkPrevIndex = max(0, min((level - 1) / 5, 2));

			if (trkIndex != trkPrevIndex)
			{
				// Fade out previous track
				gameMusic[trkPrevIndex].Fade(3000);

				// Fade in next track
				gameMusic[trkIndex].Start(true);
				gameMusic[trkIndex].SetVolume(0.0f);
				gameMusic[trkIndex].Fade(3000, 1.0f, false);
			}
		}
	}
}

We compare the player’s level from before and after the bucket is updated to see if we have leveled up. If so, we calculate the index of the track that would be playing (from 0-2) on the previous level and the new level. If there is no change, then no action needs to be taken. However, if the track indexes are different, we fade out the track that was playing on the previous level, and simultaneously fade in the track for the new level.

(Note: the one-argument call to Fade() fades out a track over the specified number of milliseconds. The 3-argument call fades a track in to the specified volume level where 1 is the maximum volume. Calling Start() resets the track position to the start of the song)

Fade out track when game ends

We add a line in SimpleTetris::SetGameState() to calculcate the currently playing track index:

// Currently playing song for states that need it
int trkIndex = min(level / 5, 2);

Then modify the GameOver state selector as follows:

case GameOver:
	gameMusic[trkIndex].Fade(300);
	gameOverEffect.Play();
	break;
Pausing and unpausing the music when the game is paused or unpaused

When pausing, we need to fade the current track out:

case Pause:
	gameMusic[trkIndex].Fade(300);
	bucket.Pause();
	break;

When unpausing, we need to fade the current track in, but only if we have come from the Pause state, not from the main menu:

case Playing:
	if (previousState == Menu)
		menuMusic.Fade(2000);

	if (previousState == Pause)
	{
		gameMusic[trkIndex].SetPaused(false);
		gameMusic[trkIndex].Fade(300, 1.0f, false);

		bucket.Pause(false);
	}
	break;

This concludes the code on sound effects and music. The game now has menu music which starts and stops appropriately, in-game music which changes between levels, and sound effects that trigger on various in-game events!

Simple2D Example: Using scenes to add throw-away animations

Figure 1. Level Up text animating automatically in an overlay scene

Figure 1. Level Up text animating automatically in an overlay scene. The text drifts upwards and fades in and out over 3 seconds.

As with many graphics engine middleware products, Simple2D allows you to layer objects onto so-called scenes, which are rendered on-screen along with the rest of the application’s graphics output. All of the animations we created in the previous instalment of this series relied on classes to manage the animation which were created and destroyed as needed. For throw-away or incidental animations, where we just want to fire-and-forget, there is an easier way.

Consider the case of leveling up. It would be nice to flash up a message on the screen when the level increases, and we would like to animate this so it fades in and out and drifts upwards, but we don’t care to manage the per-frame details ourselves, we just want to set the starting animation parameters, set the animation in motion, and let it be automatically destroyed when it is finished.

In the class definition, add a scene:

// Game overlay scene
Scene gameOverlay;

This is a blank scene which we can just add throw-away animations to at will. In the game state code, when a new game starts, we configure the scene to be rendered on top of (after) the rest of the game’s graphics (in SimpleTetris::SetGameState()):

case Playing:
	if (previousState == Menu)
	{
		menuMusic.Fade(2000);
		SetScene(gameOverlay);
		SetRenderSceneAfter(true);
	}
...

The call to Simple2D::SetRenderSceneAfter() tells Simple2D to render the game graphics generated in SimpleTetris::DrawScene() first, then the overlay scene afterwards.

The actual animation is created in SimpleTetris::UpdateObjects() if the player levels up and is quite straightforward:

if (level > prevLevel)
{
	// Play level up sound
	levelEffect.Play();

	// Display level up overlay
	Label levelUpText(0, 0, "LEVEL " + StringFactory(level), MakeBrush(Colour::White),
		MakeTextFormat(L"Candara", 48.0f, DWRITE_TEXT_ALIGNMENT_CENTER, DWRITE_FONT_WEIGHT_BOLD));

	AnimationChain alphaFade;
	alphaFade.Add(Animation::FromTo(0, 1, 1500));
	alphaFade.Add(Animation::FromTo(1, 0, 1500));

	levelUpText.Bind(BindY, Animation::FromTo(300, 100, 3000));
	levelUpText.Bind(BindAlpha, alphaFade, true);
	levelUpText.ResetAnimations();

	gameOverlay.Add(levelUpText);

        /* The music track change code we wrote earlier goes here */
}

We first create a text object (Label) with the string we want to display. We then create two animations:

  • an animation for the alpha channel (transparency) of the text. Where 0 is fully transparent and 1 is fully opaque, the text fades in for 1.5 seconds then fades out for 1.5 seconds.
  • an animation for the Y position of the text, which moves from 300 to 100 (upwards) over the course of 3 seconds.

By setting the optional 3rd parameter of Bind() to true when we add the alpha channel animation, the animation as set as the master animation for the object. When the master animation completes (after 3 seconds in this case), the object it is bound to (the text label) is destroyed automatically and removed from the scene. In this way, the object is memory-managed for us and we don’t have to think about it.

Figure 2a. The main menu in SimpleTetris before a background is added

Figure 2a. The main menu in SimpleTetris before a background is added

(Technical note: when you call Scene:Add() with a reference or value as above, the object passed in the argument is cloned and it is the clone which is destroyed when it is removed from the scene. The actual Label object levelUpText is destroyed as soon as it goes out of scope at the closing brace of the if statement above. Therefore, both objects are destroyed correctly and when they should be)

Figure 2b. The main menu in SimpleTetris after the background (and interface changes) are added

Figure 2b. The main menu in SimpleTetris after the background (and interface changes) are added

Adding background images

So far all the graphics in our game have been generated by the game code itself as simple lines, rectangles or geometry objects. Of course, we will often want to use sprites. Tetris doesn’t really need any sprites for its main gameplay graphics (which is one reason it was a good example project for the 8-hour coding challenge!), but we can spruce things up with some backgrounds.

Simple2D stores sprites as Image objects, and the underlying WIC (Windows Imaging Component) architecture it uses to load image data from files will automatically convert any supported image file type (eg. PNG, JPG, GIF, BMP etc.) into a Direct2D-compatible in-memory format. I recommend you use PNG files wherever possible for two compelling reasons: 1. they support transparency (variable alpha channel) in a straightforward way, and 2. they do not have proprietary license usage restrictions like for example GIF files do.

Static backgrounds

The simplest case is a static (non-animated, non-moving) background and we can use just such a background for the game’s menu screens. Assuming we have defined BGMenu as an Image object in the class definition, we can load the image in the class constructor as follows:

BGMenu = MakeImage(IDB_BGMenu, "PNG");

Once again we are using embedded resources. If you want to load a file directly you can just call MakeImage(“filename”) instead.

Displaying the background is extremely simple. We simply add the drawing call in SimpleTetris::DrawScene() when the game state is one of the menu states:

// Menu background image
if (gameState == Menu || gameState == EnterName || gameState == EditControls)
{
	BGMenu->Draw(0, 0, 0.5f);
}

The first two arguments to Draw() are the top-left x/y co-ordinates to plot the image at. (0, 0) is the top-left corner of the screen. The third argument is the opacity (alpha) of the image. By setting it to 0.5 instead of 1.0, the background is dimmed somewhat so that it does not diminish the visibility of the interface buttons or high-score table. If you try changing this to 1.0, you will see that the result looks rather ‘cluttered’.

Figure 3a. The main game screen before a rotating background is added

Figure 3a. The main game screen before a rotating background is added

Note that, of course, the background must be rendered before the rest of the objects to be displayed, otherwise they will be occluded by the background image, so this code comes before any of the other rendering code for the menus in DrawScene().

In-game rotating background images selected based on the current level

Figure 3b. The main game screen after a background is added. The background rotates at a linear speed in a continuous loop, and fades to a different background when the level changes.

Figure 3b. The main game screen after a background is added. The background rotates at a linear speed in a continuous loop, and fades to a different background when the level changes.

The idea behind a background which fades as the level changes is essentially identical to that of music with fades on level changes, except that we have to handle the fade ourselves. We also make the background rotate using a simple animation function and a bit of maths.

Load the graphics

Start by loading the graphics of each background into an array in the class constructor, much as we did with the music:

BGStage[0] = MakeImage(IDB_BGStage1, "PNG");
BGStage[1] = MakeImage(IDB_BGStage2, "PNG");
BGStage[2] = MakeImage(IDB_BGStage3, "PNG");
Setup the animations

This is also done in the class constructor:

// Background rotation
backgroundRotation = Animation(Animations::Linear, 60000, 360.0f, 0.0f, Animation::Repeat, false);

// Fade animation
fadeAnimation = Animation(Animations::Linear, 4000, 0.8f, 0.0f, Animation::Clamp);
fadeAnimate = false;

The first animation function will return a floating point value from 0-360 (degrees of rotation) which repeats every 60 seconds. The second animation is only used when fading between two backgrounds, and returns a value from 0.0-0.8 (the alpha channel of the background to fade in), over the course of 4 seconds. Why use 0.8 instead of 1.0 for the maximum alpha value? For the same reason we used 0.5 on the menu background: so that the main game display is not overwhelmed by any vivid colours in the background.

We also define a boolean value fadeAnimate to indicate whether a background fade is currently in progress.

Render the in-game background

This is achieved by the following code at the very start of SimpleTetris::DrawScene() (once again, we put it at the start so that the rest of the game graphics aren’t occluded by the background):

// Draw this in playing, pause game over states
if (gameState == Playing || gameState == Pause || gameState == GameOver)
{
	// Work out current background image
	int imgIndex = min(level / 5, 2);

	// Calculate alpha for background image
	float bgAlpha = 0.8f;

	if (fadeAnimate)
		bgAlpha = static_cast<float>(fadeAnimation.GetAnimOffset());

	// Render background image
	BGStage[imgIndex]->Draw(-ResolutionX/2, -ResolutionY/2,
		static_cast<int>(ResolutionX * 1.5), static_cast<int>(ResolutionY * 1.5),
		bgAlpha,
		static_cast<float>(backgroundRotation.GetAnimOffset()));

	if (fadeAnimate)
	{
		int imgPrevIndex = max(0, min((level - 1) / 5, 2));

		BGStage[imgPrevIndex]->Draw(-ResolutionX/2, -ResolutionY/2,
			static_cast<int>(ResolutionX * 1.5), static_cast<int>(ResolutionY * 1.5),
			0.8f - bgAlpha,
			static_cast<float>(backgroundRotation.GetAnimOffset()));
	}
...

The background changes on levels 1, 5 and 10 – the same as for the music – so the calculation of the background index value based on the level number is the same as for the music too. There are two possibilities:

  • we are not currently transitioning between two backgrounds (fadeAnimate == false), in which case the alpha channel of the current background is 0.8, and we just render the current background
  • we are currently transitioning between two backgrounds (fadeAnimate == true), in which case we plot both the current background and the background of the previous level. The alpha channel value of the current background is rising as it fades in (from 0.0 to 0.8), and is the result of the output of fadeAnimation (which is reset when the level counter increments). The alpha channel of the background being faded out is 0.8 – bgAlpha, in other words the inverse, as its alpha channel decreases from 0.8 to 0.0 at the same time.

To achieve rotation, we simply pass into the drawing function the output of backgroundAnimation which runs continuously on the 0-360 degree cycle per 60 seconds, then resets to 0 again. Since rotating the background at its normal size would result in some areas of the screen not being painted, we scale the background size up by a factor of 50% (multiply by 1.5), and offset the top-left corner by negative half of the screen resolution so that the centre point of the background is still the centre point of the screen at all times. The drawing function applies the rotation to the top-left co-ordinate of the image, so we don’t have to care about taking the rotation angle into account when we specify the co-ordinate.

Triggering the fade animation

Back in our code in SimpleTetris::UpdateObjects() which detects when the level has changed, we reset the fade animation each time this occurs:

// Move to new background image and music if needed
if (level > prevLevel)
{
	fadeAnimate = true;
	fadeAnimation.Reset();
...

Astute readers will note that the fade occurs on every level change, not just the ones where the actual background images is changed. However, fading from one background to the same background using the technique described will have no visible effect, due to the way the alpha channel values are combined when rendering takes place. So, we ultimately get the result we want.

Initialization when a new game starts

When the game starts, we disable the fade animation in SimpleTetris::newGame() as there should be no fade when level 1 begins:

// Background image not currently fading
fadeAnimate = false;
Dealing with pause and unpause events

The background animation should be paused when the game is paused and vice versa. We deal with this in the game state code in SimpleTetris::SetGameState():

case Pause:
	gameMusic[trkIndex].Fade(300);
	backgroundRotation.Pause();    /* NEW CODE */
	bucket.Pause();
	break;
...
case Playing:
	if (previousState == Menu)
	{
		menuMusic.Fade(2000);
		SetScene(gameOverlay);
		SetRenderSceneAfter(true);
	}

	if (previousState == Pause)
	{
		gameMusic[trkIndex].SetPaused(false);
		gameMusic[trkIndex].Fade(300, 1.0f, false);

		backgroundRotation.Pause(false);    /* NEW CODE */
		bucket.Pause(false);
	}

	break;
...

That wraps up background animations!

Stateful game options

Figure 4. The game options user interface

Figure 4. The game options user interface

Most games have options which are stateful (meaning, their values are preserved when the game application exits and are re-loaded next time it starts up).

In this example, we look at how to set the overall volume of the music and sound effects. SimpleFMOD deals with the actual volume processing details for us by separating music and effect playback into distinct channel groups (see Cutting Your Teeth on FMOD Part 2: Channel Groups for the gory details), so we only need to concern ourselves with how to actually store and set the values here.

In-game options storage

This can simply be done using regular member variables or a class of public properties to store and retrieve the game options. Here we use the very simple implementation of two integers storing values from 0-100 for the music and effects, called volumeMusic and volumeEffects respectively. These are defined in the main game class, their values loaded from a file when the game application starts, and saved whenever the user modifies them via the options interface (see below).

File storage

We could use a game options class and boost::serialization to handle this as we did with the high score table, and for games with many options, this is a smart choice. We use this technique extensively in 2D Platform Games Part 9: Storing Levels In Files and 2D Platform Games Part 10: Improved Level Management and Storage to store entire levels with a complex structure, so check those articles for more details on using Boost for the job.

In the name of simplicity, we’ll use straightforward C++ file streams with a Boost archive here (no class serialization). After including fstream, boost/archive/text_oarchive and boost/archive/text_iarchive at the top of your code, saving and loading an options file can be implemented as follows:

// Save game options
void SimpleTetris::SaveOptions(string path)
{
	std::ofstream OptionsFile(path, std::ios::out);
	boost::archive::text_oarchive oa(OptionsFile);

	oa << volumeMusic;
	oa << volumeEffects;

	OptionsFile.close();
}

// Load game options
void SimpleTetris::LoadOptions(string path)
{
	// Make a default controls file if none exists
	DWORD attr = GetFileAttributes(path.c_str());
	if (attr == INVALID_FILE_ATTRIBUTES)
	{
		volumeMusic = 100;
		volumeEffects = 100;

		SaveOptions(path);
	}

	std::ifstream OptionsFile(path);
	boost::archive::text_iarchive ia(OptionsFile);

	ia >> volumeMusic;
	ia >> volumeEffects;

	OptionsFile.close();
}

Important to note is that the load function checks if the file exists, and if not, sets default values for the options (full volume in this case) and creates the options file.

Initialization when the application starts

In the class constructor, we need to load the options file and configure any required in-game settings:

// Load game options
LoadOptions(DataPath + "options.dat");

...

// Set master volumes
audioEngine.SetMasterVolumeMusic(static_cast<float>(volumeMusic) / 100);
audioEngine.SetMasterVolumeEffects(static_cast<float>(volumeEffects) / 100);

The volume setters in SimpleFMOD accept values from 0.0-1.0, and we are storing volumes as values from 0-100, so we scale them down appropriately before passing them to the volume setter functions.

Options interface

The game will require a user interface to allow the options to be set. This is a messy and game-dependent business that I am going to gloss over here. Our implementation in Simple2D uses a Scene object for the options menu with some Buttons for saving and Slider controls for configuring the volumes themselves. This looks as shown in Figure 4, and you can check the source code for the full implementation (most of the work is initialization code performed in the class constructor, and a new game state MenuOptions is added for when the options screen is displayed). I will just touch on the callback functions which execute when the slider values are changed, and these are as follows:

boost::function<void (Slider &)> onChangeMusic = [this] (Slider &s)
{
	volumeMusic = s.GetValue();
	audioEngine.SetMasterVolumeMusic(static_cast<float>(volumeMusic) / 100);
};

boost::function<void (Slider &)> onChangeEffects = [this] (Slider &s)
{
	volumeEffects = s.GetValue();
	audioEngine.SetMasterVolumeEffects(static_cast<float>(volumeEffects) / 100);
};

(these lambda functions are defined in the class constructor – you can pass pointers to regular functions if you prefer, or if the required code is long)

We update the appropriate volume variable by fetching the slider’s value with GetValue(), and update the actual master volumes immediately by calling SimpleFMOD’s volume setter functions as appropriate.

Creating an intro sequence

Figure 5a. The awkward piece is hoisted into place in the intro sequence

Figure 5a. The awkward piece is hoisted into place in the intro sequence

What professional game would be complete without an intro sequence? This is a sequence of animations which runs once when your game starts to introduce the game. It often includes a company logo, trademark logos of licensed products you used in the game’s development and so forth, as well as copyright and online play notices.

Figure 5b. The cow's head is animated with a sprite sheet

Figure 5b. The cow’s head is animated with a sprite sheet

The code for an intro sequence is entirely game-dependent, so the example presented below is purely for illustration purposes; however, it should provide an excellent basis to describe how to create any complex chain of timed animations, regardless their intended use.

To understand the code that follows, it is really beneficial to run the executable linked at the top of the article so you can see exactly what happens, as it is difficult to explain in words (and indeed, I don’t explain it at all below, I assume you have watched it several times to understand the flow of events).

The boilerplate code

Before we do anything, we need to load all the resources we need for the intro sequence. As usual, this is done in the class constructor:

...
// Sound effects
landIntroEffect = audioEngine.LoadSoundEffect(IDMP3_LandIntroEffect, "MP3");
introChainEffect = audioEngine.LoadSoundEffect(IDWAV_IntroChain, "WAVE");
introTruckEffect = audioEngine.LoadSoundEffect(IDWAV_IntroTruck, "WAVE");
introCowEffect = audioEngine.LoadSoundEffect(IDWAV_IntroCow, "WAVE");
...
// Load graphics
studioLogo = MakeImage(IDB_StudioLogo, "PNG");
introChain = MakeImage(IDB_IntroChain, "PNG");
introFMOD = MakeImage(IDB_FMOD_Logo, "PNG");
introBoost = MakeImage(IDB_Boost_Logo, "PNG");

(watch and listen to the intro sequence to understand what these various resources represent)

We want the game to start with the intro sequence, so at the end of the class constructor, set the game state:

// Start with intro sequence
SetGameState(IntroSequence);

(instead of the main menu as we used earlier)

Note this is done after loading the volume options from file, so these options will also apply to the intro sequence.

In SimpleTetris::SetGameState(), we trigger the creation of the intro sequence animations themselves (which also sets the active scene in Simple2D to render the intro sequence and begins it running):

switch (gameState) {

case IntroSequence:
	generateIntroSequence();
	break;
...

We will put all the work of generating the intro sequence into generateIntroSequence() below.

Finally, we need to turn off the intro sequence scene when the main menu is entered (this will be entered automatically when the final animation completes, see below):

...
case Menu:
	// Stop intro sequence
	ClearScene();
...
Generating the animation sequence
Figure 5c. The trademark logos and copyright notices are displayed in the second screen of the intro sequence

Figure 5c. The trademark logos and copyright notices are displayed in the second screen of the intro sequence

We can now look at the actual problem in hand. The sequence can be broken down into some high-level steps:

Figure 6. The composite studio logo sprite sheet with animated cow's head

Figure 6. The composite studio logo sprite sheet with animated cow’s head

  • display the falling pieces
  • animate the cow’s head
  • show the trademark logo screen
  • switch to the main menu

To reduce the complexity, we will split these first three stages into three separate Simple2D Scenes. The scene, scene object and animation object classes in Simple2D provide many callback hooks – including time-based hooks – and these will prove invaluable to keeping the code as simple as possible. Remember that scenes provide the ability to create fire-and-forget animations which trigger at specific time offsets, so without this, we would have to figure out how to render the animations ourselves, frame by frame, retaining and updating all of the positional and alpha channel data ourselves by hand. Instead, our approach here will be to simply specify when everything should happen and what the animation paths/functions should be, trigger the start of the sequence and then ignore it until it finishes. With a complex animation sequence such as this, this is hugely simpler than managing everything on a frame-by-frame basis.

All of the following code goes in generateIntroSequence(). We first have to do quite a bit of initialization, which should be mostly self-explanatory:

// Get size of the logo
size.width = 1920;
size.height = 1200;

// Number of pieces we are splitting it into
int xPieces = 10;
int yPieces = 6;

// Size in the source rectangle of each piece
int srcWidth = static_cast<int>(size.width) / xPieces;
int srcHeight = static_cast<int>(size.height) / yPieces;

// Top-left screen position of logo (480,300 is on-screen logo size)
int xL = (ResolutionX - 480) / 2;
int yL = (ResolutionY - 300) / 2;

// Width and height of entire logo
int xW = static_cast<int>(size.width) / 4;
int yH = static_cast<int>(size.height) / 4;

// Size of each piece on the screen
int wS = xW / xPieces;
int hS = yH / yPieces;

Although the studio logo is stored as a single sprite, we are going to use Simple2D’s sprite sheet capabilities to cut the logo into pieces in-memory. This saves us from storing loads of rectangles with bits of the logo in separately. Since the logo is stored at 1920×1200, we scale it down by a factor of 4 in each direction (for a total scaling down of 16) when we set xW and yH. The calculation of xL and yL ensures the logo is positioned in the centre of the screen.

Note that when we talk about the source rectangle, we are referring to the pixels in the logo image itself. The size on which it is rendered on the screen (the target rectangle) is different.

We next need to create a map of falling pieces and their timings. We make a temporary struct for this, where each element describes the tiles to use and the fall time and fall speed for one Tetris piece. A Tetris piece always consists of 4 squares (tiles), so we store the x and y tile co-ordinates for four tiles in each element. The fallDelay variable specifies how many milliseconds to wait since the previous piece started falling before starting to make the piece being described fall. The fallTime variable specifies how many milliseconds it will take the piece to fall, allowing us to create the effect where the pieces fall gradually faster and faster as the sequence progresses by reducing the fall time of each successive piece. It also allows us to make the chained piece (which I will call the awkward piece below) to fall very slowly. The tiles are indexed from 0-9 horizontally and 0-5 vertically, where (0,0) is the top-left tile and (9,5) is the bottom-right tile.

// Add falling Tetris pieces
typedef struct {
	int tileX[4];
	int tileY[4];
	int fallDelay;
	int fallTime;
} Piece;

int numPieces = 15;
int awkwardPieceID = 10;

Piece pieces[] = {
	{
		{ 0, 0, 1, 2 },
		{ 4, 5, 5, 5 },
		0, 750
	},
	{
		{ 8, 7, 8, 9 },
		{ 4, 5, 5, 5 },
		300, 700
	},
	{
		{ 2, 3, 3, 4 },
		{ 4, 4, 5, 5 },
		200, 650
	},
	{
		{ 6, 7, 5, 6 },
		{ 4, 4, 5, 5 },
		150, 600
	},
	{
		{ 9, 9, 9, 9 },
		{ 1, 2, 3, 4 },
		150, 550
	},
	{
		{ 1, 0, 1, 1 },
		{ 2, 3, 3, 4 },
		150, 550
	},
	{
		{ 4, 5, 4, 5 },
		{ 3, 3, 4, 4 },
		150, 550
	},
	{
		{ 2, 3, 2, 3 },
		{ 2, 2, 3, 3 },
		150, 550
	},
	{
		{ 0, 1, 2, 0 },
		{ 1, 1, 1, 2 },
		150, 550
	},
	{
		{ 0, 1, 2, 3 },
		{ 0, 0, 0, 0 },
		150, 550
	},
	// The 'awkward' piece
	{
		{ 4, 3, 4, 4 },
		{ 0, 1, 1, 2 },
		1000, 4000
	},
	//
	{
		{ 8, 6, 7, 8 },
		{ 2, 3, 3, 3 },
		8200, 600
	},
	{
		{ 7, 8, 6, 7 },
		{ 1, 1, 2, 2 },
		150, 600,
	},
	{
		{ 5, 5, 6, 5 },
		{ 0, 1, 1, 2 },
		150, 600,
	},
	{
		{ 6, 7, 8, 9 },
		{ 0, 0, 0, 0 },
		150, 500
	}
};

We can now create the animation. We will ignore the awkward piece processing and sound for now. Here is the code:

int totalDelay = 0;

for (int p = 0; p < numPieces; p++)
{
	totalDelay += pieces[p].fallDelay;

	for (int i = 0; i < 4; i++)
	{
		SpriteSheet img(studioLogo, 2, 2, 0);

		img.SetRectWH(pieces[p].tileX[i] * wS + xL, 0, wS, hS);
		img.SetSourceRectWH(pieces[p].tileX[i] * srcWidth, pieces[p].tileY[i] * srcHeight, srcWidth, srcHeight);

		AnimationChain anim;

		anim.Add(Animation::WaitAt(-1000, totalDelay));
		anim.Add(Animation(Animations::Sin, pieces[p].fallTime, -400, yL + hS * pieces[p].tileY[i], 0.25f, 0.0f));

		img.Bind(BindY, anim);

		intro.Add(img);
	}
}

Each Tetris piece has four tiles as mentioned above, so we create an animation for each tile, for each Tetris piece. The logo is actually stored as a sprite sheet of 3840×2400 for the cow movement (see Figure 6 and code below) so we only need the top-left quarter of it here. We get this by shearing it when we load it as a sprite sheet into four quarters and setting the frame of animation to zero so that only the top-left quarter is used. This is done in the SpriteSheet constructor call by specifying 2 tiles in x and 2 tiles in y, and an initial frame of 0.

SetRectWH specifies the rectangular area on the screen where the tile will be rendered. We set its X position, width and height, but leave Y at 0 as we will animate this value later.

SetSourceRectWH specifies the rectangular area in the current frame of the sprite sheet that will be used as the tile to render. We simply multiply the tile X and Y indices by the width and height of a tile in the source rectangle to obtain the source tile co-ordinates.

We then create the vertical animation. We keep a cumulative count of how much time will have elapsed so far in timeDelay. The first animation in the chain keeps the tile off the top of the screen until it is time for it to start falling. The second animation makes the tile fall vertically (increases its Y co-ordinate) from 400 pixels above its finishing point (which is just off the top of the screen) to its finishing point (the final target rectangle of the tile), in fallTime milliseconds, using the first quarter of a sine wave in reverse as the speed (which essentially means the piece accelerates continuously as it falls, rather than falling at a linear speed).

Finally, we bind the created animation chain to the Y co-ordinate of the tile and add it to the scene.

Handling the awkward piece

First, at the top of the per-tile loop, we introduce an X offset to the target rectangle such that when the awkward piece falls, it is offset by 2 tiles’ worth of pixels to the right:

int xOffset = (p == awkwardPieceID? wS * 2 : 0);

img.SetRectWH(pieces[p].tileX[i] * wS + xL + xOffset, 0, wS, hS); /* CHANGED CODE */

The source rectangle remains the same, of course.

The second animation in the animation chain above is now configured depending on whether it is the awkward piece falling, or another piece. The line of code is replaced with:

// Same as before if not handling the awkward piece
if (p != awkwardPieceID)
	anim.Add(Animation(Animations::Sin, pieces[p].fallTime, -400, yL + hS * pieces[p].tileY[i], 0.25f, 0.0f));

// Handle the awkward piece
else
{
	Animation a = Animation::FromPlus(yL + hS * pieces[p].tileY[i] - 250, 250, pieces[p].fallTime);
	anim.Add(a);
}

This changes the fall behaviour of only the awkward piece, such that it falls linearly from 250 pixels above its finishing point (which is just off-screen) to its vertical finishing point. Since we set fallTime for this piece to 4 seconds in the struct definition, it falls very slowly. Since it will be hanging from a chain which slowly moves down, this is what we want.

We also need to handle the X movement of the awkward piece, which we do by adding a further animation just before the end of the tile loop:

if (p == awkwardPieceID)
{
	AnimationChain anim;
	anim.Add(Animation::WaitAt(pieces[p].tileX[i] * wS + xL + xOffset, totalDelay + pieces[p].fallTime));

	Animation a = Animation::FromPlus(pieces[p].tileX[i] * wS + xL + xOffset, -xOffset, 4500);
	anim.Add(a);

	img.Bind(BindX, anim);
}

Here we first wait for the tile to be in its final Y position, which happens at totalDelay + pieces[p].fallTime milliseconds into the total intro sequence, then we shift it left from its offset X position to its final position over the course of 4.5 seconds.

Adding the chain

We add the chain which lowers the awkward piece into place (see Figure 5a) inside the first if statement which determines if we are handling the awkward piece, after configuring its Y co-ordinate animation. At the start of the intro sequence code, we do a bit more initialization:

// Chain scale factor
int chainW = 165;
int chainH = 1925;
float chainScale = 0.05f;
int chainXTweak = 20;

The chain is stored as a 165×1925 sprite, so we scale it to 5% size. chainXTweak is added to the chain’s offset from the tile’s X position so that it appears to be holding the centre of the tile rather than the left-hand side.

The code to create the chain is as follows:

// y animation for awkward piece (same as above)
Animation a = Animation::FromPlus(yL + hS * pieces[p].tileY[i] - 250, 250, pieces[p].fallTime);
anim.Add(a);

/* NEW CODE */
// Add the chain
if (i == 0)
{
	Sprite chain(introChain);

	chain.SetRectWH(pieces[p].tileX[i] * wS + xL + xOffset + chainXTweak, 0, static_cast<int>(chainW * chainScale), static_cast<int>(chainH * chainScale));
	chain.SetSourceRectWH(0, 0, chainW, chainH);

	AnimationChain animY;
	animY.Add(Animation::WaitAt(-10000, totalDelay));
	animY.Add(Animation::FromPlus(yL + hS * pieces[p].tileY[i] - chainH * chainScale - 250, 250, pieces[p].fallTime));
	animY.Add(Animation::WaitAt(yL + hS * pieces[p].tileY[i] - chainH * chainScale, 4500));
	animY.Add(Animation::FromPlus(yL + hS * pieces[p].tileY[i] - chainH * chainScale, -250, 1500));
	chain.Bind(BindY, animY);

	AnimationChain animX;
	animX.Add(Animation::WaitAt(pieces[p].tileX[i] * wS + xL + xOffset + chainXTweak, totalDelay + pieces[p].fallTime));
	animX.Add(Animation::FromPlus(pieces[p].tileX[i] * wS + xL + xOffset + chainXTweak, -xOffset, 4500));
	chain.Bind(BindX, animX);

	intro.Add(chain);
}

First note that this code runs four times because it repeats for each tile in the Tetris piece, but we only want one chain, so we use the somewhat messy if (i == 0) test to only run the code when the first tile is being processed. It doesn’t matter which tile we use, as long as we only add one chain.

After loading the chain sprite, we set its source rectangle to use the whole sprite, and the target rectangle to be positioned centered horizontally with the tile that we want it to appear to be attached to. The Y position of the target rectangle is not set yet.

The Y animation of the chain is created in four steps:

  1. Wait at some irrelevant off-screen location until the awkward piece is about to fall. We create the chain only when processing the awkward piece so totalDelay will be set nicely to precisely this time offset already.
  2. Move the chain down the same number of pixels (250) and at the same rate (pieces[p].fallTime) as the awkward piece. The subtraction of chainH * chainScale ensures the the bottom of the chain exactly touches the top of the awkward piece tiles.
  3. Retain the same Y co-ordinate for 4.5 seconds (while the chain and awkward piece move left)
  4. Whisk the chain away upwards 250 pixels over the course of 1.5 seconds. The rest of the pieces continue falling while this happens because its animation time is not added to totalDelay.

The X animation of the chain is calculated as follows:

  1. Stay at the initial position (horizontally centered with the tile) until the awkward piece has finished falling
  2. Move the chain left for 4.5 seconds
Adding the sound effects

We can ensure sound effects run when they are supposed to by attaching triggers to play them to the animation event handlers. This is done by calling Animation::SetStartEventHandler or Animation::SetDoneEventHandler depending on whether you want the animation to play when the animation starts or ends.

Once again, four tiles are processed per Tetris piece but we only want one sound, so for the sake of readability we introduce a flag soundLink inside the per-tile loop such that sounds only get attached to the animation of the first tile in the piece:

bool soundLink = (i == 0);

We attach the chain sound to the start of the awkward piece falling by changing the awkward piece’s Y animation like this:

Animation a = Animation::FromPlus(yL + hS * pieces[p].tileY[i] - 250, 250, pieces[p].fallTime);

if (soundLink)
	a.SetStartEventHandler([this] { introChainEffect.Play(); });

anim.Add(a);

We attach the reversing truck sound to the start of the awkward piece moving left by changing the awkward piece’s X animation like this:

AnimationChain anim;
anim.Add(Animation::WaitAt(pieces[p].tileX[i] * wS + xL + xOffset, totalDelay + pieces[p].fallTime));

Animation a = Animation::FromPlus(pieces[p].tileX[i] * wS + xL + xOffset, -xOffset, 4500);

if (soundLink)
	a.SetStartEventHandler([this] { introTruckEffect.Play(); });

anim.Add(a);

(note we can see there that these events can be set for any animation in an animation chain, not just the start or end of the entire chain)

Finally, we can add the effect which plays whenever a piece lands (except the awkward piece) by attaching an event to when each piece’s Y animation finishes like this:

if (p != awkwardPieceID && soundLink)
	if (p < numPieces - 1)
		anim.SetDoneEventHandler([this] { landIntroEffect.Play(); });
	else
		anim.SetDoneEventHandler([this] { landIntroEffect.Play(); SetScene(intro2); });

Note that this code plays another crucial role: when the final piece has finished falling, we move onto the second stage of the intro sequence – the cow head animation – by calling SetScene(intro2);.

Animating the cow’s head

Since we have the four frames of animation needed in the studio logo sprite sheet already (see Figure 6), this is mercifully simple compared to the falling Tetris pieces animation. Here is the complete code (see Figure 5b):

// Cow movement and fade out of studio logo
SpriteSheet img(studioLogo, 2, 2);
img.SetRectWH(xL, yL, xW, yH);

AnimationChain cowMovement;
cowMovement.Add(Animation::WaitAt(0, 500));
cowMovement.Add(Animation::WaitAt(0.25, 400));
for (int i = 0; i < 2400; i += 100)
	cowMovement.Add(Animation::FromTo(0.5, 1.0, 100));
cowMovement.Add(Animation::WaitAt(0.25, 1000));
cowMovement.Add(Animation::WaitAt(0, 2200));
img.SetAnimation(cowMovement);

AnimationChain fader;
fader.Add(Animation::WaitAt(1, 500));
fader.Add(Animation::WaitAt(1, 4000, [this] { introCowEffect.Play(); }));
fader.Add(Animation::FromTo(1, 0, 2000));

fader.SetDoneEventHandler([this] { SetScene(intro3); });

img.Bind(BindAlpha, fader);

intro2.Add(img);

We first load the sprite sheet and set the target rectangle to the same centerized screen position we used for the falling pieces animation.

The method SpriteSheet::SetAnimation() expects a function which returns a decimal value from 0-1, indicating the position through the series of animation frames. The value is scaled up to calculate the current frame. Since we are using 4 frames here, we use the values 0, 0.25, 0.5 and 0.75 above to indicate frames 1, 2, 3 and 4 respectively.

The animation of the cow’s head is generated as follows:

  1. wait for half a second displaying the 1st frame (this gives a delay between the last piece falling and the cow animation starting)
  2. wait for 400ms (0.4 seconds) displaying the 2nd frame (this is the cow’s head zoomed in, see the top-right image in Figure 6)
  3. display frames 3 and 4 alternately for 2.4 seconds (this is the cow’s head looking left and right, see the bottom two images in Figure 6), with each frame being displayed for 0.05 seconds at a time (the animation in the loop stays within 0.5-0.74 for 0.05 seconds then 0.75-0.99 for 0.05 seconds for the total animation time of 0.1 seconds), repeating 24 times.
  4. wait for 1 second displaying the 2nd frame (the cow’s head zoomed in)
  5. wait for 2.2 seconds displaying the 1st frame (the original studio logo)

Note that the total animation time is 0.5 + 0.4 + 2.4 + 1.0 + 2.2 = 5.5 seconds, which is relevant for the fading out of the logo, generated as follows:

  1. wait for half a second at full opaqueness (matches step 1 above)
  2. play the cow mooing sound and wait for another 4 seconds at full opaqueness (if these two waits were combined into one 4.5 second wait, we would not be able to play the cow sound 0.5 seconds in – we have to attach the event handler to the start or end of the wait)
  3. fade out over 2 seconds

The fade out therefore begins 4.5 seconds into the animation, which is 1.2 seconds into step 5 of the cow animation above – ie. after the visible animation is finished and the standard studio logo has been displayed still for 1.2 seconds.

The final step in the code triggers the 3rd part of the intro sequence to begin (the trademark logo screen) after the fade out has completed.

Displaying the trademark logos

There is really no complexity here, it is just a case of displaying static images and text in the appropriate (centerized) positions, and making sure all the elements get faded in and out:

// Licensing logos
Sprite fmodLogo(introFMOD, (ResolutionX - 270) / 2, 80);
Sprite boostLogo(introBoost, (ResolutionX - 252) / 2, 270);

Label licensingText(0, 400, "SimpleTetris, © Katy Coe 2012-2013, all rights reserved - www.djkaty.com\nSimple2D, © Katy Coe 2012-2013, all rights reserved\nFMOD Sound System, copyright © Firelight Technologies Pty, Ltd., 1994-2012.\nBoost, distributed under the Boost Software License, Version 1.0.",
	MakeBrush(Colour::White), MakeTextFormat(L"Lucida Sans Unicode", 10.0f, DWRITE_TEXT_ALIGNMENT_CENTER));

AnimationChain fader2;
fader2.Add(Animation::WaitAt(0, 500));
fader2.Add(Animation::FromTo(0, 1, 1000));
fader2.Add(Animation::WaitAt(1, 3000));
fader2.Add(Animation::FromTo(1, 0, 1000));
fader2.Add(Animation::WaitAt(0, 500));

fader2.SetDoneEventHandler([this] { SetGameState(Menu); });

fmodLogo.Bind(BindAlpha, fader2);
boostLogo.Bind(BindAlpha, fader2);
licensingText.Bind(BindAlpha, fader2);

intro3.Add(fmodLogo);
intro3.Add(boostLogo);
intro3.Add(licensingText);

The fade waits at black for 0.5 seconds, fades in over 1 second, waits 3 seconds, fades out over 1 second and waits at black for 0.5 seconds. Note that this alpha animation is applied to all 3 objects equally (aside: Simple2D 1.10 – released after this code was written – now allows you to create object groups and control the alpha of all of contained objects with a single animation)

Note that when the fade is completed, the game state is set to the main menu, which will – from the boiler plate code at the start of this section – turn the intro sequence off.

Final matters

You may want to allow the user to skip the intro sequence by pressing a key or clicking a mouse button. This can be done trivially as follows:

// Add event handlers
intro.SetKeyDownEventHandler([this] (int, int, bool) -> bool { SetGameState(Menu); return true; });
intro2.SetKeyDownEventHandler([this] (int, int, bool) -> bool { SetGameState(Menu); return true; });
intro3.SetKeyDownEventHandler([this] (int, int, bool) -> bool { SetGameState(Menu); return true; });

intro.SetMouseButtonEventHandler([this] (UINT, int, int, WPARAM) -> bool { SetGameState(Menu); return true; });
intro2.SetMouseButtonEventHandler([this] (UINT, int, int, WPARAM) -> bool { SetGameState(Menu); return true; });
intro3.SetMouseButtonEventHandler([this] (UINT, int, int, WPARAM) -> bool { SetGameState(Menu); return true; });

Note the event handlers must be set for all three intro scenes.

Finally, to make the intro sequence start at all when the application starts, we have to set the active scene at the end of generateIntroSequence():

SetScene(intro);

Note this resets all the animations to their starting points. We did not create the animations above in the pause-on-create state, so their counters start running immediately. This call resets them, however, so there is no problem. Similarly, when the scene is changed to intro2 and intro3 later, their animations are also reset when the scene is activated.

Oh, you wanted game credits too?
Figure 7. Scrolling credits scene

Figure 7. Scrolling credits scene

The supplied source code also shows how to make an animated credits sequence with a rotating background where the credits scroll up the screen like the credits of a movie (see Figure 7). I’m not going to explain this here but it is fairly simple – see the source code for the details.

I’m Done Now

We’ve covered quite the range of topics here! I hope you can use the information within to add a bit of polish to your game project too. Please leave feedback below!

Next time, we’ll show how to add network leaderboards to the game. Start configuring your web servers!

Advertisement

Share your thoughts! Note: to post source code, enclose it in [code lang=...] [/code] tags. Valid values for 'lang' are cpp, csharp, xml, javascript, php etc. To post compiler errors or other text that is best read monospaced, use 'text' as the value for lang.

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: