2D Platform Games Part 4: Moving Platforms and Crush Detection
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.
This article builds upon the demo project created in 2D Platform Games Part 3: Scrolling and Parallax Backgrounds. 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.
It’s common for platform games to feature platforms which move from side to side, up and down, or both. While a moving platform is nothing more than a static platform which has its position adjusted each frame, it presents some special problems with respect to handling the player. In this article, we will learn:
- How to add arbitrary movement to platforms of our choice
- How to make the player move relative to the platform’s position when standing on it (so that the motion of the player follows the motion of the platform)
- How to detect whether the player has been squashed or crushed
Re-factoring our project
Currently, we are storing the list of world objects and their bounding boxes in separate arrays. For moving platforms we will need to add additional information to each object (ie. its movement parameters), and later on in the series when we introduce other behaviours (like slippery platforms) we will need to add still further information, so now would be a good time to re-factor our code to store all the information for a single platform in a struct
so we don’t end up with a mess of arrays.
Start by defining:
// An individual platform or surface struct Platform { // The geometry (shape) of the surface Geometry geom; // The bounding box of the surface D2D1_RECT_F bounds; };
This new Platform
definition will be instanced once for each platform in the game, and can store as much information as desired simply by adding new variables inside the definition.
In the main game class definition, replace the old arrays and array count with a vector:
// The number of objects in the game world int worldObjectCount; // The geometry of all the landscape objects (platforms and walls) Geometry *worldObjects; // Pre-calculated bounding boxes for all the landscape objects D2D1_RECT_F *worldObjectBoundingBoxes;
should be deleted and replaced by:
// All the landscape objects (platforms and walls) vector<Platform> worldObjects;
Change the definition of the list of objects intersecting the player’s bounding box this frame from:
std::set<int> boundObjects;
to:
std::set<Platform *> boundObjects;
In SetupResources()
where the world geometry is created:
- add a call to
worldObjects.clear()
at the top - change
worldObjectCount = 22
toint worldObjectCount = 22
since we are only going to use this variable locally now - remove the line which creates an array of Geometry objects (
worldObjects = new Geometry[worldObjectCount]
) - underneath the array definition of all the co-ordinates of the platforms, re-write the rest of the function as follows:
// Generate the game world landscape geometry for (int o = 0; o < worldObjectCount; o++) { Platform p; // Create geometry ObjectDefinition &obj = objdef[o]; if (obj.shape == Rectangle) p.geom = MakeRectangleGeometry(obj.width, obj.height); if (obj.shape == Ellipse) p.geom = MakeEllipseGeometry(obj.width, obj.height); p.geom.SetTransform( p.geom.Scale(obj.scaleX, obj.scaleY, obj.scalePoint) * p.geom.Skew(obj.skewAngleX, obj.skewAngleY, obj.skewPoint) * p.geom.Rotate(obj.rotAngle, obj.rotPoint) * p.geom.Move(obj.topLeftX, obj.topLeftY)); p.geom.SetAutoAdjustBrushTransform(true); // Create bounding box p.bounds = p.geom.GetBounds(); worldObjects.push_back(p); } return true; }
ReleaseResources()
is no longer needed so you should delete the function and its declaration in the class definition.
For the rest of the code, replace all loops like:
for int (i = 0; i < worldObjectCount; i++) // or o instead of i
with:
for (auto it = worldObjects.begin(); it != worldObjects.end(); it++)
Replace all references to worldObjectBoundingBoxes[o]
with it->bounds
.
Replace all references to worldObjects[*it]
with (*it)->geom
.
Change the line which adds an intersecting bounding box to the list of geometries to check for collisions from:
boundObjects.insert(o);
to:
boundObjects.insert(&(*it));
Finally, in DrawScene()
, re-write the section which draws the world geometry as:
// Draw the landscape objects int clippedScenery = 0; for (auto it = worldObjects.begin(); it != worldObjects.end(); it++) { if (it->bounds.left < -scrollX + ResolutionX && it->bounds.right >= -scrollX && it->bounds.top < -scrollY + ResolutionY && it->bounds.bottom >= -scrollY) { SetBrush(tile); it->geom.Fill(); } else clippedScenery++; }
If you run the code now, it should compile and behave exactly as before. With the formalities out of the way, we can now move on to the good stuff.
Setting up moving platforms
To make platforms move, we need for each one:
- A flag specifying whether the platform moves or is static (still)
- Functions defining movement in X and Y
- To simplify calculations, the original x,y origin of the platform before it started moving (to determine offsets)
- A function which updates the platform’s position and pre-computed bounding box each frame
We can deal with all of this by adding some items to the definition of Platform:
struct Platform { ... // Specifies whether the platform is moving and needs a per-frame update bool moving; // The initial X and Y position of a moving platform int defaultX; int defaultY; // The X and Y movement functions Animation x; Animation y; // Update the position of a platform if it is moving void Update(); };
In SetupResources()
, add the following code inside the main for loop to make all platforms static by default and store their origin:
// Set defaults p.moving = false; p.defaultX = obj.topLeftX; p.defaultY = obj.topLeftY;
At the end of SetupResources()
, we can configure two platforms to make them move:
// Two moving platforms worldObjects[11].moving = true; worldObjects[11].x = Animation(Animations::Cos, 2500, 60, -20, 0.0f, 1.0f, Animation::Repeat, false); worldObjects[11].y = Animation::WaitAt(0, 1000); worldObjects[12].moving = true; worldObjects[12].x = Animation::WaitAt(0, 1000); worldObjects[12].y = Animation(Animations::Linear, 1000, 50, -25, 0.0f, 1.0f, Animation::Reverse, false);
We use Simple2D’s Animation
helper class here to create movement functions. The first parameter specifies the function to use (and this can be a user-defined function as well as the built-in ones), the second is the period (length) of the animation in milliseconds, the third is a multiplier which modifies the range of values the function returns (normally for example, Animations::Cos
returns a value between -1 and 1, and Animations::Linear
returns a value between 0 and 1), the fourth is an offset added to the result, the fifth and sixth are clamping factors which we will ignore here (just set them to 0.0f and 1.0f), the seventh specifies whether the animation will loop from the end to the start (Animation::Repeat
) or go forwards, then backwards and so on (Animation::Reverse
), and the eighth specifies whether to pause the animation when it is created (again, just set this to false
every time here).
Our first platform moves horizontally, but according to a cosine function, not in a linear fashion. Run the example EXEs provided to see how this kind of movement looks. Each frame, a point on the cosine curve is selected as the X movement offset from the platform’s origin, in the range -50 to 10. The full range of the movement of the platform takes 2.5 seconds. See Figure 1 for further explanation: at the start of the animation, we are 0 milliseconds and 0% through the animation, so the left-hand most point of the cosine curve is selected, which moves the platform 10 pixels to the right of its origin. Over the next 1250 milliseconds (50% of the animation), the curve is followed to accelerate the platform to the left then decelerate and come to rest at 50 pixels left of its origin (the centre point on the graph). The process then repeats for the right-hand half of the curve and second half of the animation. Sines and cosine curves are useful in many situations for creating this smooth kind of movement where there is fast movement in the middle of the animation but the object starts and stops slowly and gracefully.
The second platform moves vertically in the range -25 to 25 offset from its Y origin over a period of 1 second. After 1 second, the animation reverses and it moves from 25 to -25 over the next second, then repeats.
To update each platform’s position each frame, we use the following function:
// Update the position of a platform if it is moving void Platform::Update() { if (moving) { // Undo the previous translation and re-calculate for this frame geom.SetTransform(geom.GetTransform() * geom.Move(static_cast<int>(-bounds.left + defaultX + x.GetAnimOffset()), static_cast<int>(-bounds.top + defaultY + y.GetAnimOffset()))); bounds = geom.GetBounds(); } }
At the time this function is called, the platform geometry is already in its currently animated position (not at its origin), so we first reverse it by subtracting the left and top edges of the platform’s current bounding box (which puts it at the world origin), then adding its default position (which puts it at its own origin) then the positional offset from the movement function. GetAnimOffset()
returns a value from the movement function depending on the amount of time elapsed since the animation started, so the movement will be correct even if the frame rate drops.
We hook this function into each per-frame update of the game world by adding the following code at the very start of UpdateObjects()
:
// Update the position of any moving platforms for (auto it = worldObjects.begin(); it != worldObjects.end(); it++) it->Update();
That’s it! If you run the updated code you should now have two smoothly moving platforms which you can still stand and walk on as before.
Source code and compiled EXE for this section
Death is Always a Problem
There hasn’t been any way to die in our game so far, but with moving objects comes the possibility that the player may get wedged, squashed or crushed between objects. You can see what happens if we don’t check for this: run the code we just made and stand on the left-hand side of the vertically moving platform. When it moves upwards and the player’s head collides with the sloped platform above, the collision detection code goes crazy trying to apply vertical corrections. It can’t win, of course, because there is no correct place for the player to go, so we must check for the crush condition to avoid these unwanted effects.
So, first of all we’ll need a couple of things in the main game class definition: a flag to mark whether we have been crushed and a function to re-spawn the player (reset his/her position and speed in the game world) if a crush has occurred:
// Is the player dead? bool dead; // Re-spawn the player if he/she dies void spawnPlayer();
In the class constructor, remove the lines which set up playerX
, playerY
, speedX
, speedY
and jumping
and add a call to spawnPlayer()
at the end:
// Set player defaults spawnPlayer();
I put the spawn function after DrawScene()
but you can place it wherever you see fit:
// Spawn the player void MyProgram::spawnPlayer() { // Set starting position playerX = 120.f; playerY = 400.f; // No starting movement speedX = 0.f; speedY = 0.f; // We are not dead dead = false; // We are not jumping jumping = false; }
This is called at the start of the game and (when we add it in a moment) upon being crushed. Normally when the player dies you’d play an animation and send him/her back to the most recent checkpoint, but here we just throw them back to the start of the level.
There are many different ways to tell if a player is stuck in multiple pieces of geometry and by how much. The method I demonstrate below triggers a crush event if either both the left and right sides of the player are touching geometry, or both the top and bottom sides of the player are touching geometry. This makes sense if you think about it: if we are standing safely in a corner, we don’t want to be marked as dead just because the bottom and left or bottom and right of the player are touching walls – we need to be touching something from above and below at the same time (not enough vertical room to stand up), or from the left and right (not enough horizontal room to be in that location), or both.
At the moment our collision detection marks collisions from above and below, but collisions in X don’t differentiate between left and right, so the first thing we need to do is de-compose contactX
into two collision flags contactXleft
and contactXright
and update our collision detection code in UpdateObjects()
to use them properly. Here is a list of changes in the order they appear in UpdateObjects()
(all lines previously containing contactX
are changed):
... // Flags to specify what kind of collision has occurred bool contactXleft = true, contactXright = true, contactYbottom = true, contactYtop = true; ... // Iterate the contact solver for (int iteration = 0; iteration < iterations && (contactXleft || contactXright || contactYbottom || contactYtop); iteration++) ... // No collisions found yet contactXleft = contactXright = contactYbottom = contactYtop = false; ... // 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++) ... /* Replace the following: */ if (abs(nextMoveX - originalMoveX) > 0.01f) { contactX = true; } /* with: */ if (nextMoveX > originalMoveX) { contactXleft = true; } if (nextMoveX < originalMoveX) { contactXright = true; } ... // The player can't continue jumping if we hit the side of something, must fall instead // Without this, a player hiting a wall during a jump will continue trying to travel // upwards. You may or may not want this behaviour. if ((contactXleft || contactXright) && contactYtop && speedY < 0) speedY = nextMoveY = 0; ... if (contactXleft || contactXright) { playerX += nextMoveX; speedX = 0; }
Now we can flag collisions from all four directions, we can test for crush events. The contact*
flags are reset for every object being tested, but we need to see if we are being touched from multiple sides by multiple objects, so we create a new set of collision flags which are nullified at the start of each iteration of the contact solver and aggregate collisions over all the objects being tested.
Inside the iteration loop, right before the start of the object testing loop:
// Reset the player collision point hit checks for crushing // Unlike the contact* variables, these are not reset after each object is tested // as we need to know whether the player is colliding with multiple objects from // different directions at the same time to detect being crushed or squashed bool cPtop = false, cPbottom = false, cPleft = false, cPright = false;
In penetration resolution, we now set one or more of these flags in the event of a collision:
Replace:
case 0: nextMoveY += intY; break; case 1: nextMoveY -= intY; break; case 2: nextMoveX += intX; break; case 3: nextMoveX -= intX; break;
with:
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;
Just before the iteration loop closes, we now check for a crush event and set the death flag appropriately:
// Check if the player is dead from being crushed if ((cPleft && cPright) || (cPtop && cPbottom)) dead = true;
Finally, at the very end of UpdateObjects()
, we check the flag and re-spawn the player if they have been crushed:
// If the player is dead, re-spawn if (dead) spawnPlayer();
Now compile your project and stand on the left side of the vertically moving platform again. When your head hits the platform above, you should magically re-appear in the original start position!
Source code and compiled EXE for this section
Player Movement when on a Moving Platform
So that’s moving platforms all done and dusted, right? Sorry, you’re not getting away that easy! In platform games, it is normally the desired behaviour that when the player stands ‘still’ on a moving platform, he or she is moved relative to the platform’s movement. In other words, when not trying to move, the player should be seen to be at rest relative to the platform he or she is standing on, but if the platform is moving, the player should also be moving relative to the rest of the game world.
That was a rather technical explanation which may be cleared up by an example: imagine you are standing on an escalator or airport conveyor belt. You are standing still, as in, you are not trying to walk in any direction. But the escalator you are standing on is moving, so although you are not actively walking anywhere, you are still moving relative to everything not on the escalator, because the escalator is moving you in the same direction as its own movement.
If you try the project as it is now, and stand on the horizontally moving platform, you will stay completely still while the platform moves left and right underneath you. The problem also exists with the vertically moving platform but is more subtle: penetration resolution pushes you up a pixel or two each frame when you are standing on the platform as it moves upwards, and gravity pulls you down onto it as it moves downwards, so it appears to work, however you are jittering up and down as you constantly flip-flop between being in mid-air and standing on the platform on alternate frames.
What we need to do is detect each frame whether we are standing on a moving platform, and if so, move the player by exactly the same X and Y amount that the platform moves to give the impression of ‘sticking’ to it. The first problem then, is to figure out which platform we are standing on.
In the game class definition:
// The platform we are currently standing on Platform *standingPlatform;
Immediately before the start of the contact solver iteration loop in UpdateObjects()
:
// We shall re-calculate which platform we are standing on standingPlatform = nullptr; Platform *standing = nullptr;
We’re going to re-calculate every frame which platform we’re currently standing on (or none if in mid-air) so we reset this at the start of each per-frame update.
We perform the actual check right at the end of object testing loop (inside it, to test each object):
// If we are in contact with a platform's top edge, note which one if (contactYbottom && !contactYtop && !contactXleft && !contactXright) standing = *it;
It stands to reason that with standard gravity, if only the bottom of the player is touching a surface (and no other part of the player) once collision correction has been applied, then the surface being touched is the top surface of the object, so we are standing on it. Note that this variable may be overwritten several times as each object is tested – if the player is standing on several overlapping platforms, the one you define last in your game world (ie. the one that will appear at the front when rendered) is considered to be the one the player is standing on.
Immediately after the player’s position (playerX
and playerY
) are updated, update which platform we are standing on:
// Update which platform we are standing on standingPlatform = standing;
Finally, in spawnPlayer()
, add a line to set the current platform to be none (mid-air):
// This will be set during the first frame following spawning standingPlatform = nullptr;
If you don’t do this, the player may disappear on the first frame due to the code trying to use an uninitialized pointer and getting confused.
Now we know in standingPlatform
which piece of world geometry we are currently standing on each frame, if any, so we can now test each frame if it is a moving platform and move the player as well if so. We have another problem though, as we now need to know how much to move the player by. To calculate this, we modify Platform
so that it stores the bounding box of the moving platform on the previous frame as well as the current frame. The difference between the origin co-ordinates of the two bounding boxes is the amount of X and Y movement that the platform has made this frame.
In the definition of Platform
:
// The bounding box of the platform on the previous frame // (used to calculate movement distance to adjust the player's position when standing on the platform) D2D1_RECT_F previousBounds;
Re-write the movement update code as follows:
// Update the position of a platform if it is moving void Platform::Update() { if (moving) { // Undo the previous translation and re-calculate for this frame geom.SetTransform(geom.GetTransform() * geom.Move(static_cast<int>(-bounds.left + defaultX + x.GetAnimOffset()), static_cast<int>(-bounds.top + defaultY + y.GetAnimOffset()))); // Store previous bounding box and set new one previousBounds = bounds; bounds = geom.GetBounds(); } }
This is the same as before except that now we store the bounding box from the previous move before overwriting it with the updated bounding box.
Now we can calculate which platform we are standing on and how far it has moved this frame. The code goes right before we set standingPlatform
to nullptr
before the iteration loop:
// If the player is standing on a moving platform, specify an offset to the desired // movement vector to also move him/her relative to the platform's speed float nextMoveXextras = 0, nextMoveYextras = 0; if (standingPlatform) if (standingPlatform->moving) { // Compute how many pixels the platform has moved by since the last frame nextMoveXextras = (standingPlatform->bounds.left - standingPlatform->previousBounds.left); nextMoveYextras = (standingPlatform->bounds.bottom - standingPlatform->previousBounds.bottom); }
We store the amount the platform has moved in nextMoveXextras
and nextMoveYextras
. Now we just need to apply the same movement to the player. This is actually slightly tricky since our player movement is controlled by a time-stepped movement vector (speedX
and speedY
), whereas here we want to apply a movement in absolute pixels.
After the iteration loop closes, we change the player position update code from:
playerX += LinearMovement(speedX, updateTick); playerY += LinearMovement(speedY, updateTick);
to:
playerX += LinearMovement(speedX, updateTick) + (!contactXleft && !contactXright? nextMoveXextras : 0); playerY += LinearMovement(speedY, updateTick) + (!contactYtop && !contactYbottom? nextMoveYextras : 0);
Note we only apply the movements if no collision happened on the corresponding axis, otherwise we discard them.
Unfortunately, because this extra movement is not part of the player’s ordinary movement vector, it is not automatically included as part of the total desired movement in the collision detection code. To address this, we change the desired absolute next move in pixels at the start of (inside) the contact solver iteration loop from:
float nextMoveX = LinearMovement(speedX, updateTick); float nextMoveY = LinearMovement(speedY, updateTick);
to:
float nextMoveX = LinearMovement(speedX, updateTick) + nextMoveXextras; float nextMoveY = LinearMovement(speedY, updateTick) + nextMoveYextras;
When a collision is corrected but we may still have more contact solver iterations to go, we also need to make sure the extra movement due to a moving platform is cancelled out for the next iteration once the correction has been applied to the player’s position, in the same way as the player’s ordinary movement vector is cancelled out. At the end of the iteration loop right before the check for crushing/death, change the player position update code to:
// 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; } if (contactXleft || contactXright) { playerX += nextMoveX; nextMoveXextras = 0; speedX = 0; }
This is the same code as before, just with the lines to zeroise nextMoveXextras
and nextMoveYextras
added.
The flow of execution is now such that the collision detection code checks the player’s desired final position including any movement due to standing on a moving platform. If a collision is detected, correction is applied as normal; if not, both the player’s ordinary movement vector and any movement due to standing on a moving platform are added to his/her position.
Source code and compiled EXE for this section
And breathe…
Not only do we now have a working moving platform system, but thanks to the Platform
struct, the added ability of our collision detection code to ascertain which platform we are standing on and to differentiate collisions in all four directions, we are now in an excellent position to add new types of platform to our game. In Part 5, we’ll look at platforms which change the physics model of the player when standing on them, like slippery ice and sticky treacle-like platforms. Until next time!
Footnote
Part 11 contains some bug fixes to the crush detection code in this article.
-
January 21, 2013 at 17:332D Platform Games Part 3: Scrolling and Parallax Backgrounds « Katy's Code
-
January 21, 2013 at 21:412D Platform Games Part 5: Surface Dynamics (slippery and sticky platforms) « Katy's Code
-
February 19, 2013 at 05:422D Platform Games Part 11: Collision Detection Edge Cases for The Uninitiated « Katy's Code
-
February 19, 2013 at 05:552D Platform Games Part 1: Collision Detection for Dummies « Katy's Code