Thursday 13 February 2014

2D Physics in Unity with Raycasts: Slopes

Hello, faithful readers!

I have promised this update for a while, but have failed to deliver on account of being busy. I have done lots of acting in the last couple of weeks, and it has been great fun, but I'm starting to be sick of people and want to hole myself up with my squirrels for a while! Doing this tutorial (and more in the future) will help me forget other people exist for a while.

I actually did the coding for this within days of receiving the request, so this is far overdue.

The last post I did talked about jumping, and if you missed that you may want to look at it, though it's not necessary for this tutorial. What is necessary, though, is a basic understanding of the system: how I deal with gravity, and how I deal with lateral movement. Without having read these I'm afraid you'll be lost =)

Now, On To The Good Stuff

The first thing I did while trying to develop this system was put a slope in my scene and try it out. Turns out it kind of works already! Because of the way it's set up (the downward rays checking from the middle of the character), it teleports you to the point of contact in any case, so whether going up or down you'll still be in line with the slope in some respect.

It's far from perfect though. At first glance:
  • the character stops every time he reaches something like his max speed, because the side-check rays find the slope and think it's a wall;
  • the system only works when going up slopes of more than 90º (as in running to the left); 5+º (running to the right) slopes have you sink into them most of the way and cause all sorts of bugs;
  • we move the same speed as we would on a flat surface (which may or may not be desired); and
  • we jump straight up (again, we might want this but we'd like to have the option).
So that's just a couple of problems but they're still going to take a lot of work to sort. We're going to need some inspiration...

Case Studies

An important thing to do when tackling this sort of problem is ask yourself, "what have other people done?"

Super Mario World

For the first problem, I'm going to call on Super Mario World for the SNES.

What I'm going to for this is calculate the angle of the slope we're standing on. I will then use that  For reference, I'm going to link another guy, Jdaster64, who did an in-depth analysis of this stuff. Astounding.


From this, we see there are 4 angles of slope in Super Mario World: 11º, 23º, 45º and 67º. For 11º, there's no change for anything. So that tells us: we should consider implementing some threshold angles. For instance:
  • At 23º we reduce the player's max speed going uphill, and increase it going down;
  • At 45º the player starts moving automatically down the hill;
  • At 67º the player will have a very hard time controlling movement, and will even fall off the platform should they move with the slope enough.
We don't have to choose these exact angles, but we should set it up so that no matter the angle, we react accordingly. This means we're doing a general system, not a specific one - which may be the wrong thing to do, but that's what programming is all about!

Guacamelee

Guacamelee takes a much more laissez-faire approach to slopes, which is appropriate for its genre as Metroidvania (which always includes lots of combat). Ease of control is of paramount importance in this genre, so introducing hidden mechanics like those in SMW is not practical.

Guacamelee only has a couple of slope values, but in general it's a binary issue: either you can walk on the slope, or you can't. If you can walk on the slope you move at the same X speed as normal and jump straight up.

Frankly, this is the modern trend. Most platformers choose to doff the slope physics given to us by arguably Nintendo's greatest platformer. That's fine! Things are moving forward! With that in mind I won't go into how to bring SMW style slope mechanics to our projects just yet. I will focus on the most important problems.

Disclaimer: since the last tutorial on lateral movement, I changed this whole system to Unity's 2D physics. I did this because it's much easier to achieve a slanted collision with the 2D Polygon Collider than it is to make a 3D mesh in Blender or something and import it. If you can manage that last part go ahead; the system is almost identical.

What's Different in Physics2D?

Not much, really. The biggest thing is the Raycast method. Instead of returning a boolean, it returns a value directly to a RaycastHit2D object (making this a required step instead of an optional one). This is okay since we use RaycastHits anyway and it's just as easy to check if a RaycastHit2D hits something than it is to check a boolean.

Here's what the code now looks like:



Each Vector3 has been replaced with Vector2, we now test to see if (hitInfo.fraction > 0) instead of if (connected). hitInfo no longer has a .distance variable, so we have to make due with the hitInfo.fraction they give us. Other than those, pretty much the same thing.

Fix: Collision Problem (slopes aren't walls, silly)

The most immediate concern for us is the fact that we collide with slopes as if they're walls. I noticed this even on the slightest of curves, so it's time to mess with our lateral movement scripts a little.

What do we need to change?

The problem with the code as it stands is that the moment one of the rays hits something, the system goes "OH YEAH I got something it's a wall f'sho don't worry about it I got dis." This is not necessarily the case! I'll need to check how many rays hit, and retrieve the normal of the surface I hit. After all, if the slope is steep, we might hit the wall with multiple rays. This will also give us the tools to apply this to the downward rays.

This is what the new version of my Raycast section for lateral movement looks like:

The first thing you may notice reading from the top is that I've removed the +/- margin from the start and end points. This means I'll be checking right from the bottom to the very top of my character's collider:




The whole reason I had that margin to begin with was so that I wouldn't collide against the floor as if it were a wall. Now that I'm going to be calculating the angle between multiple rays' connection points, this is no longer relevant.

You'll also notice that instead of just using one RaycastHit2D, I'm using an array of them. This way I can reference any or all of them in or out of my for() loop.

When One Ray Just Isn't Enough

Now, first: how do we know we've connected with more than one ray? With a little variable I like to call lastFraction.




lastFraction does what it suggests: stores the value 'fraction' of the previous connecting ray's RaycastHit2D object. If its value is 0, that means no other ray has connected thus far, and we won't bother checking an angle or anything. We will simply store the current fraction in lastFraction.

Disclaimer: As it is, lastFraction is within the if(hitInfos[i].fraction > 0) clause, which means it doesn't get reset after a successful connection. As a result, any two successful rays will trigger a check. If you want to make it so that only 2 consecutive rays trigger it, put the last line of this block of code as the last note of your for () loop.

What's Your Angle?

To calculate the angle between two points, we'll use Vector2.Angle(). At first I tried just plugging both points into this and using that but that's not how Vector2.Angle() works. No, for Vector2.Angle, you pretty much always want the first argument to be Vector2.right, which is (1,0), or 0º. Then give it the difference between the two points, which will give you the delta of that Vector:



Now we see if it's within the proper range of what we want. In this case, I've set angleLeeway to 5, meaning if angle is less than 5 (or more than -5), we'll ignore it because it's not a wall. Of course, you may want to tell the player he can't climb up an 85º wall, so you can change angleLeeway as you see fit.

That's it for that! Your player should no longer think slopes are walls.

Fix: Sink Problem (The One Ray To Rule Them All)

The other major problem is that the player object seems to sink into the slope. In fact, the script will always use the first ray that connects as the reference for where it should be along the y axis. In this case, it's ray on the far left, so that's why when travelling right, he'll 'sink' into the slope.

So, to solve this problem, I've once again conjured Guacamelee as my reference. Here's how much Juan cares about slopes:
Why yes, this costume does imply that I 100%'d the game! Thanks for noticing! ^_^
Wow, Juan. Your one foot is barely touching the floor. Very suave. Realistic... oh well, we can't all be perfect!

This is actually a very sensible and intuitive solution. As a player, you might not even notice it without having it pointed out! It's certainly much less weird than if his foot were inside the floor. This also allows the level designer to put slopes of any angle and it doesn't matter a bit.

The Short Stick

Achieving this is surprisingly simple, from what we have so far. The system shoots multiple rays down from the central X axis of the player. All you have to do is determine which of those rays is the shortest, and use that in your calculations!

Here's what the final code looks like for this:



The first thing you'll notice if you're clever is that I'm once more using an array of RaycastHit2D. This is important for this problem, but we'll also use it later if we're going to implement slide-down-the-slope or jump-at-an-angle features down the line.

For Loop, For Loop on the Wall, Who's the Shortest of Them All?

To find the shortest ray, all you have to do is declare the variable smallest fraction and assign it later on.




We start it at Mathf.Infinity because everything is smaller than infinity :)

Also declare indexUsed, which will tell us what position of our RaycastHit2D array we want to reference when we finally give the command to hit the ground.

Then we want to assign these variables. Again, it's trivial:








Any fraction smaller than the smallestFraction value will set a new smallestFraction, and with it, the indexUsed variable. We use the index after the for() loop if connected is true, and go about our business as usual.



Since the system already teleports you to where the ray hit, there's no need to change anything else!

That's it for now...

I hope this helped those of you who are following me and have wanted to use slopes in your games. This system makes it so you can have slopes of any angle, with a cutoff angle where they start to be considered walls.

Next time I will go into more complex nuances of slopes, as described in the SMW breakdown. I don't think it'll be super complicated but it will be worth doing! If you have any questions at all, feel free to leave a comment. I will answer as soon as I can!

Thanks for reading.

Stay bright, burning star!

-mysteriosum(the deranged hermit)

27 comments:

  1. Hi Travis,
    Thanks for going over this. I wound up doing something similar while I was waiting for you to get to this one.

    One thing I couldn't get though, was having it not horizontally collide with a slope thinking it was a wall. For some reason I just couldn't think of doing it the way you did haha. It all works perfect now! Thank you!

    One thing I did differently was calculating the angle of the slope.
    I used RaycastHit.normal to compare instead of the RaycastHit.point.

    I'll also have to try your translating down method to keep the player on the ground sometime too. I wound up going a different route for that.

    With my system I calculate the y position of the next frame by using a Raycast at the x position of the next frame and taking it's RaycastHit.point.y at that location.

    Thanks again!

    ReplyDelete
  2. I've run into a new roadblock that I can't seem to figure out a solution to.
    I know it's not related to this particular post but if you could maybe go over this in one of your future posts it would be really appreciated.

    I've implemented moving platforms in my project and things work fine.
    The only problem is I seem to pass through moving platforms if they're moving towards me horizontally or vertically. Moving away from me things seem to work fine.

    ReplyDelete
    Replies
    1. ah it's because the platforms were moving too fast. If they pass the raycast origin in one frame of movement then it breaks through.

      Delete
    2. I'm not sure if it's something I did wrong but following your code it seems like the player will stick at corners of a platform. Like if you edge off and quickly move towards the ledge the player will stick and pop back up to the top of the ledge again instead of falling.

      Delete
    3. doh I left out the margins.... I understand why those are there now hahaha

      Delete
    4. Hey Jesse! I actually wrote you a response but somehow it got deleted... very annoying.

      Anyway the margins are there exactly for that, yes - but the problem is that the player's box collider will overlap a collision if you're falling and you're not checking the very corner of your collider. So you can get rid of the the margin if you're ok with the player teleporting up half his height to the top of a platform. If your centre is touching wall it won't do that, since the 'fraction' of the 2d ray is 0 (or if you're still using 3D rays you can say 'if hitinfo.distance > 0).

      There's some give and take. I decided being able to stay on a platform right to the edge was more important than stopping the player from teleporting up half his height, but also I have a low acceleration so the player can't really hug the wall right after he falls off a platform. So use what works for you :)

      Delete
    5. Also I believe my next post will be on moving platforms, and I'm planning something a little more robust than what's already set up, so... stay posted :D

      Delete
    6. Awesome man thank you! I just finished up my moving platforms yesterday and it seems pretty solid now.
      One thing I did was always having collision checks for both left and right so the player can be pushed by a moving platform even if he's not facing it.

      I also tried doing one way platforms, that was pretty easy!
      I want to try making it so the player can jump down from one way platforms as well by pressing down and jump, haven't gave that one a shot yet.

      I'm also gonna give ladders a shot today! :)
      In my head the only tricky part would be when the player reaches the top of the ladder and how things should act. I'm guessing the top will function similar to a one way platform.
      I plan on using rigidbodies as triggers for that. I haven't had a need for a rigidbody up until this point.

      I look forward to your approach for moving platforms. I struggled with that one for quite a while. :D

      Delete
    7. Also I'm really looking forward to get into the camera stuff.
      https://www.youtube.com/watch?v=TCIMPYM0AQg
      I was watching thing and it amazes me how much went into making cameras for games like that. So much functionality and you wind up never even thinking about it when you're playing! Blew my mind

      Delete
    8. My one-way platforms thing is up! I'll get to moving platforms within a week hopefully.

      Camera stuff is awesome. If you need a quick, nice camera-follow script, just do: Vector3.Lerp(camera.position, new Vector3(avatar.position.x, avatar.position.y, camera.position.z), 0.1f);

      Delete
  3. Hi, First thanks for your article it is quite instructive but has i was reading i noticed you used the collider.bounds to create your rectangle and then later you say you made the switch to 2D but bounds does not exists yet for collider2D as of 4.3.4. How did you do it?

    ReplyDelete
    Replies
    1. this might be helpful:
      http://forum.unity3d.com/threads/211486-Physics2D-Bounds-and-Extents

      Delete
    2. indeed helpful until we get something better! :) thx for sharing

      Delete
    3. Hey David!

      This is the code I use to define my box:

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


      Hope that helps!

      Delete
  4. Hi Travis,

    thanks for doing this tutorial. I'm having trouble understanding what you wrote underneath "What's your Angle?"

    "Now we see if it's within the proper range of what we want. In this case, I've set angleLeeway to 5, meaning if angle is less than 5 (or more than -5), we'll ignore it because it's not a wall."

    When you say the slope is not a wall, does that mean the character will be going through the slope? I'm confused because you set your X velocity to 0 at that point, which would mean the character wouldn't be moving

    ReplyDelete
    Replies
    1. Hello Alex,

      I'm glad you enjoyed my tutorials =]

      The idea in that part is this: I shoot my raycasts out, then determine what angle the wall is tilted at. If the angle is 90° straight up I know it's a wall. So, in case you want to have a slightly angled wall (or a floating point is off or something), I will consider any angle from 85° to 95° to be a wall. It's a fail safe really.

      I hope that answers your question!
      -Travis

      Delete
    2. This comment has been removed by the author.

      Delete
  5. Hello Travis.

    Thank you for this great tutorial. Helped me a lot to understand how slopes work.

    One quick comment. I noticed you are updating vertical position inside the FixedUpdate() method. In my test here the motion stutters when climbing slopes.

    So, I changed your code in a way that the Translate method is called only once, inside the Update() method. The result was way smoother.

    According to the unity community FixeUpdate() should only be used to calculate physics.

    ReplyDelete
    Replies
    1. Hey, glad you liked it! I used FixedUpdate for everything, including updating the camera's position, so it all looks pretty smooth. But hey - do whatever works for you! That's the whole point of this really, to get you thinking of your own solutions!

      Keep it up,-
      -Travis

      Delete
    2. Hi, I wrote a comment, I clicked on preview and it got deleted... but nevermind. I copied the code, but have three issues.

      1) if I write "Debug.DrawRay ((origin and direction of the vertical and horizontal rays))" The horizontal rays are ok but the vertical rays are a bit out of place.

      2) ok, I don't know what's happening here, but my character collides with the ground even if the ground doesn't have the tag "normalCollisions". Actually if it has that layer the character doesn't collide with the ground. Does that suppose to happen?

      3) once my character has reached maximum (or minimum) x speed, i stop pressing the left or right button. After that, my character decelerates to 0, and then something weird happens (I'm not touching anything now). The character keeps wobbling around, left and right like crazy fast. He isn't moving though, he's just jerking around in place. If you wait a long time though the character will be at a completely different position... So what did I do wrong?

      Delete
    3. Yes, sometimes the Blogger comment system deletes posts and it's really annoying!

      The problems you're having certainly sound bizarre. The best advice I can give you is to completely rebuild the code from top to bottom, without copying it. Do it one step at a time: make a ray pointing in one direction. Get info on that ray: how long is it, where does it impact, where does it start, etc. Make sure everything is in order before adding anything else.

      The reason all my code is in images and not text is so that you can't copy-paste it, to encourage you to make your own thing. Use my steps as a guideline, a series of ideas, not a strict how-to. Small variations - layers, components, language, versions of Unity - can make copying code frustrating and slow.

      Sorry I don't have time to debug your stuff but debugging one's own code is an important skill in and of itself!

      Best,
      -Travis

      Delete
    4. Yes, I'm rebuilding the code and making sure everything is right. Thanks for your advices.

      Delete
  6. Hello,
    Im having trouble with the raycast of the gravity portion of this. My player never becomes grounded and just rests on the polygon2d collider. When the rays are shown, they are attached to the player, but they appear to point way from the world center and increase in distance the further my player is away from world center. No matter what I try I cannot seem to resolve this issue (probably due to my lack of understanding. Iam using javascript for the project and havnt had to much trouble converting it over, also my scale is very tiny.

    ReplyDelete
    Replies
    1. Nevermind...slowly figuring it out..1. Rigidbody is kinematic should be false i guess..2. my layermask initializing was incorrect

      Delete
  7. This comment has been removed by the author.

    ReplyDelete
  8. I seem to be one step away to getting this to work. My player isfalling to theground, then hovering upwards slowly like in space(if gravity of unityphysics = 0). If I set the rigidbody2d to "is kinematic" I'm falling through the collider. Setting gravity to 1 makes him bounce over the floor. He's also bugging through every wall now.

    For short: I need to get a rid of the rigidbody2D without delete it, like I could do with the 3DRigidbody.

    ReplyDelete
  9. Hey Travis, I love the tutorials. I have some questions about this method though... If you raycast each axis seperately, doesn't that allow for the object to end up inside corner tiles for a frame, or collide with the edge of a tile that they would have otherwise diagonally moved past without touching?

    Am I missing something, or are these scenarios that are rare enough that you don't worry about them?

    ReplyDelete