« Back to tutorial listing

— 2D Santa game —
Part 12: Enchanted Snowman #2

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

Introduction

Having added in our first enchanted snowman, we're ready to add in the second. The first snowman tossed a snowball up into the air, while this second one is going to toss his head! Not only that, but the fiend is going to be shooting magic carrots at Santa, in a bid to disrupt Christmas. Just as with the snowball, a hit will result in an immediate game over.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa12 to run the code. Press Space to play. Use the same controls as before. As you play, enchanted snowmen will appear, tossing snowballs into the air, as well as their own heads, shooting carrots at you. Avoid the snowballs, heads, and carrots, and continue to deliver gifts and collect sacks. The game will continue for as long as you are able to maintain your Xmas Spirit (and don't crash into a house or other hazard). When you're finished, close the window to exit.

Inspecting the code

Adding in this new "enemy" is extremely easy, as they are similiar to the previous one in several aspects! In fact, you're going to feel a sense of déjà vu as we go through the code for our second enchanted snowman. Let's dive in.

Starting with defs.h:


enum
{
	// snipped

	ET_HEADLESS_SNOWMAN,
	ET_SNOWMAN_HEAD,
	ET_CARROT
};

We've added in three new entity types: ET_HEADLESS_SNOWMAN, to define the headless snowman himself; ET_SNOWMAN_HEAD, to define the headless snowman's head; and ET_CARROT, the type that will be used for the carrot that is fired out.

Over next to structs.h, where we've added in one new struct:


typedef struct
{
	double thinkTime;
	int    startY;
	double carrotTimer;
} SnowmanHead;

SnowmanHead is similar to the Snowball we created in the last part, but with an additional field. `carrotTimer` is a control variable that will set how often the snowman's head can fire out a new carrot. The other fields operate the same way as the snowball, as we'll see.

We've added in three new compilation units to this part, that we'll attempt to work through in a logical order. First up is headlessSnowman.c:


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

	if (texture == NULL)
	{
		texture = getAtlasImage("gfx/headlessSnowman.png", 1);
	}

	x = SCREEN_WIDTH + 200 + rand() % 100;
	y = GROUND_Y - texture->rect.h;

	if (canAddEntity(x, y, texture->rect.w, texture->rect.h))
	{
		initSnowmanHead(x + 5, y - 16);

		e = spawnEntity();
		e->type = ET_HEADLESS_SNOWMAN;
		e->x = x;
		e->y = y;
		e->texture = texture;

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

Very similar to our original snowman's init function, so we'll only cover this briefly. We're yet again ensuring we have room to add the snowman before doing so (via canAddEntity). Once confirmed, the only difference compared to the previous setup is that we're calling initSnowmanHead, passing over the `x` and `y` positions we want, as well as some minor adjustments to place the head correctly. We'll come to the initSnowmanHead function in a little bit. For our main entity, we're setting the `x`, `y`, `texture`, `tick`, `draw`, and `touch` functions.

Before that, let's consider the other functions. Starting with `tick`:


static void tick(Entity *self)
{
	self->x -= stage.speed * app.deltaTime;

	self->dead = self->x < -self->texture->rect.w;
}

We're moving the snowman to the left, removing it when it goes offscreen. `draw` is up next:


static void draw(Entity *self)
{
	blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);
}

We're just rendering the snowman, using its `texture`. Finally, we have `touch`:


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

Once more, we're checking if the player has collided with the snowman, and calling killPlayer.

As you can see, other than the init function, the Headless Snowman is very much the same as the original Snowman. Let's now look at snowmanHead.c, the compilation unit that will control the bouncing of the head. Once again, you will find parts of this very familiar. Starting with initSnowmanHead:


void initSnowmanHead(int x, int y)
{
	Entity      *e;
	SnowmanHead *s;

	if (texture == NULL)
	{
		texture = getAtlasImage("gfx/snowmanHead.png", 1);
	}

	s = malloc(sizeof(SnowmanHead));
	memset(s, 0, sizeof(SnowmanHead));
	s->thinkTime = FPS;
	s->startY = y;
	s->carrotTimer = rand() % (int)FPS * 3;

	e = spawnEntity();
	e->type = ET_HEADLESS_SNOWMAN;
	e->x = x;
	e->y = y;
	e->texture = texture;
	e->tick = tick;
	e->draw = draw;
	e->touch = touch;

	e->data = s;
}

As with our snowball, this function takes two arguments - `x` and `y`, that will be used to determine where the head is initially positioned. We create a SnowmanHead, setting its thinkTime to 1 second (FPS), and also set its `startY` to the value of `y`. We're also setting the carrotTimer to a random value between 0 and 3 seconds. We're then setting all the usual pieces of data for the entity, such as the `x`, `y`, `texture`, `tick`, `draw`, and `touch`.

Now to `tick`, where things actually get a bit more interesting:


static void tick(Entity *self)
{
	SnowmanHead *s;

	s = (SnowmanHead *)self->data;

	if (s->thinkTime > 0)
	{
		s->thinkTime = MAX(s->thinkTime - app.deltaTime, 0);

		if (s->thinkTime == 0)
		{
			self->dy = -(12 + rand() % 5);

			s->carrotTimer = rand() % (int)FPS * 3;
		}
	}
	else
	{
		self->dy += 0.2 * app.deltaTime;

		self->y += self->dy * app.deltaTime;

		s->carrotTimer -= app.deltaTime;

		if (s->carrotTimer <= 0)
		{
			initCarrot(self->x, self->y);

			s->carrotTimer = rand() % (int)FPS;
		}

		if (self->y > s->startY)
		{
			self->y = s->startY;

			s->thinkTime = FPS + rand() % (int)FPS;
		}
	}

	self->x -= stage.speed * app.deltaTime;

	if (self->x < -self->texture->rect.w)
	{
		self->dead = 1;
	}
}

A little bit more interesting, yes, but this is actually rather similar to the `tick` function in snowball.c. Our snowman head is bound by the same jumping logic, where it will rise up into the air by a random velocity if its thinkTime is 0, before returning back to its starting point (`startY`) as gravity takes hold. The major difference here, however, is that while in the air (thinkTime > 0), we are decreasing the value of carrotTimer. If it hits 0 or less, we're going to call initCarrot, passing over the head's `x` and `y` as the position we want to spawn the carrot at. With that done, we're reset carrotTimer to a random value of FPS (1 second). This means that while airborne our head can issue quite a number of carrots! The carrotTimer resets to longer when it lands back on the ground (up to 3 seconds).

As with all other entities in the game, the head is moved to the left as its `x` decreases by the value of Stage's `speed`, being removed from the game once it goes off the edge of the screen.

We'll briefly look at the `draw` and `touch` functions before we move on from snowman.c. First `draw`:


static void draw(Entity *self)
{
	blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);
}

We're just rendering the entity using its `texture`. `touch` follows:


static void touch(Entity *self, Entity *other)
{
	if (other == stage.player)
	{
		killPlayer(self->x, self->y);

		self->dead = 1;
	}
}

We're checking if the player has hit the head, and calling killPlayer if so (and passing over the head's `x` and `y`). As with the snowball, we're also removing the head, by setting its `dead` flag to 1.

As you can see, the snowman head behaves very much like the snowball, except that it has the ability to shoot a volley of carrots!

Let's now move over to carrot.c, where we're defining all our carrot entity functions. First to initCarrot:


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

	if (texture == NULL)
	{
		texture = getAtlasImage("gfx/carrot.png", 1);
	}

	e = spawnEntity();
	e->type = ET_CARROT;
	e->x = x;
	e->y = y;
	e->texture = texture;
	e->tick = tick;
	e->draw = draw;
	e->touch = touch;
}

We're building our carrot entity, setting its attributes (and using the `x` and `y` that were passed into the function). All good, let's move on to `tick`:


static void tick(Entity *self)
{
	self->x -= (stage.speed + 7) * app.deltaTime;

	if (self->x < -self->texture->rect.w)
	{
		self->dead = 1;
	}
}

Here, we're decreasing the carrot's `x` according to the `speed` of Stage, plus a little extra (7) to make it move faster than the scene itself. Note that the carrots only ever fly from right to left; we never attempt to shoot the player in the back! Otherwise, once the carrot moves the left of the screen, we're removing it.

`draw` follows:


static void draw(Entity *self)
{
	blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);
}

We're simply rendering the carrot, using its `texture`.

And finally, we have `touch`:


static void touch(Entity *self, Entity *other)
{
	if (other == stage.player)
	{
		killPlayer(self->x, self->y);

		self->dead = 1;
	}
}

As expected, we're testing if the carrot has come into contact with the player, and calling killPlayer. As with the snowball and snowman head, we're passing over the carrot's `x` and `y` values, so that the explosion texture on the player appears where the carrot struck the sleigh.

That's all for carrot.c. With our Headless Snowman, his head, and the carrot all done, we need only update stage.c to put him into the game. So, let's head over there now, and update addObject:


static void addObject(void)
{
	int n;

	objectSpawnTimer -= stage.speed * app.deltaTime;

	if (objectSpawnTimer <= 0)
	{
		n = rand() % 100;

		// snipped

		else if (n < 65)
		{
			initHeadlessSnowman();
		}

		objectSpawnTimer = FPS * 5 + ((int)FPS * rand() % 5);
	}
}

We've added an extra if check here for `n`. Now, if `n` is less than 65, we'll be calling initHeadlessSnowman. So, there's now a chance that we'll add a gift sack, a coal sack, a regular snowman, and a headless snowman. Any other value of `n` will result in nothing happening.

We're done! We've added in all our collectables and hazards now, making our game more or less complete. We can score points, earn a place on the highscore table, and all the rest.

It could be prettier though, right? I mean, shouldn't it be snowing? A nice wintery scene would be the icing on the cake. So, in the next few parts we're going to enter the prettification phase, where we make our whole game a bit nicer to look at. We'll start with some gentle snowfall.

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

Desktop site