In this tutorial you'll learn how to create moving platforms and make sure that objects that are riding them will preserve their relative position. We'll also handle the case of being crushed between a platform and the ground.
Prerequisites
This tutorial is based on the Basic Platformer Physics series. Specifically, we'll be using the code based on the 8th part of the tutorial as the starting point, with a few modifications. Check out the tutorial series, and particularly the last part. The principles behind the implementation will stand even if you're using a different physics solution, but the code will be compatible with the version presented in the tutorial series.
Demo
You can download the demo from the tutorial attachments. Use the WASD keys to move the character, Space to spawn a clone character, and P to spawn a moving platform. The right mouse button creates a tile. You can use the scroll wheel or the arrow keys to select a tile you want to place. The sliders change the size of the player's character.
The demo has been published under Unity 2017.2b4, and the source code is also compatible with this version of Unity.
Implementation
Moving Platforms
First of all, let's create a script for a moving platform.
Initialization
Let's start by creating the object's class.
public class MovingPlatform : MovingObject { }
Now let's initialize some basic parameters of the object in the init function.
public void Init() { mAABB.HalfSize = new Vector2(32.0f, 8.0f); mSlopeWallHeight = 0; mMovingSpeed = 100.0f; mIsKinematic = true; mSpeed.x = mMovingSpeed; }
We set the size and speed, and we make the collider kinematic, which means that it won't be moved by regular objects. We also set the mSlopeWallHeight
to 0, which means that the platform will not climb the slopes—it'll always treat them as walls.
Behaviour
The behaviour for this particular moving platform will be just this: start movement right, and whenever you meet an obstacle, change the direction 90 degrees clockwise.
public void CustomUpdate() { if (mPS.pushesRightTile && !mPS.pushesBottomTile) mSpeed.y = -mMovingSpeed; else if (mPS.pushesBottomTile && !mPS.pushesLeftTile) mSpeed.x = -mMovingSpeed; else if (mPS.pushesLeftTile && !mPS.pushesTopTile) mSpeed.y = mMovingSpeed; else if (mPS.pushesTopTile && !mPS.pushesRightTile) mSpeed.x = mMovingSpeed; UpdatePhysics(); }
Here's the pattern visualized:
Gluing the Character to the Platform
Right now, if a character stands on a platform, the platform will simply slide from underneath it, as if there was no friction between the objects. We'll try to remedy that, by copying the offset of the platform.
Determine the Parent Object
First of all, we want to be aware of what object, if any, is our character standing on. Let's declare a reference to that object in the MovingObject
class.
public MovingObject mMountParent = null;
Now, in the UpdatePhysicsResponse
, if we detect that we are colliding with an object below us, we can assign this reference. Let's create a function that will assign the reference first.
public void TryAutoMount(MovingObject platform) { if (mMountParent == null) { mMountParent = platform; } }
Now let's use it in appropriate places, that is wherever we say that our object is colliding with another object below it.
else if (overlap.y == 0.0f) { if (other.mAABB.Center.y > mAABB.Center.y) { mPS.pushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { TryAutoMount(other); mPS.pushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } continue; }
The first place is when we check if the objects are touching.
if (overlap.y < 0.0f) { mPS.pushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { TryAutoMount(other); mPS.pushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); }
The second place is when they are overlapping.
Now that we've got this covered, let's handle the movement for our object. Let's modify the UpdatePhysics
function from the previous tutorial.
Let's declare a class variable for the offset that we need to move our character.
public Vector2 mOffset;
Now let's replace the old local offset with the class one.
mOffset = mSpeed * Time.deltaTime;
In case the object is on a platform, let's add the platform's movement to the offset.
mOffset = mSpeed * Time.deltaTime; if (mMountParent != null) { if (HasCollisionDataFor(mMountParent)) mOffset += mMountParent.mPosition - mMountParent.mOldPosition; else mMountParent = null; }
Note that we're also checking here if we're still in touch with the object. If that's not the case then we set the mMountParent
to null
, to mark that this object is no longer riding on any other.
Next, let's move the position of our object by that offset. We're not going to use our Move
function, but simply change the position. So in the collision check between the objects, which takes place right after the UpdatePhysics
, we'll get the result for the positions in this frame instead of the previous one.
mOffset = mSpeed * Time.deltaTime; if (mMountParent != null) { if (HasCollisionDataFor(mMountParent)) mOffset += mMountParent.mPosition - mMountParent.mOldPosition; else mMountParent = null; } mPosition += RoundVector(mOffset + mReminder); mAABB.Center = mPosition;
Now let's move to the UpdatePhysicsP2
, which is called after the collisions between the objects have been resolved. Here we undo our previous movement, which hasn't been checked for whether it's valid or not.
public void UpdatePhysicsP2() { mPosition -= RoundVector(mOffset + mReminder); mAABB.Center = mPosition;
Next, we proceed to UpdatePhysicsResponse
to apply a move out of overlap with other objects. Here, previously we were modifying the position directly, but now instead let's modify the mOffset
, so this position change gets resolved later when we use our Move
function.
if (smallestOverlap == Mathf.Abs(overlap.x)) { float offsetX = overlap.x * speedRatioX; mOffset.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f) { mPS.pushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPS.pushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } } else { float offsetY = overlap.y * speedRatioY; mOffset.y += offsetY; offsetSum.y += offsetY; if (overlap.y < 0.0f) { mPS.pushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { TryAutoMount(other); mPS.pushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } }
Now we can go back to the UpdatePhysicsP2
, where we simply call the UpdatePhysicsResponse
and Move
functions as we did earlier, to get the correct position state.
mPosition -= RoundVector(mOffset + mReminder); mAABB.Center = mPosition; UpdatePhysicsResponse(); if (mOffset != Vector2.zero) Move(mOffset, mSpeed, ref mPosition, ref mReminder, mAABB, ref mPS);
Fix the Update Order
Because of the way we order the physics updates, if the child object is updated before the parent, the child will be constantly losing contact with the platform when traveling up/down.
To fix this, whenever we set the mMountParent
, if the platform is behind the child in the update queue, we swap those two, so the parent object always updates first. Let's do that modification in the TryAutoMount
function.
public void TryAutoMount(MovingObject platform) { if (mMountParent == null) { mMountParent = platform; if (platform.mUpdateId > mUpdateId) mGame.SwapUpdateIds(this, platform); } }
As you can see, if the update id of the platform object is greater than the child, the objects' update order gets swapped, removing the problem.
That's pretty much it when it comes to gluing the character to the moving platform.
Detect Being Crushed
Detecting being crushed is pretty simple. In the UpdatePhysicsResponse
, we need to see if the overlap against a kinematic object moves us into a wall.
Let's take care of the X axis first:
if (smallestOverlap == Mathf.Abs(overlap.x)) { float offsetX = overlap.x * speedRatioX; mOffset.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f) { mPS.pushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPS.pushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } }
If the object is on our right side and we are already pushing against a left wall, then let's call a Crush
function, which we'll implement later. Do the same for the other side.
if (overlap.x < 0.0f) { if (other.mIsKinematic && mPS.pushesLeftTile) Crush(); mPS.pushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { if (other.mIsKinematic && mPS.pushesRightTile) Crush(); mPS.pushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); }
Let's repeat that for the Y axis.
if (overlap.y < 0.0f) { if (other.mIsKinematic && mPS.pushesBottomTile) Crush(); mPS.pushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { if (other.mIsKinematic && mPS.pushesTopTile) Crush(); TryAutoMount(other); mPS.pushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); }
The Crush
function will simply move the character to the center of the map for the demo.
public void Crush() { mPosition = mMap.mPosition + new Vector3(mMap.mWidth / 2 * Map.cTileSize, mMap.mHeight / 2 * Map.cTileSize); }
The result is the character being teleported when it's being crushed by a platform.
Summary
This was a short tutorial because adding moving platforms is not a big challenge, especially if you know the physics system well. Borrowing from all the code in the physics tutorial series, it was actually a very smooth process.
This tutorial has been requested a few times, so I hope you find it useful! Thanks for reading, and see you next time!