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 Battle for the Solar System (Complete)

The Pandoran war machine ravaged the galaxy, driving the human race to the brink of destruction. Seven men and women stood in its way. This is their story.

Click here to learn more and read an extract!

« Back to tutorial listing

— 2D platformer tutorial —
Part 5: Moving platforms

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. Extract the archive, run cmake CMakeLists.txt, followed by 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. Close the window by clicking on the window's close button.

Inspecting the code

Adding moving platforms is not tricky; they're just entities that move back and forth between two points. The hardest part of dealing with moving platforms is dealing with the collision response, as we need to move an entity a platform comes into contact with around. To start with, we'll add a new flag to defs.h:


#define EF_PUSH       (2 << 2)

The EF_PUSH flag will be used to tell moving platforms that they carry entities and push them around. We also need to add some new variables to the Entity struct:


struct Entity {

	...

	void (*tick)(void);

	...

	Entity *riding;

	...
};

We've added a tick function pointer so that we can drive the logic of our moving platforms. This will be called once per frame when we process our entities. The riding variable will point to an entity that another entity is currently sitting on. These are both used in the doEntities function in entities.c, which we'll made some updates to:


void doEntities(void)
{
	Entity *e;

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

		if (e->tick)
		{
			e->tick();
		}

		move(e);
	}

	for (e = stage.entityHead.next ; e != NULL ; e = e->next)
	{
		if (e->riding != NULL)
		{
			e->x += e->riding->dx;

			push(e, e->riding->dx, 0);
		}

		e->x = MIN(MAX(e->x, 0), MAP_WIDTH * TILE_SIZE);
		e->y = MIN(MAX(e->y, 0), MAP_HEIGHT * TILE_SIZE);
	}
}

As before, we're looping through all the entities in the world, but as well as moving them we're also calling their tick function if it's been assigned. In this tutorial only the moving platforms will make use of them. We're then looping through all the entities again, only this time we're checking to see if they're currently riding on something. If the entity is, we're updating its x variable and then calling a new function called push. This function simply calls our moveToWorld and moveToEntities:


static void push(Entity *e, float dx, float dy)
{
	moveToWorld(e, dx, dy);

	moveToEntities(e, dx, dy);
}

As can be seen, the entity being pushed will simply act as though its moving normally, with all the relevant collision checks against the world, etc. being made. We're not calling move this time because move does things like apply gravity, which we've already done. The move function itself has also been updated:


static void move(Entity *e)
{
	...

	if (e->riding != NULL && e->riding->dy > 0)
	{
		e->dy = e->riding->dy + 1;
	}

	...
}

We're once again testing if the current entity is riding something. If it is and the thing it's riding is moving down the screen (dy > 0), we want to set the rider's dy to that of the thing it's riding, plus 1. Of course, this could have consequences for our game. It would mean that Pete (or any other entity does so) wouldn't be able to jump since this code would also override the jump movement. To overcome this, we simply state that if the player jumps, they are no longer riding anything:


if (app.keyboard[SDL_SCANCODE_I] && player->isOnGround)
{
	player->riding = NULL;

	player->dy = -20;
}

We've only got one more function to update to allow platforms to carry entities and push them around. This will be done in the moveToEntities 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 (other->flags & EF_SOLID)
			{
				...

				if (dy > 0)
				{
					e->isOnGround = 1;

					e->riding = other;
				}

				...
			}
			else if (e->flags & EF_PUSH)
			{
				other->x += e->dx;
				push(other, e->dx, 0);

				other->y += e->dy;
				push(other, 0, e->dy);
			}
		}
	}
}

Most things stay the same, but we're now making some additional checks. If the current entity has hit something solid and we're moving down the screen, we're telling the entity that it is on the ground and also now riding on the thing it made contact with. This will mean that, according to the second loop of doEntities, the entity will now start moving with the platform. Go back and look at the doEntities function again and you'll see it in action.

However, if the entity that has been touched isn't solid (other's flags do not contain EF_SOLID) things become interesting. If we've got the EF_PUSH flag set we're going to move the other thing out of the way:


else if (e->flags & EF_PUSH)

This is done by adding the current entity's dx to the other's x, and then calling push. In effect, we're treating the other entity as though it is trying to move in the same direction as the pusher. As we're doing this for both dx and dy, a platform will now push things about. Note that while this works well for a platform pushing an entity up, it works less well when moving down. This is due to the speed of gravity vs the speed of the platform, which is why we have we added those new lines in the move function. Without these, an entity sitting on a platform moving down will appear to hop and bounce: falling, stopping, falling, stopping, etc. This can be seen in action by commenting out:


e->dy = e->riding->dy + 1;

With that done, we just need to create the moving platforms. We'll do this by updating the addEntFromLine function:


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

	if (strcmp(name, "BLOCK") == 0)
	{
		initBlock(line);
	}
	else if (strcmp(name, "PLATFORM") == 0)
	{
		initPlatform(line);
	}
	else
	{
		SDL_LogMessage(SDL_LOG_CATEGORY_APPLICATION, SDL_LOG_PRIORITY_WARN, "Unknown entity '%s'", line);
	}
}

In it, we're now calling separate functions to create the blocks and platforms. We'll look at the initPlatform function, as the initBlock function was more or less covered in the previous tutorial (it has been shifted into block.c). The initPlatform function lives in a file called platform.c:


void initPlatform(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 %f %f", &e->sx, &e->sy, &e->ex, &e->ey);

	e->x = e->sx;
	e->y = e->sy;

	e->tick = tick;

	e->texture = loadTexture("gfx/platform.png");
	SDL_QueryTexture(e->texture, NULL, NULL, &e->w, &e->h);
	e->flags = EF_SOLID+EF_WEIGHTLESS+EF_PUSH;
}

We create an entity and then read all the relevant details of our platform from the line that was passed over to the function. For the platform, we read in the start x and y (sx and sy), and end x and y (ex and ey). We assign the current x and y to the start x and y, and then assign the tick function pointer. Finally, we load the platform's texture and assign its flags. Note that we're assigning the EF_PUSH flag to the platform. This is important for our platform to behave as expected: it should push Pete (and other entities) around when it encounters them.

The last thing to look at is the tick code for the platform:


static void tick(void)
{
	if (abs(self->x - self->sx) < PLATFORM_SPEED && abs(self->y - self->sy) < PLATFORM_SPEED)
	{
		calcSlope(self->ex, self->ey, self->x, self->y, &self->dx, &self->dy);

		self->dx *= PLATFORM_SPEED;
		self->dy *= PLATFORM_SPEED;
	}

	if (abs(self->x - self->ex) < PLATFORM_SPEED && abs(self->y - self->ey) < PLATFORM_SPEED)
	{
		calcSlope(self->sx, self->sy, self->x, self->y, &self->dx, &self->dy);

		self->dx *= PLATFORM_SPEED;
		self->dy *= PLATFORM_SPEED;
	}
}

The tick function controls the movement of the platform. Essentially, the platform is told to move to the end if it's at the start, and vice versa. So that we don't overshoot the start or end, we test if the absolute of the platform's x minus its start x is less than the speed it's moving (we also do this for the y). The direction of the platform is reversed if both of these are true. We call calcSlope to work out the direction the platform needs to move in, passing over the entity's dx and dy variables, and then multiplying the result by PLATFORM speed.

And that's it! We now have a platform game with a number of usable features. All that's missing is the pizza for Pete to collect, and some music and sounds effects. We'll put these finishing touches in during the final tutorial.

Purchase

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

From itch.io

It is also available as part of the SDL2 tutorial bundle:

Mobile site