Collision Model

This page describes the collision model for Super Mario Limitless. Additionally, it includes a basic explanation of 2D collision detection and resolution.

Collision Shapes
In SML, there are two primary shapes that can be checked for collision - the bounding rectangle and the right triangle, represented by  and , respectively. Both of these types derive from the  interface, which defines a property that returns the shape of the object and a method that returns the resolution distance, but more on that later.

Bounding Rectangle
The bounding rectangle is a two-dimensional shape made of four line segments. Each line segment is parallel to one of the axes and perpendicular to the other. Two of the line segments are vertical lines, parallel to the Y axis, and the length of these lines is called the height. The other two line segments are horizontal lines. parallel to the X axis, and the length of these lines is called the width.

Rectangles are fundamentally defined by four numbers: From these four numbers, the following properties can be derived:
 * X: The distance in pixels between the Y axis and the left edge of the rectangle.
 * Y: The distance in pixels between the X axis and the top edge of the rectangle.
 * Width: The distance in pixels between the left edge and the right edge of the rectangle.
 * Height: The distance in pixels between the top edge and the bottom edge of the rectangle.
 * Top (float): The Y component.
 * Bottom (float): The sum of the Y component and the height.
 * Left (float): The X component.
 * Right (float) The sum of the X component and the width.
 * Center (Vector2): (x = X + (Width / 2), y = Y + (Height / 2))
 * TopCenter (Vector2): (x = X + (Width / 2), y = Y)
 * BottomCenter (Vector2): (x = X + (Width / 2), y = Bottom)
 * LeftCenter (Vector2): (x = Left, y = Y + (Height / 2))
 * RightCenter (Vector2): (x = Right, y = Y + (Height / 2))
 * TopLeft (Vector2): (x = Left, y = Right)
 * TopRight (Vector2): (x = Right, y = Top)
 * BottomLeft (Vector2): (x = Left, y = Bottom)
 * BottomRight (Vector2): (x = Left, y = Top)

Right Triangle
The right triangle is a two-dimensional shape that is best described as a rectangle split in half corner-to-corner. Two of the line segments of the original rectangle are preserved, and two are lost. In place of the lost line segments is a single, sloped line segment that stretches between the corners of the remaining two lines. There is also a normal line that runs coincident to the sloped line segment, but this typically isn't used in collision handling.

A right triangle is defined with a rectangle as the bounds and an instance of an enumeration that states which two sides of the rectangle are "split off". The following properties can be derived: *Point90 (Vector2): The point of the right angle. The positions of Point1 and Point2 vary by which sides are sloped:
 * Point1 (Vector2): The point at the bottom.
 * Point2 (Vector2): The point at the top.
 * Slope (float): Rise over run - height over width.
 * Y-Intersect (float): In algebra, lines are defined using the linear equation y = mx + b, where the variable b is the y-intersect. This is the point on the Y-axis where the coincident line intersects it. Solved for XNA's flipped Y-axis, the equation to get the Y-intercept of a line is b = mx + y.

Getting a Point on the Sloped Line
For rectangles, getting a point along one of the lines is trivial. All four lines are straight, meaning that every point on each line is always equal to at least one of the four cardinal direction properties.

Right triangles are a little different, as the third side is not defined by a single property. The slope-intercept form of y = mx + b can be solved for any right triangle with a defined slope (m), y-intersect (b), and any x-value. Now, XNA flips the Y-axis (positive Y goes down, not up), so the equation is changed to y = b - mx. Therefore, as long as you have an X value, you can get the Y value directly on the slope. In the RightTriangle class, this is solved for us, we just need to call GetPointOnLine(float x).

If the intention is to just get a point on the slope (and not its coincident line), simple range checking is needed. If the X value is less than the Left property of the bounds, or if the X value is more than the Right property of the bounds, then there is no point on the slope, and a zero vector is returned; otherwise, the standard slope-intercept equation can be used. The RightTriangle method GetPointOnSlope(float x) can be used for this result.

Is one rectangle intersecting another?
Let's start with two rectangles, A and B. We want to check if, true or false, A is intersecting B.

First, we can apply the separation of axes theorem: if there's one axis on which one rectangle isn't intersecting another, then the rectangles aren't intersecting.

Our first check is on the X-axis. If the right edge of A is to the left of the left edge of B, then A is to the left of B and they're not intersecting.

if (a.Right < b.Left) {       return false; }

Next, if the left edge of A is to the right of the right edge of B, then A is to the right of B and they're not intersecting.

else if (a.Left > b.Right) {       return false; } Now, we handle the Y-axis, but it's mostly the same. If the bottom edge of A is higher than the top edge of B, A is above B and they're not intersecting.

else if (a.Bottom < b.Top) {       return false; }

Finally, if the top edge of A is lower than the bottom edge of B, A is below B and they're not intersecting.

else if (a.Top > b.Bottom) {       return false; }

If we've gotten this far, we can know for certain that A and B are intersecting.

else {       return true; }

Performance wise, this isn't too bad. In the best case, we do one comparison; in the worst case, we do four.

How far is a rectangle intersecting another?
Of course, merely knowing that one rectangle is intersecting another probably isn't enough to resolve it. I mean, we can offset A randomly until the above check returns false, but I'm pretty sure that would get you fired, or at least get you glaring looks.

In the above section, we checked if a rectangle was colliding another, and returned false if we knew that it wasn't. Now, in the code to actually get the resolution distance, we can't return false - instead, we return Vector2.Zero.

The good thing about resolving collision is that it's entirely possible to handle one axis at a time. We'll start with the X axis, and then move on to the Y-axis.

If we've gotten past the above checks, we know that we're intersecting, we just don't know by how much. First, we need to consider that there are two cases of intersections that also, very conveniently, tells us which direction we need to move the rectangle by.

The first case is that the right edge of A is between the left and right edges of B. The left edge of A is not intersecting. B.Left < A.Right < B.Right In this case, the resolution is the distance between the right edge of A and the left edge of B. resolution = A.Right - B.Left Now, the direction in which to resolve such a collision is to the left. Because the direction of left has a negative X, we need to make the resolution negative. resolution = -resolution The second case is that the left edge of A is between the left and right edges of B. The right edge of A is not intersecting. B.Left < A.Left < B.Right In this case, the resolution is the distance between the right edge of B and the left edge of A. resolution = B.Right - A.Left The direction to resolve is, in this case, to the right. Since the direction of right has a positive X, the resolution does not need to be negated.

Resolution along the Y axis is pretty much the same, just substitute Left for Top and Bottom for Right in the above examples.

Case 1: B.Top < A.Bottom < B.Bottom resolution = A.Bottom - B.Top resolution = -resolution

Case 2: B.Top < A.Top < B.Bottom resolution = B.Bottom - A.Top

Applying the Shallowest Edge
There's a glaring flaw in the above model: almost always, the resolution will push A in two directions. That causes A to always be pushed to one of the corners of B instead of one of B's sides. That's obviously not how games handle it. So, what do they do?

We can apply the shallowest edge theorem to it - given a resolution distance, the component with the smallest value has the direction in which we should resolve. Let's consider the two cases.

In the first case, demonstrated in the image to the right, the X component of the resolution is less than the Y component. The most likely case that would cause this case is that some sprite ran into a solid block. In these cases, we always want to resolve either left or right - horizontally. In the example image, we want to resolve to the left, and the distance we want to push it is the X component, the smaller of the two. The Y component can just be ignored. All we have to do is set the Y component to zero to get the right resolution. resolution.Y = 0 The second case, demonstrated in the image to the right, is where the Y component is less than the X component. The most likely cause here is that a sprite fell onto a block from above, or it jumped into a block from below. In these cases, we want to resolve up or down - vertically. In the example image, we want to resolve up, and the distance we want to push it is the Y component, the smaller of the two. The X component can just be ignored. All we have to do is set the X component to zero to get the right resolution. resolution.X = 0 There's a corner case (literally) where the X and Y components are equal. I choose just to handle these horizontally, as if the X component was less than the Y component.

Note: don't forget that, if we want to resolve up or to the left, the resolution distance has to be negative. It's not a difficult thing, but it's very important to remember it.

Solving Rectangle-Triangle Intersections
In some ways, handling rectangle/triangle intersection is tricky, and in other ways it's simple. There are two critical things you have to remember about resolving these collisions:

Here's the entire process of getting the resolution distance between a rectangle (A) and a triangle (C). The interesting thing to note about slope collisions is that there's never any horizontal resolution distance. As such, shallowest edge doesn't really apply. This changes things, but just a little.
 * 1) The bottom-center/top-center point is the one checked against the slope, NOT the rectangle.
 * 2) Triangles have rectangular bounds which are checked first. If the resolution between the rectangle and the bounds rectangle resolves in the direction of one of the straight sides, then we can resolve it like a normal rectangle-rectangle intersection.
 * 1) Check the rectangle A against the rectangular bounds of C (let's call the bounds rectangle R).
 * 2) If A is not intersecting R, there's no collision, so let's return Vector2.Zero.
 * 3) If A is intersecting R, first check is the collision resolves in one of the directions of the flat sides. For example, on a TopLeft triangle, we'd want to check the bottom and the right sides.
 * 4) If the resolution's direction is the same direction as one of the straight sides, then we can just handle the collision as a rectangle-rectangle collision. If not...
 * 5) Get the position of the bottom-center (for TopLeft/TopRight triangles) or the top-center (for BottomLeft/BottomRight triangles) point of A. Next, we want to use the slope-intercept formula (for XNA, y = b - mx) to get the point on the slope for the X-coordinate of A.
 * 6) For TopLeft/TopRight triangles, the bottom-center point can be in one of two places - either it's above the slope or below the slope. If it's above the slope, we're not intersecting. If it's below the slope, the resolution is up, and the distance is the distance between the bottom-center point and the slope.
 * 7) For BottomLeft/BottomRight triangles, the top-center point can be in one of two places - either it's below the slope or above the slope. If it's below the slope, we're not intersecting. If it's above the slope, the resolution is down, and the distance is the distance between the top-center point and the slope.

Sprite Embedding
What if a sprite is encased inside of a wall? In this case and others like it, we consider the sprite to be embedded, and it doesn't follow the normal collision handling rules. The conditions to determine if a sprite is embedded are as follows. An embedded sprite is pushed to the left automatically every frame. It does so as long at least as one of the above conditions are true.
 * A sprite is embedded if it resolves on both horizontal and/or both vertical directions. For example, a sprite sandwiched between two tiles that are very close to each other might resolve both up and down.
 * A sprite is embedded if the final resolution distance (see The Collision Check Cycle below) is larger than it is.

The Sticky Corners Problem
So, we can tell if two rectangles are intersecting, and we can get the distance and direction to resolve it by. This should work in most cases, right? Wrong.

Consider a normal tile-based platformer. The player and other sprites will be usually standing on a row of rectangular tiles, all of which have equal Y components (they're all on the same distance from the top of the level). The question is, using the model described above, will the player and other sprites be able to walk across the top of the row of tiles? The answer is no, and here's why.

In the image on a right, we have a sprite colliding with a row of tiles. All the tiles have the same Y-coordinate and differing X-coordinates. The sprite is colliding with two tiles.

In the first collision on the left, the X component is less than the Y component, so the resolution is to the left. In the second collision, the Y component is less than the X component, and the resolution is up. Now, the second collision is what we want, but the first collision is not what we want. There's no reason why we'd want to resolve horizontally while walking across a flat row of tiles, just like there's no reason to be pushed back while walking along a flat section of ground. If we let this run, the sprite will be bounced back every time it collides with the leftmost tile, and it won't make any movement across the ground.

We can solve this, but in order to do so, we need to describe a bit of a framework.

Levels
The basic area of gameplay in platformers is the level. For this page, we'll consider a level to be a collection of tiles and sprites. Tiles come in two varieties: rectangular and sloped. Rectangular tiles contain an instance of a BoundingRectangle, and sloped tiles contain an instance of a RightTriangle. BoundingRectangles and RightTriangles have methods to return a resolution distance for any collision.

In order to check that there are collisions in a level, one must check a sprite to tiles near it. Since a mere list of tiles has no concept of "near", a different approach is taken.

The QuadTree is a commonly used type of spatial partioning that divides a large rectangular area into a group of smaller rectangular areas called cells. Sprites and tiles are placed within these cells, and sprites are checked against the tiles in its own cell (or cells if it's within more than one), rather than all the tiles in a level.

QuadTrees traditionally break down their space by counting how many objects are in a given cell, and splitting that cell into four cells if there's more than a certain number of objects within them.

SML implements what I call a lazy Quadtree. Rather than create cells based on how many objects are within them, the lazy Quadtree is merely composed of cells of a constant size. It should be somewhat faster in the assignment of objects to cells, and I don't expect that there will be too many sprites or tiles within one cell in most cases.