Tuesday, December 17, 2013

2D Platformer Collision Detection in Unity

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:
  1. Determine current player direction and movement
  2. For each axis, cast multiple outward rays
  3. 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.

1 comment:

  1. Great article, I tried to implement this in my project, Its a very clever way !

    ReplyDelete