In the previous installment of the series, we implemented a collision detection mechanism between the game objects. In this part, we'll use the collision detection mechanism to build a simple but robust physical response between the objects.
The demo shows the end result of this tutorial. Use WASD to move the character. The middle mouse button spawns a one-way platform, the right mouse button spawns a solid tile, and the spacebar spawns a character clone. The sliders change the size of the player's character.
The demo has been published under Unity 5.4.0f3, and the source code is also compatible with this version of Unity.
Collision Response
Now that we have all the collision data from the work we've done in the previous part, we can add a simple response to colliding objects. Our goal here is to make it possible for the objects to not go through each other as if they were on a different plane—we want them to be solid and to act as an obstacle or a platform to other objects. For that, we need to do just one thing: move the object out of an overlap if one occurs.
Cover the Additional Data
We'll need some additional data for the MovingObject
class to handle the object vs. object response. First of all, it's nice to have a boolean to mark an object as kinematic—that is, this object will not be pushed around by any other object.
These objects will work well as platforms, and they can be moving platforms as well. They are supposed to be the heaviest things around, so their position will not be corrected in any way—other objects will need to move away to make room for them.
public bool mIsKinematic = false;
The other data that I like to have is information on whether we're standing on top of an object or to its left or right side, etc. So far we could only interact with tiles, but now we can also interact with other objects.
To bring some harmony into this, we'll need a new set of variables which describe whether the character is pushing something on the left, right, top, or bottom.
public bool mPushesRight = false; public bool mPushesLeft = false; public bool mPushesBottom = false; public bool mPushesTop = false; public bool mPushedTop = false; public bool mPushedBottom = false; public bool mPushedRight = false; public bool mPushedLeft = false; public bool mPushesLeftObject = false; public bool mPushesRightObject = false; public bool mPushesBottomObject = false; public bool mPushesTopObject = false; public bool mPushedLeftObject = false; public bool mPushedRightObject = false; public bool mPushedBottomObject = false; public bool mPushedTopObject = false; public bool mPushesRightTile = false; public bool mPushesLeftTile = false; public bool mPushesBottomTile = false; public bool mPushesTopTile = false; public bool mPushedTopTile = false; public bool mPushedBottomTile = false; public bool mPushedRightTile = false; public bool mPushedLeftTile = false;
Now that's a lot of variables. In a production setting it would be worth turning these into flags and having just one integer instead of all these booleans, but for the sake of simplicity we will handle leave these as they are.
As you may notice, we have quite fine-grained data here. We know whether the character pushes or pushed an obstacle in a particular direction in general, but we can also easily inquire whether we're next to a tile or an object.
Move Out of Overlap
Let's create the UpdatePhysicsResponse
function, in which we'll handle the object vs. object response.
private void UpdatePhysicsResponse() { }
First of all, if the object is marked as kinematic, we simply return. We don't handle the response because the kinematic object need not respond to any other object—the other objects need to respond to it.
if (mIsKinematic) return;
Now, this is assuming that we won't need a kinematic object to have the correct data concerning whether it's pushing an object on the left side, etc. If that's not the case, then this would need to be modified a bit, which I'll touch on later down the line.
Now let's start to handle the variables which we've just recently declared.
mPushedBottomObject = mPushesBottomObject; mPushedRightObject = mPushesRightObject; mPushedLeftObject = mPushesLeftObject; mPushedTopObject = mPushesTopObject; mPushesBottomObject = false; mPushesRightObject = false; mPushesLeftObject = false; mPushesTopObject = false;
We save the results of the previous frame to the appropriate variables and for now assume that we're not touching any other object.
Let's start iterating through all our collision data now.
for (int i = 0; i < mAllCollidingObjects.Count; ++i) { var other = mAllCollidingObjects[i].other; var data = mAllCollidingObjects[i]; var overlap = data.overlap; }
First off, let's handle the cases in which the objects are barely touching each other, not really overlapping. In this case, we know we don't really have to move anything, just set the variables.
As mentioned before, the indicator that the objects are touching is that the overlap on one of the axes is equal to 0. Let's start by checking the x-axis.
if (overlap.x == 0.0f) { }
If the condition is true, we need to see whether the other object is on the left or right side of our AABB.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { } else { } }
Finally, if it's on the right then set the mPushesRightObject
to true and set the speed so it's no greater than 0, because our object can no longer move to the right as the path is blocked.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { } }
Let's handle the left side the same way.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } }
Finally, we know that we won't need to do anything else here, so let's continue to the next loop iteration.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } continue; }
Let's handle the y-axis the same way.
if (overlap.x == 0.0f) { if (other.mAABB.center.x > mAABB.center.x) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } continue; } else if (overlap.y == 0.0f) { if (other.mAABB.center.y > mAABB.center.y) { mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } continue; }
This is also a good place to set the variables for a kinematic body, if we need to do so. We wouldn't care if the overlap is equal to zero or not, because we are not going to move a kinematic object anyway. We'd also need to skip the speed adjustment as we wouldn't want to stop a kinematic object. We'll skip doing all this for the demo, though, as we're not going to use the helper variables for kinematic objects.
Now that this is covered, we can handle the objects which have properly overlapped with our AABB. Before we do that, though, let me explain the approach I took to collision response in the demo.
First of all, if the object does not move, and we bump into it, the other object should remain unmoved. We treat it as a kinematic body. I decided to go this way because I feel it's more generic, and the pushing behaviour can always be handled further down the line in the custom update of a particular object.
If both objects were moving during the collision, we split the overlap between them based on their speed. The faster they were going, the larger part of the overlap value they will be moved back.
The last point is, similarly to the tilemap response approach, if an object is falling and while going down it scratches another object even by one pixel horizontally, the object will not slide off and continue going down, but will stand on that one pixel.
I think this is the most malleable approach, and modifying it should not be very hard if you want to handle some response differently.
Let's continue the implementation by calculating the absolute speed vector for both objects during the collision. We'll also need the sum of the speeds, so we know what percentage of the overlap our object should be moved.
Vector2 absSpeed1 = new Vector2(Mathf.Abs(data.pos1.x - data.oldPos1.x), Mathf.Abs(data.pos1.y - data.oldPos1.y)); Vector2 absSpeed2 = new Vector2(Mathf.Abs(data.pos2.x - data.oldPos2.x), Mathf.Abs(data.pos2.y - data.oldPos2.y)); Vector2 speedSum = absSpeed1 + absSpeed2;
Note that instead of using the speed saved in the collision data, we're using the offset between the position at the time of collision and the frame prior to that. This will just be more accurate in this case, as the speed is representing movement vector before the physical correction. The positions themselves are corrected if the object has hit a solid tile, for example, so if we want to get a corrected speed vector we should calculate it like this.
Now let's start calculating the speed ratio for our object. If the other object is kinematic, we'll set the speed ratio to one, to make sure that we move the whole overlap vector, honoring the rule that the kinematic object should not be moved.
float speedRatioX, speedRatioY; if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else { }
Now let's start with an odd case in which both objects overlap each other but both have no speed whatsoever. This shouldn't really happen, but if an object is spawned overlapping another object, we'd like them to move apart naturally. In that case, we'd like both of them to move by 50% of the overlap vector.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else { if (speedSum.x == 0.0f && speedSum.y == 0.0f) { speedRatioX = speedRatioY = 0.5f; } }
Another case is when the speedSum
on the x axis is equal to zero. In that case we calculate the proper ratio for the y axis, and set that we should move 50% of the overlap for the x axis.
if (speedSum.x == 0.0f && speedSum.y == 0.0f) { speedRatioX = speedRatioY = 0.5f; } else if (speedSum.x == 0.0f) { speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; }
Similarly we handle the case where the speedSum
is zero only on the y-axis, and for the last case we calculate both ratios properly.
if (other.mIsKinematic) speedRatioX = speedRatioY = 1.0f; else { if (speedSum.x == 0.0f && speedSum.y == 0.0f) { speedRatioX = speedRatioY = 0.5f; } else if (speedSum.x == 0.0f) { speedRatioX = 0.5f; speedRatioY = absSpeed1.y / speedSum.y; } else if (speedSum.y == 0.0f) { speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = 0.5f; } else { speedRatioX = absSpeed1.x / speedSum.x; speedRatioY = absSpeed1.y / speedSum.y; } }
Now that the ratios are calculated, we can see how much we need to offset our object.
float offsetX = overlap.x * speedRatioX; float offsetY = overlap.y * speedRatioY;
Now, before we decide whether we should move the object out of collision on the x axis or the y axis, let's calculate the direction from which the overlap has happened. There are three possibilities: either we bumped into another object horizontally, vertically, or diagonally.
In the first case, we want to move out of overlap on the x-axis, in the second case we want to move out of the overlap on the y axis, and in the last case, we want to move out of overlap on whichever axis had the least overlap.
Remember that to overlap with another object we need the AABBs to overlap each other on both x and y axes. To check whether we bumped into an object horizontally, we'll see if the previous frame we were already overlapping the object on the y-axis. If that's the case, and we haven't been overlapping on the x-axis, then the overlap must have happened because in the current frame the AABBs started to overlap on the x axis, and therefore we deduct that we bumped into another object horizontally.
First off, let's calculate if we overlapped with the other AABB in the previous frame.
bool overlappedLastFrameX = Mathf.Abs(data.oldPos1.x - data.oldPos2.x) < mAABB.HalfSizeX + other.mAABB.HalfSizeX; bool overlappedLastFrameY = Mathf.Abs(data.oldPos1.y - data.oldPos2.y) < mAABB.HalfSizeY + other.mAABB.HalfSizeY;
Now let's set up the condition for moving out of the overlap horizontally. As explained before, we needed to have overlapped on the y axis and not overlapped on the x axis in the previous frame.
if (!overlappedLastFrameX && overlappedLastFrameY) { }
If that's not the case, then we will move out of overlap on the y-axis.
if (!overlappedLastFrameX && overlappedLastFrameY) { } else { }
As mentioned above, we also need to cover the scenario of bumping into the object diagonally. We bumped into the object diagonally if our AABBs weren't overlapping in the previous frame on any of the axes, because we know that in the current frame they are overlapping on both, so the collision must have happened on both axes simultaneously.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && overlappedLastFrameY)) { } else { }
But we want to move out of the overlap on the axis in case of a diagonal bump only if the overlap on the x axis is smaller than the overlap on the y-axis.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { } else { }
That's all the cases solved. Now we actually need to move out the object from the overlap.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { mPosition.x += offsetX; if (overlap.x < 0.0f) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } } else { }
As you can see, we handle it very similarly to the case where we just barely touch another AABB, but additionally we move our object by the calculated offset.
The vertical correction is done the same way.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && !overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { mPosition.x += offsetX; if (overlap.x < 0.0f) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } } else { mPosition.y += offsetY; if (overlap.y < 0.0f) { mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } }
That's almost it; there's just one more caveat to cover. Imagine the scenario in which we land on two objects simultaneously. We have two nearly identical collision data instances. As we iterate through all the collisions, we correct the position of the collision with the first object, moving us up a bit.
Then, we handle the collision for the second object. The saved overlap at the time of collision is no longer up to date, as we have already moved from the original position, and if we were to handle the second collision the same we handled the first one, we would again move up a little bit, making our object get corrected twice the distance it was supposed to.
To fix this issue, we'll keep track of how much we've already corrected the object. Let's declare the vector offsetSum
right before we start iterating through all the collisions.
Vector2 offsetSum = Vector2.zero;
Now, let's make sure to add up all the offsets we applied to our object in this vector.
if ((!overlappedLastFrameX && overlappedLastFrameY) || (!overlappedLastFrameX && !overlappedLastFrameY && Mathf.Abs(overlap.x) <= Mathf.Abs(overlap.y))) { mPosition.x += offsetX; offsetSum.x += offsetX; if (overlap.x < 0.0f) { mPushesRightObject = true; mSpeed.x = Mathf.Min(mSpeed.x, 0.0f); } else { mPushesLeftObject = true; mSpeed.x = Mathf.Max(mSpeed.x, 0.0f); } } else { mPosition.y += offsetY; offsetSum.y += offsetY; if (overlap.y < 0.0f) { mPushesTopObject = true; mSpeed.y = Mathf.Min(mSpeed.y, 0.0f); } else { mPushesBottomObject = true; mSpeed.y = Mathf.Max(mSpeed.y, 0.0f); } }
And finally, let's offset each consecutive collision's overlap by the cumulative vector of corrections we've done so far.
var overlap = data.overlap - offsetSum;
Now if we land on two objects of the same height at the same time, the first collision would get processed properly, and the second collision's overlap would be offset to zero, which would no longer move our object.
Now that our function is ready, let's make sure we use it. A good place to call this function would be after the CheckCollisions
call. This will require us to split our UpdatePhysics
function into two parts, so let's create the second part right now, in the MovingObject
class.
public void UpdatePhysicsP2() { UpdatePhysicsResponse(); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; }
In the second part we call our freshly finished UpdatePhysicsResponse
function and update the general pushes left, right, bottom, and top variables. After this, we only need to apply the position.
public void UpdatePhysicsP2() { UpdatePhysicsResponse(); mPushesBottom = mPushesBottomTile || mPushesBottomObject; mPushesRight = mPushesRightTile || mPushesRightObject; mPushesLeft = mPushesLeftTile || mPushesLeftObject; mPushesTop = mPushesTopTile || mPushesTopObject; //update the aabb mAABB.center = mPosition; //apply the changes to the transform transform.position = new Vector3(Mathf.Round(mPosition.x), Mathf.Round(mPosition.y), mSpriteDepth); transform.localScale = new Vector3(ScaleX, ScaleY, 1.0f); }
Now, in the main game update loop, let's call the second part of the physics update after the CheckCollisions
call.
void FixedUpdate() { for (int i = 0; i < mObjects.Count; ++i) { switch (mObjects[i].mType) { case ObjectType.Player: case ObjectType.NPC: ((Character)mObjects[i]).CustomUpdate(); mMap.UpdateAreas(mObjects[i]); mObjects[i].mAllCollidingObjects.Clear(); break; } } mMap.CheckCollisions(); for (int i = 0; i < mObjects.Count; ++i) mObjects[i].UpdatePhysicsP2(); }
Done! Now our objects cannot overlap over each other. Of course, in a game setting we'd need to add a few things such as collision groups, etc., so it's not mandatory to detect or respond to collision with every object, but these are things which are dependent on how you want to have things set up in your game, so we are not going to delve into that.
Summary
That's it for another part of the simple 2D platformer physics series. We made use of the collision detection mechanism implemented in the previous part to create a simple physical response between objects.
With these tools it's possible to create standard objects such as moving platforms, pushing blocks, custom obstacles, and many other kinds of objects which cannot really be a part of the tilemap, but still need to be part of the level terrain in some way. There's one more popular feature that our physics implementation is lacking still, and those are the slopes.
Hopefully in the next part we'll start extending our tilemap with the support for these, which would complete the basic set of features a simple physics implementation for a 2D platformer should have, and that would end the series.
Of course, there's always room for improvement, so if you have a question or a tip on how to do something better, or just have an opinion on the tutorial, feel free to use the comment section to let me know!