Trouble with Solid Collision Response

Hello everyone!
I have an issue that I’ve trying to sort out on-and-off for about a month now. I’m making a platformer game, and I’m trying to make it so that the player can’t go through tiles, as one does. Here is my code:

                    if (Velocity.X < 0)
                    {
                        if (HitBox.Left < tile.Right)
                        {
                            Position.X = tile.Right;
                        }
                    }
                    else if (Velocity.X > 0)
                    {
                        if (HitBox.Right > tile.Left)
                        {
                            Position.X = tile.Left - Width;
                        }
                    }

                    if (Velocity.Y < 0)
                    {
                        if (HitBox.Top < tile.Bottom)
                        {
                            Position.Y = tile.Bottom;
                        }
                    }
                    else if (Velocity.Y > 0)
                    {
                        if (HitBox.Bottom > tile.Top)
                        {
                            Position.Y = tile.Top - Height;
                        }
                    }

This code runs for each tile that the player is colliding with.

Here is what is confusing me:

  1. When I am only checking on one axis, it runs as expected; the player can’t phase through tiles on said axis.
  2. When I am checking both axes (like in the code above), and moving the player only on one axis at a time (cardinal directions), it runs as expected again.
  3. When he player is trying to move through a tile (towards the tile), and it is being moved on the other axis as well (parallel to the tile), things get complicated. At this point, I believe whether the player is moving on the x or y axis is irrelevant (correct me if I’m wrong. If I’m moving the player into a tile, and moving them in the negative direction on the other axis, it gets snapped to the positive (right or bottom) side of the current tile (maintaining that direction results in this happening frame after frame, resulting in the player quickly sliding in that direction). If I’m moving the player into a tile, and moving them in the positive direction on the other axis, it get snapped to the negative (left or top) side of the current tile.

My goal is simple. The player shouldn’t be able to go through tiles. For some reason this has been incredibly difficult to accomplish, and I’m not sure why what I’m doing doesn’t work. I’ve messed around with adding continue statements in the code, and updating the hitbox to the position, but nothing has worked. I tried to write out my situation as clearly as possible, but if somebody thinks it would be helpful for me to provide a video, just tell me. Thanks in advance.

Your code isn’t working because it just checks the X axis first and the Y axis second. If the player is below a tile, and moves diagonally up-left into the tile, instead of stopping the player at the bottom edge, it will move the player to the left edge, since the left axis code runs first.

So, if that’s the case, how do you determine what side the player should be moved to? It’s a problem I had some struggles with until I figured out a solution. I once wrote a blog post about it and it would have been really easy if I could just link you to it, but unfortunately I made the mistake of deleting the blog post some time back and it is unrecoverable. I still have the text for the blog post, but I don’t have the illustrations I made that went along with it. I’ll see if I can summarize it in a intelligible way.

Store the location of the player from the previous frame. This means having two Rectangle fields, one storing the player’s current location, and one storing the player’s previous location.

With that information, there are four cases where determining the side that collision occurred is simple, but only if just one of the cases is true, and the rest are false.

  • The right side of the previous hitbox is to the left of the wall’s hitbox (left side collision)
  • The left side of the previous hitbox is to the right of the wall’s hitbox (right side collision)
  • The top side of the previous hitbox is below the wall’s hitbox (bottom side collision)
  • The bottom side of the previous hitbox is above the wall’s hitbox (top side collision)

If more than one of those statements is true, then you’ll need to use more than just the player’s previous hitbox in order to resolve the collision properly: you’ll need the character’s velocity. In other words, you’ll need to know how the character moved, both horizontally and vertically, between the previous frame and the current frame. The velocity can be used to determine exactly what side was “hit” first.

To get the correct side, you’ll need to compare the slope of the velocity to the slope of the line going between the two closest corners of the tile and the player’s previous hitbox.

Rather than reinventing the wheel, you can just use the code I made:

using Microsoft.Xna.Framework;
using System;

namespace YourNamespaceHere
{
    /// <summary>An enumeration of the possible sides at which 2D collision occurred.</summary>
    [Flags]
    public enum CollisionSide
    {
        /// <summary>No collision occurred.</summary>
        None = 0,
        /// <summary>Collision occurred at the top side.</summary>
        Top = 1,
        /// <summary>Collision occurred at the bottom side.</summary>
        Bottom = 2,
        /// <summary>Collision occurred at the left side.</summary>
        Left = 4,
        /// <summary>Collision occurred at the right side.</summary>
        Right = 8
    }

    /// <summary>A collection of helper methods for 2D collision detection and response.</summary>
    public static class CollisionHelperAABB
    {
        /// <summary>Calculates which side of a stationary object 
        /// a moving object has collided with.</summary>
        /// <param name="movingObjectPreviousHitbox">The moving object's previous hitbox,
        /// from the frame prior to when collision occurred.</param>
        /// <param name="stationaryObjectHitbox">The stationary object's hitbox.</param>
        /// <param name="movingObjectVelocity">The moving object's velocity 
        /// during the frame in which the collision occurred.</param>
        /// <returns>The side of the stationary object the moving object has collided with.</returns>
        public static CollisionSide GetCollisionSide(
            Rectangle movingObjectPreviousHitbox,
            Rectangle stationaryObjectHitbox,
            Vector2 movingObjectVelocity)
        {
            double cornerSlopeRise = 0;
            double cornerSlopeRun = 0;

            double velocitySlope = movingObjectVelocity.Y / movingObjectVelocity.X;

            //Stores what sides might have been collided with
            CollisionSide potentialCollisionSide = CollisionSide.None;

            if (movingObjectPreviousHitbox.Right <= stationaryObjectHitbox.Left)
            {
                //Did not collide with right side; might have collided with left side
                potentialCollisionSide |= CollisionSide.Left;

                cornerSlopeRun = stationaryObjectHitbox.Left - movingObjectPreviousHitbox.Right;

                if (movingObjectPreviousHitbox.Bottom <= stationaryObjectHitbox.Top)
                {
                    //Might have collided with top side
                    potentialCollisionSide |= CollisionSide.Top;
                    cornerSlopeRise = stationaryObjectHitbox.Top - movingObjectPreviousHitbox.Bottom;
                }
                else if (movingObjectPreviousHitbox.Top >= stationaryObjectHitbox.Bottom)
                {
                    //Might have collided with bottom side
                    potentialCollisionSide |= CollisionSide.Bottom;
                    cornerSlopeRise = stationaryObjectHitbox.Bottom - movingObjectPreviousHitbox.Top;
                }
                else
                {
                    //Did not collide with top side or bottom side or right side
                    return CollisionSide.Left;
                }
            }
            else if (movingObjectPreviousHitbox.Left >= stationaryObjectHitbox.Right)
            {
                //Did not collide with left side; might have collided with right side
                potentialCollisionSide |= CollisionSide.Right;

                cornerSlopeRun = movingObjectPreviousHitbox.Left - stationaryObjectHitbox.Right;

                if (movingObjectPreviousHitbox.Bottom <= stationaryObjectHitbox.Top)
                {
                    //Might have collided with top side
                    potentialCollisionSide |= CollisionSide.Top;
                    cornerSlopeRise = movingObjectPreviousHitbox.Bottom - stationaryObjectHitbox.Top;
                }
                else if (movingObjectPreviousHitbox.Top >= stationaryObjectHitbox.Bottom)
                {
                    //Might have collided with bottom side
                    potentialCollisionSide |= CollisionSide.Bottom;
                    cornerSlopeRise = movingObjectPreviousHitbox.Top - stationaryObjectHitbox.Bottom;
                }
                else
                {
                    //Did not collide with top side or bottom side or left side;
                    return CollisionSide.Right;
                }
            }
            else
            {
                //Did not collide with either left or right side; 
                //must be top side, bottom side, or none
                if (movingObjectPreviousHitbox.Bottom <= stationaryObjectHitbox.Top)
                    return CollisionSide.Top;
                else if (movingObjectPreviousHitbox.Top >= stationaryObjectHitbox.Bottom)
                    return CollisionSide.Bottom;
                else
                    //Previous hitbox of moving object was already colliding with stationary object
                    return CollisionSide.None;
            }

            //Corner case; might have collided with more than one side
            //Compare slopes to see which side was collided with
            return GetCollisionSideFromSlopeComparison(potentialCollisionSide,
                velocitySlope, cornerSlopeRise / cornerSlopeRun);
        }

        /// <summary>Gets which side of a stationary object was collided with by a moving object
        /// by comparing the slope of the moving object's velocity and the slope of the velocity 
        /// that would have caused the moving object to be touching corners with the stationary
        /// object.</summary>
        /// <param name="potentialSides">The potential two sides that the moving object might have
        /// collided with.</param>
        /// <param name="velocitySlope">The slope of the moving object's velocity.</param>
        /// <param name="nearestCornerSlope">The slope of the path from the closest corner of the
        /// moving object's previous hitbox to the closest corner of the stationary object's
        /// hitbox.</param>
        /// <returns>The side of the stationary object with which the moving object collided.
        /// </returns>
        static CollisionSide GetCollisionSideFromSlopeComparison(
            CollisionSide potentialSides, double velocitySlope, double nearestCornerSlope)
        {
            if ((potentialSides & CollisionSide.Top) == CollisionSide.Top)
            {
                if ((potentialSides & CollisionSide.Left) == CollisionSide.Left)
                    return velocitySlope < nearestCornerSlope ? 
                        CollisionSide.Top : CollisionSide.Left;
                else if ((potentialSides & CollisionSide.Right) == CollisionSide.Right)
                    return velocitySlope > nearestCornerSlope ? 
                        CollisionSide.Top : CollisionSide.Right;
            }
            else if ((potentialSides & CollisionSide.Bottom) == CollisionSide.Bottom)
            {
                if ((potentialSides & CollisionSide.Left) == CollisionSide.Left)
                    return velocitySlope > nearestCornerSlope ? 
                        CollisionSide.Bottom : CollisionSide.Left;
                else if ((potentialSides & CollisionSide.Right) == CollisionSide.Right)
                    return velocitySlope < nearestCornerSlope ? 
                        CollisionSide.Bottom : CollisionSide.Right;
            }
            return CollisionSide.None;
        }

        /// <summary>Returns a Vector2 storing the correct location of a moving object 
        /// after collision with a stationary object has been resolved.</summary>
        /// <param name="movingObjectHitbox">The hitbox of the moving object colliding with a
        /// stationary object.</param>
        /// <param name="stationaryObjectHitbox">The hitbox of the stationary object.</param>
        /// <param name="collisionSide">The side of the stationary object with which the moving
        /// object collided.</param>
        /// <returns>A Vector2 storing the corrected location of the moving object 
        /// after resolving its collision with the stationary object.</returns>
        public static Vector2 GetCorrectedLocation(Rectangle movingObjectHitbox,
            Rectangle stationaryObjectHitbox, CollisionSide collisionSide)
        {
            Vector2 correctedLocation = movingObjectHitbox.Location.ToVector2();
            switch (collisionSide)
            {
                case CollisionSide.Left:
                    correctedLocation.X = stationaryObjectHitbox.X - movingObjectHitbox.Width;
                    break;
                case CollisionSide.Right:
                    correctedLocation.X = stationaryObjectHitbox.X + stationaryObjectHitbox.Width;
                    break;
                case CollisionSide.Top:
                    correctedLocation.Y = stationaryObjectHitbox.Y - movingObjectHitbox.Height;
                    break;
                case CollisionSide.Bottom:
                    correctedLocation.Y = stationaryObjectHitbox.Y + stationaryObjectHitbox.Height;
                    break;
            }
            return correctedLocation;
        }

        /// <summary>Returns the distance between the centers of two Rectangles.</summary>
        /// <param name="firstRectangle">The first Rectangle to compare.</param>
        /// <param name="secondRectangle">The second Rectangle to compare.</param>
        /// <returns>The distance between the centers of the two Rectangles.</returns>
        public static float GetDistance(Rectangle firstRectangle, Rectangle secondRectangle)
        {
            Vector2 firstCenter = new Vector2(firstRectangle.X + firstRectangle.Width / 2f,
                firstRectangle.Y + firstRectangle.Height / 2f);
            Vector2 secondCenter = new Vector2(secondRectangle.X + secondRectangle.Width / 2f,
                secondRectangle.Y + secondRectangle.Height / 2f);
            return Vector2.Distance(firstCenter, secondCenter);
        }

        /// <summary>Returns the squared distance between the centers of two Rectangles.</summary>
        /// <param name="firstRectangle">The first Rectangle to compare.</param>
        /// <param name="secondRectangle">The second Rectangle to compare.</param>
        /// <returns>The squared distance between the centers of the two Rectangles.</returns>
        public static float GetDistanceSquared(
            Rectangle firstRectangle, 
            Rectangle secondRectangle)
        {
            Vector2 firstCenter = new Vector2(firstRectangle.X + firstRectangle.Width / 2f,
                firstRectangle.Y + firstRectangle.Height / 2f);
            Vector2 secondCenter = new Vector2(secondRectangle.X + secondRectangle.Width / 2f,
                secondRectangle.Y + secondRectangle.Height / 2f);
            return Vector2.DistanceSquared(firstCenter, secondCenter);
        }
    }
}

Thank you for this, it looks very comprehensive. Is there not a simpler way to do this? All of the tutorials I’ve read seem to ignore this issue and I’m just curious about why that is.

Well, there is a simpler way, but it could lead to incorrect resolution of the collision. You could simply check which side the intersecting player hitbox is closest to, or which side to move the player to that will require the least movement. Those solutions may be “good enough”, depending, but if you want to resolve AABB (axis-aligned bounding box) collisions as accurately as possible for corner cases, my method will do it.

I’ve created an illustration to explain what I mean:

collision resolution explanation

In this illustration, the light green box is the player’s location on the previous frame. The darker green box is the player’s location on the current frame, the one where collision occurred. The blue box is a stationary object, like a wall.

The green line traces the path of the player’s top-left corner between frames. As you can see, it “hits” the wall on its bottom edge, meaning the player’s location should be resolved to be below the wall. But if you simply moved the player the minimum distance possible, or to the closest side, it would resolve the player to be to the right of the wall.

My code essentially compares the two lines seen in the illustration, the line drawn from the closest corner of the player’s previous location to the closest corner of the wall, and the line drawn from the corner of the player’s previous hitbox to the corner of the player’s current hitbox. By comparing the slope of the two lines, the correct side to resolve the player to can be determined.

Thank you this is helpful. I’ll absolutely give this a try.

No problem! Hopefully that helps. Another thing to keep in mind is the “catching” problem. Here’s a modified quote from my lost blog post:

This collision detection and response algorithm works great when you’re only considering two objects, but what if, for instance, your main character moved in such a way that his or her hitbox is now intersecting with the hitboxes of two or more walls? Since the algorithm can’t account for more than one hitbox for the stationary object, you would have to run it on each wall individually. This leads to an unfortunate problem: the result of the collision resolution will depend on which wall you run the algorithm on first. This can lead to your character “catching” on walls that were flush with other walls.

Solution: Prioritize closest objects

By first resolving collision with the objects closest to the hitbox from the previous frame, the collision algorithm will behave properly; its behavior will stay the same regardless of the order of the objects in your game object collection, and it won’t exhibit the “catching” behavior.

Also, keep in mind that with this type of collision detection and response method, it’s possible for a moving object to “tunnel” through another object if it’s moving so fast that its hitbox skips over the other object’s hitbox between frames. If you run into that problem, you’ll either need to lower the speed of the object or do multiple collision checks per frame in a step-wise fashion along the path that the moving object takes. Or you could use ray-casting collision techniques, but I don’t know how to do that.