Home > Game Development Walkthroughs > 2D Platform Games Part 6: Creating Platforms and Geometry the Player can be Inside (Ladders, Ropes and Water)

2D Platform Games Part 6: Creating Platforms and Geometry the Player can be Inside (Ladders, Ropes and Water)

January 28, 2013 Leave a comment Go to comments

IMPORTANT! All of the articles in this series require you to have either Visual Studio 2010 or the Visual C++ 2010 Redistributable Package (4.8MB) installed in order to be able to try the pre-compiled EXEs provided with the examples. The Redistributable Package can be freely bundled with your own applications.

IMPORTANT! From Part 6 onwards, compatibility with Windows Vista in the pre-compiled EXEs has been dropped. To run the pre-compiled EXEs, you must have Windows 7 Service Pack 1 with Platform Update for Windows 7 installed, or Windows 8.

This article builds upon the demo project created in 2D Platform Games Part 5: Surface Dynamics (slippery and sticky platforms). Start with 2D Platform Games Part 1: Collision Detection for Dummies if you just stumbled upon this page at random!

Download source code and compiled EXE for the code in this article.

Introduction

So far all the geometry in our game world has followed the same rules:

  1. You can stand on top of a platform
  2. You can’t be inside a platform, and if you would be, you are moved away from it (all platforms are solid)
  3. You can only move left and right, and jump
  4. You can always jump (as long as nothing is in the way)
  5. The platforms can be at any angle but you can only walk up slopes of a certain maximum gradient dictated by the force of gravity
  6. The physics model of the player changes depending on the type of surface (platform) being stood on
  7. The physics model of the player when in mid-air remains the same as the last platform that was stood on

Some items we may want to add to the game world do not follow these rules. Consider ladders, ropes and bodies of water:

  1. You can be at any position inside them
  2. You can move up and down at constant velocities (therefore, gravity should not apply while inside the object)
  3. You should not be able to jump when inside the object (for ladders and ropes however, you should be able to jump away from the object)
  4. The physics model of the player after leaving the object should not be the same as when the player was inside it
  5. Some types of object should not be able to be stood on top of (ropes and water), others should (ladder)

Enabling geometry to work in this way obviously requires some significant changes to our collision detection and resolution model, and some changes to how the player physics work.

There are two obvious approaches to tackle this problem:

  1. Use separate collision detection models for objects you can and can’t be inside of, and a separate array of game world objects for each.
  2. Merge the behaviour into the existing collision detection model and keep all of the game world objects in a single array, with flags specifying the desired behaviour for each platform (surface) type

The second option is the more complex, but also the more flexible, and is the approach we will take in this article. We will only look at the implementation of ladders today, but most of the principles apply equally to ropes, water and other objects the player can be inside.

Setting up for ladders

We’ll start off with our first stab at how ladders should work using the rules above, but first we must get the obligatory boilerplate code out of the way.

Add a new surface type Ladder:

// Possible platform types
typedef enum {
	Normal = 0,
	Frictionless = 1,
	Sticky = 2,
	Ladder = 3
} SurfaceType;

In the properties of each surface type, we now need to specify the fixed up and down movement speeds of the player when he/she tries to move up or down inside the object. We also need to add a Y (vertical) deceleration factor to stop the player from moving indefinitely once up or down movement begins and the player lets go of the movement key. Finally, our current global setting for gravity needs to become part of the surface type properties instead so that the effect of gravity can be changed depending on the surface the player is in contact with.

// Properties of each platform type
struct Surface {
...
// Y-acceleration to be applied on platforms allowing up/down movement (eg. ladders)
// and player does not attempt to move up or down, or jump
float decY;

// Y-acceleration to be applied due to the force of gravity
float accY;

// The absolute Y movement speed of the player on platforms allowing up/down movement (eg. ladders)
// when the player tries to move up or down
float speedUp;
float speedDown;
...
};

In the definition of each individual platform or surface, we need to add a flag specifying whether the collision detection code should allow the player to be inside the surface:

// An individual platform or surface
struct Platform {
...
// For ladders and other objects you should be able to walk through,
// this flag disables collision resolution and allows the player to move
// around inside the object
bool allowInside;
...
};

As before we keep a note of the current physics being used for the player in the main game class, so we must add a variable to store the current Y deceleration factor:

// Main game class
class MyProgram : public Simple2D
{
...
// The amount of Y deceleration to apply on each frame when the player does not try to move up or down
// This is updated depending on the type of the last surface stood on, and reset when in mid-air
float decY;
...
};

accY already exists in the definition but will now be used to store the current gravity setting instead of as a global gravity setting. We only need to know the allowed fixed velocity vertical movement speed of the player while he/she is inside an object, so there is no need to store this globally – we can just look it up from the object definition of the object the player is currently inside.

In the class constructor, we add a sprite for the ladder:

surfaces[Ladder].tile = MakeBrush(MakeImage(L"ladder.png"), Custom, D2D1_EXTEND_MODE_CLAMP, D2D1_EXTEND_MODE_WRAP);

Notice the last two arguments here: they specify that the ladder texture should tile vertically (the last argument) but not horizontally. This is in contrast to the other platform types we have created so far, which tile horizontally but not vertically. Ropes would work the same way, and water geometry would typically be a large arbitrary shape whose texture should tile in both directions.

Remove the global gravity setting from the class constructor:

/* REMOVE THIS LINE */
accY = 30.f * mScale;

Set defaults for each of the existing surface types:

...
surfaces[Normal].accY = 30.f * mScale;
surfaces[Normal].decY = 0.f * mScale;
surfaces[Normal].speedUp = 0.f;
surfaces[Normal].speedDown = 0.f;
...
surfaces[Frictionless].accY = 30.f * mScale;
surfaces[Frictionless].decY = 0.f * mScale;
surfaces[Frictionless].speedUp = 0.f;
surfaces[Frictionless].speedDown = 0.f;
...
surfaces[Sticky].accY = 30.f * mScale;
surfaces[Sticky].decY = 0.f * mScale;
surfaces[Sticky].speedUp = 0.f;
surfaces[Sticky].speedDown = 0.f;
...

Note these all use the previously default gravity force, and all prevent the player from directly moving up or down by pressing an up or down movement key. They can, of course, still walk up and down slopes.

We now create a definition for the ladder surface type:

surfaces[Ladder].accXf = 12.f * mScale;
surfaces[Ladder].accXb = 12.f * mScale;
surfaces[Ladder].decX = 60.f * mScale;
surfaces[Ladder].accY = 0.f * mScale;
surfaces[Ladder].decY = 1000.f * mScale;
surfaces[Ladder].speedUp = 120.f * mScale;
surfaces[Ladder].speedDown = 120.f * mScale;
surfaces[Ladder].maxSpeedX = 5.0f * mScale;
surfaces[Ladder].maxSpeedY = 10.0f * mScale;

The player can move left and right on the ladder at the normal speed, but a strong X (sideways) deceleration force is applied such that he/she stops moving as soon as the left or right movement button is released.

By setting accY to zero, we disable gravity altogether. We then allow up and down movement by setting speedUp and speedDown to non-zero values. Finally we apply a strong Y (vertical) deceleration force so that the player stops moving as soon as the up or down movement button is released.

In the object creation for loop in SetupResources() where each individual platform is created, remember to prevent the player from moving inside the platforms by default:

// Create the game world landscape
for (int o = 0; o < worldObjectCount; o++)
{
    Platform p;
...
    p.allowInside = false;
}

We’ll then change one of the platforms we created for our demo level to a ladder at the end of SetupResources():

// Ladder
worldObjects[18].surface = Ladder;
worldObjects[18].allowInside = true;

Notice we set allowInside to allow the player to be inside the ladder.

Since we are now storing gravity (and Y deceleration) in each surface type rather than globally, we need to update the code right after the player is moved and current platform being stood on detected to update accY and decY in the current physics settings:

// Update which platform we are standing on
standingPlatform = standing;

// The physics applied to the player depend on the type of surface we are standing on
if (standingPlatform)
{
	accXf = surfaces[standingPlatform->surface].accXf;
	accXb = surfaces[standingPlatform->surface].accXb;
	decX = surfaces[standingPlatform->surface].decX;

    /* These two lines of code are new */
	accY = surfaces[standingPlatform->surface].accY;
	decY = surfaces[standingPlatform->surface].decY;

	maxSpeedX = surfaces[standingPlatform->surface].maxSpeedX;
	maxSpeedY = surfaces[standingPlatform->surface].maxSpeedY;
}

Also reset accY and decY when the player is re-spawned in spawnPlayer():

void MyProgram::spawnPlayer()
{
...
accY = surfaces[Normal].accY;
decY = surfaces[Normal].decY;
...
}

Now we have setup the basics and all the flags and settings we need, we have to implement the desired behaviour in our game code.

Handling player up and down movement

At the moment we are using a variable moveRequest to note if the player has tried to move left or right (but not jump) on the current frame, so that we can apply deceleration in the direction of movement if the player hasn’t pressed a movement button. As we will need to process X and Y deceleration separately, I have for clarity in the code replaced all references to moveRequest with moveRequestX.

The first step is to check if the player has pressed up or down. If so, and they are standing on (or in) a platform which allows up and down movement, we set the player’s movement vector for the next frame to speedUp or speedDown for the current platform as appropriate in the player movement code:

// Walk up and down platforms which allow it (eg. ladders)
if (GetAsyncKeyState(VK_UP))
{
	if (standingPlatform)
	{
		if (surfaces[standingPlatform->surface].speedUp != 0.f)
			speedY = -LinearMovement(surfaces[standingPlatform->surface].speedUp, updateTick);
			moveRequestY = true;
	}
}

if (GetAsyncKeyState(VK_DOWN))
{
	if (standingPlatform)
	{
		if (surfaces[standingPlatform->surface].speedDown != 0.f)
			speedY = LinearMovement(surfaces[standingPlatform->surface].speedDown, updateTick);

		moveRequestY = true;
	}
}

For our X deceleration when the player stops moving left and right, we have the code already:

if (!moveRequestX)
{
	if (speedX < 0) speedX += LinearMovement(decX, updateTick); 	if (speedX > 0) speedX -= LinearMovement(decX, updateTick);

	// Deceleration may produce a speed that is greater than zero but
	// smaller than the smallest unit of deceleration. These lines ensure
	// that the player does not keep travelling at slow speed forever after
	// decelerating.
	if (speedX > 0 && speedX < LinearMovement(decX, updateTick)) speedX = 0;
	if (speedX < 0 && speedX > LinearMovement(-decX, updateTick)) speedX = 0;
}

The new code for Y deceleration is placed immediately after this and is almost exactly the same:

// Decelerate the player's vertical movement if up or down wasn't pressed
// AND we are not jumping
// Applying vertical deceleration while jumping distorts the effect of gravity
if (!moveRequestY && !jumping)
{
	if (speedY < 0) speedY += LinearMovement(decY, updateTick); 	if (speedY > 0) speedY -= LinearMovement(decY, updateTick);

	// Deceleration may produce a speed that is greater than zero but
	// smaller than the smallest unit of deceleration. These lines ensure
	// that the player does not keep travelling at slow speed forever after
	// decelerating.
	if (speedY > 0 && speedY < LinearMovement(decY, updateTick)) speedY = 0;
	if (speedY < 0 && speedY > LinearMovement(-decY, updateTick)) speedY = 0;
}

In fact, the only difference here is that we check whether the player is jumping, and only apply Y deceleration if he/she isn’t. This check is important: picture the scene where the player jumps away from a ladder. The Y deceleration is strong, which means the player would end up stopping in mid-air almost immediately, so we must not apply this deceleration during a jump so that gravity can do its work.

There is another problem with jumping away from, falling off or walking off the side of a ladder: gravity does not apply when on ladders, and since we store the physics model of the last used platform as the current physics model, the player will not fall when transitioning from a ladder to mid-air. To fix this, we check if the player is in mid-air and apply standard gravity if so:

// Update which platform we are standing on
standingPlatform = standing;

// The physics applied to the player depend on the type of surface we are standing on
if (standingPlatform)
{
	accXf = surfaces[standingPlatform->surface].accXf;
	accXb = surfaces[standingPlatform->surface].accXb;
	decX = surfaces[standingPlatform->surface].decX;
	accY = surfaces[standingPlatform->surface].accY;
	decY = surfaces[standingPlatform->surface].decY;
	maxSpeedX = surfaces[standingPlatform->surface].maxSpeedX;
	maxSpeedY = surfaces[standingPlatform->surface].maxSpeedY;
}

/* THE CODE BELOW THIS LINE IS NEW */

// We are in flight. Use standard gravity.
// Including the Y-deceleration is also important here because transitioning from a platform
// such as a ladder with no Y-deceleration to flight will cause gravity to malfunction
// if the Y-deceleration is not reset.
else
{
	accY = surfaces[Normal].accY;
	decY = surfaces[Normal].decY;
}

Collision Detection for Ladders

The bulk of the modifications depend on whether the allowInside flag we introduced earlier is set or not for the platform the player is colliding with.

Speculative Contacts

Updating the speculative contacts code to work with ladders is a cinch: we don’t care if we are about to move into a ladder, so we can just skip it altogether!

Simply add an if statement just inside the loop which iterates over each world object, which encloses the entire speculative contacts code to cause it to be ignored if the player is allowed inside the object being tested:

// Iterate over each object whose bounding box intersects with the player's bounding box
// until a collision is found
for (auto it = boundObjects.begin(); it != boundObjects.end() && !contactXleft && !contactXright && !contactYbottom && !contactYtop; it++)
{
	// If collision resolution is disabled for the object, skip speculative contacts altogether
	// since we don't need to know if a collision is coming up

	if (!(*it)->allowInside)
	{
		// ================================================================================
		// Speculative contacts section
...
    }
...
	// ================================================================================
	// Penetration resolution
...

This now leaves all the work of processing ladder collisions to the penetration resolution contact solver.

Penetration Resolution

There is of course no need to actually resolve penetration into a ladder. However, we would like to know if the player is touching a ladder so we can set the correct physics model. We therefore modify the penetration resolution code to merely perform hit-testing on the player’s collision points for objects we can be inside, without correcting the player’s position.

Replace the directional testing loop with:

// If collision resolution is disabled for the object, we aren't going to change the movement vector
// so we store a copy so we can restore it after the hit testing is completed
float preresMoveX = nextMoveX, preresMoveY = nextMoveY;

for (int dir = 0; dir < 4; dir++)
{
    /* COLLISION TESTING NOW USES preresMoveX/preresMoveY instead of nextMoveX/nextMoveY */
	if (intersection.ContainsPoint(static_cast<int>(collisionPoint[dir*2].x + playerX + preresMoveX),
					static_cast<int>(collisionPoint[dir*2].y + playerY + preresMoveY))
	|| intersection.ContainsPoint(static_cast<int>(collisionPoint[dir*2+1].x + playerX + preresMoveX),
    				static_cast<int>(collisionPoint[dir*2+1].y + playerY + preresMoveY)))
	{
		switch (dir) {

        /* OLD CODE - REMOVE THIS */
		case 0: nextMoveY += intY; cPtop = true; break;
		case 1: nextMoveY -= intY; cPbottom = true; break;
		case 2: nextMoveX += intX; cPleft = true; break;
		case 3: nextMoveX -= intX; cPright = true; break;

        /* NEW CODE - ADD THIS */
		case 0: if (!(*it)->allowInside) { nextMoveY += intY; cPtop = true; } else contactYtop = true; break;
		case 1: if (!(*it)->allowInside) { nextMoveY -= intY; cPbottom = true; } else contactYbottom = true; break;
		case 2: if (!(*it)->allowInside) { nextMoveX += intX; cPleft = true; } else contactXleft = true; break;
		case 3: if (!(*it)->allowInside) { nextMoveX -= intX; cPright = true; } else contactXright = true; break;
	}
}

/* ADD IF CONDITION AROUND THIS CODE */
if (!(*it)->allowInside)
{
	if (nextMoveY > originalMoveY) { contactYtop = true; }
	if (nextMoveY < originalMoveY) { contactYbottom = true; }  	if (nextMoveX > originalMoveX) { contactXleft = true; }
	if (nextMoveX < originalMoveX) { contactXright = true; }
}

As you can see, the code for objects we are not allowed inside remains the same, whereas the code for objects are are allowed inside merely sets the contact* flags without changing the player’s movement vector. Note that we cannot use the bottom set of if statements to do this since the current and penetration-resolved vectors will always be the same (since no correction is applied), so we do it in the switch statement instead. Note also we do not set the crush-detection cP* flags (which check for death by crushing by world geometry) when colliding with an object we can be inside, since the player clearly cannot be crushed by an object he/she is allowed to be inside.

If we are touching any side of a ladder, we should end the current jump if the player is jumping, as we want the player to ‘stick’ to the ladder. The following code goes immediately after the above:

// If we have just hit *any* surface of a platform from anywhere
// else and collisions are disabled for the object,
// reset the jumping flag to allow a new jump. Once we touch such
// an object we are considered to be 'inside' it, causing the jump
// to end (eg. ladders)

if ((contactYtop || contactYbottom || contactXleft || contactXright) && (*it)->allowInside)
	jumping = false;

Previously we had code that stops vertical movement if the player hits the side of an object while in mid-air. We don’t want to do this for objects we can be inside, so we modify the code as follows:

// The player can't continue jumping if we hit the side of something, must fall instead
// Without this, a player hitting a wall during a jump will continue trying to travel
// upwards. You may or may not want this behaviour.

/* OLD CODE - REMOVE THIS */
if ((contactXleft || contactXright) && contactYtop && speedY < 0)
    speedY = nextMoveY = 0;

/* NEW CODE - ADD THIS */
if ((contactXleft || contactXright) && contactYtop && speedY < 0 && !(*it)->allowInside)
	speedY = nextMoveY = 0;

Note this is the same code we used before to prevent the player from continuing to jump when hitting the side of a wall, we just change it to add an extra test so that the correction only applies when the player touches an object he/she is not allowed to go inside.

We now consider the code which determines the platform the player is standing on. Previously this required contact with only the bottom of the player and no other side, as shown:

// If we are in contact with a platform's top edge, note which one
if (contactYbottom && !contactYtop && !contactXleft && !contactXright)
	standing = *it;

This code still applies for normal objects, but for ladders (and other objects we can be inside) we need to determine the platform differently: essentially, we are considered to be ‘on’ (ie. in) the platform/object if any side of the player is touching it. In addition, the code below resets the hit-testing flags after our ladder processing code is done so that no corrections are applied to his/her movement vector (no penetration resolution, only hit-testing):

if ((*it)->allowInside)
{
	// If collisions are disabled for the object but we are touching it,
	// we consider ourselves to be standing on it (eg. ladders)
	if (contactXleft || contactXright || contactYtop || contactYbottom)
		standing = *it;

	// Reset the movement vector so no collision correction occurs
	// for objects where collisions are allowed
	nextMoveX = preresMoveX;
	nextMoveY = preresMoveY;

	// When collision detection is disabled for the object, no contact is
	// considered to have occurred
	contactXleft = contactXright = contactYtop = contactYbottom = false;
}

That’s our basic ladder code done, so let’s compile it and see how we’re doing! The source code below contains additional explanatory comments to those shown in the text.

Source code and compiled EXE for this section

Some Unexpected Behaviours

Our work so far is a good start, but there are a few problems:

  1. If you stand at the top of the ladder and move up, the player appears to bounce up and down on top of the ladder
  2. Jumping while standing still on the ladder makes the player instantly move up a few pixels then come an immediate stop
  3. Jumping away from the ladder while standing in it does not work
  4. Falling onto the top of a ladder (or jumping up and down while standing on top of it) allows the player to fall through the top pixels of the ladder, rather than coming to rest at its top edge as you might expect

Let’s tackle them one at a time.

Detecting which sides of the player are touching an object

The solution to these problems require us to know precisely which sides of the player are in contact with the object. The player bounces when moving up at the top of the platform because he/she attempts to continue climbing, but gravity pulls him/her back down again (problem 1 above). This can be resolved by only allowing upwards movement when at least one side of the player besides the bottom side is touching the ladder (ie. at any point except when the player is standing on the top edge of the ladder).

Preventing jumping straight upwards while standing still on the ladder but allowing the player to jump sideways can be handled by using the sides of the ladder the player is touching to determine whether or not to allow a jump to begin when the player presses the jump button (problems 2 and 3).

Falling through the top of the ladder requires us to enable penetration resolution for the top side of the ladder, but only when the player’s bottom edge is above it (ie. falling from elsewhere onto the ladder’s top edge) (problem 4).

First then, we must add code to the hit-testing section to note which sides of the player are touching a platform, once we have determined which platform the player is in contact with. In the main game class definition:

/* EXISTING CODE */
// The platform we are currently standing on and which parts of the player are touching it
// Used for various calculations and restraints
Platform *standingPlatform;

/* NEW CODE */
bool standingL, standingR, standingT, standingB;

Near the start of UpdateObjects():

/* EXISTING CODE */
// We shall re-calculate which platform we are standing on
standingPlatform = nullptr;
Platform *standing = nullptr;

/* NEW CODE */
// Also store which part of the platform is touching the player
// Used to alter the player's allowed movements for eg. ladders
standingL = standingR = standingT = standingB = false;

At the end of penetration resolution, we alter the platform detection code to store each side of the player touching it as follows:

// If we are in contact with a platform's top edge, note which one
if (contactYbottom && !contactYtop && !contactXleft && !contactXright)
{
	standing = *it;
	standingB = true;
	standingL = standingR = standingT = false;
}

if ((*it)->allowInside)
{
	// If collisions are disabled for the object but we are touching it,
	// we consider ourselves to be standing on it (eg. ladders)
	if (contactXleft || contactXright || contactYtop || contactYbottom)
	{
		standing = *it;
		standingT = contactYtop;
		standingB = contactYbottom;
		standingL = contactXleft;
		standingR = contactXright;
...

Preventing upwards movement when the player is standing on top of a ladder

Now we have the platform side collision flags, we can implement the desired changes to the player’s behaviour.

To prevent upward movement when standing on top of a ladder, we simply add a condition when the player presses the up button to disallow the movement if only the bottom edge of the player is touching the platform (which by definition means the player must be standing on its top edge):

// Walk up and down platforms which allow it (eg. ladders)
if (GetAsyncKeyState(VK_UP))
{
    // The standing* flags are important here. We don't want to allow upwards movement on a
	// platform which we are standing on the upper edge of (eg. ladder). Therefore the contact
	// the bottom of the player is making with the platform is ignored, but some other part of
	// the player must be touching the platform. When standing at the top of the platform,
	// this will not be the case and upwards movement will be prevented.
	if (standingPlatform)
	{
        /* REPLACE THIS CONDITION */
		if (surfaces[standingPlatform->surface].speedUp != 0.f)

        /* WITH THIS CONDITION */
		if (surfaces[standingPlatform->surface].speedUp != 0.f && (standingL || standingR || standingT))
			speedY = -LinearMovement(surfaces[standingPlatform->surface].speedUp, updateTick);

		moveRequestY = true;
	}
}

Note that downwards movement does not have the same complications as we still want to be able to move down onto the ladder when we are standing on its top edge; additionally, when the player leaves the bottom of the ladder by continuing to move down, he/she will simply begin to fall through empty space. Therefore, no changes to downwards movement are needed.

If you now run the new code, you will see that if you come to rest with the bottom of the player exactly on the top edge of the ladder and try to move up, nothing happens. This is what we want. Unfortunately, with the solution of one problem comes the appearance of another: when not quite at the top of the ladder, upwards movement will still propel the player into mid-air, gravity will cause the player to fall back into the ladder, and if the up movement button is held down, this movement repeats causing a new type of bouncing. This is related to the fall-through problem mentioned above (problem 4) and we shall address this when we deal with fall-through later on.

Source code and compiled EXE for this section

Jumping behaviour while on a ladder

There are two separate problems here:

  1. We don’t want the player to be able to jump straight upwards while standing completely still on the ladder, as this doesn’t make sense
  2. We want to allow the player to jump sideways off the ladder

The first problem is easy to solve and simply requires us to add a condition when the player presses the jump button to check which parts of the player are in the ladder and whether he/she is moving left or right at the moment the jump button is pressed. If the player isn’t moving left or right and the player’s top and bottom edges are wholly contained within the ladder, jumping should be prevented. The code for this follows in the overall solution below.

The second problem is more tricky. The ladder causes the player to stick immediately when jumping because on the next frame the player will still be touching the ladder, so the jump flag will be reset and the ladder’s strong Y deceleration applied, essentially only allowing one frame of jumping before the player sticks back to the ladder. To solve this, we can, during a jump away from a ladder, completely remove the ladder from the collision detection equation and essentially pretend it doesn’t exist at all. To be able to do this, we have to store which platform (ie. ladder) the player last jumped away from so we can exclude it from collision testing until the jump is over.

This partially solves the problem, but our previous code still applies the physics model of the platform we are currently touching, which means there will be no gravity until we are no longer colliding with the ladder. This will cause a jump to make the player shoot upwards at high speed as there is no downward force acting on him/her. To solve this, we check each frame if we are jumping and whether the platform we just jumped from allows the player to be inside it (ie. is a ladder or rope), and if so, uses the default physics model instead. Think of it as a temporary override to the physics model until the jump comes to an end.

Let’s look at the code. In the main game class definition we need a pointer to the platform we last jumped from:

// The platform we jumped from (if currently jumping). This is only needed for platforms
// with no collision detection (eg. ladders) to allow its physics model to be ignored
// while we are jumping while still inside them, otherwise you get strange effects.
Platform *jumpPlatform;

At the start of UpdateObjects(), make a note of which platform we were standing on in the previous frame:

// We shall re-calculate which platform we are standing on
// We need the platform we were previously standing on to detect transitions
// between flight and standing on a platform (generally for objects with disabled collision detection)
Platform *previousPlatform = standingPlatform;

Normally we reset the jumping flag if the player touches any side of an object he/she is allowed inside. We add a condition so that this only occurs if the platform we are now touching is not the same as the one we were touching in the last frame, therefore taking the platform we jumped from out of the equation when testing for collisions during a jump. In other words, if we were touching a ladder on the last frame and pressed the jump button, and we are still touching the same ladder on this frame, do not reset the jumping flag – allow the jump to continue instead. In penetration resolution:

// If we have just hit *any* surface of a platform from anywhere
// else and collisions are disabled for the object,
// reset the jumping flag to allow a new jump. Once we touch such
// an object we are considered to be 'inside' it, causing the jump
// to end (eg. ladders)

// The check to make sure the platform we have just hit is not the
// last one we are standing on is important. If we jumped away from
// a platform with disabled collision detection, we don't want to
// be 'caught' in it before the jump has brought us far enough away
// from it to no longer be touching the player

/* CHANGE THIS CONDITION */
if ((contactYtop || contactYbottom || contactXleft || contactXright) && (*it)->allowInside)

/* TO THIS CONDITION */
if ((contactYtop || contactYbottom || contactXleft || contactXright) && previousPlatform != (*it) && (*it)->allowInside)
	jumping = false;

Use the standard physics model if we have just jumped from a platform we are allowed inside (ie. jumped from a ladder) for the reasons explained above (this code goes immediately before player keyboard input is processed):

// If we are jumping from a platform with no collision detection (eg. ladder),
// use normal physics rather than the physics of the platform. If we don't do this
// for ladders, there will be no gravity until we are no longer touching the ladder.
if (jumping)
	if (jumpPlatform->allowInside)
	{
		accXf = surfaces[Normal].accXf;
		accXb = surfaces[Normal].accXb;
		decX = surfaces[Normal].decX;
		accY = surfaces[Normal].accY;
		decY = surfaces[Normal].decY;
		maxSpeedX = surfaces[Normal].maxSpeedX;
		maxSpeedY = surfaces[Normal].maxSpeedY;
	}

Finally, solve problem 2 above by adding conditions to prevent jumping in unwanted scenarios when the player pressed the jump button. If a jump is allowed, store a pointer to the platform we jumped from:

// Jump if not already jumping and the jump key was released earlier
if (GetAsyncKeyState(' ') && !jumping && !jumpKeyDown)
{
	// We are allowed to jump under three conditions:
	// 1. We are not already jumping (prevents re-jumping in mid-air)
	// and either:
	// 2a. We are standing on the top of a platform's surface
	// or:
	// 2b. We are standing inside a platform with collision detection disabled (eg. ladder)
	// and attempting to jump left or right. Jumping straight upwards while in a vertical
	// platform is prevented. Note this condition can only be true for platforms without
	// collision detection, since if we are standing in both the top and bottom of a normal
	// platform, we have been crushed and are dead
	if ((standingB && !standingT)
		|| (standingB && standingT && speedX != 0))
	{
		jumping = true;
		jumpKeyDown = true;
		jumpPlatform = standingPlatform;
			speedY = -jumpStartSpeedY;
	}
}

Excellent. Now run the updated code and you should find that jumping sideways off a ladder works correctly with the correct physics, while jumping directly upwards is disallowed.

Source code and compiled EXE for this section

The Fall-Through Problem

So far so good. Now we just have to deal with the problem of the player being able to fall into the top of the ladder rather than coming to rest with the player’s bottom edge at the ladder’s top edge. This is quite a subtle problem and requires some slightly hair-raising changes to the penetration resolution code. First though, we shall introduce a new flag to the definition of Platform to indicate whether the player is allowed to fall into the top of an object he/she is allowed inside. For ropes and water, you should presumably be allowed to fall into them, but for ladders we want to land on top of them for a more natural-seeming behaviour.

// An individual platform or surface
struct Platform {
...
// For platforms with allowInside set, this flag specifies whether
// you can enter the object by falling through the top surface
// (subject to its surface's physics rules)
// or whether the player should come to rest when landing on it
// (and may then move downards through it)
bool allowFallThrough;
...
};

In SetupResources() add a line in the per-object loop to disallow fall-through by default:

p.allowFallThrough = false;

and modify our ladder so that fall-through is allowed:

// Ladder
worldObjects[18].surface = Ladder;
worldObjects[18].allowInside = true;
worldObjects[18].allowFallThrough = false;

Now for the gory details. In the penetration detection’s directional testing loop, we have a series of case statements like this:

case 0: if (!(*it)->allowInside) { nextMoveY += intY; cPtop = true; } else contactYtop = true; break;
case 1: if (!(*it)->allowInside) { nextMoveY -= intY; cPbottom = true; } else contactYbottom = true; break;
case 2: if (!(*it)->allowInside) { nextMoveX += intX; cPleft = true; } else contactXleft = true; break;
case 3: if (!(*it)->allowInside) { nextMoveX -= intX; cPright = true; } else contactXright = true; break;

We are going to change only the 2nd item (case 1), which applies an upward penetration correction if the player’s bottom edge falls into an object. The updated switch statement – with comments – looks like this:

// For each case, the movement vector is corrected to avoid a penetration collision

// Falling into an object with collision resolution disabled is a special case (case 1).
// If we have just landed on the object from flight (falling), we can either be
// allowed to continue falling into the object or to stop at the object's
// surface. The latter is the desired behaviour for ladders. In other words,
// downward collision detection is temporarily turned on when falling into
// such an object from flight. When already touching or inside the object,
// moving downwards is allowed so no penetration resolution is applied.

switch (dir) {
case 0: if (!(*it)->allowInside) { nextMoveY += intY; cPtop = true; } else contactYtop = true; break;

case 1: if (!(*it)->allowInside || (!previousPlatform && !(*it)->allowFallThrough))
	{
		nextMoveY -= intY;
		cPbottom = true;

		if ((*it)->allowInside)
			contactYbottom = true;
	}
	else contactYbottom = true;
	break;

case 2: if (!(*it)->allowInside) { nextMoveX += intX; cPleft = true; } else contactXleft = true; break;
case 3: if (!(*it)->allowInside) { nextMoveX -= intX; cPright = true; } else contactXright = true; break;
}

What happens here is we have added an extra possibility for upwards penetration resolution to occur: if we were not standing on any previous platform (in flight), but now we are, and the platform we are now on does allow you to be inside it, but does not allow fall-through. This is the cumulative meaning of the if statement in case 1 above. We also need to make sure to set contactYbottom for cases when the player is allowed in the object, since this was previously handled by the else part of the if statement.

Finally we come to the code which disables penetration resolution – ie. correction of the player’s movement vector – when colliding with an object the player is allowed inside. Previously of course, correction was never applied, but now we do want the player’s position to be corrected if the player is allowed inside the object, was not previously standing on a platform, the player’s bottom edge (at least) is now touching the top edge (or below) of the object, and fall-through is not allowed. In all other cases, we disable corrections as before. The code is (and replaces the previous version of the same if statement):

if ((*it)->allowInside)
{
	// If collisions are disabled for the object but we are touching it,
	// we consider ourselves to be standing on it (eg. ladders)
	if (contactXleft || contactXright || contactYtop || contactYbottom)
	{
		standing = *it;
		standingT = contactYtop;
		standingB = contactYbottom;
		standingL = contactXleft;
		standingR = contactXright;
	}

	// If we have just touched down on the surface of a platform where fall-through
	// is not allowed, allow the player's Y position to be corrected.
	if (previousPlatform != (*it) && contactYbottom && !(*it)->allowFallThrough)
	{
	}

    // When collision detection is disabled for the object, no contact is
	// considered to have occurred
	// In all other cases, ie. when standing on/in the same platform as the last frame,
	// or fall-through is allowed, do not apply penetration correction
	else
		contactXleft = contactXright = contactYtop = contactYbottom = false;
}

I have written the statement slightly obscurely for clarity (ironically). Recall that correction is applied in the specified axis if one of the contact* flags for that axis is set, otherwise it is discarded. Therefore we turn off all contact* flags under normal circumstances, only leaving contactYbottom set to cause upwards correction in the disallowed fall-through case just discussed.

Phew eh? Give it a whirl and you should find the entire ladder sorcery works perfectly!

Source code and compiled EXE for this section

Climbing our way to success

In Part 7 of this series, we’ll look at how to animate your player character depending on where the player is and what type of movement is taking place, before getting back to some more collision detection nitty-gritty. Until next time!

Advertisements
  1. January 28, 2013 at 22:08

    May I suggest you try to use static linking of the CRT, this way your applications will not require any particular runtime to be installed.

    • January 28, 2013 at 22:20

      That’s fair, although the articles are really aimed at people with Visual Studio installed already. They will have to install Platform Update for Windows 7 anyway as it updates DirectX DLL components 😦

  1. January 28, 2013 at 21:31
  2. January 28, 2013 at 23:43
  3. January 29, 2013 at 02:02
  4. February 19, 2013 at 05:42
  5. February 19, 2013 at 05:55

Share your thoughts! Note: to post source code, enclose it in [code lang=...] [/code] tags. Valid values for 'lang' are cpp, csharp, xml, javascript, php etc. To post compiler errors or other text that is best read monospaced, use 'text' as the value for lang.

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: