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
SDL 1 tutorials (outdated)

Latest Updates

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

The Legend of Edgar 1.36
Sun, 1st January 2023

SDL2 map editor tutorial [UPDATED]
Sat, 10th September 2022

All Updates »

Tags

android (3)
battle-for-the-solar-system (10)
blob-wars (10)
brexit (1)
code (6)
edgar (9)
games (41)
lasagne-monsters (1)
making-of (5)
match3 (1)
numberblocksonline (1)
orb (2)
site (1)
tanx (4)
three-guys (3)
three-guys-apocalypse (3)
tutorials (15)
water-closet (4)

Books


Firmware

HF-Tech's chips have changed the world. Embedded into the heads of over 90% of the world's population, they have cured autism, dementia, provided intelligence boosts, and helped to ease some of the more mundane tasks in life. Daniel Blair, hacker and Workshop member, however is not convinced that everything is as rosy as it seems. But is he looking in all the wrong places..?

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Santa game —
Part 11: Enchanted Snowman #1

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

Introduction

Santa's night is not going as well as he'd planned - his reindeer are all laid up in bed, and he's forgotten to bring all the gifts and coal with him on his delivery run. His elves are helping out with their magic, but it's not exactly working as expected, and now the nearby snowmen are hurling snowballs at him. The snowballs will instantly destroy the sleigh if they make contact (because they're magic), so they are best avoided. The snowman and snowball here will represent the first of our two planned hazards.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa11 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. Avoid the snowballs 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). When you're finished, close the window to exit.

Inspecting the code

Adding in our snowmen is a simple task, since both the snowman and the snowball are nothing more than entities.

Starting with defs.h:


enum
{
	// snipped

	ET_SNOWMAN,
	ET_SNOWBALL
};

We've added in ET_SNOWMAN and ET_SNOWBALL, to represent the types for a snowman and a snowball, respectively.

Next up is an update to structs.h:


typedef struct
{
	double thinkTime;
	int    startY;
} Snowball;

We've added in a struct to define our snowball. `thinkTime` is used to control the delay between the ball hopping up into the air, while `startY` stores the starting point on the y axis. We don't have a struct for the snowman, since he is simply an entity.

Speaking of the snowman, let's look at him next. All our snowman's logic and rendering live, predictably, in snowman.c. It contains the standard set of functions, so let's start with initSnowman:


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

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

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

	if (canAddEntity(x, y, texture->rect.w, texture->rect.h))
	{
		initSnowball(x - 10, y - 2);

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

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

We start by loading the texture we want to use, before then setting up the snowman's position. We choose an `x` position between 200 and 300 pixels beyond the right-hand side of the screen, and a `y` position with the bottom of the snowman (according to its texture) on the ground (GROUND_Y). We then use canAddEntity, the function we saw when originally adding in houses, to see if we can place the snowman in the desired spot, before doing so. We don't want our snowman to be behind houses or other things; he should be quite visible, so the player can spot him, and know to avoid him.

With our position decided upon, we next call initSnowball. Just like our chimneys that are created by our houses, our snowballs are created by our snowmen. Our snowballs are not a part of our snowmen, however, being funtionally independent entities. We call initSnowball, passing over our `x` and `y` positions to the function, with some adjustments so that the snowball is in the hand of the snowman. We'll see initSnowball in a little bit. For now, we continue to create our snowman, setting up all the regular fields: `x`, `y`, `type`, `texture`, `tick`, `draw`, and `touch`.

You'll find that the rest of the snowman is very standard. Starting with `tick`:


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

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

We move the snowman according to the speed of our Stage, and remove it once it has left the right-hand side of the screen.

The `draw` function follows:


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);
	}
}

Nothing more than a simple check to see if the player hits the snowman. If they do, we'll call killPlayer.

That's all for snowman.c. As you can see, our snowman really is just a static entity that creates a snowball at the same time as itself.

Now over to snowball.c, where we setup and handle our snowball. Compared to the snowman, things are a little more interesting. We'll start with initSnowball:


void initSnowball(int x, int y)
{
	Entity   *e;
	Snowball *s;

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

	s = malloc(sizeof(Snowball));
	memset(s, 0, sizeof(Snowball));
	s->thinkTime = FPS;
	s->startY = y;

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

	e->data = s;
}

We're creating Snowball data here (`s`), to go with our snowball entity (`e`). We're setting the snowball's thinkTime to one second, and it's `startY` to the value of `y` that we're passing into the function. We then set the usual `x`, `y`, `type`, `texture`, `tick`, `draw`, and `touch`.

The `tick` function comes next, and is where we drive the snowball's behaviour:


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

	s = (Snowball *)self->data;

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

		if (s->thinkTime == 0)
		{
			self->dy = -(12 + rand() % 5);
		}
	}
	else
	{
		self->dy += 0.2 * app.deltaTime;

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

		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;
	}
}

The snowman, being a static entity, is not responsible for tossing the snowball in the air. Instead, the snowball itself jumps up. We start by checking if the snowball's thinkTime is greater than 0, and decreasing it if so. If it falls to 0 or less, we're setting its `dy` to a value between -16 and -12. In effect, once the snowball's thinkTime expires, the snowball will hop up into the air, at a random velocity. Essentially, when our snowball's thinkTime is greater than 0, we consider it to be at rest, in the snowman's hand.

We see this next, in the else clause - the snowball is now considered to be airborne, and so we apply gravity to it. We increase the value of its `dy`, and add that to its `y`. At some point, the snowball will start to return to the ground. We test to see if the snowball has therefore gone past its starting point (`y` > `startY`), and if so clamp its `y` to `startY`, and set its thinkTime to a value between 1 and 2 seconds.

All in all, this means that the snowball will hop up into the air, come down, pause a moment, and then hop back up again. The effect will be as though the snowman is tossing the ball into the air, since it will always land back in his hand for a moment. Of course, the ball is just bouncing up and down by itself. The snowman has nothing to do with it.

Finally, we move the snowball left at the same speed as Stage, and set its `dead` flag if it goes off screen.

That's the main logic done, so we'll look at the remaining functions. Starting with `draw`:


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

We're just rendering the snowball, using its `texture`. Next up is `touch`:


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

		self->dead = 1;
	}
}

As one might expect, we're checking if the snowball has touched the player, and calling killPlayer if so. Notice that we're passing over the snowball's x and y values, so that the hit texture that appears on the player is located where the snowball made contact. This looks a little nicer than centering the explosion on the player. Finally, we're setting the snowball's `dead` flag to 1, so it, too, is removed from the game.

That's it for snowman.c and snowball.c. We've only got one more thing to do, and our first snowman is ready for action. Heading over to stage.c, where we've updated addObject:


static void addObject(void)
{
	int n;

	objectSpawnTimer -= stage.speed * app.deltaTime;

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

		// snipped

		else if (n < 50)
		{
			initSnowman();
		}

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

We've added in an extra check to our random spawning. If `n` is less than 50, we're calling initSnowman. This means there's now a chance that we'll add a gift sack, a coal sack, or a snowman.

Excellent! That was nice and simple, eh? Adding in this simple hazard has made our game a little more challenging, so we don't just need to collect sacks and aim at chimneys.

We did say that there were two types of enchanted snowman, however. Therefore, in the next part we'll add our second snowman, one that will make things a bit trickier than before.

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