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

Android Games

DDDDD
Number Blocks
Match 3 Warriors

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 (First Edition) (The Battle for the Solar System)

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 an Imperial nation's civil war than either the Confederation Stellar Navy or the government are willing to let on.

Click here to learn more and read an extract!

« Back to tutorial listing

— Creating a vertical shoot 'em up —
Part 8: Enemy attack patterns (full sequence)

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

Introduction

We've just two attack pattern types left to implement, both of which will follow pre-defined (albeit random) paths. These paths will make the aliens move from one location to the next across the screen, before returning to their original start point. It will look a bit like old-school vertical shooters, although maybe not as fancy.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./shooter2-08 to run the code. You will see a window open like the one above. Use the arrow keys to move the fighter around, and the left control key to fire. Play the game as normal, and battle the aliens and bosses. Note how the little pink aliens and little orange aliens (when they turn up) move from point to point on the screen. When you're finished, close the window to exit.

Inspecting the code

We've added just one new struct to structs.h to support our new alien:


typedef struct {
	double startDelay;
	SDL_Point waypoints[MAX_WAYPOINTS];
	SDL_Point *waypoint;
	int numWaypoints;
	double dx;
	double dy;
	int smooth;
	double timeout;
	double reload;
	double damageTimer;
} RandomAlien;

RandomAlien is so called as the aliens move to random positions on screen. Although we've got two different types of these path-following aliens, they share so much in common that they can share one struct, with just one redundant field between the two (`timeout`). startDelay is the start delay of the aliens. `waypoints` is an array of SDL_Points (x and y coordinates) that will tell our aliens where to move to. `waypoint` is a pointer to the `waypoints` array, allowing us to track the current waypoint in the array. numWaypoints tells us how many waypoints we've defined of our maximum (it may be fewer than MAX_WAYPOINTS). `dx` and `dy` are the velocities along the x and y axis that the alien will follow. `smooth` is a flag to tell the alien whether its movement style should mean it accelerates towards the waypoints or moves at a fixed speed, while `timeout` is used in conjuction with the smooth aliens to assist with their waypoint transitions. `reload` and damageTimer we've seen before.

Our new aliens are defined in randomAlien.c. Many of the functions will look familiar, but we'll cover them anyway. We'll start with initRandomAlien:


void initRandomAlien(int startDelay, int x, int y, SDL_Point waypoints[], int numWaypoints, int smooth)
{
	Entity *e;
	RandomAlien *r;
	int i;

	r = malloc(sizeof(RandomAlien));
	memset(r, 0, sizeof(RandomAlien));

	r->startDelay = startDelay;
	r->reload = rand() % (int) FPS;
	r->numWaypoints = numWaypoints;
	r->smooth = smooth;

	for (i = 0 ; i < numWaypoints ; i++)
	{
		r->waypoints[i].x = waypoints[i].x;
		r->waypoints[i].y = waypoints[i].y;
	}

	r->waypoint = r->waypoints;
	r->numWaypoints--;

	if (littlePinkAlienTexture == NULL)
	{
		littlePinkAlienTexture = getAtlasImage("gfx/littlePinkAlien.png", 1);
		littleOrangeAlienTexture = getAtlasImage("gfx/littleOrangeAlien.png", 1);
		bulletTexture = getAtlasImage("gfx/alienDownBullet.png", 1);
	}

	e = spawnEntity(ET_ALIEN);

	if (r->smooth)
	{
		e->texture = littleOrangeAlienTexture;
		e->health = 3;
		r->timeout = TIMEOUT;
	}
	else
	{
		e->texture = littlePinkAlienTexture;
		e->health = 2;
	}

	e->data = r;

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

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

Our initRandomAlien function takes a number of parameters: the startDelay, the `x` and `y` starting coordinates, an array of `waypoints` (as SDL_Points), an int telling us how many waypoints we have supplied, and the `smooth` flag.

We start by mallocing and memsetting a RandomAlien, then assigning its startDelay, a random `reload` time, the number of waypoints (numWaypoints), and the `smooth` flag. We then copy the waypoint data that was passed to the function into the Random alien's `waypoints` array, using a for-loop, up to numWaypoints. With that done, we point the RandomAlien's `waypoint` pointer at its `waypoints` array, effectively assigning it the first item in the array. We then decrement the RandomAlien's numWaypoints. We do this because numWaypoints will be used to track how many waypoints we've hit during our movement phase. When there are no waypoints remaining, the alien will automatically die (we'll see more on this in a bit). As we've started towards our first waypoint, we'll decrement our `numWaypoints` counter right away.

We're then testing if we need to load our textures, by testing if littlePinkAlienTexture is NULL. We're loading three textures here - two for the aliens and one for the bullet they will fire. We're then creating the entity itself. Note how we're testing the `smooth` flag. If the `smooth` flag is set to 1 (true), we're assigning the littleOrangeAlienTexture, setting the `health` to 3, and setting the RandomAlien's `timeout` to TIMEOUT (defined as 3 * FPS = three seconds). Otherwise, we're using the littlePinkAlienTexture and setting the `health` to 2. In effect, the RandomAliens will have different images and `health` depending on their behaviour. We'll discuss what `timeout` is for when we come to looking at the smooth aliens's movement.

The usual entity field assignments follow, including the positons and function pointers.

Our `tick` function comes next:


static void tick(Entity *self)
{
	RandomAlien *r;

	r = (RandomAlien*) self->data;

	r->startDelay -= app.deltaTime;

	if (r->startDelay <= 0)
	{
		self->x += r->dx * app.deltaTime;
		self->y += r->dy * app.deltaTime;

		if (r->smooth)
		{
			moveSmooth(self, r);
		}
		else
		{
			moveRigid(self, r);
		}
	}

	r->reload = MAX(r->reload - app.deltaTime, 0);

	if (r->reload == 0)
	{
		if (rand() % 10 == 0)
		{
			fireBullet(self);
		}

		r->reload = FPS;
	}

	r->damageTimer = MAX(r->damageTimer - app.deltaTime, 0);

	if (player->health > 0 && collision(self->x, self->y, self->texture->rect.w, self->texture->rect.h, player->x, player->y, player->texture->rect.w, player->texture->rect.h))
	{
		self->takeDamage(self, 1);

		player->takeDamage(player, 1);
	}

	stage.hasAliens = 1;
}

Again, very much like all the other aliens we've defined so far. Our aliens don't move until their startDelay has hit 0, at which point we're adding the RandomAlien's `dx` and `dy` to their `x` and `y`. However, we're then testing the RandomAlien's `smooth` flag. If it's 1, we're calling moveSmooth. Otherwise, we're calling moveRigid. Nothing else in this function is any different to the other aliens, so we'll jump straight to the movement functions. We'll start with moveRigid:


static void moveRigid(Entity *self, RandomAlien *r)
{
	double diffX, diffY;

	diffX = fabs(self->x - r->waypoint->x);
	diffY = fabs(self->y - r->waypoint->y);

	r->dx = r->dy = 0;

	if (diffX > MAX_MOVE_SPEED)
	{
		if (self->x < r->waypoint->x)
		{
			r->dx = MAX_MOVE_SPEED;
		}

		if (self->x > r->waypoint->x)
		{
			r->dx = -MAX_MOVE_SPEED;
		}
	}

	if (diffY > MAX_MOVE_SPEED)
	{
		if (self->y < r->waypoint->y)
		{
			r->dy = MAX_MOVE_SPEED;
		}

		if (self->y > r->waypoint->y)
		{
			r->dy = -MAX_MOVE_SPEED;
		}
	}

	if (r->dx == 0 && r->dy == 0)
	{
		if (r->numWaypoints-- > 0)
		{
			r->waypoint++;
		}
		else
		{
			self->health = 0;
		}
	}
}

The function takes the Entity and the RandomAlien we're currently working with, and basically determines the direction the alien will move. The first thing we do is calculate the distance between the alien and the waypoint, on both the x and y axis, and storing them in variables called diffX and diffY. We're then setting the RandomAlien's `dx` and `dy` to 0, to tell it not to move. We're then checking if our diffX is greater than our MAX_MOVE_SPEED, effectively letting us know if the alien's `x` coordinate is far from the waypoint's `x`. If so, we'll want to tell the alien to move closer. We then test whether the alien's `x` is lower than the waypoint's `x`, and set the RandomAlien's `dx` to MAX_MOVE_SPEED if so. If the alien's `x` is greater than the waypoint's `x`, we'll set the RandomAlien's `dx` to the negative of MAX_MOVE_SPEED.

Basically, we're testing to see if the alien's `x` is within a threshold distance of our waypoint's `x`. If so, the alien's `dx` will remain at 0. If not, we'll set the `dx` to make the alien move closer to the waypoint's `x`. This is necessary in order to stop the alien from juddering about when moving and trying to align itself perfectly. So long as the alien is close enough, we'll tell it not to bother adjusting itself any further. The same logic is applied to the y axis.

We then test to see if the alien's `dx` and `dy` are 0. If so, it means that no movement adjustments were made and the alien has reached its waypoint (more or less). We now decrease the RandomAlien's numWaypoints. If there are still waypoints to visit, we'll increment the RandomAlien's `waypoint` array pointer, to move to the next waypoint. Otherwise, having now visited all its waypoints (the final waypoint is always offscreen), we remove the alien by setting its `health` to 0.

That's our rigid movement done. Any RandomAliens who do not have a `smooth` flag set will move in a straight line to their goal.

moveSmooth is the function that will be employed for those with the `smooth` flag set:


static void moveSmooth(Entity *self, RandomAlien *r)
{
	if (self->x < r->waypoint->x)
	{
		r->dx += ACCELERATION_RATE * app.deltaTime;
	}

	if (self->x > r->waypoint->x)
	{
		r->dx -= ACCELERATION_RATE * app.deltaTime;
	}

	if (self->y < r->waypoint->y)
	{
		r->dy += ACCELERATION_RATE * app.deltaTime;
	}

	if (self->y > r->waypoint->y)
	{
		r->dy -= ACCELERATION_RATE * app.deltaTime;
	}

	r->dx = MAX(MIN(r->dx, MAX_MOVE_SPEED), -MAX_MOVE_SPEED);
	r->dy = MAX(MIN(r->dy, MAX_MOVE_SPEED), -MAX_MOVE_SPEED);

	r->timeout -= app.deltaTime;

	if (r->timeout <= 0)
	{
		if (r->numWaypoints-- > 0)
		{
			r->waypoint++;
		}
		else if (!collision(self->x, self->y, self->texture->rect.w, self->texture->rect.h, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT))
		{
			self->health = 0;
		}

		r->timeout = TIMEOUT;
	}
}

Our moveSmooth function differs from our moveRigid function by instead of moving the aliens at a fixed speed, we will have them accelerate towards the waypoint. We're first testing if the alien's `x` is lower than the waypoint's `x`. If so, we're incrementing the RandomAlien's `dx` by ACCELERATION_RATE. If the alien's `x` is lower than the waypoint's `x`, we're decreasing the RandomAlien's `x` by ACCELERATION_RATE. The same is true of handling the y axis. In effect, this means that the alien will be speeding up and slowing down as it moves between waypoints, resulting in smooth movement. We're limiting the RandomAlien's `dx` and `dy` values to MAX_MOVE_SPEED (both positive and negative) to stop it from accelerating to ludicrous speeds.

We're then decreasing the RandomAlien's `timeout`. If `timeout` falls to 0 or lower, we're decrementing the RandomAlien's numWaypoints. Like our moveRigid function, we're checking if there are waypoints left to visit and incrementing the RandomAlien's `waypoint` pointer if there are. Otherwise, we're checking to see if the alien has exited the screen and setting its `health` to 0 to remove. Finally, we're resetting the RandomAlien's `timeout` to TIMEOUT. The reason we're not checking to see if the alien has arrived at the waypoint is that the nature of our accelerating movement pattern makes this very hard to pin down. More often than not, our alien will begin to orbit the waypoint while trying to reach it. Assessing the distance to the waypoint doesn't always solve this issue, either. You may have noticed this orbitting happening in other video games, where a missile or some other object circles an entity endlessly, that it should be tracking. Therefore, giving our aliens a timeout to reach the waypoint before moving on solves the problem. In fact, it actually has the nice side effect that from time to time our aliens will orbit the waypoint for a while, before moving on, making the attack pattern a bit more interesting to look at.

That's largely it for our RandomAlien's behaviour. We'll quickly cross off the other function in the file, starting with `draw`:


static void draw(Entity *self)
{
	RandomAlien *r;

	r = (RandomAlien*) self->data;

	blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);

	if (r->damageTimer > 0)
	{
		SDL_SetTextureBlendMode(self->texture->texture, SDL_BLENDMODE_ADD);
		blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);
		SDL_SetTextureBlendMode(self->texture->texture, SDL_BLENDMODE_BLEND);
	}
}

A standard alien draw function, rendering the alien normally, and then once again with additive blending set if the RandomAlien's damageTimer is greater than 0.

takeDamage is next:


static void takeDamage(Entity *self, int amount)
{
	self->health -= amount;

	if (self->health == 0)
	{
		self->die(self);
	}

	((RandomAlien*) self->data)->damageTimer = 8;
}

Also standard - we're decreasing the alien's `health` by the amount passed into the function and killing it if the health falls to 0 or less. The RandomAlien's damageTimer is set to 8 in all cases.

The `die` function follows:


static void die(Entity *self)
{
	stage.score++;

	addExplosion(self->x + (self->texture->rect.w / 2), self->y + (self->texture->rect.h / 2));

	if (--stage.numWaveAliens == 0)
	{
		addPointsPod(self->x, self->y);
	}
}

Like all other aliens, we're increasing our score, adding an explosion, and adding a PointsPod if we've killed all the aliens in the wave.

The last function is fireBullet:


static void fireBullet(Entity *self)
{
	Bullet *b;

	b = spawnBullet(self);
	b->texture = bulletTexture;
	b->x = self->x + (self->texture->rect.w / 2) - (bulletTexture->rect.w / 2);
	b->y = self->y + self->texture->rect.h;
	b->dy = 10;
}

Again, the same as for the other aliens. Common functions like this could be moved into a file called aliens.c and referenced from the individual aliens, in order to cut down on such repeat code. Something to keep in mind for future!

That's our RandomAlien fully defined. Fitting him into the rest of the game is rather simple, as we need only update wave.c. The nextWave function is updated by adding to the existing switch statement:


void nextWave(void)
{
	if (!setupNextWave)
	{
		setupNextWave = 1;

		waveStartTimer = FPS;

		stage.waveNum++;
	}
	else
	{
		waveStartTimer = MAX(waveStartTimer - app.deltaTime, 0);

		if (waveStartTimer <= 0)
		{
			srand(waveSeed);

			switch (rand() % 4)
			{
				case 0:
					addSwingingAliens();
					break;

				case 1:
					addSwoopingAliens();
					break;

				case 2:
					addStraightAliens();
					break;

				case 3:
					addRandomAliens();
					break;

				default:
					break;
			}

			waveSeed = rand() % 99999;

			setupNextWave = 0;

			srand(time(NULL));
		}
	}
}

We've added a new function called addRandomAliens. As we now have 4 functions for creating our aliens, we're increasing our switch's random from 3 to 4, and adding addRandomAliens as case 3.

The addRandomAliens function itself looks to have quite a lot going on, but it's much simpler than it appears:


static void addRandomAliens(void)
{
	int i, n, x, y, delay, smooth, numWaypoints;
	SDL_Point waypoints[MAX_WAYPOINTS];

	n = 7 + rand() % 6;
	delay = 15 + rand() % 35;
	numWaypoints = 2 + rand() % 4;
	smooth = rand() % 2;

	switch (rand() % 3)
	{
		case 0:
			x = SCREEN_WIDTH / 2;
			y = -200;
			break;

		case 1:
			x = -200;
			y = SCREEN_HEIGHT / 4;
			break;

		case 2:
			x = SCREEN_WIDTH + 200;
			y = SCREEN_HEIGHT / 4;
			break;

		default:
			break;
	}

	for (i = 0 ; i < numWaypoints ; i++)
	{
		waypoints[i].x = 100 + (rand() % (SCREEN_WIDTH - 200));
		waypoints[i].y = 100 + (rand() % (SCREEN_HEIGHT - 200));
	}

	waypoints[numWaypoints - 1].x = x;
	waypoints[numWaypoints - 1].y = y;

	for (i = 0 ; i < n ; i++)
	{
		initRandomAlien(i * delay, x, y, waypoints, numWaypoints, smooth);
	}

	stage.numWaveAliens = n;
}

We're setting a load of variables, like we do for when creating other alien waves. `n` is the number of aliens to create, while `delay` is the start delay between each alien. Next, we're setting a variable called numWaypoints to a random of 2 - 5. This governs how many waypoints we want our RandomAliens to travel between. There's then a 50-50 chance that a variable called `smooth` will be set to 1. This will be used to determine whether our aliens follow a rigid or smooth path to their waypoints.

We're then randomly selecting the alien's start position, by performing a switch against a random of 3. If the result is 0, the aliens will start at the top of the screen. If 1, they will start off at the left-hand side of the screen. If 2, the aliens will start off at the right-hand side of the screen.

After this, we're creating the waypoints. Based on the number of waypoints we chose earlier (numWaypoints) we're filling that number in our array with random x and y values, using a for-loop. Notice how after the for-loop is complete, we're setting the final entry in our waypoint array (numWaypoints - 1) to the starting position. This means that the RandomAlien will always move back offscreen once their pattern is complete.

Finally, we're using a for-loop to create all our RandomAliens (up to `n`), calling initRandomAlien and feeding in all the variables we've so far defined. Stage's numWaveAliens is then set to the number of aliens we created.

Wow! Our game is more or less done. We've got all our aliens waves, power-ups, and bosses. All that remains is to make some gameplay tweaks, then create a title screen, a highscore table, add in some configuration options, and throw in some sound and music. We'll be doing all this in our final step, as the finishing touches.

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