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

Latest Updates

SDL2 Shooter 2 tutorial
Tue, 13th July 2021

SDL2 Widget tutorial
Fri, 18th June 2021

SDL2 Adventure tutorial
Tue, 8th June 2021

New tutorials
Tue, 11th May 2021

Orb source code
Sun, 25th April 2021

All Updates »

Tags

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

Books


The Third Side (Battle for the Solar System, #2)

The White Knights have had their wings clipped. Shot down and stranded on a planet in independent space, the five pilots find themselves sitting directly in the path of the Pandoran war machine as it prepares to advance The Mission. But if they can somehow survive and find a way home, they might just discover something far more worrisome than that which destroyed an empire.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D platformer tutorial —
Part 6: Finishing touches

Introduction

Note: this tutorial series builds upon the ones that came before it. If you aren't familiar with the previous tutorials, or the prior ones of this series, you should read those first.

This tutorial will explain how to add and control a character on a basic 2D map. Unpack the code and then type make to build. Once compiling is finished type ./ppp05 to run the code.

A 1280 x 720 window will open, with a series of coloured squares displayed over a light blue background. Pete, the player character, will drop to the ground from the top-left of the screen. He can be controlled using A and D to move him left and right, and using I to jump. Pressing Space will reset Pete to the starting point. If you're not fond of these controls, they can be easily changed in player.c. Black squares with white outlines are entities that Pete can stand on. Moving platforms will carry Pete around if he stands on them, and push him out of the way if he attempts to block them. Collect pizza slices by walking into them. Close the window by clicking on the window's close button.

Inspecting the code

We're going to forego adding a title screen and highscore table this time; there is only one level and no real way for Pete to lose a life, so we'll be jumping straight into the game. We're also going to be skipping over drawing text and playing sounds, as these were covered in earlier tutorials.

We're going to start with looking at the changes to structs.h, and the Entity object:


struct Entity {
	...

	int health;

	...

	float value;

	....

	void (*touch)(Entity *other);

	...
};

We're adding in a health variable and a function pointer for touch, and also a variable called value. Every Entity created will need to have at least 1 health point. An entity with <= 0 health will be removed. We'll see this in a bit (shouldn't come as any surprise to those who have been following these tutorials before). We're also updating the Stage struct to add in some data about the pizza slices that are available on the stage:


typedef struct {
	...

	int pizzaTotal, pizzaFound;
} Stage;

The pizzaTotal and pizzaFound variables will be used to give guidance to the player as to their progress. This information will be displayed on the hud. Now let's look at the updates to entities.c. The first update to doEntities is rather routine:


void doEntities(void)
{
	...

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

		if (e->health <= 0)
		{
			if (e == stage.entityTail)
			{
				stage.entityTail = prev;
			}

			prev->next = e->next;
			free(e);
			e = prev;
		}

		prev = e;
	}

	...
}

As can be seen, we're testing to see if the entity in question has <= 0 health and deleting it if so. Nothing special. Again, we just need to ensure that when an entity is created we give it at least one health point so that it is not immediately removed from the world. This includes Pete, platforms, blocks, etc. The next update is to moveToEntities. This is where we'll be making use of the touch function:


static void moveToEntities(Entity *e, float dx, float dy)
{
	...

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

			if (e->touch)
			{
				e->touch(other);
			}
		}
	}
}

After two entities had collided with each other, we are calling the touch function (if it's set). We pass the reference to the entity that we hit (other) over to the one that made contact. As this is a function pointer, we can define whatever action should be taken. Something to note is that this touch function may be called more than once when an entity moves. This is because we move on one axis at a time. For this tutorial, it's not a big deal. However, it might be prudent for a larger game to make a note of the unique contacts that were made and deal with them just once.

The final update to entities.c involves adding the pizza slices, in addEntFromLine:


static void addEntFromLine(char *line)
{
	...

	else if (strcmp(name, "PIZZA") == 0)
	{
		initPizza(line);
	}

	...
}

If while reading in the entity data we encounter a line that starts with PIZZA, we'll call a new function called initPizza. This function exists in a new file called pizza.c:


void initPizza(char *line)
{
	Entity *e;

	e = malloc(sizeof(Entity));
	memset(e, 0, sizeof(Entity));
	stage.entityTail->next = e;
	stage.entityTail = e;

	sscanf(line, "%*s %f %f", &e->x, &e->y);

	e->health = 1;

	e->texture = loadTexture("gfx/pizza.png");
	SDL_QueryTexture(e->texture, NULL, NULL, &e->w, &e->h);
	e->flags = EF_WEIGHTLESS;
	e->tick = tick;
	e->touch = touch;

	stage.pizzaTotal++;
}

The function is quite simple: it just creates an Entity and sets all its relevant details (including setting the health to 1!). We also increment stage.pizzaTotal for each slice created, so we can inform the player how many slices are available on the level. We want our pizza slices to float, so we make them weightless by setting flags to EF_WEIGHTLESS. We also set the pizza's tick and touch functions. Starting with tick:


static void tick(void)
{
	self->value += 0.1;

	self->y += sin(self->value);
}

We're increasing the pizza's value variable by 0.1 per frame and then using the sin of that amount to adjust the pizza's y value. The entity's value variable is just a general purpose variable that we're abusing; it could be used for a number of other things. By adding the sin of value to y, we cause the pizza slice to bob up and down on the spot. This looks nicer than them just sitting there. The touch function is more interesting:


static void touch(Entity *other)
{
	if (self->health > 0 && other == player)
	{
		self->health = 0;

		stage.pizzaFound++;

		if (stage.pizzaFound == stage.pizzaTotal)
		{
			playSound(SND_PIZZA_DONE, CH_PIZZA);
		}
		else
		{
			playSound(SND_PIZZA, CH_PIZZA);
		}
	}
}

We want to test what's touched our pizza. If it's the player and the pizza slice is still alive (health > 0), we will be collecting it. The pizza's own health is set to 0 and the number of pizza slices found is incremented. We then check to see if we've found all the pizza slices or still have some remaining, and play the appropriate sound. We could do anything we want in this touch function, such as creating more pizza slices, ending the level, trigger an event, etc.

Finally, let's take a quick look at the drawHud function. It lives in stage.c and is rather simple:


static void drawHud(void)
{
	SDL_Rect r;

	r.x = 0;
	r.y = 0;
	r.w = SCREEN_WIDTH;
	r.h = 35;

	SDL_SetRenderDrawBlendMode(app.renderer, SDL_BLENDMODE_BLEND);
	SDL_SetRenderDrawColor(app.renderer, 0, 0, 0, 196);
	SDL_RenderFillRect(app.renderer, &r);
	SDL_SetRenderDrawBlendMode(app.renderer, SDL_BLENDMODE_NONE);

	drawText(SCREEN_WIDTH - 5, 5, 255, 255, 255, TEXT_RIGHT, "PIZZA %d/%d", stage.pizzaFound, stage.pizzaTotal);
}

All we're doing is creating a transparent black strip at the top of the screen and overlaying some text. We're doing this by setting up an SDL_Rect, calling SDL_SetRenderDrawBlendMode with SDL_BLENDMODE_BLEND to tell the renderer to draw alpha colors, setting SDL_SetRenderDrawColor to a transparent black, and then calling SDL_RenderFillRect to draw our black strip. With that done, we're calling drawText, setting the appropriate location and color, and outputting the pizza stats from stage. You will see the pizzaFound counter increment as pizza is collected.

And there you have it: a basic, but playable, platform game. This game could easily be extended to feature larger levels, more objects and items to collect, and more things to do. Hopefully, you'll have found this useful as the basis for creating your own platform game.

Purchase

The source code for all parts of this tutorial (including assets) is available here:

It is also available as part of the SDL2 tutorial bundle (with on-going updates):

Comments

Mobile site