NOTE: Please see my
addendum regarding this solution, which is ultimately flawed as presented in this post.
Unity is a 3D engine that comes with built-in physics engines (PhysX for 3D, Box2D for 2D). However, if you're aiming to develop a 2D platformer, you'll quickly find that it's extremely difficult, I'll go as far as to say impossible, to achieve that "platformer feel" using these physics engines. For your main entities, you're going to have to roll a variation of your own.
Furthermore, if you attempt to use the supplied character controller package for your player in a 2D platformer, you'll also quickly discover that the collision detection and overall controls just don't feel right, no matter how hard you tweak it. This is primarily due to that the character controller package uses a capsule collider, which makes pixel perfect collision detection on edged surfaces problematic. So once again, you need to roll your own controller and collision detection system.
Since Unity is a 3D engine and regardless of the type of game you're developing (3D or 2D), you're game is being developed in a 3D space. You probably can achieve some canonical tile-based solutions for collision detection (perform check-ahead on the tile the player is heading into, and determine appropriate collision to take, if any, or example), but it's best not to wrestle against the engine. The best solution I've found is to use ray casting.
Ray casting seems to refer to different things (see
Wikipedia), but the ray casting I'm referring to is the method of casting rays from an origin towards a direction and determine what intersects the ray, if anything. We can use this method to handle collision detection, casting rays from our player in both the x and y axes to learn about the environment surrounding the player and resolve any collisions.
The basic steps of the algorithm is as follows:
- Determine current player direction and movement
- For each axis, cast multiple outward rays
- For each ray cast hit, alter movement values on axis
I want to note that a lot of the following code is originally based off of the fantastic
platformer game tutorial found on the Unity forums. However, I've heavily modified it to fix some bugs, mainly with corner collisions, which we'll go into depth.
Here's the entire class that performs the ray casts for collision detection.
It's important to note that this class is called inside of a separate entity controller (
BasicEntityController) that handles calculating acceleration and creating the initial movement
Vector3 object.
BasicEntityCollision takes the movement and position
Vector3 objects and adjusts them based on any possible collision detected from the ray casts.
The
Init method does some one time initialization of required fields, such as setting reference to the controlling entities
BoxCollider, setting the collision
LayerMask, etc.
The
Move method accepts two
Vector3 objects and a
float.
moveAmount is, as the name implies, the amount to move
before collision detection, as calculated by
BasicEntityController.
position is the current entity position in the game world.
dirX is the current direction the entity is facing.
Move will determine the final x (
deltaX) and y (
deltaY) values to apply to
moveAmount after all collision detection.
Move starts the ray casting along the y-axis of the entity, followed by the x-axis of the entity, but only if they are moving left or right; we won't cast x-axis rays when the entity is idle. We then set the
finalTransform based on
deltaX &
deltaY and return it so that the entity can finally use it to
Translate!
Let's dive into the two key ray casting methods,
yAxisCollisions &
xAxisCollisions. First, note that both methods perform at least three different ray casts along the entities
BoxCollider, on each axis. This allows us to get complete coverage for the entity.
 |
Each line represents a ray cast. |
yAxisCollisions starts by determining which direction the entity is currently heading along the y-axis (up or down), and calculates separate x and y values to be used to create the Ray objects to be casted along the box collider (from left to right, top or bottom). yAxisCollisions calls two different
for loops based on which way the entity is currently facing. If we are facing towards the right, it'll start the ray casts on the right side of the entity, else it'll start on the left side of the entity. This was done to prevent a bug that saw the entity falling through the collision layer when moving to the right and downward due to a gap that was being created (because we break the
for loop after the first ray hit we encounter) when the entity would collide with the corner of a tile.
When
Physics.Raycast returns
true, that means a ray cast has hit. We obtain the distance of the hit from the ray origin, and calculate a new
deltaY to apply to the final move transform. We pad this value slightly to prevent the entity from falling through the collision layer accidentally.
 |
We pad the deltaY value slightly to keep the entity above the collision layer, to avoid accidental fall throughs. |
With our
deltaY calculated, we move on to the x-axis (again, only if the entity is actually moving along the x-axis).
xAxisCollisions is very similar to
yAxisCollisions, but simpler. We don't worry about which direction the entity is facing, instead we worry whether the entity is either moving on the ground or currently in mid air. If we are moving mid air, there's a high risk of landing on the corner of a tile, which we could fall through. To help prevent that, we cast a larger range of ray casts along the x-axis (4 instead of 3) with the outer 2 rays that are casted slightly outside of the box collider width. When a hit is detected on the x-axis, we simply set our
deltaX to 0 and return it.
 |
When moving through the air, we cast a wider range of rays slightly outside of the entity's boxCollider width. |
And that's the magic behind using ray casting to perform collision detection. The
finalTransform will be sent to the entity to be used with the
Translate method. Here's a small video clip showing the
Debug.DrawRay calls. When a ray is hit, it's colored yellow.
For further reading on the topic of a 2D platformer controller for Unity, there's an excellent
blog post on Gamasutra by Yoann Pignole that goes into great detail.
You can also see a more complete implementation of the collision detection code at the following
Gist.