TROUT WARS - Game programming with SDL

From LXF Wiki

Bringing Trout Wars to a conclusion is Paul Hudson, and he's very emotional indeed...

Although working with Trout Wars has been lots of fun, an unfinished code project is about as welcome as a porcupine at a balloon festival, so it's time to end it. Of course, we plan to end it in style ­ there's still lots to do, and we're going to try to cover it all over the next four pages! Fortunately, at this point most of the hard work is done, and I hope you'll agree that object orientation really does make life easier for applications where there are clearly defined functionality partitions, as seen here. All that's left for us to do is the eye candy [Hurrah! Finally, there are some pictures to look at! [Ed], which means we've saved the easy bits to last.

So, onto the specifics of what we're going to be working on this month. So far we've got a game that loads levels, enemies and explosions, and where you can control the player to destroy fish as you battle your way through the levels. We now just need to fix some of the remaining features so the game works properly, add in a menu screen, a stats bar that explains how the player is doing, and add a brief intro sequence so it looks like we've got a story of sorts.

Table of contents

Level up!


First we need to finish our code for loading levels. Last issue we just set up the game system to read levels in from a text file and have it create enemies at the right times. What we really need to do is also have the game show the title of the level before play begins, and also tot up the scores at the end of the level, based upon the player's skill during that level. What this requires is a new variable, PlayState, that stores whether the intro text is being shown (psIntro), whether the player is shooting fish (psPlaying), or whether they're seeing their scores for that level (psScore).

As you should have guessed, we'll be using an enum for this, so put these two lines of code in TroutWars.h beneath the definition of the enum ClevelEventType:

enum CPlayState { psIntro, psPlaying, psScore };
enum CGameState { gsIntro, gsMainMenu, gsOptions,
gsDoPlay, gsGameOver };

The CGameState enum will be used later ­ just put it in for now and ignore it. Then, down inside the class definition for CTWGame and just before the CTWGame() function, add the following two lines:

bool GameInProgress;
CPlayState PlayState;

The PlayState line there is what we're working on right now, and we'll be using GameInProgress later. Actually implementing PlayState is much harder, though, although not because the code is complex ­ it's just very long. Irrespective of whether we're in psIntro, psPlaying or psScore, there are some things that are always the same: the stars move and the player is drawn, and we also need to flip the main screen. Everything else needs to be inside a switch/case statement that draws the correct content according to the play state.

For psIntro, this should create a surface that shows the name of the current level and displays it in the middle of the screen, then switches the play state to psPlaying after about two seconds. The psPlaying play state is also quite easy because it's just the code we have right now. We'll come back to the psScore state soon. For now, let's get the intro working. There are some new variables involved in placing the intro text on screen ­ we need an SDL surface, a font for the text and a character buffer for rendering. The SDL surface is easy enough. If you check the new source code, you'll see SDL_Surface txtSpare in there, which is just a scrap bit of SDL_Surface we'll keep around to use when we need it, like now.

The font is also quite easy because quite a few issues ago we rendered Trout Wars onto the screen in the Free Sans font. You may well still have a definition for fntMain in your source code. If so, all you need to do is rename that as fntStats and then add these lines of code to your CTWGame() constructor in TroutWars.cpp:

TTF_Init();
this->fntStats = TTF_OpenFont("freesans.ttf", 14);

You may still have most of that in there, so just edit it as necessary. Next, we need to create a new function called NewGame(), which will be called whenever the player wants to start a new game in Trout Wars. This is unheard of right now ­ when we run the program, we go straight into a game.

What we need to have is the situation where starting the game presents us with choices to start a new game, set game options or quit. Once we have that, we want players who press Escape in-game to get back to the main menu, while pressing Escape again should return them to the game. Selecting the New Game option should reset the game back to its initial state, which is where this function comes in ­ it's less of a reset than restarting a game. Here's the code for it:

void CTWGame::NewGame() {
  this->GameInProgress = true;
  this->PlayState = psIntro;
  NumActiveExplosions = 0;
  Player->LevelStartTime = SDL_GetTicks();
  Player->Reset();
  while (Enemies.size() != 0) {
    delete Enemies[0];
    std::vector<CEnemy*>::iterator DeadEnemy = Enemies.
begin();
    Enemies.erase(DeadEnemy);
  }
  char* tempstring = new char[255];
  sprintf(tempstring, "Score: 0");
  txtScore = TTF_RenderText_Blended(fntStats, tempstring, clrWhite);
  sprintf(tempstring, "Accuracy: 0%%");
  txtAccuracy = TTF_RenderText_Blended(fntStats,
tempstring, clrWhite);
  delete[] tempstring;
}

There's our GameInProgress variable in action. The penultimate change you need to make is to remove the line at the end of CTWGame() that calls SDL_AddTime () because this should only be done at the end of the level intro.

Finally, call game->NewGame() just before game->Play() in main(). That's enough changes to have our intro text shown so go and give it a try!

Settling the score


After levels have finished, we need to print out level scores for the player so that they get some sense of achievement. This is easy. The first step is to add a new line of code in your LevelEventTick() function. The first and third lines of code below are there already ­ you just need to insert the second line.

SDL_RemoveTimer(LevelEventTicker);
PlayState = psScore;
Player->LevelEndTime = CurrentTime;
The new menu system is perhaps the best-looking part of the game ­ we build up the player's expectations and then cold-heartedly smash them to pieces!
The new menu system is perhaps the best-looking part of the game ­ we build up the player's expectations and then cold-heartedly smash them to pieces!

That tells the game to move to scoring as soon as all the enemies have been destroyed. From there, the next step lies in DrawScene(), where we switch on PlayState. Right now we've got psIntro and psPlaying in there, so add in psScore. I won't go over the code in much depth here because it's mostly just about displaying text and you can read that in the source code. There is one bit I want to explain briefly, which is the way that the Level Complete text gets moved to the top of the screen. The line you should be looking at is:

textpos -= (int)(textpos / 10);

This subtracts from textpos (which is the Y value that's used to render where the text is placed) one tenth of its current value. So, if textpos starts off being 350, it will take 35 off and leave 315. Then it takes another 10% off (31), leaving 284, then another 10% (28) leaving 256, and so on. As you can see, the amount taken off decreases each time because of the percentage, which means we'll get movement that starts off fast and slows down smoothly, and all with just one line of code.

Next, we need to fill in the UpdateAccuracy() and UpdateScore() functions that we created last issue ­ we want them to calculate the figures, then put them on the screen. The first step here is to add SDL_Surfaces into TroutWars.h to show the text, so put these lines into CTWGame:

SDL_Surface* txtScore;
SDL_Surface* txtAccuracy;

Now amend your UpdateAccuracy() and UpdateScore() so they look like this:

void CPlayer::UpdateAccuracy() {
 int accuracy;
 if (shotsfired == 0) {
   accuracy = 0;
 } else {
   accuracy = (int)(((float)shotshit/(float)shotsfired)*100);
 }
 char* accstring = new char[255];
 sprintf(accstring, "Accuracy: %d%%", accuracy);
 // clean the surface first, if necessary
 if (game->GameInProgress) SDL_FreeSurface(game->txtAccuracy);
 game->txtAccuracy = TTF_RenderText_Blended(game->fntStats, accstring, clrWhite);
 delete[] accstring;
}
void CPlayer::UpdateScore() {
 char* scorestring = new char[255];
 sprintf(scorestring, "Score: %d", score);
  // clean the surface first, if necessary
  if (game->GameInProgress) SDL_FreeSurface(game-
>txtScore);
  game->txtScore = TTF_RenderText_Blended(game-
>fntStats, scorestring, clrWhite);
  delete[] scorestring;
}

That renders all the accuracy and score information to surfaces, but doesn't put those surfaces on-screen. In my code I've also added calls to SDL_FreeSurface() to clean up the resources. For that, we're going to create another new function, DrawStats(). This will draw both the score and accuracy values onto the screen when called, and should look like this:

void CTWGame::DrawStats() {
  int maxright = 30;
  DrawImage(txtScore, maxright, 10);
  maxright += txtScore->w + 20;
  DrawImage(txtAccuracy, maxright, 10);
}

The maxright variable is used to make sure that each new item along the top is adequately spaced out from the previous item. You should then call DrawStats() in DrawScene(), just before the call to SDL_Flip(). If you try that out quickly, you'll see that the player can now move over the scoring text. This makes visibility a bit low, and so the last touch we want to add here is to render a grey box beneath the text so that it becomes a real stats bar, and then prohibit both the player and the fish from moving over it.

Before the call to DrawStats() in DrawScene(), you need to add the following line:

SDL_FillRect(sfcScreen, rectTopBar, SDL_MapRGB(sfcScreen->format, 64, 64, 64));

This draws a rectangle of the shape rectTopBar in a darkish grey (red 64, green 64, blue 64). The rectTopBar variable is new, so add these lines into the constructor for CTWGame():

rectTopBar = new SDL_Rect;
rectTopBar->h = 36;
rectTopBar->w = SCREEN_WIDTH;
rectTopBar->x = 0;
rectTopBar->y = 0;

You'll also need to declare rectTopBar in TroutWars.h. Once that's done, the grey bar will get drawn, but there's nothing to stop things moving underneath it. We can accomplish this by adding two more #defines into TroutWars.h, just below the #define for SCREEN_HEIGHT:

#define SCREEN_STATSBARSPACE 38
#define SCREEN_HEIGHT_USABLE (SCREEN_HEIGHT ­
SCREEN_STARSBARSPACE)

So, although we've defined the actual rectangle of the stats bar to be 36 pixels high, I've set the game up to have 38 pixels of space around it just to make sure. To make the game actually use this value, a few changes are required:

In LevelEventTick(), randrange() is used to get a value
    between 0 and SCREEN_HEIGHT. That 0 should become
    SCREEN_STATSBARSPACE.
In CPlayer::MoveUp(), change the 0 to SCREEN_
    STATSBARSPACE.
In CStarField::CStarField(), the YPos value of new stars are
    created with randrange() 0 and SCREEN_HEIGHT_TRIMMED.
    Change the 0 to SCREEN_STARSBARSPACE.

Running the game now should have corrected the problem. We've got a fully working stats bar and so we can crack on with some of the other things we need to sort out.


The menu system


This is the single hardest change we need to make, so I've kept it until last. Right now our game launches straight into Play() when created. Instead, we need to switch to something like we have in DrawScene(), where we do different things depending on the currently selected option. We already added the CGameState enum for this very purpose, and we also need to add the CTWGame::Run() function. There needs to be a clear distinction between running the game and playing the game ­ the former is where we start up the binary and watch the intro sequence or play with the menu, and the latter is where we're actually shooting fish.

We need nine new surfaces: one for a large logo of the game producer (us!), another for a small version of the same, one for the logo of the game, two for the New Game option (normal and highlighted), two for the Options option, and two for the Quit option. To get these surfaces, add the following lines to your CTWGame in TroutWars.h:

SDL_Surface* sfcHudzillaLogo;
SDL_Surface* sfcMiniHudzilla;
SDL_Surface* sfcMainTitle;
SDL_Surface* sfcNewGame;
SDL_Surface* sfcNewGameOver;
SDL_Surface* sfcOptions;
SDL_Surface* sfcOptionsOver;
SDL_Surface* sfcQuit;
SDL_Surface* sfcQuitOver;

Naturally, you'll want to change the Hudzilla part to your own branding! Now go to TroutWars.cpp and add lines to load your bitmaps inside CTWGame(). For example:

sfcMainTitle = LoadImage("maintitle.bmp");

With that done, go down to the main() function and change game->Play() to game->Run(), then delete the NewGame() call. This is no longer necessary because it will be called by our menu system. Add the declaration of CTWGame::Run() to TroutWars.h, then bring up my version of TroutWars.cpp so I can run you through how the function works.

First, we set up the `done' variable that will be used in our render loop, as seen elsewhere in this guide. We also call SDL_ShowCursor(0), which equates to `hide the cursor' ­ we're not clicking on anything, so there's no reason to have it. Next up, we calculate the position for the logo and the main title on the screen. The logo, which is used in a brief game intro, should be centred horizontally and vertically, whereas the game title needs to be centred horizontally but placed towards the top of the screen. The OptionsLeft variable is there because all the menu option pictures I've put together are the same width, with the text centred, so we just need to calculate their positions once.

Here you can see the stats bar ready to spring into action. Now if only the darn fish didn't move quite so fast!
Here you can see the stats bar ready to spring into action. Now if only the darn fish didn't move quite so fast!


Moving on, MenuState is defined and set to gsIntro so that the game plays the intro sequence first. Also, an integer MenuSelected is declared and set to 1 (New Game), then we're into the render loop and playing the standard game music. The game intro is just lots of text being displayed, as is the main menu screen ­ they should be self-explanatory from the code, so I'll pick up from the gsDoPlay case.

Again, we see the GameInProgress variable being used to decide whether we need to start a new game or not. If it's set to true, we call DoIntro() (we'll come to that in a moment), then NewGame() to reset things, then Play(). Otherwise, we just reset the level event ticker and go back to the play loop. This is important because the game ticker shouldn't be advancing while people are viewing the menu screen. Moving further on, when the game has returned we need to go back to the gsMainMenu setting, kill the game music and go back to `New Game' as the default menu option. There's a gsGameOver option in there that's left blank ­ something for you to try later!

Finally, there's the event loop, which is where we make the menu come to life. If Escape is pressed and we're in a game, it returns to the game. Otherwise it selects the Quit button and, if pressed again, quits the game. The Down and Up cursor keys handle menu selection by altering the MenuSelected integer, while hitting Enter activates each menu item. There's no code in there to handle an options screen right now. Again, this is something you can try yourself.

All that's left now is the DoIntro() function, which is just another function for displaying text at specific delays. Take a look at my code to see my example.


It's finished!


And so we come to the somewhat abrupt ­ but hopefully satisfactory ­ ending for Trout Wars. No matter how happy you are, I can guarantee that Nick's even happier. However, there are quite a few hooks in there that are just waiting to be used. The player can't die, for example, although lives are there; the enemy fish don't fire back, although our enemytypes file allows for that; and there's only one type of laser, although there's the code in there to handle more. These are things I hope you'll try out for yourself, and really go to town with.

Hopefully you've learnt a lot about SDL through reading this tutorial, and maybe even a thing or two about C++ as well. C++ really is such a flexible language that I find it simply irresistible, and I hope that at least a little bit of that enthusiasm has rubbed off onto you! LXF

Finishing a level now gives you completion bonuses depending on your performance.
Finishing a level now gives you completion bonuses depending on your performance.