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 Honour of the Knights (Second Edition) (Battle for the Solar System, #1)

When starfighter pilot Simon Dodds is enrolled in a top secret military project, he and his wingmates begin to suspect that there is a lot more to the theft of a legendary battleship and the Mitikas Empire's civil war than the Helios Confederation is willing to let on. Somewhere out there the Pandoran army is gathering, preparing to bring ruin to all the galaxy...

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a Run and Gun game —
Part 11: More gameplay tweaks

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

Introduction

We're closing in on having the main framework for our game finished. We just need to make a few more gameplay tweaks in order to make it complete. This part will feature the first set of gameplay tweaks, with the others coming in the next part, along with effects such as explosions and handling the player death.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./gunner11 to run the code. You will see a window open like the one above. The same controls from past tutorials apply. There are just three enemies on this level, to demonstrate the gameplay changes. Notice how the blue soldier is unable to see the player until both doors have been opened and a clear line of sight has been established. Also notice how the second green soldier doesn't fire if his buddy is blocking his LOS when he sees the player. When you're finished, close the window to exit.

Inspecting the code

We've made a number of tweaks to a number of files to support our initial batch of gameplay changes. We'll start with defs.h:


#define EF_BLOCKS_LOS          (2 << 3)

We've introduced a new flag called EF_BLOCKS_LOS. This flag, when applied to an entity, will tell our line of sight code that this entity blocks line of sight. This can be applied to non-solid entities, such as enemies. We've also added in a new enum:


enum {
	LAYER_FOREGROUND,
	LAYER_BACKGROUND
};

This enum will be used to determine which "layer" our entities should be drawn, as either the foreground (0 - default) or background (1). If we now look at structs.h, we can see that we've added in a new field to Entity to support this:


struct Entity {
	double x;
	double y;
	double dx;
	double dy;
	int facing;
	int onGround;
	int dead;
	int layer;
	SDL_Rect hitbox;
	AtlasImage *texture;
	unsigned long flags;
	void (*data);
	void (*tick)(Entity *self);
	void (*draw)(Entity *self);
	void (*touch)(Entity *self, Entity *other);
	void (*takeDamage)(Entity *self, int amount, Entity *attacker);
	Entity *next;
};

`layer` is the only field we need here. Everything else remains the same.

Now let's have a look at how we're going to use our new enum. You'll remember that doors in the previous parts would raise when opened, but would cover the map. It would look better if they were behind the map when they opened. Turning to doors.c, we've made one minor change to start supporting this. Looking at initDoor:


static void initDoor(Entity *e)
{
	Door *d;

	d = malloc(sizeof(Door));
	memset(d, 0, sizeof(Door));

	e->data = d;
	e->flags = EF_WEIGHTLESS + EF_SOLID + EF_INTERACTIVE;
	e->layer = LAYER_BACKGROUND;

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

We've told our door entity that it's `layer` is LAYER_BACKGROUND. And that's all we need to do here.

If we now turn to entities.c, we've made a change to drawEnitites:


void drawEntities(int layer)
{
	Entity *e, *candidates[MAX_QT_CANDIDATES];
	int i;

	getAllEntsWithin(stage.camera.x, stage.camera.y, SCREEN_WIDTH, SCREEN_HEIGHT, candidates, NULL);

	for (i = 0, e = candidates[0] ; i < MAX_QT_CANDIDATES && e != NULL ; e = candidates[++i])
	{
		if (e->layer == layer)
		{
			e->draw(e);
		}
	}

	if (app.dev.showHitboxes)
	{
		for (i = 0, e = candidates[0] ; i < MAX_QT_CANDIDATES && e != NULL ; e = candidates[++i])
		{
			drawOutlineRect(e->hitbox.x - stage.camera.x, e->hitbox.y - stage.camera.y, e->hitbox.w, e->hitbox.h, 255, 255, 0, 255);
		}
	}
}

Our drawEntities function now take a parameter called `layer`. We're using this parameter when we're looping through all our entities to draw, by testing if the entity's `layer` is the same as the `layer` we passed in. In other words, if we pass in LAYER_BACKGROUND, we'll only draw the entities whose `layer` is also LAYER_BACKGROUND. If the parameter is LAYER_FOREGROUND, we'll only draw the entities whose `layer` is also LAYER_FOREGROUND.

Now, we turn to stage.c, we've updated `draw`:


static void draw(void)
{
	drawSky();

	drawEntities(LAYER_BACKGROUND);

	drawMap();

	drawEntities(LAYER_FOREGROUND);

	drawBullets();

	drawHud();
}

Now, instead of calling drawMap and then drawEntities, we're first calling drawEntities with LAYER_BACKGROUND, then drawMap, then drawEntities with LAYER_FOREGROUND. In effect, our doors will be rendered first, then the map, then our other entities. This means our doors will now render behind the map. Of course, we can expand this code to other entities if we wish, not just doors. You will have noticed that there is a chance for some optimisation here, as we're searching the quadtree twice for the entities to draw. This is something we could tweak in our final part.

Another gameplay aspect we want to look into is that enemies can shoot and kill one another. Fixing this issue is very easy. If we turn to greenSoldier.c, we need only update the takeDamage function:


static void takeDamage(Entity *self, int amount, Entity *attacker)
{
	EnemySoldier *s;

	if (attacker == stage.player)
	{
		s = (EnemySoldier*) self->data;

		s->life -= amount;

		s->damageTimer = 4;

		self->facing = stage.player->x < self->x ? FACING_LEFT : FACING_RIGHT;

		s->thinkTime = FPS / 4;

		if (s->life <= 0)
		{
			self->dead = 1;
		}
	}
}

Our takeDamage function takes a parameter called `attacker`, to say who inflicted the damage. To prevent enemies from harming one another, we need only test if the `attacker` is the player (`stage.player`). If it is, the damage code and response will proceed as normal. Otherwise, nothing will happen. Note that by design, the bullet that hit the enemy will be absorbed and vanish (see bullets.c). The above has been done for both greenSoldier.c and blueSoldier.c.

Another gameplay fix we want to make is to prevent enemies from shooting backwards. If an enemy starts firing, they will remain facing the same direction, even if the player moves past them. To fix this, we can make a small adjustment to `tick`:


static void tick(Entity *self)
{
	// snipped

	else if (s->reload == 0)
	{
		s->shotsToFire--;

		self->facing = (stage.player->x > self->x) ? FACING_RIGHT : FACING_LEFT;

		fireBullet(self);
	}

	// snipped
}

Now, before firing, an enemy will check which way they need to face. We do this by testing the player's `x` against the enemy's `x`. If the player's `x` is greater than the enemy's `x`, the player is to their right. We therefore assign the enemy's `facing` to FACING_RIGHT. Otherwise, we'll set it to FACING_LEFT.

Another minor change we want to make is to damage the player if they run into an enemy. This, again, is simple. Our green and blue soldiers currently don't implement a `touch` function. We can therefore define one as such:


static void touch(Entity *self, Entity *other)
{
	if (other == stage.player)
	{
		other->takeDamage(other, 1, self);
	}
}

We're just checking what touched the enemy (`self`). If it's the player (`other`), we'll call takeDamage for `other`, passing in the player, 1 point of damage, and the enemy itself. And that's all we need to do. Now, when a player runs into an enemy, they'll take 1 point of damage.

Sticking with the enemies for the moment, we can look into how we're using our new EF_BLOCKS_LOS flag. Taking initGreenSoldier as an example:


void initGreenSoldier(Entity *e)
{
	EnemySoldier *s;

	s = malloc(sizeof(EnemySoldier));
	memset(s, 0, sizeof(EnemySoldier));

	if (standTexture == NULL)
	{
		standTexture = getAtlasImage("gfx/sprites/greenSoldierStand.png", 1);

		bulletTexture = getAtlasImage("gfx/sprites/enemyBullet.png", 1);
	}

	s->life = 5;
	s->thinkTime = FPS * (1 + rand() % 3);

	e->facing = FACING_RIGHT;
	e->flags = EF_BLOCKS_LOS;
	e->texture = standTexture;
	e->data = s;

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

We're setting the entity's `flags` to EF_BLOCKS_LOS to tell our code that the enemy soldier will block the line of sight of others.

To see how this works, let's take a look at ai.c, starting with canSeePlayer:


int canSeePlayer(Entity *e)
{
	return traceMap(e) && traceEntities(e);
}

canSeePlayer is called by enemies, to check if they have a clear line of sight to the player. Before, we were just calling traceMap. Now, we're also calling a new function named traceEntities:


static int traceEntities(Entity *e)
{
	int i, sx, sy, ex, ey, x1, y1, x2, y2;
	Entity *candidates[MAX_QT_CANDIDATES], *other;

	sx = e->x + (e->texture->rect.w / 2);
	sy = e->y;

	ex = stage.player->x + (stage.player->texture->rect.w / 2);
	ey = stage.player->y;

	x1 = MIN(sx, ex);
	y1 = MIN(sy, ey);
	x2 = MAX(sx, ex);
	y2 = MAX(sy, ey);

	getAllEntsWithin(x1, y1, x2, y2, candidates, e);

	for (i = 0, other = candidates[0] ; i < MAX_QT_CANDIDATES && other != NULL ; other = candidates[++i])
	{
		if ((other->flags & (EF_SOLID|EF_BLOCKS_LOS)) && lineRectCollision(x1, y1, x2, y2, other->x, other->y, other->texture->rect.w, other->texture->rect.h))
		{
			return 0;
		}
	}

	return 1;
}

The idea behind this function is to test whether there are any entities between the observer (the enemy) and the player. The function will make use of line-rectangle collision detection, testing whether a line drawn from the enemy to the player intersets with either anything solid (or flagged as blocking) on the way.

To start with, we're defining several variables. `sx` and `sy` are the starting x and starting y of the line. We're using the entity's `x` position, plus half their texture width as `sx`, and their `y` value as `sy`. `ex` and `ey` are the ending x and y of the line. We're defining these as the player's `x` plus half their texture width as `ex`, and the player's `y` as `ey`.

With our line's start and end point defined, we then want to collect all entities that might be crossed by it. We going to search our quadtree to grab the entities, but we require a rectangular area for our getAllEntsWithin function. We can find this easily by determining the top-left and bottom-right of our line, by using MIN and MAX macros with our `sx`, `sy`, `ex`, and `ey` variables. The results are assigned to `x1`, `y1`, `x2`, and `y2`, giving us the corners of our rectangle. We then pass these into getAllEntsWithin, as well as an array to hold the entities (`candidates`), while telling the function to ignore the observer (`e`).

Next, for each of the entities that are returned, we're testing first the entity's `flags`. We're only interested in entities that are solid (EF_SOLID) or block the line of sight (EF_BLOCKS_LOS). If so, we're calling a new function named lineRectCollision. This function tests whether a line intersects with a rectangle. We'll see more on this in a little while. Should both these conditions hold true, we're going to return 0 (false) to say that the line of sight has been blocked. However, if we process all our entities and not find any entities interferring with our line of sight, we'll return 1.

Of note is that we've updated traceMap to also use this line-rectangle collision detection, as it's a bit more accurate:


static int traceMap(Entity *e)
{
	int x, y, sx, sy, ex, ey, x1, y1, x2, y2, mx1, my1, mx2, my2;

	sx = (e->x + (e->texture->rect.w / 2));
	sy = e->y;

	ex = (stage.player->x + (stage.player->texture->rect.w / 2));
	ey = stage.player->y;

	x1 = MIN(sx, ex);
	y1 = MIN(sy, ey);
	x2 = MAX(sx, ex);
	y2 = MAX(sy, ey);

	mx1 = x1 / MAP_TILE_SIZE;
	my1 = y1 / MAP_TILE_SIZE;
	mx2 = x2 / MAP_TILE_SIZE;
	my2 = y2 / MAP_TILE_SIZE;

	for (x = mx1 ; x <= mx2 ; x++)
	{
		for (y = my1 ; y <= my2 ; y++)
		{
			if (isInsideMap(x, y) &&
				stage.map[x][y] != 0 &&
				lineRectCollision(x1, y1, x2, y2, x * MAP_TILE_SIZE, y * MAP_TILE_SIZE, MAP_TILE_SIZE, MAP_TILE_SIZE))
			{
				return 0;
			}
		}
	}

	return 1;
}

It mostly works in much the same way as traceEntities, in the rectangular area to test. With this done, we're dividing `x1`, `y1`, `x2`, `y2` by MAP_TILE_SIZE to find the area of the map to test, assigning these to `mx1`, `my1`, `mx2`, and `my2`. We're then looping through the tiles in this area. The LOS is considered blocked if the tile indexes are inside the map (isInsideMap) the map tile is a non-zero value, and a line-rectangle intersection (lineRectCollision) occurs in the area occupied by the map tile. Again, if this happens we'll return 0. If we reach the end of the function without an intersection occuring, we'll return 1.

That's this part almost done. Before finishing up, we'll look at the lineRectCollision function that lives in util.c:


int lineRectCollision(int x1, int y1, int x2, int y2, int rx, int ry, int rw, int rh)
{
	return
		// top
		lineLineCollision(x1, y1, x2, y2, rx, ry, rx + rw, ry) ||

		// bottom
		lineLineCollision(x1, y1, x2, y2, rx, ry + rh, rx + rw, ry + rh) ||

		// left
		lineLineCollision(x1, y1, x2, y2, rx, ry, rx, ry + rh) ||

		// right
		lineLineCollision(x1, y1, x2, y2, rx + rw, ry, rx + rw, ry + rh)
	;
}

To test our line-rectangle collisions, we're actually testing our line (`x1`, `y1`, `x2`, `y2`) against all four sides of our rectangle (`rx`, `ry`, `rw`, `rh`). For this purpose, we're calling another function named lineLineCollision. We'll test first the top of the rectangle, then the bottom, then the left, then the right. If our line intersects any of these lines, we'll consider that the line has intersected the rectangle.

The line-line collision test is not something we'll discuss, as it's one of many different implementations that has proven to meet our needs. If you're interested, the implementation can be seen below:


int lineLineCollision(int x1, int y1, int x2, int y2, int x3, int y3, int x4, int y4)
{
	float denominator, numerator1, numerator2, r, s;

	denominator = ((x2 - x1) * (y4 - y3)) - ((y2 - y1) * (x4 - x3));

	if (denominator == 0)
	{
		return 0;
	}

	numerator1 = ((y1 - y3) * (x4 - x3)) - ((x1 - x3) * (y4 - y3));
	numerator2 = ((y1 - y3) * (x2 - x1)) - ((x1 - x3) * (y2 - y1));

	r = numerator1 / denominator;
	s = numerator2 / denominator;

	return (r >= 0 && r <= 1) && (s >= 0 && s <= 1);
}

That's some of the outstanding gameplay tweaks done. We've now only got to handle the player's death and add in some effects (explosions, weapon impacts, etc), and then we can move onto creating a full level.

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