Home > Game Development Walkthroughs > 2D Platform Games Part 8: Pass-through Platforms

2D Platform Games Part 8: Pass-through Platforms

January 29, 2013 Leave a comment Go to comments

IMPORTANT! From Part 8 onwards, you no longer require the Visual C++ 2010 Redistributable Package installed in order to be able to try the pre-compiled EXEs provided with the examples. Thanks to Dbug for reminding me that libraries can be statically linked 😛

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 7: Player Character Animation. 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.

Figure 1. Rainbow Islands uses only pass-through platforms for the entire game

Pass-through platforms are a type of platform commonly found in video games whereby the player can jump up through the platform from underneath, landing on its solid top surface. The platform behaves like a normal platform in all respects, except that when the player jumps up from underneath and hits it, he/she continues travelling upwards instead of encountering a solid obstacle. Games such as the all-time classic Rainbow Islands and more modern affairs such as the amusing time-waster Monsters (Probably) Stole my Princess! use pass-through platforms exclusively.

For those of you who followed the ladders tutorial, implementing pass-through platforms is going to be a familiar story: define a new surface type and its physics properties, add some flags to the platform definition class to specify which platforms should have the new behaviour enabled, create a platform of the new surface type, and change the collision detection code to deal with the new possible behaviour. So, let’s not waste any time and crack on with that inevitable boilerplate code so we can look at the real meat of the problem: the collision detection.

Setting up for Pass-through Platforms

Add a new surface type, JumpThrough:

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

In our definition of Platform which defines the properties of each individual platform in the game, we add some new behavioral flags:

struct Platform {
...
bool allowFromLeft;
bool allowFromRight;
bool allowFromAbove;
bool allowFromBelow;
...
};

These flags will disable collision detection when the player is moving in a specific direction. For our pass-through platforms, we want to disable collision detection from all directions except coming from above the platform. You might initially think we just need to disable collision detection from below the platform, but it is important to disable collision detection for when the player is moving left or right as well or there will be problems with the left and right edges of the platform.

Note that although we only really need to use allowFromAbove in this particular example, we kill two birds with one stone by implementing all four flags at the same time. Later, we can use these to make one-way doors, or floors the player can fall through but not climb back up again. Build in flexibility if you know you need it later and it’s not much extra effort.

It’s important to note that by enabling one or more of these flags, we are effectively saying that the player can be inside the object, and even more importantly, that the player must still be allowed to move in the opposite direction (ie. down, if the pass-through platform prevents moving down into it from above) until he/she has passed completely through the platform. This is a problem we will deal with in the collision detection code.

Load a new sprite for the new platform type in the class constructor:

surfaces[JumpThrough].tile  = MakeBrush(MakeImage(L"thru-tile.png"), Custom, D2D1_EXTEND_MODE_WRAP);

This is just the same as our previous platform sprite loaders.

Create the surface properties for pass-through platforms, also in the class constructor:

surfaces[JumpThrough].accXf = 12.f * mScale;
surfaces[JumpThrough].accXb = 12.f * mScale;
surfaces[JumpThrough].decX = 15.f * mScale;
surfaces[JumpThrough].accY = 30.f * mScale;
surfaces[JumpThrough].decY = 0.f * mScale;
surfaces[JumpThrough].speedUp = 0.f * mScale;
surfaces[JumpThrough].speedDown = 0.f * mScale;
surfaces[JumpThrough].maxSpeedX = 5.0f * mScale;
surfaces[JumpThrough].maxSpeedY = 10.0f * mScale;

There is nothing special here: pass-through platforms use exactly the same player physics model as normal platforms.

In the object creation loop in SetupResources(), set the defaults for each platform to not allow pass-through from any direction:

p.allowFromLeft = false;
p.allowFromRight = false;
p.allowFromAbove = false;
p.allowFromBelow = false;

Change one of the platforms in our demo level to be a pass-through platform:

// Pass-through platform
worldObjects[10].surface = JumpThrough;
worldObjects[10].allowFromBelow = true;
worldObjects[10].allowFromLeft = true;
worldObjects[10].allowFromRight = true;

…and that’s it. Now let’s look at how to implement these new flags.

Collision detection for pass-through platforms

Speculative contacts

We will want to run the speculative contact solver for pass-through objects, but only for the directions in which pass-through is not allowed. In these directions, the platform acts as a normal, solid structure. In directions where pass-through is allowed, the platform acts like an object you can be inside (such as a ladder or water) and should be ignored. This is a pretty easy change to make, just add a few lines of flag-checking code inside the speculative contacts directional testing loop:

// We will test the four possible directions of player movement individually
// dir: 0 = top, 1 = bottom, 2 = left, 3 = right
for (int dir = 0; dir < 4; dir++)
{
...
	// Skip the test if we are allowed to collide with the object from the current
	// direction of movement

	if (dir == 0 && (*it)->allowFromBelow) continue;
	if (dir == 1 && (*it)->allowFromAbove) continue;
	if (dir == 2 && (*it)->allowFromRight) continue;
	if (dir == 3 && (*it)->allowFromLeft)  continue;
...
}

This solves the bullet-through-paper problem for the sides of the object which cannot be passed through, when the player is wholly on that side of the object. For the sides that can be passed through, there is no bullet-through-paper problem so the speculative contacts code is not needed.

Penetration Resolution

As usual, it comes down to the penetration resolution code to do the filthy work. Once again our changes start in the directional testing loop, and we clutter up the switch statement with some more conditions as follows.

Old code:

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;
}

New code:

switch (dir) {
case 0: if (!(*it)->allowInside && !(*it)->allowFromBelow) { nextMoveY += intY; cPtop = true; } else contactYtop = true; break;
case 1: if ((!(*it)->allowInside || (!previousPlatform && !(*it)->allowFallThrough)) && !(*it)->allowFromAbove)
	{
		nextMoveY -= intY;
		cPbottom = true;

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

As you can see, we have added a series of conditions which only correct the player’s movement vector if there is a collision with the side of an object where pass-through is not allowed. If pass-through is allowed, no change is made to the movement vector and a contact* flag is set to indicate which side of the player is touching the object, just as if it was an object the player is allowed inside (such as a ladder).

Notice that this code only sets the contact* flags for sides where pass-through is allowed. Luckily, for the sides where pass-through isn’t allowed, the existing code will already work to set the remaining flags since the movement vector will have been changed in one or more axes:

// Allowed pass-through directions on pass-through platforms are not considered as
// collisions overall but the contact* flags are retained temporarily so we can use
// them in pass-through platform handling below. They are cancelled out afterwards.
// Note that for pass-through platforms, this code segment only sets contact* flags
// for sides where pass-through is not allowed. For sides where pass-through is allowed,
// the flags are set in the loop above.

/* THIS CODE IS ALREADY PRESENT */
if (!(*it)->allowInside)
{
	if (nextMoveY > originalMoveY) { contactYtop = true; }
	if (nextMoveY < originalMoveY) { contactYbottom = true; }
	if (nextMoveX > originalMoveX) { contactXleft = true; }
	if (nextMoveX < originalMoveX) { contactXright = true; }
}

Post-processing for pass-through platforms

Let’s review where we are so far: at this point, we have the player’s movement vector corrected for collisions with sides of the object that do not allow pass-through (but left alone for those that do), and each side of the player touching the object is marked in one or more of the contact* flags. The corrected movement vector has not been applied to the player’s position yet, and we will now use the contact* flags to decide how to process it and whether we want the correction applied or not. The following code segments deal exclusively with pass-through platforms.

Since we have already prevented the player’s movement vector from being corrected when he/she collides with an object from a side where pass-through is allowed, you may wonder why we need any additional processing at all – and that would be a reasonable assumption. So by way of example, consider the following situation:

  • the pass-through platform allows the player to jump up from underneath it but not fall through the top
  • the player’s bottom edge is colliding with the platform
  • the player is moving upwards

What happens? The penetration resolution contact solver corrects the player’s movement vector upwards so that his/her bottom edge is at rest on the platform’s top edge, then contactYbottom is set thus causing the correction to be applied. The visual result is that as soon as the bottom edge of the player enters the pass-through platform, he/she is pinged immediately upwards to its surface. We can fix this by adding a rule which only allows the correction to be applied if the bottom edge of the player is the only edge touching the pass-through platform (indicating the player is more or less on top of it give or take a couple of pixels). That way, the player can move without correction up through the platform until he/she reaches the top of it.

Now assume we have fixed this problem, and consider what happens next when the player reaches the top of the platform:

  • the pass-through platform allows the player to jump up from underneath it but not fall through the top
  • the player’s bottom edge is the only edge colliding with the platform
  • the player is moving upwards (in the middle of a jump)

What happens now? The rule we just added causes the player to be snapped onto the platform surface, which causes it to be set as the platform currently being stood on, which causes the jump to end. The visual result is that when a player jumps up through a pass-through platform, the jump will come to an abrupt end as soon as the player clears the top of the platform with his/her feet (player’s bottom edge is the only edge colliding with the platform).

For these reasons, we need post-processing of the contact flags specifically for pass-through platforms. Let’s look initially at the case of the pass-through platform with a solid top surface that the player can jump up from underneath, as we have been discussing in this article:

if (!(*it)->allowInside)
{
	// If the top side is solid but we are not yet all the way through the platform,
	// don't apply penetration resolution to force the player up to the surface
	if (contactYbottom && (contactYtop || contactXleft || contactXright || preresMoveY < 0) && !(*it)->allowFromAbove && !standingPlatform)
		contactYbottom = false;

The purpose of this and the following if statements below is to turn off contact flags for directions in which we don’t want the player’s movement vector to be corrected for the reasons outlined above.

First of all, in the outer if statement, we do not want to cancel out detected contacts for items we are allowed to be inside (eg. ladder), as we need this information to make the player behave correctly when they are inside such an object.

The inner if statement considers the case where the bottom edge of the player is somewhere (anywhere) in the pass-through platform (contactYbottom is set). The contact flag is turned off (therefore preventing upward correction of the player’s movement vector) only when all of these conditions are satisfied:

  1. the bottom edge of the player is somewhere (anywhere) in the pass-through platform
  2. the player is not currently standing on any other platform or the pass-through platform itself (in flight – see note below)
  3. the platform does not allow pass-through from the top side
  4. at least one other side of the player is in the pass-through platform or the player is moving upwards

The last condition is really the key here: the collision will be ignored if the player is still jumping, or part of the player besides his/her bottom edge is inside the platform. This prevents both of the ‘snap-to-surface’ problems discussed earlier.

Note: unlike ladders and other objects you can be wholly inside from any direction, pass-through platforms are not considered to be ‘being stood on’ when you are inside them like ladders are. The above if statement ensures this is the case, in fact, because contactYbottom – the flag used to test if the player is standing on a platform – will only ever still be set at the end of the processing when the player’s bottom edge is the only edge touching the platform and the player is not moving upwards as a result of a jump. (Downward collisions like this are a special case for this particular type of pass-through object, because for one-way doors and fall-through floors, you will never be considered to be standing on them at all.)

The remaining if statements to deal with the aforementioned one-way doors and fall-through floors are basically the same, with the relevant contact and behaviour flags replaced:

	// If the bottom side is solid but we are not yet all the way through the platform,
	// don't apply penetration resolution to force the player underneath
	if (contactYtop && (contactYbottom || contactXleft || contactXright || preresMoveY > 0) && !(*it)->allowFromBelow && !standingPlatform)
		contactYtop = false;

	// The same for one-way left and right 'doors'
	if (contactXleft && (contactXright || contactYtop || contactYbottom || preresMoveX > 0) && !(*it)->allowFromRight && !standingPlatform)
		contactXleft = false;

	if (contactXright && (contactXleft || contactYtop || contactYbottom || preresMoveX < 0) && !(*it)->allowFromLeft && !standingPlatform)
		contactXright = false;
}

We’ve got the worst of it out of the way, but there are a few more things we need to add.

At this point the contact* flags are set for all sides of the pass-through object the player is colliding with – even those we don’t want to apply corrections for. We therefore mask out the flags so that those sides where collisions are allowed are set to false so that no correction is applied in the corresponding direction:

contactYtop &= !(*it)->allowFromBelow;
contactYbottom &= !(*it)->allowFromAbove;
contactXleft &= !(*it)->allowFromRight;
contactXright &= !(*it)->allowFromLeft;

This bit of logic code basically says, if allowFromBelow is set, then set contactYtop to false otherwise leave it at its current value, and so on.

Further down in the code, we currently have something like this where the player’s position is corrected:

// If a contact has been detected, apply the re-calculated movement vector
// and disable any further movement this frame (in either X or Y as appropriate)
if (contactYbottom || contactYtop)
{
	playerY += nextMoveY;
	nextMoveYextras = 0;
	speedY = 0;

	if (contactYbottom)
		jumping = false;
}

The inner condition with resets the jumping flag is no longer sufficient as it may stop a jump when the player is passing through a pass-through platform. Remove those two lines of code and, right above where we masked out the contact* flags above, add a new improved jump end test condition:

// If we have just hit the surface of a platform from falling and
// collisions are enabled for the object, reset the jumping flag
// to allow a new jump.
// (works also for pass-through platforms due to the manipulation of contactYbottom in the above code)
if (contactYbottom && !previousPlatform && !(*it)->allowInside && !(*it)->allowFromAbove)
	jumping = false;

That leaves us with just one final change to make, and that is to the code which kills the player’s upwards movement if the player collides on both his/her top and either the left or right side with any platform he/she is not allowed inside, while moving upwards:

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

This is meant to prevent you from jumping into ceilings and sloped walls, but causes the precise type of pass-through platform we are trying to make (the one where you can jump up from underneath it) fail to work, so we need to add an extra condition at the end:

if ((contactXleft || contactXright) && contactYtop && speedY < 0 && !(*it)->allowInside && !(*it)->allowFromBelow)
	speedY = nextMoveY = 0;

Now, if we are allowed to collide with the object from below (which for our pass-through platforms, we are), this condition fails and no change is made to the player’s vertical movement.

That Passed Right Over My Head

Pass-through platforms present a subtle set of problems which require quite delicate solutions as seen in the code above. I hope this tutorial enlightened you somewhat! Feel free to check out the source code and EXE linked at the top of the article, and leave your comments below! Note that in this version of the example code, I have added additional comments and also some on-screen statistics and information that I didn’t cover in the articles.

In Part 9, we’ll look at – *gasp* – making a level editor for our game. Until next time!

Footnote

Part 11 contains some bug fixes to the code in this article.

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.

This site uses Akismet to reduce spam. Learn how your comment data is processed.