« Back to tutorial listing

— 2D Santa game —
Part 6: Scoring

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

Introduction

We can deploy presents, but they simply pass through the houses and chimneys, and hit the ground. That's not what we want. What we want is to chuck a gift down an awaiting chimney, to score points. In this part, we're going to do just that. Just be careful not to deliver gifts those who are Naughty, and coal to those who are Nice. That will result in us losing points!

Extract the archive, run cmake CMakeLists.txt, followed by make, and then use ./santa06 to run the code. You will see a window open like the one above, with the scene moving from right to left. Use the controls defined in the last part. To score points, drop a gift down the chimney of a house with the lights all off (a Nice house), and a lump of coal to a house with the upstairs light on (a Naughty house). 100 points will be earned for a correct delivery (with 25 extra points for each additional delivery). 100 points will be lost for an incorrect delivery, so mind what you throw. When you're finished, close the window to exit.

Inspecting the code

So far, we've not been doing much other than adding in simple entities that move across the screen. From now on, however, we're going to start implementing our game's logic in anger. In this part we'll be introducing the first major step of our game loop, that will eventually lead us to the endgame phase. Let's get started.

First, to structs.h:


typedef struct
{
	int    complete;
	int    naughty;
	int    points;
	double pointsY;
	double pointsLife;
	double shudder;
} Chimney;

We've updated Chimney, to add in several new fields. `complete` is a flag to mark whether we've deposited a gift or coal down this chimney; `points` is the number of points this chimney has accumulated (may be negative); pointsY is the vertical positions of the points text that rises above the chimney when a gift or coal is deposited; pointsLife is how much life the points text has, and therefore how long it will continue to display; and `shudder` is a variable that will control the left-right shaking of the chimney when either gift or coal first enters it.

We've also made a small update to Stage:


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

We've add a `score` field, to record the total number of points the player has earned. The points in the chimneys are really only there for aesthetic reasons.

Moving on now to chimney.c, where the bulk of the updates in this part have been done. As we'll see, the chimneys will react to gifts and coals themselves, handling their logic and rendering when called on to do so by the doEntities loop in entities.c.

Starting with initChimney:


Entity *initChimney(int naughty)
{
	Entity  *e;
	Chimney *c;

	// snipped

	e->draw = draw;
	e->touch = touch;

	e->data = c;

	return e;
}

We've made just one addition here - assigning the `touch` function to the entity's `touch` pointer.

Next, we've made some updates to the `tick` function:


static void tick(Entity *self)
{
	Chimney *c;

	c = (Chimney *)self->data;

	c->pointsY = MAX(c->pointsY - app.deltaTime, self->y - 50);
	c->pointsLife = MAX(c->pointsLife - app.deltaTime, 0);
	c->shudder = MAX(c->shudder - app.deltaTime, 0);

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

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

Here, we've added in some code to handle the new fields we've added to the Chimney struct. Firstly, we're decreasing the value of pointsY, that will cause the Y position of our text to move up the screen, topping out at 50 pixels above the chimney itself. Next, we're decreasing the value of pointsLife, so that the points text will only display for a short period. Finally, we're decreasing the value of the chimney's `shudder`, limiting it to 0, causing the rapid shaking of the chimney to stop.

These values largely come into play in the `draw` function, that we'll look at next:


static void draw(Entity *self)
{
	Chimney *c;
	char     pointsText[5];
	int      x, g, b;

	c = (Chimney *)self->data;

	if (!c->complete)
	{
		blitAtlasImage(self->texture, self->x, self->y, 0, SDL_FLIP_NONE);
	}
	else
	{
		x = sin(c->shudder) * 5;

		blitAtlasImage(cleanChimneyTexture, self->x + x, self->y, 0, SDL_FLIP_NONE);
	}

	if (c->points != 0 && c->pointsLife > 0)
	{
		sprintf(pointsText, "%d", c->points);

		g = b = c->points > 0 ? 255 : 0;

		drawText(pointsText, self->x + (self->texture->rect.w / 2), c->pointsY, 255, g, b, TEXT_ALIGN_CENTER, 0);
	}
}

This is where we'll be rendering both the points display for the chimney, and also making the chimney shudder when a gift / coal enters it. We first check if the chimney's `complete` flag is 0, and if so render the chimney as normal. If the chimney is `complete`, we'll use a new texture called cleanChimneyTexture. This is just a visual indication that a gift has been thrown down the chute; it's to give the impression that the snow covering was removed by the action. When it comes to drawing the `complete` chimney, we're also shifting its `x` position using the value of it's `shudder`'s sine, multiplied by 5.

Next, we're testing if the chimney's `points` value is not 0 and its pointsLife is greater than 0, before rendering the value of points, using drawText (from text.c). The text will be centered over the chimney, at pointsY on the vertical. If `points` is positive, the text will be white, otherwise it will show red.

That's the rendering done, so now we can look at the most important function in chimney. The `touch` function will handle our logic for what happens when another object collides with the chimney:


static void touch(Entity *self, Entity *other)
{
	Chimney *c;
	int      score;

	if (other->type == ET_GIFT || other->type == ET_COAL)
	{
		c = (Chimney *)self->data;

		score = 0;

		if ((!c->naughty && other->type == ET_GIFT) || (c->naughty && other->type == ET_COAL))
		{
			if (!c->complete)
			{
				score = 100;
			}
			else
			{
				score = 25;
			}
		}
		else
		{
			score = -100;
		}

		stage.score += score;

		c->complete = 1;
		c->shudder = FPS / 2;
		c->points += score;
		c->pointsY = self->y - 25;
		c->pointsLife = FPS * 2;

		other->dead = 1;
	}
}

Our `touch` function takes both the chimney itself (`self`) and the object that has touched it (`other`). We're only interested in gifts (ET_GIFT) or coal (ET_COAL), so we ignore everything else. Next, we test if the correct object has touch the chimney. A gift must touch a Nice chimney, coal must touch a Naughty one. Should this be correct, we'll award 100 points for the initial correct delivery (based on the value of the `complete` flag), and 25 subsequent points for all the rest. If the object is the wrong type, we'll deduct 100 points. As you can see, it's very easy to rack up a negative score by giving all the bad kids presents!

We then add the score to Stage's `score`, and set the chimney's `complete` flag to 1. We then set the `shudder` amount, increase `points` by `score`, set the pointsY to a position slightly below the chimney's own `y` (this position will reset every time we score or lose points), and set the pointsLife to two seconds. Finally, we kill the touching object (`other`) by setting its `dead` flag to 1.

So, whenever a gift or coal makes contact with a chimney, the chimney will react based on the type of gift, adjusting Stage's `score` and its own points tally accordingly.

The last thing we do in chimney.c is to update loadTextures:


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

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

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

We're loading the next cleanChimneyTexture here.

We're not quite done yet, as we now need to update entities.c, to actually process the collision checks. We must first update 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->touch != NULL)
		{
			doCollisions(e);
		}

		// snipped

		prev = e;
	}
}

After calling the entity's `tick`, we're testing if they have a `touch` function set, and calling a new function named doCollisions if so, passing over the entity (`e`) to the function.

The doCollisions function is quite simple:


static void doCollisions(Entity *e)
{
	Entity *other;

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

We're looping through all the entities in Stage and testing if any of them (`other`) overlap with the current entity (`e`), by calling the `collision` function from util.c. If any of them do, we call the entity's `touch` function, passing over `e` and `other`.

Note something we're doing here - the gifts / coal aren't checking if they've hit a chimney, but the other way around. We want to do this so that the chimney itself can decide how it wants to handle being touched by the foreign object.

There we go. We can now not only deploy gifts and coal, but also send them down chimneys to score points. Having also added in our support for entity collisions means we will find ease in supporting hazards in future parts. An optimisation that we could make to our collision checks is to add everything to a quadtree (as we did in SDL2 Gunner) so that we only check collisions against objects that are relevant to the current context; our crude doCollisions function displays an O(N-1) problem at the moment. It's something we'll fix later.

So, where are we now? Well, we can score points, but we've no idea how many we've accumulated so far. We also need to be able to show the player how many gifts and coal they currently have, since ultimately the supply will be limited. In the next part, we'll introduce our HUD, so that these things can be tracked.

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