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


H1NZ

Arriving on the back of a meteorite, an alien pathogen has spread rapidly around the world, infecting all living humans and animals, and killing off all insect life. Only a handful are immune, and these survivors cling desperately to life, searching for food, fresh water, and a means of escape, find rescue, and discover a way to rebuild.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Shoot 'Em Up Tutorial —
Part 15: Title screen and finishing touches

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.

Our game is missing just one thing: a title screen. In this tutorial, we'll set one up and add some finishing touches to our game. Unpack the code and then type make to build. Once compiling is finished type ./shooter15 to run the code.

A 1280 x 720 window will open, with a colorful background. The title screen will be displayed, as in the screenshot above. Press the left control key to start. A spaceship sprite will also be shown. 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. Score points by collect points pods released from destroyed enemy ships. The highscore table is shown upon the player's death. If the player has earned a highscore, they will be prompted to enter their name. The highscore table is then shown and the game can be played again. Close the window by clicking on the window's close button.

Inspecting the code

The last few things that we've added to our code is the title screen and some tweaks. Not a great number of updates, so this final part of the tutorial will be quite short (and thankfully our final 10% didn't take 90% of the time!). First, let's look at the new title.c file. It contains just four functions. We'll go through them in order:


void initTitle(void)
{
	app.delegate.logic = logic;
	app.delegate.draw = draw;

	memset(app.keyboard, 0, sizeof(int) * MAX_KEYBOARD_KEYS);

	sdl2Texture = loadTexture("gfx/sdl2.png");
	shooterTexture = loadTexture("gfx/shooter.png");

	timeout = FPS * 5;
}

initTitle is called to initilize the title screen. We assign the delegate's logic and draw functions to our local ones, clear the keyboard input, and then load two textures - sdl2Texture and shooterTexture. These are for our title logo. Our logo is composed of two pieces, and we'll be drawing them separately so that we can add an effect that we'll see in a bit. We also set a timeout variable to 5 seconds. This variable will be used to change between the title page and the highscore table when timeout hits 0.

Moving onto the logic function, we won't see anything too unusual:


static void logic(void)
{
	doBackground();

	doStarfield();

	if (reveal < SCREEN_HEIGHT)
	{
		reveal++;
	}

	if (--timeout <= 0)
	{
		initHighscores();
	}

	if (app.keyboard[SDL_SCANCODE_LCTRL])
	{
		initStage();
	}
}

We're drawing our background and starfield, and then incrementing a variable called reveal. Again, this will be used for drawing our logo. We're capping the value this can reach at SCREEN_HEIGHT, just to prevent it from eventually overflowing and wrapping around. We're then decrementing timeout. If timeout is less 1, we're calling initHighscores to show the highscore table. This will make the game look a bit like the attract mode in arcade games. Finally, we're checking to see if the left control page (i.e., the fire button) has been pressed. If so, we're starting the game.

That's our logic done. Turning to our draw function:


static void draw(void)
{
	drawBackground();

	drawStarfield();

	drawLogo();

	if (timeout % 40 < 20)
	{
		drawText(SCREEN_WIDTH / 2, 600, 255, 255, 255, TEXT_CENTER, "PRESS FIRE TO PLAY!");
	}
}

Again, we're drawing our background and starfield, and then calling a new function called drawLogo (see below). We're also drawing our PRESS FIRE TO PLAY text. Note that we're testing the timeout variable before doing so. We're calculating the modulus of 40 and then checking to see if the result is less than 20. In effect, this will cause our text to blink off and on. The drawLogo function is a little more interesting:


static void drawLogo(void)
{
	SDL_Rect r;

	r.x = 0;
	r.y = 0;

	SDL_QueryTexture(sdl2Texture, NULL, NULL, &r.w, &r.h);

	r.h = MIN(reveal, r.h);

	blitRect(sdl2Texture, &r, (SCREEN_WIDTH / 2) - (r.w / 2), 100);

	SDL_QueryTexture(shooterTexture, NULL, NULL, &r.w, &r.h);

	r.h = MIN(reveal, r.h);

	blitRect(shooterTexture, &r, (SCREEN_WIDTH / 2) - (r.w / 2), 250);
}

Here, we're drawing our two logo textures that we loaded earlier. However, what we're also doing is drawing only a portion of each, according to the value of reveal. First, we're using SDL_QueryTexture to get the width and height of each texture, and storing these in the w and h of the SDL_Rect we've declared. We're then setting the rect's height as the lower value of r.h or reveal, and finally drawing the texturing using blitRect. Setting the rect's h to the lower value will cause it to slowly draw itself from top to bottom as reveal is incremented. We're also centering the logos by dividing SCREEN_WIDTH by two and subtracting half of the logo's width from it.

That's all for title.c. It's quite simple, really. All that's left to do with it is to make sure that we call initTitle at startup:


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

Calling initTitle in main.c (in place of initHighscores) means that the title screen will be the first thing shown.

There have been some other little tweaks throughout the code that we'll now touch on. None of these are essential, but more like finishing touches. Starting with highscores.c, we've made changes to the logic and draw functions:


static void logic(void)
{
	...
	if (--timeout <= 0)
	{
		initTitle();
	}
	...
}

As with title.c, we've added a timeout variable that we'll use to flip back and forth between the title screen and highscores. We've also updated the draw function to add in the blinking PRESS FIRE ... text:


static void draw(void)
{
	...
	if (timeout % 40 < 20)
	{
		drawText(SCREEN_WIDTH / 2, 600, 255, 255, 255, TEXT_CENTER, "PRESS FIRE TO PLAY!");
	}
}

One thing that you've perhaps noticed when playing the game is that the enemies sometimes spawn off the bottom of the screen. We can fix this in doEnemies:


static void doEnemies(void)
{
	...
	if (e != player)
	{
		e->y = MIN(MAX(e->y, 0), SCREEN_HEIGHT - e->h);

		if (player != NULL && --e->reload <= 0)
		{
			fireAlienBullet(e);
	...
}

The above tweak will use our MIN and MAX marcos to ensure that the entity's y never goes below 0 and never higher than SCREEN_HEIGHT minus their own height. This keeps the enemies on screen at all time. Another minor tweak we've made is to the enemies' movement:


static void spawnEnemies(void)
{
	...
	enemy->dy = -100 + (rand() % 200);
	enemy->dy /= 100;

In spawnEnemies, we've told the enemies that their dy can be a random between -1 and +1. This will mean that when created the enemies will move up and down the screen, as well as right to left. It's only a small change, but one that makes things a bit more interesting. Our final update is to drawing the points pods:


static void drawPointsPods(void)
{
	Entity *e;

	for (e = stage.pointsHead.next ; e != NULL ; e = e->next)
	{
		if (e->health > (FPS * 2) || e->health % 12 < 6)
		{
			blit(e->texture, e->x, e->y);
	...

Before drawing the points pods, we're making a simple test. If the pod's health is greater than FPS * 2 (so, two seconds) we'll always draw it. If it's not, we'll calculate the modulus of the health (to 12). We'll then only draw the pod if the result of this calculation is less than 6. In effect, this will mean that pods that have less than two seconds to live will start flashing, giving the player a visual cue that they're about to expire (and help to know whether they're worth chasing after).

Our shooter is done! Hurrah! Hopefully you will have found this tutorial series easy to follow. The next batch of tutorials will kick things up a gear and consider things like mouse tracking, scrolling maps, joysticks, and a bunch of other things.

Exercises

  • Expand the game! Add more enemies, stages ... anything you want!

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