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

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
2D run and gun
Roguelike
Medals (Achievements)
2D turn-based strategy game
2D isometric game
2D map editor
2D mission-based shoot 'em up
2D Santa game
2D split screen game
SDL 1 tutorials (outdated)

Latest Updates

SDL2 Versus game tutorial
Wed, 20th March 2024

Download keys for SDL2 tutorials on itch.io
Sat, 16th March 2024

The Legend of Edgar 1.37
Mon, 1st January 2024

SDL2 Santa game tutorial 🎅
Thu, 23rd November 2023

SDL2 Shooter 3 tutorial
Wed, 15th February 2023

All Updates »

Tags

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

Books


The Attribute of the Strong (Battle for the Solar System, #3)

The Pandoran War is nearing its end... and the Senate's Mistake have all but won. Leaving a galaxy in ruin behind them, they set their sights on Sol and prepare to finish their twelve year Mission. All seems lost. But in the final forty-eight hours, while hunting for the elusive Zackaria, the White Knights make a discovery in the former Mitikas Empire that could herald one last chance at victory.

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. Extract the archive, run cmake CMakeLists.txt, followed by 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 for purchase:

From itch.io

It is also available as part of the SDL2 tutorial bundle:

Mobile site