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


A North-South Divide

For over a hundred years, messenger Duncan has wandered the world, searching for the missing pieces of an amulet that will rid him of his curse; a curse that has burdened him with an extreme intolerance of the cold, an unnaturally long life, and the despair of watching all he knew and loved become lost to the ravages of time. But now, Duncan is close to the end of his long quest.

Click here to learn more and read an extract!

« Back to tutorial listing

— Making a 2D split screen game —
Part 14: Aliens3

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

Introduction

Where is our game set? I actually have no idea! It just takes place across some "zones". Well, we're apparently in space, because our players aren't affected by gravity. In that case, why don't we add in some aliens, just for fun? They can get in the way of the player, allow the player to score some extra points, and also even kill the player. Well, in this part, we'll add in some aliens who will do all those things.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./versus14 to run the code. You will see a window open like the one above, with each player on either side of our zone. Use the default controls (or, at your option, copy the config.json file from a previous tutorial, to use that - remember to exit the game before copying the replacement file). Play the game as normal. Notice there are three aliens in the world, a purple one and an orange one, that bounce up and down, left and right; and a red one that moves around randomly, firing. Making contact with any of the aliens kills both the alien and the player, although both will respawn. Shooting and killing the aliens will earn the player points. Once you're finished, close the window to exit.

Inspecting the code

Adding in our aliens is very easy. As with the Pods, the Aliens are entities, and we already have everything we need to handle those (as well as bullets). Therefore, the bulk of this update will be concentrated on a file called aliens.c, where we'll be defining all our alien behaviour.

Before that, let's first look at structs.h, where we've created a new struct to hold the data about our Alien:


typedef struct
{
	int     type;
	double  reload;
	uint8_t numShotsToFire;
	double  thinkTime;
	double  health;
} Alien;

`type` is the type of alien this is. `reload` is the delay between firing bullets, much like the players. numShotsToFire is the number of shots that will be fired, per burst. thinkTime is a timer for how long the alien will continue with its current action before changing (mainly changing direction). `health` is the amount of health the alien has.

With our Alien defined, we can move over to aliens.c, the new file we've created to handle our aliens. There's quite a number of functions here, but all should be quite easy to understand.

First up, we've created an enum, to handle our aliens:


enum
{
	AT_RED,
	AT_ORANGE,
	AT_PURPLE
};

AT is short for "Alien Type".

Now for our first function, initRedAlien:


void initRedAlien(int x, int y)
{
	Entity *e;

	if (redAlienModel == NULL)
	{
		redAlienModel = loadModel("data/models/redAlien");
	}

	e = initAlien(x, y, AT_RED);
	e->model = redAlienModel;

	changeDirection(e);
}

This function creates a new Red Alien, at the specified `x` and `y` location. We load the alien's model (redAlienModel) if we haven't already done so, and then make a call to a function named initAlien, passing over the `x`, `y`, and AT_RED. The initAlien function is a shared function that will setup all our alien data. We'll come to this in a bit. With our alien setup, we set its `model`, and then call a function named changeDirection, that will cause our Red Alien to choose a random direction to move in. We'll see this shortly.

Next up, we have initOrangeAlien:


void initOrangeAlien(int x, int y)
{
	Entity *e;

	if (orangeAlienModel == NULL)
	{
		orangeAlienModel = loadModel("data/models/orangeAlien");
	}

	e = initAlien(x, y, AT_ORANGE);
	e->model = orangeAlienModel;
	e->dir.x = rand() % 2 == 0 ? -SPEED : SPEED;
}

This sets up our Orange alien. As you can see, it's very similar to our Red Alien. The main difference is that we're randomly settings its horizontal direction, make it either move left or right.

Finally, we have the init function for our Purple alien:


void initPurpleAlien(int x, int y)
{
	Entity *e;

	if (purpleAlienModel == NULL)
	{
		purpleAlienModel = loadModel("data/models/purpleAlien");
	}

	e = initAlien(x, y, AT_PURPLE);
	e->model = purpleAlienModel;
	e->dir.y = rand() % 2 == 0 ? -SPEED : SPEED;
}

This is, again, similar to the Orange alien, except that when we're finished, we'll make the alien move randomly up or down.

Now for the initAlien function itself:


static Entity *initAlien(int x, int y, int type)
{
	Entity *e;
	Alien  *a;

	a = malloc(sizeof(Alien));
	memset(a, 0, sizeof(Alien));

	a->type = type;
	a->health = INITIAL_HEALTH;
	a->thinkTime = FPS * (1 + rand() % 3);

	e = spawnEntity(ET_ALIEN);
	e->position.x = x;
	e->position.y = y;
	e->angle = rand() % 360;
	e->respawnInterval = (5 + rand() % 11) * FPS;
	e->respawnPosition = e->position;
	e->radius = 24;
	e->data = a;

	e->tick = tick;
	e->draw = draw;
	e->touch = touch;
	e->takeDamage = takeDamage;
	e->die = die;
	e->respawn = respawn;

	return e;
}

It should be quite clear what's going on here - we're setting up an Alien, much like as we do with Pods and Players. We're using the `x` and `y` variables we pass into the function to set its position, and `type` to set its `type`. Our aliens have a respawn interval of between 5 and 15 seconds, so they cannot be permanently killed! Other than that, much of this we've seen before. Note that we set the type of Alien using the type passed into the function.

Now for the `tick` function, that is a little more interesting:


static void tick(Entity *self)
{
	Alien *a;

	a = (Alien *)self->data;

	switch (a->type)
	{
		case AT_RED:
			a->thinkTime -= app.deltaTime;

			move(self);

			fire(self, a);

			if (a->thinkTime <= 0)
			{
				changeDirection(self);

				a->numShotsToFire = 12 + rand() % 13;

				a->thinkTime = FPS * (2 + rand() % 3);
			}
			break;

		case AT_PURPLE:
		case AT_ORANGE:
			move(self);
			break;

		default:
			break;
	}
}

As you will have seen, the behaviour of the alien depends on its `type`. We test this, and respond accordingly. If this is a Red alien, we'll reduce its thinkTime, and then call the `move` and `fire` functions. We'll then check if it's thinkTime is 0 or less, and call changeDirection. We'll also prepare another burst of shots to fire, setting numShotsToFire to between 12 and 24, and then randomly set its thinkTime to between 2 and 4 seconds.

If this is a Purple or Orange alien, we'll call `move`.

Quite the contrast! Red aliens are a bit more complicated than the other two, who simply stick to just moving back and forth.

Let's look at `move` now:


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

	if (self->position.x < zone.bounds.x || self->position.y < zone.bounds.y || self->position.x >= zone.bounds.w || self->position.y >= zone.bounds.h)
	{
		self->position.x -= self->dir.x * app.deltaTime;
		self->position.y -= self->dir.y * app.deltaTime;

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

	self->angle += (self->dir.x + self->dir.y) * app.deltaTime;
}

As expected, when an alien moves, we're going to update its `position` according to its `dir`. Next, we test if it has hit the bounds of our Zone. If so, we'll reverse its direction, so that it bounces back the way it has come. We're also updating its `angle` based on its current direction (dir's `x` and `y` will never both be a non-zero value), so that it will spin clockwise or anti-clockwise.

Next up, we have `fire`. There should be few surprises here:


static void fire(Entity *self, Alien *a)
{
	a->reload -= app.deltaTime;

	if (a->reload <= 0 && a->numShotsToFire > 0)
	{
		fireAlienBullet(self);

		a->reload = FPS / 12;

		a->numShotsToFire--;
	}
}

We're decreasing the alien's `reload`. Once it hits 0 or less, and we have shots left to fire in our current volley, we'll call fireAlienBullet (defined in bullets.c). We then set its `reload` back to 16th of a second, and decrement numShotsToFire. All in all, this means that our alien will fire a set volley of shots within a short interval.

We'll look at the fireAlienBullet towards the end, when we get to bullets.c. For now, we'll look at changeDirection next:


static void changeDirection(Entity *self)
{
	self->dir.x = 0;
	self->dir.y = 0;

	switch (rand() % 4)
	{
		case 0:
			self->dir.x = SPEED;
			break;

		case 1:
			self->dir.x = -SPEED;
			break;

		case 2:
			self->dir.y = SPEED;
			break;

		case 3:
			self->dir.y = -SPEED;
			break;

		default:
			break;
	}
}

As expected, this function changes an Alien's direction (really, just the Red aliens do this). We start by setting both dir's `x` and `y` values to 0, and then randomly select a new direction to move in; we'll either move up, down, left, or right, depending on our random of 4. The speed of movement will use the defined SPEED value (2). That's really all there is to it!

Our `draw` function comes next:


static void draw(Entity *self, SDL_FPoint *camera)
{
	SDL_FPoint drawPosition;

	drawPosition = self->position;
	drawPosition.x -= camera->x;
	drawPosition.y -= camera->y;

	drawModel(self->model, drawPosition, self->angle);
}

Much like our Players and Pods, we're setting up the rendering position (as drawPosition), taking the camera into account, and calling drawModel. We've seen this before, so let's move on to the `touch` function:


static void touch(Entity *self, Entity *other)
{
	switch (other->type)
	{
		case ET_NONE:
			self->position.x -= self->dir.x * app.deltaTime;
			self->position.y -= self->dir.y * app.deltaTime;
			self->dir.x = -self->dir.x;
			self->dir.y = -self->dir.y;
			break;

		case ET_PLAYER:
			other->takeDamage(other, MAX_PLAYER_HEALTH, self);
			self->dead = 1;
			break;

		default:
			break;
	}
}

As with other `touch` functions, we check what has touched the Alien. If it's the world (ET_NONE), the Alien is going to reverse direction, bouncing off the world. If it's a Player, we're going to call the Player's takeDamage function, passing over MAX_PLAYER_HEALTH as the amount of damage to deal. This will effectively kill the player, unless they have a shield. At the same time, we'll flag the alien itself as `dead`.

All nice and simple. Now for the takeDamage function:


static void takeDamage(Entity *self, double amount, Entity *attacker)
{
	Alien *a;

	a = (Alien *)self->data;

	if (!self->dead)
	{
		a->health -= amount;

		if (a->health <= 0)
		{
			self->dead = 1;

			if (attacker->type == ET_PLAYER)
			{
				switch (a->type)
				{
					case AT_RED:
						((Player *)attacker->data)->score += 100;
						break;

					case AT_ORANGE:
					case AT_PURPLE:
						((Player *)attacker->data)->score += 50;
						break;

					default:
						break;
				}
			}
		}
	}
}

Essentially, this applies the damage to the alien, and flags it as `dead` if its `health` falls below 0. Note that we're first checking the Alien isn't already `dead` before subtracting the `amount`. The reason for this is because if the `attacker` is a Player, we'll want to award that Player some points. We want to make sure this only happens once (note that Alien bullets can injure other aliens, so it might not always be a player that inflicts the damage!).

Our Red alien is worth 100 points, while the other two are only worth 50. Our Red aliens are more dangerous, so are worth more.

Over to `die`:


static void die(Entity *self)
{
	Alien *a;

	a = (Alien *)self->data;

	switch (a->type)
	{
		case AT_RED:
			addDeathParticles(self->position, 255, 64, 64);
			break;

		case AT_ORANGE:
			addDeathParticles(self->position, 255, 128, 0);
			break;

		case AT_PURPLE:
			addDeathParticles(self->position, 255, 0, 255);
			break;

		default:
			break;
	}
}

As with the players, we're calling addDeathParticles. We'll colour the particles according to the `type` of alien this is.

The last function to look at is `respawn`:


static void respawn(Entity *self)
{
	Alien *a;

	a = (Alien *)self->data;

	a->reload = 0 ;
	a->numShotsToFire = 0;
	a->thinkTime = FPS * (1 + rand() % 3);
	a->health = INITIAL_HEALTH;

	self->respawnInterval = (5 + rand() % 11) * FPS;

	if (a->type == AT_RED)
	{
		changeDirection(self);
	}
}

When we respawn an Alien, we reset all its attributes, just as we do with the Player. We also randomly set its respawnInterval, so that the respawning can be a little less predictable. If this is a Red alien, we'll also call changeDirection, to make it choose a new movement direction.

That's all for aliens.c. As you can see, nothing was difficult, there was just a lot of it. We just have to do a few extra things before we finish up, so let's turn to bullets.c, where we've made some tweaks.

First, we've updated initBullets:


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

	if (playerBulletModels[0] == NULL)
	{
		playerBulletModels[0] = loadModel("data/models/player1Bullet");
		playerBulletModels[1] = loadModel("data/models/player2Bullet");
		rocketModel = loadModel("data/models/rocket");
		alienBulletModel = loadModel("data/models/alienBullet");
	}
}

Since our Red aliens can fire bullets, we're loading a model for them to use. alienBulletModel will hold this model.

Next, we come to the new fireAlienBullet function that is called by the Red aliens when they fire:


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

	b = spawnBullet();
	b->position = e->position;
	b->dir.x = 10 * sin(TO_RAIDANS(e->angle));
	b->dir.y = 10 * -cos(TO_RAIDANS(e->angle));
	b->damage = 20;
	b->health = FPS * 5;
	b->radius = 3;
	b->angle = e->angle;
	b->owner = e;
	b->model = alienBulletModel;
	b->die = bulletDie;
}

This function is very much like the firing functions used by the player. The bullet issues from the middle of the alien, and will move in the direction of their current `angle`. As our Red alien is constantly spinning, this mean that the bullets are sprayed out in all directions. Originally, I was going to make the aliens aim directly at the player, but thought it would be more interesting to create this spray effect. One could add in a Yellow alien, perhaps, that would target the player.

Finally, we just need to head over to zone.c, and update initZone:


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

	// snipped

	initPlayer(0, 900, 2100);
	initPlayer(1, 2460, 3470);

	initRedAlien(2300, 3000);
	initOrangeAlien(1400, 3200);
	initPurpleAlien(1800, 3400);

	// snipped

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

We're calling initRedAlien, initOrangeAlien, and initPurpleAlien here. Once again, these positions are hard coded, for the purposes of the demonstration.

All done! We now have some aliens that can act as obstacles to the player, and shake things up a bit. They won't be present in all our Zones in our final game, but when they are, they force the player to be a little more considerate of their environment.

With our game now mostly setup, we should look into returning to our Zones. Our player starts are currently hard-coded, which is not what we want to do going forward. We'll therefore want to load our entities from JSON files. At the same time, we'll introduce a proper game loop, randomly moving from one zone to the next each time a match is concluded.

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