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:
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.
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:
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 GrittyThis 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 GistThe 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.
SetupSo 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.
GravityLet'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:
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 nowThat'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)