Monday, 3 March 2014

2D Platformer Collision Detection with Raycasts: One-Sided Platforms & LayerMasks

Something that happened magically while developing this system was one-sided platforms. That is, platforms that you can go through easily on one side, but from the other side it doesn't work, it's a wall.

I stumbled upon an elegant solution I love very much, and I will now share it with you!

These platforms are rad


Lots of platforms in Kirby can be accessed from underneath


So you may be asking yourself, "Why do I want these platforms?" The answer is usually: they're rad (sometimes, it's: you don't). They can add a lot of depth to your game. They give options during level design, opening up puzzles, dexterity challenges, and other such intricacies.

OK that's enough talking let's code.

Layermasks & Bitwise Operations


Layermasks are integers that inform a Raycast what to pay attention to. They are key. Once you start using them, you'll never go back and you'll be very glad you have this skill in your pocket.

What is a Bitwise operation? In this case, we'll be dealing with a bit shift. This requires a little bit of binary, so if you know all this stuff you can skip right to the line of code where the binary operation is.

Binary is a counting system exactly like our normal one (Decimal), but instead of counting all the way to 9 before reaching 10, we count to... 1. 0, 1, 10, 11, 100 are 0, 1, 2, 3, 4, respectively.

In any number system, the leftmost digit (or bit in binary) refers to how many of the highest exponent of the base are in the total value. In Decimal, 854 is literally 8 * 10^2 + 5 * 10^1 + 4 * 10^0. A 0 exponent always gives you 1 no matter what you're putting to it.

So, 11011001 is:
1 * 2^7 + 1 * 2^6 + 0 * 2^5 + 1 * 2^4 + 1 * 2^3 + 0 * 2^2 + 0 * 2^1 + 1 * 2^0
which simplifies to
2^7 + 2^6 + 2^4 + 2^3 + 2^0
which simplifies to
217

For Layermasks, we will go from 2 ^ 0 to 2 ^ 31. This is because we have 32 different layers in unity, each of which is assigned an integer from 0 to 31. If you say Layermask.NameToLayer("normalCollisions") in my code it will return 31.

So, the Layermask is always a 32 bit integer. But, what matters in a Layermask is not the value in Decimal, but the status of each bit. Each  Let's say your Layermask look like this:
1000 0000
0000 0000
0000 0000
0000 0000
The value is 1, but that doesn't matter. In this case, the Layermask tells the Raycast only to pay attention to 1 layer, Layer 0. If it looked like this:
1000 0000
0000 0000
0000 0000
0000 0001
then it would look at the first and the last layer. Its value is 2147483649 but again, that doesn't matter.

So how do we make a number look like this? Luckily you don't have to spell out your own binary numbers and convert them to Decimal. You just have to do a bit shift. A bit shift looks like this:

int collisionLayer = 1 << 31;

This means you're shifting a 1 to the 31st position in a binary sense. But that's just one layer. What if we want to do more than one layer? Easy.

int collisionLayerPlus = 1 << 31 | 1 << 30 | 1 << 0;

The | character indicates we're shifting more than one bit in an integer. Now the Layermask will look for collisions in the 31st, 30th and 0th layers. For fun, this is what the bit will look like:

1100 0000
0000 0000
0000 0000
0000 0001

If you're having trouble with any of this, please google Bitwise operations, or bit shifting, or even just layer masks and someone else will give you a better explanation (because there's lots out there :).

What I did was set up a class with static variables that contain these bitwise operations so I don't have to keep doing them in my code. This is what it looks like.







That's it! That's all it is. Now we use it elsewhere.

...What? You want an explanation of this too? OK fine...

Well first you'll notice the class doesn't inherit from MonoBehaviour. That's because it doesn't need to! I'm not going to put it on an object - in fact, I'm never even going to instantiate one. All it needs is those static ints that can be accessed from anywhere, at any time. I'll do that later.

The other fun thing in here is the static constructor. I don't fully understand how a static constructor works, but I guess it probably calls that when it needs to to give values to the variables I declare above. I can't use functions like Layermask.NameToLayer in a variable declaration like that so I had to use a static constructor.

Last, softBottom is what I call the kind of platform you can jump up through. softTop is what I call platforms you can fall through but can't jump up through. They're evil. And fun! :D

Now let's use the darn things!


Let's start with platforms that you don't hit your head on. Ones you can jump through from underneath but will then land on stably. Find the code where you look for things to bump your head on.

....we haven't done that, have we? Oh...

Well, we'll do it at the same time! It's related.

Here's the final code.



You'll notice it looks a lot like our other directional checkings. That's because it is pretty much the same.

There are two main points of interest here:



Here is where the character hits his or her head. If a platform is there, teleport to the point of contact, and set your velocity to 0. The other option (a la Mario) is to have a little rebound. I like to use a fraction of the upward velocity you were moving at, assigning it as a negative velocity.

And...



This is where we use the Raylayers class. It dictates what I'm going to detect with my up-layers. In this case, I will only bump my head on things in the NormalCollisions or SoftTop layers.

Now all you have to do is assign a platform the SoftBottom layer and you won't hit your head on it!

It's really that simple. A few lines of code and a lot of functionality. It's just as easy to add the SoftTop platforms (just add the right layermask) and to add one-way sideways platforms (which are also quite interesting).

I know my explanations in this post may be a little hasty, so if you have any questions please feel free to leave them in the comments and I'll get back to you as soon as I can. This was supposed to be a really short tutorial but it ended up... not being. ^_^

Thanks for reading!

Until next time...
Stay smart, little mite.

-mysteriosum(the deranged hermit)

6 comments:

  1. oh man that is way better than my method. Thanks for helping me understand the bitshift stuff. I've seen that used in other code but I didn't understand it at all.

    I look forward to the moving platform stuff as well.
    One thing that gave me some confusion was the way the rays have been cast thus far.
    Usually the rays only cast in the direction of the players movement. But I needed a way to detect collisions on both sides in case a moving platform pushes you from behind.
    I wound up splitting the left and right collisions. I don't think it's very efficient the way I did it though. It also cause some problems with my wall jumping code because it would detect a hit at the moment I jumped away.

    For the vertical moving platforms I noticed my downwards raycasts needed to be longer to be able to detect the platform otherwise I'd get that hopping effect if it moved too far too fast. The switch for being grounded would get unchecked because the platform would move beyond the raycast during the next frame.
    My fix was to just make the raycast shoot further but this causes some slightly glitchy problems with landing on the static environment.
    It works for the most part but I can't wait to see what you come up with!

    ReplyDelete
  2. This (yours) is a pretty decent way of doing it, and I'd like to add some info to it. Static constructors are called once for each class that has one, right in the start. Before any Awake calls are made.
    Oh, and a class with only static variables that you never intend to 'new'/instantiate - you can (and IMO should) declare it as static, ie: "public static class Raylayers".

    ReplyDelete
    Replies
    1. Hey Brainswitch! Thanks for your input. Good stuff to know!

      I felt like I had tried to declare it as static before and I got a complaint from the compiler, but I just tried it now and it works. So yeah do this! I think I was trying to declare nonstatic members in the static class and that's why it hadn't worked.

      Thanks!

      Delete
  3. Hey,

    Is there any chance that you will post your whole C# script at once? I tried to convert your older posts to 2D but things got really weird and I can not get the pieces to work together. I have written a custom 2d physics "engine" for my Unity2D Jump&Run project but the slopes are the problem...

    ReplyDelete
    Replies
    1. Hey Max,

      Sorry about the delayed reply! I have just uploaded my script (PhysicsTest.cs) to Google Drive. Here is the link:

      https://drive.google.com/file/d/0B4_kE7nYzamoOW1yV2RlcTF5N1k/edit?usp=sharing

      I hope it helps! =)
      -mysteriosum(the deranged hermit)

      Delete
    2. Hey Travis,

      thank you very much! Unfortunately I can not use your slope solution in my project. But I have changed a little bit of the initial design and now these slopes are working as a speed up for the player - Once they have touched a certain area they are forced to follow the slope and get a speed boost.This will work for my current project at least ;)

      Delete