« Back to tutorial listing

— 2D Top-down shooter tutorial —
Part 2: Angles and rotation

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

A 1280 x 720 window will open, with a dark grey background. 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[1] and will always face the targetter. Close the window by clicking on the window's close button.

Inspecting the code

For drawing our titular protangonist, Donk, we need to be able to rotate his texture. Luckily, SDL supplies functions for doing this. Before we get there, we need to do some setup. We'll start with what's new in defs.h:


...
#define PI 3.14159265358979323846

#define PLAYER_SPEED 6

We've defined a value of PI, as well as the player's speed. This will control how fast Donk will move when we press the WASD keys. Next, we've added an Entity struct to structs.h:


struct Entity {
	float x;
	float y;
	int w;
	int h;
	float dx;
	float dy;
	int health;
	int angle;
	SDL_Texture *texture;
	Entity *next;
};

typedef struct {
	Entity entityHead, *entityTail;
} Stage;

As you can see, we've also introduced a Stage struct to hold a linked list of entities. Like in the 2D shoot 'em up, the Stage object will be used to hold all the crucial information about the game in progress.

We've added a new file called util.c. This file contains just one function - getAngle:


float getAngle(int x1, int y1, int x2, int y2)
{
	float angle = -90 + atan2(y1 - y2, x1 - x2) * (180 / PI);
	return angle >= 0 ? angle : 360 + angle;
}

getAngle will do just as its name suggests: it will return the angle between two points. The function makes use of our PI define, as you can see. There are many, many functions for calculating the angle between two points, so we'll not go into any sort of explanation (such things are beyond the scope of this article).

Now let's look at draw.c, where we've added a new function called blitRotated:


void blitRotated(SDL_Texture *texture, int x, int y, float angle)
{
	SDL_Rect dstRect;

	dstRect.x = x;
	dstRect.y = y;
	SDL_QueryTexture(texture, NULL, NULL, &dstRect.w, &dstRect.h);
	dstRect.x -= (dstRect.w / 2);
	dstRect.y -= (dstRect.h / 2);

	SDL_RenderCopyEx(app.renderer, texture, NULL, &dstRect, angle, NULL, SDL_FLIP_NONE);
}

This function takes four arguments: the texture we want to draw, the x and y coordinates that we want to draw it at, and the angle of rotation. To achieve our rotation, we're calling an SDL function called SDL_RenderCopyEx. This function takes several arguments of its own: the renderer, the texture, the source rectangle, the destination rectangle, the angle we want to rotate it at, the center point about which we want to rotate, and a SDL_RendererFlip value to state which type of flipping action we want to use. There's quite a lot to take in here, but in our case things are simple. We want to use our renderer and the texture we supplied, so we pass these two as the first couple of arguments. The next two arguments will use NULL as the source, meaning we want to copy the entire texture over, and the destRect will be use as our destination rect. This rect has been setup to use the x and y coordinates we supplied, and also the width and height of the texture. Note how we're always shifting destRect's x and y by half the width and height of the texture, to center it correctly. The final three arguments of SDL_RenderCopyEx are the angle we've supplied, and NULL and SDL_FLIP_NONE. NULL will mean that SDL will rotate the texture about its center (which is what we want) and SDL_FLIP_NONE will mean that no flipping (horizontal or vertical) will take place. More info on the function can be found on the SDL wiki: https://wiki.libsdl.org/SDL_RenderCopyEx

With that in place, let's look at actually making use of it. We'll start with updating stage.c:


void initStage(void)
{
	...
	initPlayer();
}

The initStage function now calls a new function call initPlayer, where we'll setup Donk (see later). We've also made some additions to logic:


static void logic(void)
{
	doPlayer();

	doEntities();
}

doPlayer and doEntities are external functions that live in player.c and entities.c. We want to call these each frame. Next, draw has also been updated:


static void draw(void)
{
	drawEntities();
	...
}

Of course, we want to draw our entities, so no surprises here. Equally, if we inspect entities.c we find two functions. None of the code in here should come as a shock. We want to process and draw our entities, which is what doEntities and drawEntities will do:


void doEntities(void)
{
	Entity *e;

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

		if (e == player)
		{
			e->x = MIN(MAX(e->x, e->w / 2), SCREEN_WIDTH - e->w / 2);
			e->y = MIN(MAX(e->y, e->h / 2), SCREEN_HEIGHT - e->h / 2);
		}
	}
}

doEntities simply adds each entity's dx and dy to their x and y, causing them to move. One thing that we do in doEntities is test if the entity is the player and prevent them from leaving the screen. The calculation on the x and y, using the MIN and MAX macros, will ensure the player can never stray beyond the bounds of the screen dimensions. Next, we come to drawEntities:


static void drawEntities(void)
{
	Entity *e;

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

Here we're actually calling our blitRotated function. We're stepping through all the entities and drawing them using their texture and positions. We're also supplying their angle, so that we can rotate them as required.

Finally, we'll take a look at player.c:


void initPlayer(void)
{
	player = malloc(sizeof(Entity));
	memset(player, 0, sizeof(Entity));
	stage.entityTail->next = player;
	stage.entityTail = player;

	player->texture = loadTexture("gfx/donk.png");
	player->health = 5;
	player->x = SCREEN_WIDTH / 2;
	player->y = SCREEN_HEIGHT / 2;

	SDL_QueryTexture(player->texture, NULL, NULL, &player->w, &player->h);
}

initPlayer will create the player, set the texture, health, etc., and add them to the entities linked list in Stage. Again, no surprises. doPlayer is also very simple in nature:


void doPlayer(void)
{
	player->dx *= 0.85;
	player->dy *= 0.85;

	if (app.keyboard[SDL_SCANCODE_W])
	{
		player->dy = -PLAYER_SPEED;
	}

	if (app.keyboard[SDL_SCANCODE_S])
	{
		player->dy = PLAYER_SPEED;
	}

	if (app.keyboard[SDL_SCANCODE_A])
	{
		player->dx = -PLAYER_SPEED;
	}

	if (app.keyboard[SDL_SCANCODE_D])
	{
		player->dx = PLAYER_SPEED;
	}

	player->angle = getAngle(player->x, player->y, app.mouse.x, app.mouse.y);
}

The player will move when the WSAD keys are used. We're also doing two other things - first, we're slowing the player by multiplying their dx and dy by 0.85 at each call. This will mean that the player will decelerate, making their movement a little smoother. Next, we're setting the player's angle by calling our getAngle function, passing over the player's coordinates and the mouse's. In effect Donk will also face the mouse cursor.

We've made some big steps in this tutorial. Next, we'll look at accessing the mouse's buttons (including use of the scroll wheel) and firing weapons so that Donk can protect himself.

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] - WASD controls are up, left, down, right.

Desktop site