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

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 Red Road

For Joe Crosthwaite, surviving school was about to become more than just a case of passing his exams ...

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D Santa game —
Part 2: Houses

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

Introduction

We have our ground scrolling nicely, so it's time to move onto the next important part of our game - adding houses. In this part, we're going to introduce our houses, and have them move from right to left, along with the ground.

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa02 to run the code. You will see a window open like the one above, with the ground moving from right to left. Houses will appear at random intervals moving from the right side of the screen to the left. When you're finished, close the window to exit.

Inspecting the code

The aim of our game will be to throw gifts (and coal!) down the chimneys of houses that Santa's sleigh flies over. Our houses will come in two flavours - Nice houses (everyone is in bed, all the lights are off), and Naughty houses, where the upstairs lights are still on because the children are still awake. There will be an equal chance of both types of houses spawning.

We'll start by looking at the changes to defs.h:


enum
{
	ET_NONE,
	ET_HOUSE
};

We've added in an enum, to define our entity types (ET). You'll likely recognise this enum pattern, as used in many of the previous tutorials. Here, we've defined an entity type of NONE and another of HOUSE.

That's it for defs.h, so it's over next to structs.h, where we've introduced two new structs and also made some updates:


struct Entity
{
	int         type;
	double      x, y;
	double      dx, dy;
	AtlasImage *texture;
	int         dead;
	void(*data);
	void (*tick)(Entity *self);
	void (*draw)(Entity *self);
	void (*touch)(Entity *self, Entity *other);
	void (*die)(Entity *self);
	Entity *next;
};

We've added in an Entity struct. As you can no doubt see, it's rather par for the course. We've got a `type`; `x` and `y` variables, to hold positional data; `dx` and `dy` variables, to hold velocities; a `texture`; and a flag for marking the entity as `dead`. We're also defining some function pointers, `tick`, `draw`, `touch`, and `die`.

Moving on, we've added the House struct:


typedef struct
{
	int naughty;
} House;

At the moment, it has just one field: a variable called `naughty`. This will determine whether our house is on the Naughty or Nice list. The flag will control how it is drawn and, later on, how it responds to gift / coal deliveries.

Lastly, we've updated the Stage struct:


typedef struct
{
	double speed;
	Entity entityHead, *entityTail;
} Stage;

We've added in our entity linked list, with the head and tail variables.

Nice and straightforward so far. So, on to our first new file: house.c. This is where we'll be handling all the logic and rendering for our houses. Everything in house.c is rather easy to understand, so we'll start with initHouse:


void initHouse(void)
{
	Entity *e;
	House  *h;
	int     x, y;

	if (houseTextures[0] == NULL)
	{
		loadTextures();
	}

	x = SCREEN_WIDTH;
	y = GROUND_Y - houseTextures[0]->rect.h;

	if (canAddEntity(x, y, houseTextures[0]->rect.w, houseTextures[0]->rect.h))
	{
		h = malloc(sizeof(House));
		memset(h, 0, sizeof(House));
		h->naughty = rand() % 2;

		e = spawnEntity();
		e->type = ET_HOUSE;
		e->x = x;
		e->y = y;
		e->texture = houseTextures[rand() % NUM_HOUSE_TEXTURES];
		e->data = h;

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

This function is pretty simple to understand. We're first checking if we need to load our house textures (we've got a few different house colours, to keep things interesting), and then we're setting up two variables, `x` and `y`. `x` will be positioned off the right of the screen, while `y` aligns to the ground (based on the height of the first house texture - all houses are the same size). With that done, we test if we're free to position a house here by calling a function named canAddEntity (defined in entities.c - we'll see more on this later), passing in `x`, `y`, and the width and height of the house texture. Should we be allowed to, we're mallocing a House, spawning an entity, and assigning all the relevant details. Our house has a 50/50 chance of being Naughty or Nice. The house's texture is also chosen at random from our houseTextures array.

Nothing taxing. So, let's look at all the individual function pointers. As we saw, our Entity supports `tick`, `draw`, `touch`, and `die`. Right now, we're only setting up `tick` and `draw` here. We'll look at `tick` first:


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

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

Here, we can see why we wanted to know the speed of the Stage. Each time we call `tick` (once per frame), we're going to decrease the value of our house's `x` value by Stage's `speed`. This will make the house move left at the same speed of the ground. We then test to see if the house has moved completely off the left-hand side of the screen (taking into account its texture's width), and setting its `dead` flag to 1 if so. This will basically mean that our house will be removed from the game when its moved off screen.

Our `draw` function follows. Once again, it's easy enough to understand:


static void draw(Entity *self)
{
	House *h;

	h = (House *)self->data;

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

	if (h->naughty)
	{
		blitAtlasImage(houseLights, self->x, self->y, 0, SDL_FLIP_NONE);
	}
}

We're always drawing the house's texture. Next, we're testing if this is a Naughty house, and if so we're overlaying a texture called houseLights on top of it. The houseLights texture is a transparent PNG of the same dimensions as our house, but with the top bedroom light on.

There were a few choices when it came to indicating a Naughty house - perhaps a marker above the house itself, a radar system, or perhaps something outside the house itself. There was no particular reason for selecting the bedroom light, though the "radar" was quickly ruled out as it could be difficult to read and understand.

The final function is loadTextures:


static void loadTextures(void)
{
	int  i;
	char filename[MAX_NAME_LENGTH];

	for (i = 0; i < NUM_HOUSE_TEXTURES; i++)
	{
		sprintf(filename, "gfx/house%02d.png", i + 1);
		houseTextures[i] = getAtlasImage(filename, 1);
	}

	houseLights = getAtlasImage("gfx/houseLights.png", 1);
}

Nothing special here. As with the loadTextures function in stage.c, we're loading a series of textures, using formatted strings. We're also loading our houseLights texture here.

That's all there is to house.c for now. We'll take a brief look at entities.c, though it will once again be very familiar.

Starting with initEntities:


void initEntities(void)
{
	memset(&stage.entityHead, 0, sizeof(Entity));
	stage.entityTail = &stage.entityHead;
}

We're simply preparing the Entity linked list in Stage here.

On to doEntities:


void doEntities(void)
{
	Entity *e, *prev;

	prev = &stage.entityHead;

	for (e = stage.entityHead.next; e != NULL; e = e->next)
	{
		e->tick(e);

		if (e->dead)
		{
			if (e->die != NULL)
			{
				e->die(e);
			}

			prev->next = e->next;

			if (e == stage.entityTail)
			{
				stage.entityTail = prev;
			}

			if (e->data != NULL)
			{
				free(e->data);
			}

			free(e);

			e = prev;
		}

		prev = e;
	}
}

A standard loop to process our entities. For each entity in the loop, we're calling the `tick` function, testing whether the entity is `dead`, and calling its `die` function if on is set. Unlike some of the other games we've made, we're not pushing the entities into a "dead list", as there is no chance of dangling pointer references here, so we don't need to guard against dereferencing a NULL object.

The canAddEntity function comes next:


int canAddEntity(int x, int y, int w, int h)
{
	Entity *e;

	for (e = stage.entityHead.next; e != NULL; e = e->next)
	{
		if (collision(x, y, w, h, e->x, e->y, e->texture->rect.w, e->texture->rect.h))
		{
			return 0;
		}
	}

	return 1;
}

This function takes a rectangle as its input (`x`, `y`, `w` (width), and `h` (height)), and loops through all the entities in the stage, to see if it overlaps any of them. If so, it will return 0. Otherwise it will return 1. In short, it's testing whether the desired location is occupied. We don't want our houses (or other entities) to overlap one another.

Next up is the drawEntities function:


void drawEntities(void)
{
	Entity *e;

	for (e = stage.entityHead.next; e != NULL; e = e->next)
	{
		e->draw(e);
	}
}

Again, nothing special - we're just looping through all our entities and calling their assigned `draw` function.

spawnEntity comes next:


Entity *spawnEntity(void)
{
	Entity *e;

	e = malloc(sizeof(Entity));
	memset(e, 0, sizeof(Entity));

	stage.entityTail->next = e;
	stage.entityTail = e;

	return e;
}

Here, we're creating an entity, adding it to Stage's linked list, and returning it. We've seen this many times before, too.

That's it for entities.c, so let's return to stage.c, to look at the updates and additions we've made. Starting with initStage:


void initStage(void)
{
	memset(&stage, 0, sizeof(Stage));

	if (groundTextures[0] == NULL)
	{
		loadTextures();
	}

	initGround();

	initEntities();

	stage.speed = INITIAL_GROUND_SPEED;

	houseSpawnTimer = SCREEN_WIDTH / 8;

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

We've added a call to initEntities, and also added in a variable called houseSpawnTimer (static in entities.c), that will control how often we attempt to add in a new house. We set it to an eighth of the width of the screen to begin with, so that a house doesn't appear right away.

Next, we've updated doStage, our logic step:


void doStage(void)
{
	doGround();

	addHouse();

	doEntities();
}

We're calling a new function here, addHouse, as well as doEntities. We'll take a look at what addHouse does now:


static void addHouse(void)
{
	houseSpawnTimer -= stage.speed * app.deltaTime;

	if (houseSpawnTimer <= 0)
	{
		initHouse();

		houseSpawnTimer = rand() % SCREEN_WIDTH / 2;
	}
}

We're decreasing the value of houseSpawnTimer, according to the speed of the stage itself, so that houses will appear at a consistent rate. Whenever houseSpawnTimer falls to 0 or less, we're calling initHouse, to create a new house at the right-hand side of the screen. We're then setting houseSpawnTimer to a random value of half the screen's width. This will help to space out the houses (and our canAddEntity function will prevent overlaps).

Finally, we've updated drawStage:


void drawStage(void)
{
	drawGround();

	drawEntities();
}

We're now calling drawEntities, to render how entities.

And that's it for adding in our houses. There's still much to do, but as you can see, we've already got a nicely scrolling ground with houses erected on top of it. These houses will continue arriving from the right and move to the left for as long as the program runs.

But wait, doesn't Santa deliver presents via chimneys? Our houses don't have any chimneys just yet, so Santa would be unable to do so. We'll change that in our next part, by adding chimneys to the houses when they are created.

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