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


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

— Making a 2D split screen game —
Part 2: Steering and shooting

Note: this tutorial assumes knowledge of C, as well as prior tutorials.

Introduction

It's time to take control of our ship. Watching it spinning around is all well and good, but our game needs to be more interactive than that. In this part, we'll implement the steering and shooting, both of which are pretty easy to do.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./versus02 to run the code. You will see a window open like the one above, with the player's ship located in the middle of the screen. Use A and D to rotate the ship anti-clockwise or clockwise. Use W to accelerate and S to brake. Hold J to fire bullets. There is no limit to how fast we can fly, but if you hit the edges of the screen, you will bounce off them, and slow down a bit. Once you're finished, close the window to exit.

Inspecting the code

As already stated, adding in our steering and shooting is pretty simple, even more so if you've already followed a bunch of these tutorials before.

Let's jump in, starting with the updates to structs.h:


struct Entity
{
	uint8_t    type;
	SDL_FPoint position;
	SDL_FPoint dir;
	Model     *model;
	double     angle;
	int        radius;
	void(*data);
	void (*tick)(Entity *self);
	void (*draw)(Entity *self);
	Entity *next;
};

We've updated the Entity struct, to add in some new fields. `dir` is the direction the entity is moving (its velocity, if you will). `radius` is the bounding sphere of the entity (this will come into play much more later on, to help with collision detection). `data` is a pointer to the extended entity data, something we've seen before.

Next, we have the Player struct:


typedef struct
{
	double reload;
} Player;

Not much in it right now, other the `reload` field, that will control how often we can fire.

Finally, we have Bullet:


struct Bullet
{
	uint8_t    type;
	SDL_FPoint position;
	SDL_FPoint dir;
	int        radius;
	double     health;
	double     angle;
	Model     *model;
	Entity    *owner;
	Bullet    *next;
};

Another rather standard struct. `type` is the type of bullet this is, while `position` and `dir` are the location and velocity of the bullet. `radius` is its bounding sphere. `angle` is the bullet's angle, while `model` and `owner` are used to store the model the bullet will use when rendering, and the entity that owns the bullet (in this case, just the player). Some of these fields will come into extended use later on.

Moving on now, let's first look at the updates we've made to player.c, to handle our controls.

First, we've updated initPlayer:


void initPlayer(int x, int y)
{
	Entity *e;
	Player *p;

	p = malloc(sizeof(Player));
	memset(p, 0, sizeof(Player));

	e = spawnEntity(ET_PLAYER);
	e->position.x = x;
	e->position.y = y;
	e->radius = 15;
	e->model = loadModel("data/models/player1");
	e->data = p;

	e->tick = tick;
	e->draw = draw;
}

We're now mallocing a Player object (`p`) and setting it into the Entity's `data` field. We're also setting the Entity's `radius` to be 15. This value is of note, because it's actually smaller than the player's model. This means that later on you will notice that it's possible for parts of the ship to intersect the world, without a collision taking place. This is all fine, and something that is actually of advantage to us, in terms of gameplay. We'll discuss this more in a later part.

For now, let's look at the update to the `tick` function:


static void tick(Entity *self)
{
	Player *p;

	p = (Player *)self->data;

	steer(self);

	fire(self, p);

	self->position.x += self->dir.x * app.deltaTime;
	self->position.y += self->dir.y * app.deltaTime;

	if (self->position.x < self->radius || self->position.y < self->radius || self->position.x > SCREEN_WIDTH - self->radius || self->position.y > SCREEN_HEIGHT - self->radius)
	{
		bounce(self);
	}
}

An overhaul to the simple rotation. We're calling two new functions here, `steer` and `fire`. We're then updating the entity's `position` according to its `dir`, to make it move. Finally, we're testing if the ship has moved outside of the bounds of the screen, by checking its `radius` against the sides. If so, we're calling a function named `bounce` (we'll come to this last).

So, we have a few new functions that we need to detail. We'll move on to `steer` first:


static void steer(Entity *self)
{
	double rot;

	if (app.keyboard[SDL_SCANCODE_A])
	{
		self->angle -= 3 * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_D])
	{
		self->angle += 3 * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_W])
	{
		rot = TO_RAIDANS(self->angle);

		self->dir.x += 0.15 * sin(rot) * app.deltaTime;
		self->dir.y += 0.15 * -cos(rot) * app.deltaTime;
	}

	if (app.keyboard[SDL_SCANCODE_S])
	{
		self->dir.x *= 1 - (0.05 * app.deltaTime);
		self->dir.y *= 1 - (0.05 * app.deltaTime);
	}
}

This function, as the name implies, handles our steering. We first test if A or D are pressed, and will increase or decrease the player's `angle`, to make them turn. Next, we test if W is pressed. If so, we're going to adjust the values of `dir`'s `x` and `y`, according to our current `angle` (again, using radians, and once again with the help of some trigonometry). The short version is that we will be accelerating the player in the direction their ship is facing, following the vector of their current heading. Nice and simple. Finally, we test if S is pressed, and dappen our `dir`'s `x` and `y`, causing the player to decelerate.

Nothing taxing there, at all. The most complicated part is moving along our heading, and that's a very commonly available calculation.

Moving over to the `fire` function now:


static void fire(Entity *self, Player *p)
{
	p->reload = MAX(p->reload - app.deltaTime, 0);

	if (p->reload == 0)
	{
		if (app.keyboard[SDL_SCANCODE_J])
		{
			firePlayerBullet(self);

			p->reload = FPS / 12;
		}
	}
}

There's nothing difficult here, either. This function deals with firing the player's guns. We first decrease the value of the player's `reload`, then check if it's 0. If so, and J is pressed, we'll make a call to a function named firePlayerBullet (we'll see this shortly). Finally, we reset the player's `reload` to 1/12th of a second, thus allowing them to fire at a rate of 12 shots per second.

The final function we want to look at is `bounce`:


static void bounce(Entity *self)
{
	self->position.x += -self->dir.x * app.deltaTime;
	self->position.y += -self->dir.y * app.deltaTime;

	self->dir.x = -self->dir.x * 0.25;
	self->dir.y = -self->dir.y * 0.25;
}

As a reminder, this function is called whenever the player hits the sides of the screen. We first push the player back along the direction they have come from (effectively, returning them to the position they were in before the collision happened). With that done, we reverse the player's heading by negating their `dir`. We also dampen the velocity a bit, by quartering the result, so that we don't push the player back with the exact same force as they were originally going. As with real bouncing, some energy was lost during the impact.

That's it for player.c. All those simple additions have allowed us to take control of our player's ship, and fly it along the correct heading. Great progress!

Now over to bullets.c. This file is where we'll handle all our bullet-related tasks. This will be like something we've seen many, many times before, so we won't linger.

To start with, we have initBullets:


void initBullets(void)
{
	memset(&head, 0, sizeof(Bullet));
	tail = &head;

	player1BulletModel = loadModel("data/models/player1Bullet");
}

We setup the bullet linked list, and also load the model for the player's bullet (as player1BulletModel).

Next up, we have doBullets:


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

	prev = &head;

	for (b = head.next; b != NULL; b = b->next)
	{
		b->position.x += b->dir.x * app.deltaTime;
		b->position.y += b->dir.y * app.deltaTime;

		b->health -= app.deltaTime;

		if (!collision(b->position.x, b->position.y, b->radius, b->radius, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT))
		{
			b->health = 0;
		}

		if (b->health <= 0)
		{
			if (b == tail)
			{
				tail = prev;
			}

			prev->next = b->next;

			free(b);

			b = prev;
		}

		prev = b;
	}
}

Nothing out of the ordinary - here, we're just looping through our all our bullets, updating the positions according to their direction headings, and decreasing their `health`. If a bullet goes off-screen, we'll set its health to 0, so that it is removed from our linked list.

Over to drawBullet next:


void drawBullets(void)
{
	Bullet *b;

	for (b = head.next; b != NULL; b = b->next)
	{
		drawModel(b->model, b->position, b->angle);
	}
}

Here, as with our entity rendering, we're simpling calling drawModel, and passing over all the relevant details.

Now for firePlayerBullet:


void firePlayerBullet(Entity *e)
{
	Bullet *b;

	b = spawnBullet();
	b->position = e->position;
	b->dir.x = e->dir.x + (12 * sin(TO_RAIDANS(e->angle)));
	b->dir.y = e->dir.y + (12 * -cos(TO_RAIDANS(e->angle)));
	b->health = FPS * 3;
	b->radius = 3;
	b->angle = e->angle;
	b->owner = e;
	b->model = player1BulletModel;
}

This function, as we saw, is called whenever the player fires their guns. It accepts the player's entity (`e`) as a parameter. We start by creating a bullet (via spawnBullet), and setting all the relevant details. The bullet's `position` will be the same as the player's, with its direction (`dir`) inheriting from the player's own; we want the bullet to move ahead of the player, along the same vector, but at an increased speed. The bullet also inherits the player's `angle`. The `owner` is set to the player, and the bullet's `model` as player1BulletModel (for now).

Finally, we come to spawnBullet:


static Bullet *spawnBullet(void)
{
	Bullet *b;

	b = malloc(sizeof(Bullet));
	memset(b, 0, sizeof(Bullet));

	tail->next = b;
	tail = b;

	return b;
}

This is just a helper function, to create a bullet for us to use.

We're just about done. We just need to head over to zone.c, and tell our game loop to handle the bullets. First to initZone:


void initZone(void)
{
	memset(&zone, 0, sizeof(Zone));

	initEntities();

	initBullets();

	initPlayer(SCREEN_WIDTH / 2, SCREEN_HEIGHT / 2);

	app.delegate.logic = logic;
	app.delegate.draw = draw;
}

We've added in a call to initBullets. Next, to `logic`:


static void logic(void)
{
	doBullets();

	doEntities();
}

We're calling doBullets, to process them. Finally, we've updated `draw`:


static void draw(void)
{
	drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 24, 255);

	drawEntities();

	drawBullets();
}

We're calling drawBullet here.

That's all there is to it! A nice, short piece, that's already added in some essential functions to our game.

What would be good now is to have an arena or battle zone to fly around in. Some walls and other obstacles, that the player needs to negotiate and be constrained by. In our next part, we'll look into loading an arena, that, like most everything else in our game, will be built using triangles.

Purchase

The source code for all parts of this tutorial (including assets) is available for purchase, as part of the SDL2 tutorials bundle:

From itch.io

Mobile site