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


Alysha

When her village is attacked and her friends and family are taken away to be sold as slaves, Alysha Tanner sets out on a quest across the world to track them down and return them home. Along the way, she is aided by the most unlikely of allies - the world's last remaining dragon.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Santa game —
Part 10: Sacks

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

Introduction

Up until now, we've had no means of increasing our supplies of gifts and coal. This has meant that for the past couple of parts we cannot help but fail the game once we run out of gifts. In this part we're changing that all, by allowing the player to collect sacks containing gifts and coal, that will restock our supply. This will allow the game to keep going for far longer than before.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa10 to run the code. Press Space to play. Use the same controls as before. As you play, coal and gift sacks will appear in the air. Fly into them to collect them and increase your supplies. 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 sacks is a very simple thing indeed; they're just entities that we spawn at random intervals and can be collided with.

First, to defs.h:


enum
{
	// snipped

	ET_GIFT_SACK,
	ET_COAL_SACK
};

We're defining two new entity types: ET_GIFT_SACK, to represent a sack containing gifts, and ET_COAL_SACK, to represent a sack containing coal.

Next, to structs.h:


typedef struct
{
	int    startY;
	double bob;
	double speed;
} Sack;

We've created a new struct called Sack, to hold the data about one of our sack entities. `startY` is the vertical starting position of the sack. `bob` is the value of the bobbing amount of the sack, used to control the rate at which it rises and falls; and `speed` is the horizontal speed at which the sack moves across the screen. We're giving them random speeds to keep things interesting.

Now to sack.c, the new compilation unit we've made to handle our sack logic and rendering. Once again, there won't be anything here that you will find taxing; it will be as straightforward as already described. Starting with initSack:


void initSack(int type)
{
	Entity *e;
	Sack   *s;

	if (giftSackTexture == NULL)
	{
		giftSackTexture = getAtlasImage("gfx/giftSack.png", 1);
		coalSackTexture = getAtlasImage("gfx/coalSack.png", 1);
	}

	s = malloc(sizeof(Sack));
	memset(s, 0, sizeof(Sack));
	s->speed = (5.0 + rand() % 10) * 0.1;
	s->startY = 50 + rand() % (SCREEN_HEIGHT / 2);

	e = spawnEntity();
	e->type = type;
	e->x = SCREEN_WIDTH / 2 + (rand() % SCREEN_WIDTH * 0.35);
	e->y = s->startY;
	e->data = s;

	if (type == ET_GIFT_SACK)
	{
		e->texture = giftSackTexture;
	}
	else
	{
		e->texture = coalSackTexture;
	}

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

A standard entity spawning routine. The initSack function takes a parameter called `type`, that is the type of sack we want to make (we're assuming here we're only ever going to pass in ET_GIFT_SACK or ET_COAL_SACK - if we wanted to make this safer, we could have seperate functions calling initGiftSack and initCoalSack that would delegate to this function).

We load our textures, and then create a Sack (`s`). The `speed` to set to a random between 0.5 and 1.4, while the startY is somewhere around the upper half of the screen. The entity itself is then created, with its `x` position being somewhere towards the right-hand side of the screen (a minimum of halfway through). The appropriate texture is assigned, depending on whether this is a gift or a coal sack, as well as the `tick`, `draw`, and `touch` functions. Again, nothing surprising.

Our `tick` function follows:


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

	s = (Sack *)self->data;

	s->bob += 0.1 * app.deltaTime;

	self->y = s->startY + (16 * sin(s->bob));

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

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

Pretty standard. We're first increasing the value of sack's `bob` by a small amount, and then setting the entity's (`self`) `y` to the sack's startY, plus the sin of its `bob`, multiplied by 16. In effect, this will make it move up and down a little, about the `y` position where it was first created. We're next decreasing the sacks's `x` value according to both its speed and Stage's `speed`, so that it moves from right to left. Again, this means our sacks will move at various different speeds. Finally, we check if the sack has moved off the left-hand side of the screen, and setting its `dead` flag to 1 if so, so that our entity processing loop removes it.

The `draw` function is up next:


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`.

Now for `touch`, the most essential part of the sack's handling:


static void touch(Entity *self, Entity *other)
{
	if (other == stage.player)
	{
		if (self->type == ET_GIFT_SACK)
		{
			stage.numGifts += 5 + rand() % 5;
		}
		else
		{
			stage.numCoal += 5 + rand() % 5;
		}

		self->dead = 1;
	}
}

We're doing just what one would expect - we're testing if the thing that has touched the sack is the player, and if so we're going to increase the value of Stage's numGifts or numCoal depending on whether this is a gift or coal sack. We're increasing the amount by a random value of between 5 and 9. With that done, we set the sack's `dead` flag to 1, so that it is removed.

A very simple, but very important function, since it now allows the player to continue to play, so long as they collect gift sacks.

That's it for sack.c. In order to make use of it, we need to update stage.c, to spawn our sacks. Starting first with initStage, where we've made preperations:


void initStage(void)
{
	// snipped

	houseSpawnTimer = FPS;

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

	gameOverTimer = FPS * 5;

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

We have a new static variable in stage.c called objectSpawnTimer, that will govern how often sacks are created. Here, we're setting the spawn time to be between 5 and 9 seconds upon starting.

Next, we've updated doStage:


void doStage(void)
{
	// snipped

	if (stage.pauseTimer == 0)
	{
		doGround();

		addHouse();

		if (stage.state == SS_PLAYING)
		{
			addObject();
		}

		// snipped
	}
}

We're testing first if the game is in progress (Stage's `state` is SS_PLAYING), and then calling addObject. We don't want to be creating sacks (or any of our other interesting objects) while we're on the title screen.

The addObject function is last:


static void addObject(void)
{
	int n;

	objectSpawnTimer -= stage.speed * app.deltaTime;

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

		if (n < 15)
		{
			initGiftSack();
		}
		else if (n < 30)
		{
			initCoalSack();
		}

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

This function will decrease the value of objectSpawnTimer, and then attempt to create an object when the value falls to 0 or less. To create our objects (right now, we only have sacks), we're testing a random of 100 (assigned to a variable called `n`). If it's less than 15, we'll call initGiftSack. If it's less than 30, we'll called initCoalSack. Any other number will be ignored, for now. Finally, we reset objectSpawnTimer back to a value between 5 and 9 seconds.

And that's all there is to it! Our game is now fairer and players can enjoy it for much longer. How long they can keep going will depend on their skill in delivering gifts as the game speeds up, and also on the random chance of gift sacks appearing. Otherwise, one could argue that our game is now complete.

But we don't want to leave it here, as there's so much more we can do to make things interesting. You might recall that we said our elves were responsible for teleporting in the sacks for Santa, but that their magic has gone slightly wrong and had enchanted the nearby snowmen? Well, in the next part we're going to introduce the first of these snowmen, who will act as hazards that the player must avoid.

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