« Back to tutorial listing

— 2D Top-down shooter tutorial —
Part 6: Finishing touches

Introduction

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

This first tutorial will explain how to read the mouse in SDL2. Extract the archive, run cmake CMakeLists.txt, followed by make to build. Once compiling is finished type ./bad06 to run the code.

A 1280 x 720 window will open, with the title screen shown. It will change to the highscore table every few seconds. Click the left mouse button to start playing the game. Donk can be moved with the WSAD control scheme and will always face the targetter. Use the left mouse button to fire. The mouse wheel will cycle through the available weapons. The right mouse button will reload the pistol when it is out of ammo. Enemies will appear in the arena and may drop powerups when defeated. Keep moving and try and stay alive as long as possible. Score points by shooting enemies and picking up yellow score tokens. Enemies are worth 10 points each, score tokens 250. The player will be given a chance to enter their name after they're defeated, if they gain a high score. Close the window by clicking on the window's close button.

Inspecting the code

We've added a number of finishing touches to Battle Arena DONK! As sound, music, and highscores were handled in previous tutorials (see SDL Shooter), we'll be skipping over those and focusing on the things that are specific to this final part of the game.

To start with, we've added a new powerup to the game: this is a points powerup, that gives Donk an extra 250 points when he collects it. We've made some changes to items.c to facilitate this, adding in three new functions:


void addPointsPowerup(int x, int y)
{
	Entity *e;

	e = createPowerup(x, y);

	e->tick = pointsTick;
	e->health = FPS * 10;
	e->dx = e->dy = 0;

	e->texture = pointsTexture;
	e->touch = pointsTouch;
}

addPointsPowerup takes two arguments: an x and y coordinate. This function operates largely like the other powerup creation functions, although it overrides the default health, dx and dy, extending health to 10 seconds and zeroing the dx and dy; we don't want the points pod to move around when it is created. It also assigns the tick and touch functions to those of its own. Starting with pointsTick:


static void pointsTick(void)
{
	tick();

	self->angle += 5;

	while (self->angle >= 360)
	{
		self->angle -= 360;
	}
}

pointsTick calls the original tick, but also tells the powerup to rotate in place. To prevent strange overflow issues (although highly unlikely), we'll clamp the angle to a range of 360. Next, we'll take a look at pointsTouch:


static void pointsTouch(Entity *other)
{
	if (other == player)
	{
		self->health = 0;

		stage.score += 250;

		playSound(SND_POINTS, CH_ITEM);
	}
}

Nothing unusual here. We're checking if the thing that has touched the item is the player and then awarding 250 points. The item's health is set to 0 and we play a sound effect. As always, we need to update stage.c to allow for the creation and handling of our new powerup:


static void logic(void)
{
	...

	spawnPointsPowerup();

	...
}

We've updated logic to call a new function called spawnPointsPowerup, defined below:


static void spawnPointsPowerup(void)
{
	int x, y;

	if (--pointsSpawnTimer <= 0)
	{
		x = rand() % ARENA_WIDTH;
		y = rand() % ARENA_HEIGHT;

		addPointsPowerup(x, y);

		pointsSpawnTimer = (FPS * 3) + rand() % (FPS * 2);
	}
}

We decrement a variable called pointsSpawnTimer upon each call. Once it hits 0 or less, we'll assign x and y variables to a random of the arena's width and height. We then call our addPointsPowerup function, passing over these two variables. Essentially, this means that a points powerup will spawn somewhere in the arena at random. Finally, we tell the pointsSpawnTimer to reset to between 3 and 5 seconds.

In order to allow for the game to be replayed once the player is killed, we need to reset the stage. We do this by adding a resetStage function:


static void resetStage(void)
{
	Entity *e;
	Effect *ef;

	while (stage.entityHead.next)
	{
		e = stage.entityHead.next;
		stage.entityHead.next = e->next;
		free(e);
	}

	while (stage.bulletHead.next)
	{
		e = stage.bulletHead.next;
		stage.bulletHead.next = e->next;
		free(e);
	}

	while (stage.effectHead.next)
	{
		ef = stage.effectHead.next;
		stage.effectHead.next = ef->next;
		free(ef);
	}

	memset(&stage, 0, sizeof(Stage));

	stage.entityTail = &stage.entityHead;
	stage.bulletTail = &stage.bulletHead;
	stage.effectTail = &stage.effectHead;

	gameOverTimer = FPS * 2;
}

This will remove all the entities, bullets, and effects in the stage, so that we don't waste memory when restarting. This is then added to the initStage function, so that we can safely call it whenever we start the game:


void initStage(void)
{
	...

	resetStage();

The last thing we want to do is add in a title screen; we don't want the game to jump straight into the action when it starts. We've added a new compilation unit called title.c, containing a 4 functions. We'll start with the initTitle function:


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

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

	battleArenaTexture = loadTexture("gfx/battleArena.png");
	donkTexture[0] = loadTexture("gfx/D.png");
	donkTexture[1] = loadTexture("gfx/O.png");
	donkTexture[2] = loadTexture("gfx/N.png");
	donkTexture[3] = loadTexture("gfx/K.png");
	donkTexture[4] = loadTexture("gfx/!.png");

	donkAngle[0] = rand() % 360;
	donkAngle[1] = rand() % 360;
	donkAngle[2] = rand() % 360;
	donkAngle[3] = rand() % 360;
	donkAngle[4] = rand() % 360;

	timeout = FPS * 5;
}

As expected, we're assigning the app's logic and draw pointers to local functions, clearing the keyboard and mouse input, and then loading some textures. The main logo and each individual letter of Donk is handled separately. We're also then setting the values of an array called donkAngle to a random between 0 and 359. This will be used for animating them during our draw and logic phase. A timeout of 5 seconds before displaying the highscores is also set. This allows time for the animations to complete. Moving onto the logic handling:


static void logic(void)
{
	int i;

	battleArenaDY = MIN(battleArenaDY + 0.25, 25);

	battleArenaY = MIN(battleArenaY + battleArenaDY, 200);

	if (battleArenaY == 200)
	{
		battleArenaDY = -battleArenaDY * 0.5;

		if (battleArenaDY > -1)
		{
			battleArenaDY = 0;
		}
	}

	for (i = 0 ; i < 5 ; i++)
	{
		donkAngle[i] = MIN(donkAngle[i] + 2, 360);
	}

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

	if (app.mouse.button[SDL_BUTTON_LEFT])
	{
		initStage();
	}
}

The logic phase is mostly concerned with animating the logo. We want our logo to accelerate down the screen, stopping and bouncing when it reaches a certain point. To do this, we increment a variable called battleArenaDY by 0.25 each frame, capping it at 25. We then add battleArenaDY to battleArenaY, capping that value at 200. When battleArenaY hits 200, we set battleArenaDY to half the negative of its current value. This means that if battleArenaDY was 10, it would then be -5, and if it was 4, it would be -2. The logo then begins to move back up the screen before the acceleration of battleArenaDY pulls it back down. The bounce will become smaller and smaller with each hit. We test battleArenaDY to make sure that the bounce isn't too small and zero it if so.

We then increment all the values in our donkAngle array by 2, capping at 360. This will cause the individual letters to rotate around to their correct positions over time. As we've set a random value of each donkAngle, this will be different every time. Finally, we're decrementing our timeout and showing the highscore table as needed (initHighscores). Clicking the left mouse button will call initStage, starting the game.

Moving onto our draw functions, we'll see they're quite simple:


static void draw(void)
{
	drawLogo();

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

draw itself simply calls drawLogo and blinks the press fire text at a set interval. drawLogo is equally straightforward:


static void drawLogo(void)
{
	int x, i;

	blit(battleArenaTexture, SCREEN_WIDTH / 2, battleArenaY, 1);

	x = -300;

	for (i = 0 ; i < 5 ; i++)
	{
		blitRotated(donkTexture[i], (SCREEN_WIDTH / 2) + x, 350, donkAngle[i]);

		x += 150;
	}
}

We first blit the main battle arena texture, centering it on the horizontal. Each individual letter is then drawn, using the angle of donkAngle.

Finally, we need to update main.c to tell it to start the game with the title screen. This is a one line change:


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

That's it for Battle Arena DONK! Hopefully you'll have found this tutorial helpful. There are things that the arena could have used: obstacles and other objects to interact with. We'll learn how to deal with a proper time map in the platform game tutorial next.

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:

Desktop site