Hello again, faithful squirrels - err, readers. This is the second post in a series wherein I discuss my home-brewed 2D platformer physics in Unity3D. Last time I talked about the basics of Raycasts, then applied them to movement in one direction (gravity). This time I'll be talking about applying my Raycasting method to lateral movement. The process, as you may have guessed, is the same for lateral movement, so it won't be as long to explain as gravity since we're already familiar with the method. I'll then explain jumping, which is also super easy (1-5 lines of code, tops).
DISCLAIMER: I discovered after writing the last article that Unity implemented native 2D physics and collision detection in a version of Unity I hadn't updated to yet. I've implemented my whole thing with 2D box colliders and 2D Raycasts, but it works the same either way. So I'm going to continue this tutorial with 3D box colliders which will be handy for anyone doing a 2.5D platformer, or for other fun experimentations.
Recap
To implement gravity and collision detection, I use the following steps:- Set up variables, like gravity and max fall speed, and gather information about the GameObject;
- Apply acceleration to my movement: in this case, downward acceleration due to gravity;
- Determine whether it makes sense for me to check below me for collisions (am I on the ground? or if not, am I falling?);
- Determine where the rays I'm casting will begin (a few of them in a line);
- Determine the length of all rays (they'll be the same, and check as far as my downward velocity);
- Cast each ray and ask if it hit anything;
- Use the result (if I'm on the ground and I didn't hit something, now I'm falling, and vice versa).
Here's what the rays would look like if we drew them:
For checking above, the lines will look identical but face up, and for lateral movement they will start along a vertical cross-section and reach out to the side.
Let's Boogie
Now let's take a look at the system applied to lateral movement. Here's what the code looks like:
There are lots of similarities but also some key differences. Let's go through them.
and that's it! Unity's inputs are generally set up well enough for this purpose, but if you want to play with it then go to Edit > Project Settings > Input to look at the data structure.
One thing to note about this is I use Input.GetAxisRaw instead of Input.GetAxis. The difference between the two is when it comes to keyboard input. All Input.Axis functions return a float between -1 and 1. When reading input from a keyboard, Input.GetAxis moves slowly toward the limit rather than snapping to it (which GetAxisRaw does). For example, if I want to move the character left using the keyboard, I'll press A or the left arrow. The first frame, using Input.GetAxis, will return something like -0.15. Then each frame it will go up by that amount until it caps at -1. This isn't what we want here, since we'll be multiplying things by our input. So, we use GetAxisRaw, which will only ever return -1, 0, or 1 when using a keyboard.
I like to work with local variables - in this case newVelocityX. In C#, there's no way to alter one component of a Vector directly (in Javascript you could write "velocity.x += acceleration * input"). So, it's simpler to work with a new variable.
We're going to deal with an input of 0 differently, since that's deceleration and not an acceleration. Add acceleration * horizontalAxis to newVelocityX, then make sure it's not gone past the maximum speed. The principle is similar for deceleration.
If we are going left, we check left, otherwise we check right. If there's no input we don't check at all. If the player holds a direction towards the wall, we know it connects on that side. There is the unfortunate side effect of doing a Translate of 0 every frame this happens but the pros outweigh the cons here. If I want to code Megaman's wall jump for example, I just have to set a variable like "bool wallHanging" to true if one of my rays connects. Then I can use the input and invert it to determine the direction he'll be jumping away from the wall (or hitInfo.normal for that matter).
I never even got into layer masks in this explanation. A layer mask will let you pick and choose what objects you want to detect. So if you want to make a platform you can jump through from underneath, just tell the rays you send up not to detect those platforms. Then when you're falling the downward ray will find it and you'll be golden.
Using this method you can implement: moving platforms; slanted surfaces; icy surfaces or other movement detriments; or a whole thwack of other things I can't think of right now.
I am really enjoying doing this sort of work, and I believe my next tutorial will be about bouncing off walls (like for an astronaut in a pinball machine, let's say).
Thank you so much for reading. Until next time, keep moving, kinetic beings!
-mysteriosum, the deranged hermit.
There are lots of similarities but also some key differences. Let's go through them.
Input
Since we're now responding to the player's input, we have to get it from somewhere. My input code looks like this:and that's it! Unity's inputs are generally set up well enough for this purpose, but if you want to play with it then go to Edit > Project Settings > Input to look at the data structure.
One thing to note about this is I use Input.GetAxisRaw instead of Input.GetAxis. The difference between the two is when it comes to keyboard input. All Input.Axis functions return a float between -1 and 1. When reading input from a keyboard, Input.GetAxis moves slowly toward the limit rather than snapping to it (which GetAxisRaw does). For example, if I want to move the character left using the keyboard, I'll press A or the left arrow. The first frame, using Input.GetAxis, will return something like -0.15. Then each frame it will go up by that amount until it caps at -1. This isn't what we want here, since we'll be multiplying things by our input. So, we use GetAxisRaw, which will only ever return -1, 0, or 1 when using a keyboard.
Applying movement
This was simpler with Gravity. We just checked if the player was grounded, and if they weren't, we subtracted from their vertical velocity (capping at a maximum fall speed). This is what I do to apply lateral movement.I like to work with local variables - in this case newVelocityX. In C#, there's no way to alter one component of a Vector directly (in Javascript you could write "velocity.x += acceleration * input"). So, it's simpler to work with a new variable.
We're going to deal with an input of 0 differently, since that's deceleration and not an acceleration. Add acceleration * horizontalAxis to newVelocityX, then make sure it's not gone past the maximum speed. The principle is similar for deceleration.
Raycasts
This operation is nearly identical to that of gravity, with one exception: the direction of the rays is determined by our velocity. See here:If we are going left, we check left, otherwise we check right. If there's no input we don't check at all. If the player holds a direction towards the wall, we know it connects on that side. There is the unfortunate side effect of doing a Translate of 0 every frame this happens but the pros outweigh the cons here. If I want to code Megaman's wall jump for example, I just have to set a variable like "bool wallHanging" to true if one of my rays connects. Then I can use the input and invert it to determine the direction he'll be jumping away from the wall (or hitInfo.normal for that matter).
Only the beginning
From here, the possibilities are endless. Try something fancy! For starters I suggest figuring out how to check above the character as it's jumping, to react to ceilings using this method.I never even got into layer masks in this explanation. A layer mask will let you pick and choose what objects you want to detect. So if you want to make a platform you can jump through from underneath, just tell the rays you send up not to detect those platforms. Then when you're falling the downward ray will find it and you'll be golden.
Using this method you can implement: moving platforms; slanted surfaces; icy surfaces or other movement detriments; or a whole thwack of other things I can't think of right now.
I am really enjoying doing this sort of work, and I believe my next tutorial will be about bouncing off walls (like for an astronaut in a pinball machine, let's say).
Thank you so much for reading. Until next time, keep moving, kinetic beings!
-mysteriosum, the deranged hermit.
Thanks for the tutorials. I appreciate you actually explaining what's going on every step of the way instead of just "use this block of code and done" like I see in a lot of tutorials.
ReplyDeleteI come from the 3D modeling side of games so programming is something really new to me and I've been wanting to get into it this year.
I've been able to figure out a way to do wall jumping and running/dashing but one thing that's been giving me a really hard time is slanted surfaces or slopes with this system. I've been at it for a few weeks now and nothing I've tried has worked right.
At one point I think I actually got descents on slopes to work well but have always had problems with getting stuck while trying to go up a slope.
I'd really like to see how you handle that since your system is really close to what I've got going.
Thanks for the learnin',
Jesse
Hey Jesse!
ReplyDeleteI'm glad you garnered some knowledge from my blog. I was compelled to explain each hurdle I went through while developing this technique, and I'm glad it paid off!
Slanted surfaces in platformers are interesting, and there's a couple of questions you have to ask going in: do I go the same speed going up as going down, like in Megaman? Do I fall automatically while on a slant? Do I jump straight up or at an angle?
I haven't implemented a slant system yet but I think I'll take a crack at it tomorrow. Stay tuned and I should have it for you soon :)
Thanks for the reading!
-mysteriosum(the deranged hermit)
Cool man I'm looking forward to it!
ReplyDeleteI finally got a system that somewhat works.
I used a raycast that uses the X position of the character for the next frame as it's origin and I find the Y position of the hitpoint and directly link the character's Y position with that.
Just has problems with going from a flat surface to a sloped surface, it want's to stick cause of the side collisions checks I think.
Jesse
Hi, again! I just finished up placing in this code. I have a few things to report, it's kind of a mouthful.
ReplyDeleteThe overall question I have, before I get into the details: "I'm curious if this code is optimized to use assets of a certain size (say 5-10 units) and speed values closer to the defaults you provided in part 1."
I used substantially lower values in my early attempts here, and wound up with some issues, described blow.
1) I wasn't sure exactly where to put it within FixedUpdate(), nor was I sure if I should wrap all this in a conditional such as "if (grounded && !falling) {}" or something like that... I wound up placing it after the closing brace of the condition "if (grounded || falling)" that enclosed our gravity code, because that seemed sensible. But it would be great if you could help me reality-check -- I don't like to make assumptions if I can help it. :P
2) Sometimes my character is getting "stuck" in between "seams" in the environment geometry (my test "floor" is default unity cubes snap-aligned end-to-end). This is only a concerning because I intend to use a title-system (by Rotorz) to build my levels, and I'd like to avoid this problem if I can learn how to overcome it. Getting stuck only happens after falling, and it happens unpredictably (ie not every time, and I'm not sure what causes getting stuck). When it happens, my character seems to almost "pivot" around the origin of the side-rays. Perhaps he's slightly sunken-into the mesh? Not sure about the cure for this one. Maybe I'm using the wrong maxSpeed/maxFall/gravity values? I'm using fall and gravity of around 14, rather than what your default values were.
3) Tying into point 2, I'm noticing some further unexpected behavior when NOT stuck, but with the speed set to very low values. I used low values because they happened to fit my project at first -- I'll explain later. After seeing how this effected things, I'm considering whether I am using incorrect/problematic scale and size values when setting up my assets in the first place. Roughly what sizes are you expecting our assets and colliders etc to be?
Specifically, my problem and how I "overcame" it, in my case:
Initially, Camera viewport = 5, player collider size = .95 in X and Y, and player graphic's Pixels-to-Units (new to 4.3 & 2D tools) value was set to 20. This made the default movement values of 200 etc cause my player to just JAM up, down, and around the screen too quickly, so I made some adjustments.
I set the maxSpeed to 5 at first. Aside from just being too slow, when I released the input keys, the player's velocity would constantly alternate between slightly left and slightly right with every frame. Player would then dance around indefinitely, hedging slightly in the opposite direction I last pressed. I used Debug.DrawRay() to visualize my side-rays in red, and I could see the rays flipping back and forth in front of and in back of the player character when no input key was pressed. Similarly low speeds up to around 15 continued to induce this effect. Setting the maxSpeed to 20 seemed to eliminate the problem, and the code stabilized enough for these early experiments.
Perhaps my assets just need to be bigger over all -- my Camera is now set to 10, Pixel-to-Units is set to 10, my box collider is set to 1.9 in both dimensions. This accommodates the maxSpeed of 20, and looks identical on-screen to my smaller sizes earlier.
TL;DR
So basically doubling the size of the assets in my project and increasing the maxSpeed value to match the new scale seemed to fix the problem I was having in point 3. But am I misusing the engine by making my objects this small in the first place? Perhaps I'm operating this code at the extreme limits of its capability?
Hey again! So...
Delete1) I don't think it matters a whole lot whether you place it before or after the gravity code, but I did what you did :) So in the code, the order for me is:
-Jumping
-Gravity
-Lateral movement
2) This is a problem that solves itself in my latest post! You may want to check it out.
3) For this, it may be a problem with the acceleration constant, especially if it's larger than the max speed. It might be adding/subtracting the acceleration causing it to go above and below 0? I'm not sure but that seems sort of weird.
The scale of the objects shouldn't matter too much, but the numbers in your engine do need to change to accommodate. What I've taken to doing is having a static class with some information, like pixel dimension, and using that as a divisor for all my numbers.
The numbers as they were are for a collider of 16 x 24. If you want to make a bigger or smaller one just divide all your numbers by the appropriate amount to fit your current collider size. Since you'll be dividing a lot of numbers by this it would be handy to have a single variable that they all divide by (public const int declares an integer that can be accessed from anywhere and can't be changed and can be referenced outside of methods, which is handy).
I don't think the engine cares what dimension your stuff is as long as their relativity is consistent.
Hope I covered everything!
Ta,
-mysteriosum(the deranged hermit)
I think you answered everything!
Delete1) Thanks for the quick clarity on the code order.
2) I read ahead after I left that comment and noticed your approach in part 4 where you actually use the rays to calculate the slope in front of the player and then just test against certain limits, and I agree this would totally solve the problem! :D I'm excited for implementing that.
3) It exactly was the acceleration value. When my maxSpeed was 5, I also had my acceleration set to 4, and this is what reproduced the problem. When I lowered acceleration to 1, the maxSpeed of 5 became stable again.
Either way, it's satisfying to know how large your collider was when you started with these numbers. My camera size was so small that a 16x24 asset would overwhelm the entire play area and then some, as far as my camera size of 5 was concerned. So bringing the assets/camera size and increasing the speed acceleration values to match is just another way of using these tools, and I have a better sense of how to use them to fine-tune the motion to my needs as I go forward.
Thanks for your attentiveness! This is a really helpful tutorial series, and your answers are like adding an exponent to it all.
mysteryloaf,
DeleteIt's my pleasure. Your questions really help complete the experience since there are lots of little road blocks and other people reading them can benefit. It also encourages me to keep making these! :D
This tutorial is gold!
ReplyDelete