Saturday, 4 January 2014

2D Platformer Collision Detection with Raycasts, Part 1: Gravity

Hello, squirrelfriends! Today I'll be writing about one of the things I've done in Unity: specifically, coding my own collision detection using certain aspects of Unity's physics, and simple maths & physics (for Recess Race!).

Why not just use Unity's physics?

Good question! The short answer is Unity's physics are meant for 3D movement, and they're great for that. Their basic character controller uses a capsule collider. It's pretty essential for 3D, but it doesn't work for classic SNES era 16 bit platformer tropes.

Consider this image:
Wile E. Coyote wishes he could do this

Now, that's not realistic. You can't stand like that! but it makes sense when you think about the strictly-rectangular collision detection they had at the time. It was a hardware/software restriction but they ran with it and made the restriction into an asset. Here's the same image with an approximation of his hit box:

He benefits from intangible toes, forehead, and right fist

The tiny part of the box touching the collision tiles supports him. Walk any further and you fall off. This feels very fair for the player: he's allowed to put his centre of gravity over a pit, but he can't support himself with just his toes. A little 'cheating' is enough. It also allows for what feels like really close calls - if an enemy swoops down and skims your helmet, but you don't get hurt, it creates a "WOW that was close" moment, which is fantastic.

Consider the same scene with a capsule collider now:

Paint capsules!

The only point that will touch the floor on a capsule is the bottom point. Since it's not touching anything, you'll fall. Then the curve of the capsule hits the corner of the tile, and you slide away from the wall and you fall. It's a different effect, and not one I'm going for in Recess Race,

Build it from the ground up (using Unity's construction materials and power tools)

So it's clear we can't use the packaged Unity character controller. It is impossible to separate from the capsule collider. So we'll have to build our own, using a box collider!

Aside: Unity Colliders, Events, and Rigidbodies

When you make a new script in Unity, it gives you the Start() and Update() functions built in. These are events that are called when Unity feels like it. Among these are the OnCollisionEnter() and OnTriggerEnter() functions. Their utility is paramount, but there are some issues. Both of them require having a Rigidbody class on one of your objects. It makes sense, because if it didn't discriminate it would be checking every collider against every collider every frame and that would take aeons.

Solution: put a Rigidbody (Component > Physics > Rigidbody) on your controller, uncheck 'Use gravity', open the Constraints class and check all of the boxes. This way we can have it use the Collision events and also control its movements at all times.

This is what your Rigidbody component should look like

Intro to Raycasts

If you're familiar as heck with Raycasts, a "Ray-man" if you will, you can skip this section.

Setup

The Raycast is a versatile tool in Unity, and it's surprisingly cheap. You can probably do hundreds of raycasts per frame and it won't do much to slow down your processing speed (but, the fewer the better).

A basic raycast in code looks like this:



This bit of code shoots a ray straight up from the object's pivot (transform.location) and tells you if there's anything above you (Raycast always returns a boolean). Not super useful on its own, but that's just the beginning. There are multiple ways we can call the Physics.Raycast function (12 overloads!). Let's add a couple more to add some more utility:


Here, we've added 2 new arguments to the Raycast function: a float, 'distance', and a RaycastHit. The float just tells the engine how far to check. In this instance, it will only check as far as I'm going to move this frame, which is very useful. It will tell me if I'm going to hit the ceiling! (This code is a little dubious and I'll explain why later).

The other addition is the RaycastHit, which uses the 'out' keyword next to its argument because it receives information (is assigned values) from the function if the Raycast returns true. This is supremely useful, as it tells you exactly how far the ray travelled before it connected, the normal of the surface it connected with, as well as a reference to the collider it hit. We will be using this to great effect later on.

The last thing we'll add to the Raycast function is the Layer mask. This tells the system to look for (or ignore) specific Layers. In Unity, you can give every object a different layer (up to 32 different ones), so this is handy when checking for collisions. In this case, I've set my collision objects to the layer, "normalCollisions". Here's what it should look like in code:



You can use a layer mask to check more than one layer. This post has an explanation of how.

So now we have a ray that only looks for collisions, it's time to make multiple rays shooting out in every direction to look for collisions ^_^

The Nitty Gritty

This is the method I've developed. It's probably been developed other places, but I don't have a single place to attribute my ideas, just a lot of bits and pieces which culminated in this system.

The Gist

The idea is to shoot a number of raycasts out each side of your box collider, and if any of them connect with something, do stuff. If you're checking underneath you, you're now grounded. If you're looking left or right, you hit a wall, and if you're checking up you've hit the ceiling. It's elegant, but tricky.

Setup

So first, let's set up some variables. Here's what I've got

Disclaimer: I use FixedUpdates for my velocity calculations, and update movement in the LateUpdate() function using Time.deltaTime as a multiplier. This gives precise, predictable movements.


This should keep us going through most of the explanation
Here's what I have in functions already:
  • set up of Layer Mask;
  • definition of 'box' and 'position'. On their own they do nothing, but it saves us a lot of typing later on. Mark every time I use 'box.x' I could instead type 'collider.bounds.x' but it's much longer;
  • and the application of movement at the end. Since it happens after everything else (LateUpdate), I can use the velocity variable knowing the object hasn't moved that distance yet.

Gravity

Let's start with the simplest operation: gravity. Gravity is present in every platformer (uh, I think), so it's a good place to start. This is all in the FixedUpdate() function.

Here's what the final thing looks like:



First, let's the actual gravity numbers to the velocity. We do this first so we can predict its location when we look for the ground.



Next, I figure out exactly where I want to check beneath me. For this I'll use the 'box' variable I define at the beginning of my FixedUpdate. One of the tricky things about Raycasts is that it only connects going in to a collider, not coming out of one. We can take advantage of this, though, by starting the rays in the middle of the player's box collider, so that it doesn't ever detect it, but will detect anything outside.

My method for determining where each of my rays starts uses the Vector3.Lerp function. I figure out where  my first ray and my last ray will start, then use the Lerp function to iterate from one to the other. These are vertical rays, and will all start from the same y position in the scene.

We also have to determine how long we want these rays to be. If we're on the ground, just using a flat number (in this case I'm just using my 'margin' variable). Otherwise, we want to base it on our velocity. The trouble is, if we're falling, our velocity is negative, and Raycasts need the 'distance' variable to be positive (this is the dubiousness I spoke of earlier). So, we're going to check down only if our 'falling' variable is set to true, which happens if our velocity is below 0. Then we have to make it positive with a Mathf.Abs() function. Also add half the box height since we're starting in the middle of the box.


These two points should look like this on our box:


All of our rays will start between those two points (inclusively). To iterate between them, use a 'for' loop. Before going into the loop, set up a boolean so we can use it afterwards.



Use the Lerp to pick the start point, then add your already-determined ray length.

Finally, when we determine if all of the rays hit or not, we deal with the results.

There is a mistake here - I multiply the ray's distance by Time.deltaTime which is unnecessary and causes you to float to the ground. Ignore it!


If we're in the loop, and we meet something, we're going to want to get out of the loop so the next ray doesn't set connected to false or something silly. You'll also want to make sure you're flush with the ground, by immediately translating down to the surface, and set your vertical velocity to 0. If you're not finding anything, you're no longer grounded (then the rest takes care of itself).

Also note: you can slip in an Event or Delegate function to call when you land (or as Unity likes to do, use the "Send Message" function for something like "OnLand"). This makes it easy to set land animations, or for other objects to pass functionality for this kind of event (like if you want to poison your character and have it end when they hit the ground or something).

That's it for now

That's it for gravity, really. There are fancier things you can do like determining the slope of the platform you're on, or move with a moving platform, which are all pretty easy to do with this foundation.

All the code is in screen shots, deliberately - I have always found it more beneficial to type stuff out yourself, even copying it directly, than pasting it and checking to see if it works. I hope you agree!

Next time I'll be talking about moving left and right and hitting the ceiling. However, the techniques are largely the same, so I encourage you to give it a go yourself :)

Thanks a lot for reading. Until next time!

Stay grounded, earth wizard.

-mysteriosum(the deranged hermit)


31 comments:

  1. Hi,

    Thanks for the awesome tutorials. I've been working on implementing a custom 2D character in Unity almost in parallel to your post and they have been really useful.

    I'm running into a crazy bug and since I know you have in the past or are currently working through many of the same problems I wanted to kick it to out and see if you have ever ran into the issue.

    I've been seeing inconsistencies with OnTriggerExit. Namely that it completely fails to fire on any object that I have attached a simple SpritePhysics script to. After many trial and error attempts I've got it narrowed down to this. Any entity that you move with transform.Translate will fail to fire the OnTriggerExit function. At least that is my hypothesis.

    I have my player object setup similar to how you describe up above but I found I additionally had to give it a 2DBoxCollider set to isTrigger. This way it doesn't interfere with the custom physics but you can still easily detect collisions against other entities.

    Have you seen this issue? If so do you know of a solution? Thanks for the awesome tutorials and thanks for helping a totally random on the internet. I'll be posting this on the Unity forums as well. If I hear anything back I'll come back and leave any useful info that I uncover.

    -Keith

    ReplyDelete
    Replies
    1. Hey Keith! Thanks for reading!

      One thing that must be noted is that with the new Unity 2D physics there are also 2D collision detection events. Are you using OnTriggerExit2D?

      I have moved my stuff with transform.Translate for a while and have had success triggering all the events, but there are a couple of things that could be causing your bug. I've found there has to be a Rigidbody attached to the object being moved, not the stationary one. The stationary object should also have isTrigger set to true. You may want to uncheck isTrigger on your player to see if it works to trigger OnTriggerExit.

      Beyond that I'd have to take a look at your project, because there's surely something funky going on.

      I hope this helps!

      -mysteriosum(the deranged hermit)

      Delete
  2. Hi Travis! Thanks so much for this tutoriall! I've been through several Unity3D tuts that treated platformers as Baby-Steps, and I've walked away with poorly optimized code that made it difficult to expand upon and add any nuance to. I appreciate this tutorial series because it shows me how to approach a platformer with Big Boy Code that is robust and simple, and along the way it has cleared up several misconceptions I've had about raycasts and layer masks, etc. :D

    First I want to recommend to every newbie that they visualize their ray casts by adding the line "Debug.DrawRay( origin, Vector3.down, Color.green );" inside the for loop, just after defining the ray variable. This lets us see exactly where our rays ARE

    Second there are two slight flaws that I want to point out:

    1) Due to order of operations, the first line of the for loop, which determines the lerpAmount, would only ever range from -1 to -.25 when I ran the code, and so the rays never got beyond the right side of my collider. I had to put parentheses around "verticalRays - 1" in order to ensure that my lerpAmount made sense for the interpolation.

    2) I think layer masks default to IGNORING the layer you specify, so I could not get my rays to connect with anything until I read the extra information about layer Masks and did a bitwise operation to tell Unity to USE that layer. Your mileage may vary, but that's how it worked for me. I'm saying this to make it very clear to anyone who had the same trouble I did, and is feeling super-stuck, as I was.

    I'm ultimately glad I had to dig into these things deeper on my own to get the script to work as expected, but I imagine it could be frustrating for anyone who is a total beginner, and this article is a bit "misleading" in those two areas.

    Again, great tutorial, and I look forward to going through the next few bits of it. :D

    ReplyDelete
    Replies
    1. Greetings, mysteryloaf!

      I'm glad you appreciate it! I went through a lot of trouble arriving at this point, and comments like these make it worthwhile =)

      I never knew about Debug.DrawRay, and that's blowing my mind and thank you so much XD

      As for the flaws, you're absolutely right - I recognised and fixed the for loop when I did the next tutorials, but I must have forgotten to add an addendum. Thanks for pointing it out!

      And yes, bitwise operations are essential for proper layermasking. What I've done is actually made a static class with bitwise operations for different layermasks I'll want to set up. It makes coding one-sided platforms really easy. If you want the player to jump up through a platform, but then land on it on the way down, such an operation (with proper layering) is the way to go.

      Thanks for the help!
      -mysteriosum(the deranged hermit)

      Delete
    2. Well I'm happy you found my comment worthwhile, and I'm thrilled I was able to show you something new! XD

      I going to have to request that you share your approach to layer masking and how it ties into one-sided platforms. Those kinds of platforms add a huge amount of depth to games made in this style, and I imagine my approach might be clunky. Maybe in a future installment you could address it? Hopefully!

      Thanks again for YOUR help, and for sharing your ideas.

      Delete
  3. Hi there. Been following your raycasting series. I've been going back and trying to convert the whole thing to the new 2d setup from unity. I'm about stuck at the part of converting this:

    box = new Rect(
    collider.bounds.min.x,
    collider.bounds.min.y,
    collider.bounds.max.x,
    collider.bounds.max.y
    );

    and I cant find a collider2d equivalent for .bounds. I was wondering if you had already figured out how to do this?

    ReplyDelete
    Replies
    1. Hallo. I have the same problem. I am getting the error that there is no Collider attached to the Player but I have already attached one. Next what I did was to make a BoxCollider2D variable and initialized it in the Start() method with collider = GetComponent(). But the my Player falls through the platform. What I am doing wrong?

      Delete
    2. Hey Robert! Thanks for reading my blog =]

      If you are following the tutorial from this post, everything detailed here uses the 3D physics engine: Physics.Raycast(), BoxCollider, and Rigidbody. If you want to use BoxCollider2D you'll also have to use Rigidbody2D and Physics2D.Raycast(). The differences are detailed here:

      http://deranged-hermit.blogspot.ca/2014/02/2d-physics-in-unity-with-raycasts-slopes.html

      BoxCollider2D boxCol = GetComponent();
      box = new Rect(
      t.position.x + boxCol.center.x - boxCol.size.x/2,

      t.position.y + boxCol.center.y - boxCol.size.y/2,
      boxCol.size.x,
      boxCol.size.y
      );

      Delete
    3. Hi again, I'm typing this code for the 2d box collider, hoping that this will solve the problem somehow, but when I wrote "t" It gave me an error. I don't understand... what does "t" mean? I'm new to raycasting, and to coding, and I'm also not english, so be patient.

      Thanks

      Delete
    4. Oh! I figured it out now. it means transform.

      Delete
    5. Hi Travis. Thanks for your reply. I managed it. I have another question. The hitInfos[i].fraction have always the value 0 in my code. I am not sure what these fractions are all about. Next is the layerMask in your code. I made one platform with default settings. What should happen when the ray hits the platform? Should the Layer change to "normalCollisions"? I know these are some stupid questions, but I am not that familiar with Unity. My Character is still falling through the platform. Thx.

      Delete
    6. Same here, if I write in the code "Debug.DrawRay (origin, -Vector2.down)" right after "hitInfos[i] = Physics2D.Raycast(origin, -Vector2.up, distance, layerMask);" for 2d, and for 3d "Ray ray = new Ray (origin, Vector3.down);", I see the rays in a weird position.

      Anyone can help?

      Delete
  4. HI there, thanks for the tutorial! I'd also like to work purely within 2d. Would doing so be much different from your tutorial? Also can you tell me all of the components you use for your character and the components of the ground/platform to collide with? As I can't get my version to work, I just fall through my platform. Thanks again!

    ReplyDelete
  5. Hello! I'm glad you liked it. I do switch over to 2D physics around the 3rd or 4th article. At some point I should re-do these or at least include the new versions.

    At any rate: your player object should have:
    -Box Collider 2D;
    -Rigidbody (Gravity set to 0)
    -Your physics script

    The colliders just need a BoxCollider2D (Should not be triggers but it doesn't actually matter).

    I hope it helps!

    ReplyDelete
    Replies
    1. Hi Travis, thanks for the reply. I have an issue when trying to add both a Box Collider 2D and RigidBody, to match yours.

      I get the message: "Can't add component 'Rigidbody' to Player because it conflicts with the existing BoxCollider 2D derived component."

      I've searched online and tried adding one and then the other, in both orders. I'm using Unity 4.3.4f1

      Many Thanks

      Delete
    2. I figured it out! It has to be a Rigidbody 2D =)

      Delete
    3. a rigidbody 2d doesn't have freeze position and rotation... do I have to set the rigidbody to isKinematic?

      Delete
    4. Hi, as I said yesterday a rigidbody 2d doesn't have freeze position and rotation. If you set gravity to 0, the rigidbody will not move, but rigidbody physics will still work. That means that if you set the gravity in code, then when the player falls on the ground the rigidbody will try to stop player's y velocity but the code says that your velocity should be equal to gravity. So for a second the player will stay on the ground, but if you wait he will fall.

      freeze position and rotation avoids that.

      Is that correct?

      Than how am I going to do this with a rigidbody 2d?

      Is there a way to freeze the position and rotation in rigidbody 2d?

      thank you,

      Leonardo

      Delete
    5. I'm now stucking also at this part. The Rigidbody2D stops the script from working :/ bounces of from the ground and hovering around. Hopefully someone figured it out already and read this.

      Delete
  6. Hi travis, thanks for the awesome series. A question, will the raycasting work fine if I use Physics2D.Raycast?

    ReplyDelete
    Replies
    1. Hey Ahmed! I'm glad you like it.

      I actually converted all this to Physics2D a few months ago. I went over the changes here:
      http://deranged-hermit.blogspot.ca/2014/02/2d-physics-in-unity-with-raycasts-slopes.html

      I hope that helps!

      Delete
  7. Hi Travis, I'm Leonardo and I have a problem with this script. I wrote it 3 times and I'm sure it was right, but when I started playing the game it looks like the character goes down at a really high velocity. Then I set gravity to 0.5 and it works normally, except that the character just falls through the ground.

    The character has a rigidbody attached to it, with mass = 1 (I know it doesn't matter but just to make sure everything is right), drag = 0, angular drag = 0.5, use gravity and iskinematic unchecked, interpolate : none, CollisionDetection : discrete, and freeze position and rotation in every axis.

    The player has a boxCollider with no physics material, and it has the playerPhysics script.

    this is the script:


    using UnityEngine;
    using System.Collections;

    public class PlayerPhysics_MovementAndControls : MonoBehaviour {

    //basic physics propetries, all in units/second
    float acceleration = 4f;
    float maxSpeed = 150f;
    float gravity = 6f;
    float maxfall = 200f;
    float jump = 200f;

    //a layer mask that I set in the Start() function
    int layerMask;

    //a Rectangle class has some useful tools for us
    Rect box;

    //my 2D velocity I use for most calculations
    Vector2 velocity;

    //checks
    bool grounded = false;
    bool falling = false;

    //variables for raycasting: how many rays, etc.
    int horizontalRays = 6;
    int verticalRays = 4;
    int margin = 2; //I don't check the very


    //Use this for initialization
    void Start () {
    layerMask = LayerMask.NameToLayer("normalCollisions");
    }

    //Update is called once per frame
    void FixedUpdate () {
    //this line of code will save us a lot of typing
    box = new Rect (
    collider.bounds.min.x,
    collider.bounds.min.y,
    collider.bounds.size.x,
    collider.bounds.size.y
    );

    //an elegant way to apply gravity. Subtract from y speed, with terminal velocity=maxfall
    if (!grounded)
    velocity = new Vector2 (velocity.x, Mathf.Max (velocity.y - gravity, -maxfall));

    if (velocity.y < 0) {
    falling = true;
    }

    if (grounded || falling) { //don't check anything if I'm moving up in the air

    Vector3 startPoint = new Vector3 (box.xMin + margin, box.center.y, transform.position.z);
    Vector3 endPoint = new Vector3 (box.xMax - margin, box.center.y, transform.position.z);

    RaycastHit hitInfo;

    //add half my box height since I'm starting in the centre
    float distance = box.height / 2 + (grounded ? margin : Mathf.Abs (velocity.y * Time.deltaTime));
    //this is a ternary operator that chooses how long
    //the ray will be based on whether I'm grounded


    //check if I hit anything. Starts false because if any ray connects I'm grounded
    bool connected = false;

    for (int i = 0; i < verticalRays; i ++) {
    //verticalRays -1 because otherwise we don't get to 1.0
    float lerpAmount = (float)i / (float)verticalRays - 1;
    Vector3 origin = Vector3.Lerp (startPoint, endPoint, lerpAmount);
    Ray ray = new Ray (origin, Vector3.down);

    connected = Physics.Raycast (ray, out hitInfo, distance, layerMask);

    if (connected) {
    grounded = true;
    falling = false;
    transform.Translate (Vector3.down * (hitInfo.distance - box.height / 2));
    velocity = new Vector2 (velocity.x, 0);
    break;
    }
    }

    if (!connected) {
    grounded = false;
    }
    }
    }

    void LateUpdate () {
    //apply movement. Time.deltaTime=time since last frame
    transform.Translate(velocity * Time.deltaTime);
    }
    }


    The ground under the player has the layer 'normalCollisions' (if it matters)

    The last thing I'm not sure of is in image n.9. At the end of the third if statement (if (grounded || falling) ) there is an open graph bracket, but I don't see the close graph bracked anywhere.

    Thanks in advance!

    Leonardo Temperanza.

    ReplyDelete
  8. Hello Travis,

    Your Tutorial was a savior as I needed help urgently and it was very helpful.
    Thank you so much, and I would recommend your website to all learners.

    Cheers!
    Sagar

    ReplyDelete
  9. Hi, im working in a project where the player jump to ceiling and stick to it, walk and run in the ceiling, when the player jump again fall in to the ground, i try many tutorial stuff but only work for one time the secont time show error o the player cant jump normal, please helpme with a tutorial with inverse gravity like you explain this tutorial..
    thanks

    ReplyDelete
  10. Thank you for the tutorials. As someone who has just recently found both the time and the motivation to finally work with Unity, and learn some C#, I have found these tutorials on 2D platforming physics very helpful. I've had some troubles with the standard 2D physics offered with the Unity engine. If you're available, maybe through Skype, or perhaps G+, I'd love to talk with you, maybe ask a few questions regarding scripting? I'm sure you're not exactly in the business of lessons, as it were, but regardless, I'd love to ask you a couple questions. Thanks!
    ~Xen

    ReplyDelete
  11. Hi, I don't know if this site is still active and if you're still answering questions, but I have some problems with collisions. It's not that they don't work, but that they are very (very!) inconsistent. If I keep running into a wall to test the collision the player eventually just pass through it, or sometimes when falling he just goes through the ground, it doesn't happen all the time, but it happens enough to be unacceptable. Do you know what the problem might be?

    Thanks already, your tutorials are awesome. \o

    ReplyDelete
    Replies
    1. Hey there! It is still active, sort of. I don't do much coding these days (I've teamed up with real programmers!) but all I can say is: Debug.Log. Everything. Every step of the way, Debug.Log(pretty much every value in the equation). Debug.DrawRay can help too though I don't use it much.

      Hope that helps!
      -T

      Delete
  12. Vector3 startPoint = new Vector3 (box.xMin + margin, box.center.y, transform.position.z);
    Vector3 endPoint = new Vector3 (box.xMax - margin, box.center.y, transform.position.z);

    If box.xMin = 0 (where the box begins) if you add the margin (+2) startPoint will be at x = 2, which is to the right of the box, and 2 units away from where the box begins.

    If box.xMax = 1 (where the box ends) if you subtract margin (-2) endPoint will be at x = -2, which is to the left of the box, and 2 units away from where the gox begins.

    This effectively swaps places of intended startPoint and endPoint and also well outside of the box collider/box.

    ReplyDelete
  13. Hi. It's worth noting that, at least if you build your level from tiles, you don't need to cast more than two rays in any given direction, as long as they are closer together than the size of your tiles. That is, if your tile is, say, one unit wide, then as long as the "feet" are closer than a distance of one unit, there will never be a need to cast more than two rays. One of the two will always hit.

    ReplyDelete

  14. Hi, I have been trying a similar approach, however I have realised there is a fundamental flaw in this technique. Because the rays are cast only in cardinal directions, it is possible to penetrate into an obstacle when moving diagonally, corner to corner. Even when the diagonal velocity will bring the object inside a collider, neither the horizontal nor the vertical rays will hit with the collider, so the collision is not detected. In the past I have had success when casting rays in the actual direction of the velocity, but there is some additional logic required to validate hits, especially if you want to be able to slide across a surface.

    ReplyDelete
  15. It doesn't work for me, my character just speeds up downwards and keeps falling indefinitely.

    ReplyDelete