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


Project Starfighter

In his fight back against the ruthless Wade-Ellen Asset Protection Corporation, pilot Chris Bainfield finds himself teaming up with the most unlikely of allies - a sentient starfighter known as Athena.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Top-down shooter tutorial —
Part 3: Mouse buttons and shooting

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 ./bad03 to run the code.

A 1280 x 720 window will open, with a dark grey background over which a light purple grid is shown. A targetter will be shown that will track the mouse movements. The main character, Donk, will also display. 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. Close the window by clicking on the window's close button.

Inspecting the code

Reading the description and looking at the screenshot above, it should be clear that there have been a number of changes made in this part of the tutorial. Some of these we'll be skipping over (such as the text drawing) as they have been covered in previous tutorials. We'll focus on the mouse interactions and firing the bullets.

Starting with handling the mouse buttons, we'll take a look at the changes to input.c. We've added two new functions - doMouseButtonUp and doMouseButtonDown:


void doMouseButtonUp(SDL_MouseButtonEvent *event)
{
	app.mouse.button[event->button] = 0;
}

void doMouseButtonDown(SDL_MouseButtonEvent *event)
{
	app.mouse.button[event->button] = 1;
}

These two functions are called whenever we want to process an SDL_MouseButtonEvent. This event contains information about a mouse event, including the button that was pushed. We use this button to set the state of the value of respective index in our app.mouse.button array, either 0 or 1 depending on whether the button was released or pressed. The SDL_MouseButtonEvent contains quite a lot of information. Full details can be found at https://wiki.libsdl.org/SDL_MouseButtonEvent.

We handle the actual event processing in doInput:


void doInput(void)
{
	...
	case SDL_MOUSEBUTTONDOWN:
		doMouseButtonDown(&event.button);
		break;

	case SDL_MOUSEBUTTONUP:
		doMouseButtonUp(&event.button);
		break;

	case SDL_MOUSEWHEEL:
		app.mouse.wheel = event.wheel.y;
		break;
}

SDL_MOUSEBUTTONDOWN and SDL_MOUSEBUTTONUP event types will call doMouseButtonDown and doMouseButtonUp, passing over the button event data. We also test for the mouse wheel by checking an event called SDL_MOUSEWHEEL. The wheel event contains a lot of information, but for now we're just going to use the y value. This will tell up whether the wheel was moved up or down (or was stationary). A y value of greater 0 will mean the wheel was scrolled up, while a y value of less than 0 will mean it was scrolled down. The wheel event in SDL also supports moving the wheel left and right. Once full, full info can be found on the SDL wiki: https://wiki.libsdl.org/SDL_MouseWheelEvent.

For actually making use of the mouse, we make some updates to player.c:


void doPlayer(void)
{
	...
	if (player->reload == 0 && stage.ammo[player->weaponType] > 0 && app.mouse.button[SDL_BUTTON_LEFT])
	{
		fireDonkBullet();

		stage.ammo[player->weaponType]--;
	}

	if (app.mouse.wheel < 0)
	{
		if (--player->weaponType < WPN_PISTOL)
		{
			player->weaponType = WPN_MAX - 1;
		}

		app.mouse.wheel = 0;
	}

	if (app.mouse.wheel > 0)
	{
		if (++player->weaponType >= WPN_MAX)
		{
			player->weaponType = WPN_PISTOL;
		}

		app.mouse.wheel = 0;
	}

	if (app.mouse.button[SDL_BUTTON_RIGHT])
	{
		if (player->weaponType == WPN_PISTOL && stage.ammo[WPN_PISTOL] == 0)
		{
			stage.ammo[WPN_PISTOL] = 12;
		}

		app.mouse.button[SDL_BUTTON_RIGHT] = 0;
	}
}

There's a number of new updates happening here, so let's go through them in order. First, we're checking to see if the player's reload is 0, the current weapon has ammo (by checking stage.ammo) and if the left mouse button is held down (the relevant array index of app.mouse.button will have been set in doInput). If so, we're firing a bullet and removing some ammo from the player's supply. Next, we're testing the mouse wheel. If the wheel has been pushed down (app.mouse.wheel has a negative value) we're selecting Donk's previous weapon, looping back to the top if we go beyond the start of the array. We're also then setting the value of app.mouse.wheel to 0. This is important because we only want to read the value once. We could do this in our doInput method, but doing it here allows us to consume the event once it's been read. We perform the same test for the mouse wheel being pushed down, stepping forward through the weapon array as needed and looping around when we pass the end. Again, we're setting app.mouse.wheel to 0. Finally, we're going to check to see if the right mouse button has been pushed. If so, and the player has the pistol selected and it's out of ammo, we're reset the ammo back to 12. Note that we're resetting app.mouse.button[SDL_BUTTON_RIGHT] to 0 after the test to see if it's held down. This is just to prevent the player from holding the button and automatically reloading every time they run out of bullets.

That's it for reading and using the mouse buttons. As you can see, it's quite simple. Let's now look at the code we've added for handling shooting. We've created a new file called bullets.c to deal with this aspect of the game. It contains several functions, so let's look at them one at a time, starting with initBullets:


void initBullets(void)
{
	donkBullet = loadTexture("gfx/donkBullet.png");
}

We're loading a texture for Donk's bullets. No big deal. Next, fireDonkBullet:


void fireDonkBullet(void)
{
	switch (player->weaponType)
	{
		case WPN_UZI:
			fireDonkUzi();
			break;

		case WPN_SHOTGUN:
			fireDonkShotgun();
			break;

		default:
			fireDonkPistol();
			break;
	}
}

In this function we're testing the type of weapon the player is currently using and calling the appropriate function. We're defaulting to fireDonkPistol. Once again, we'll look at the functions in order:


static void fireDonkUzi(void)
{
	Entity *b;

	b = malloc(sizeof(Entity));
	memset(b, 0, sizeof(Entity));
	stage.bulletTail->next = b;
	stage.bulletTail = b;

	b->x = player->x;
	b->y = player->y;
	b->texture = donkBullet;
	b->health = FPS * 2;
	b->angle = player->angle;

	calcSlope(app.mouse.x, app.mouse.y, b->x, b->y, &b->dx, &b->dy);

	b->dx *= 16;
	b->dy *= 16;

	player->reload = 4;
}

There shouldn't be any surprises here - we're creating an Entity and adding it to the linked list in stage. Next, we're setting the bullet's x and y coordinates to that of the player, setting the texture, and setting the health to about two seconds. Note how we're also telling the bullet to use the player's angle. This is so that when they are drawn they are rotated around the order that Donk was facing at the time. In order to determine the direction the bullet needs to travel we're calling a function called calcSlope to get the bullet's dx and dy (see the shooter tutorial for more on this). We're then multiplying bullet's dx and dy by 12 to give them some speed. Finally, we're setting the player's reload to 4, so that another bullet can be fired again in 4 frames' time. This will allow our uzi to fire quickly.

Our shotgun firing code is a little more complicated:


static void fireDonkShotgun(void)
{
	Entity *b;
	int i, destX, destY;
	float dx, dy;

	calcSlope(app.mouse.x, app.mouse.y, player->x, player->y, &dx, &dy);

	dx = player->x + (dx * 128);
	dy = player->y + (dy * 128);

	for (i = 0 ; i < 8 ; i++)
	{
		b = malloc(sizeof(Entity));
		memset(b, 0, sizeof(Entity));
		stage.bulletTail->next = b;
		stage.bulletTail = b;

		b->x = player->x + rand() % 16 - rand() % 16;
		b->y = player->y + rand() % 16 - rand() % 16;
		b->texture = donkBullet;
		b->health = FPS * 2;
		b->angle = player->angle;

		destX = dx + (rand() % 24 - rand() % 24);
		destY = dy + (rand() % 24 - rand() % 24);

		calcSlope(destX, destY, b->x, b->y, &b->dx, &b->dy);

		b->dx *= 16;
		b->dy *= 16;
	}

	player->reload = FPS * 0.75;
}

We create 8 bullets at once and will want these to spread out, like pellets. In order to achieve this, we need to aim at a location in front of Donk. First, we calculate the location of the zone by calling calcSlope, using the player's coordinates and the location of the mouse. Using our calculated dx and dy, we project forward 128 pixels from the player. We'll see why this important in a moment. Next, we set up our for loop, creating one bullet at a time. We set the bullet's x and y coordinates to the player's own, +- 16 pixels to give them a small random starting point. We set the texture, health, and angle as per our uzi and then come to the important step. In order to create a spread, we choose a random position around the point we calculated earlier (dx and dy), with +- 24 pixels, which we place into two variables called destX and destY. The reason we need to project forward and not simply use the mouse's x and y coordinates is due to proximity. If the mouse cursor is very close to Donk, the pellets will fly in every direction, something we don't want[1]. Now that we have our random point, we calculate the bullet's dx and dy as we did for the pistol. Finally, the player's reload is set to three quarters of a second, making them fire the shotgun quite slowly.

In contrast, fireDonkPistol is very straightforward:


static void fireDonkPistol(void)
{
	Entity *b;

	b = malloc(sizeof(Entity));
	memset(b, 0, sizeof(Entity));
	stage.bulletTail->next = b;
	stage.bulletTail = b;

	b->x = player->x;
	b->y = player->y;
	b->texture = donkBullet;
	b->health = FPS * 2;
	b->angle = player->angle;

	calcSlope(app.mouse.x, app.mouse.y, b->x, b->y, &b->dx, &b->dy);

	b->dx *= 16;
	b->dy *= 16;

	player->reload = 16;
}

It's pretty much the same as firing the uzi, but with a longer reload time (those two functions could actually be merged into one, to save code lines). The two other functions in bullets.c are doBullets and drawBullets. These do as one might expect - they process and draw the bullets (keeping in mind the angle of the bullet as they do):


void doBullets(void)
{
	Entity *b, *prev;

	prev = &stage.bulletHead;

	for (b = stage.bulletHead.next ; b != NULL ; b = b->next)
	{
		b->x += b->dx;
		b->y += b->dy;

		if (--b->health <= 0)
		{
			if (b == stage.bulletTail)
			{
				stage.bulletTail = prev;
			}

			prev->next = b->next;
			free(b);
			b = prev;
		}

		prev = b;
	}
}

void drawBullets(void)
{
	Entity *b;

	for (b = stage.bulletHead.next ; b != NULL ; b = b->next)
	{
		blitRotated(b->texture, b->x, b->y, b->angle);
	}
}

What's going on with these should be quite clear. Looking quickly at stage.c, we've added a couple of new functions - drawHud and drawWeaponInfo:


static void drawHud(void)
{
	drawText(10, 10, 255, 255, 255, TEXT_LEFT, "HEALTH:%d", player->health);

	drawText(250, 10, 255, 255, 255, TEXT_LEFT, "SCORE:%05d", 0);

	drawWeaponInfo("PISTOL", WPN_PISTOL, 550, 10);

	drawWeaponInfo("UZI", WPN_UZI, 800, 10);

	drawWeaponInfo("SHOTGUN", WPN_SHOTGUN, 1050, 10);
}

The drawHud function simply draws some information about the player's health and score (unused), and the weapons they have. Looking at drawWeaponInfo:


static void drawWeaponInfo(char *name, int type, int x, int y)
{
	int r, g, b;

	if (player->weaponType == type)
	{
		r = b = 0;
		g = 255;
	}
	else
	{
		r = g = b = 255;
	}

	drawText(x, y, r, g, b, TEXT_LEFT, "%s:%03d", name, stage.ammo[type]);
}

We're simply drawing the name of the weapon and the ammo available. One thing we're testing is whether the weapon we're drawing the information for is the player's current weapon. If so, we're drawing the text in green. Otherwise, it's drawn in white.

We've now got our mouse controls locked down, and the ability to select and fire weapons. Its's time to add some enemies and items to the mix. We'll do so in our next tutorial.

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:

Footnotes

[1] - While it is technically possible for the mouse's x and y to be perfectly over Donk's x and y, this situation is very unlikely to happen, so we can safely ignore it for now.

Mobile site