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)