Tuesday 28 January 2014

2D Platforming Physics With Raycasts: Jumping

Hello, faithful reader(s)!

It occurs to me that I didn't actually explain how I do the jumping in my system. It's simple, but there's a couple of nuances that should be discussed.

Basic Version

Jumping in Unity can be as simple as:

void Update (){
     if (grounded && Input.GetButtonDown("Jump"){
          velocity = new Vector2(velocity.x, impulse);
     }
}

This works, but there's a hidden mechanic in most of the platformers you play that you might not realise: you can actually press the jump button right before you land, and jump immediately as you hit the ground. If this feature is absent, your game will feel glitchy. The player expects this leeway. This means we're going to have to record when, not just whether,  the jump button is pressed.

Disclaimer: Unity has a function, Input.GetButtonDown(). I don't trust it - I find sometimes it just doesn't register. So I work around it with Input.GetButton, which returns true every frame the button is pressed.

Better version

My code looks something like this:

void Update (){
     bool input = Input.GetButton("Jump");
     if (input && !lastInput){
          jumpPressedTime = Time.time;
     }
     else if (!input){
          jumpPressedTime = 0;
     }

     if (grounded && Time.time - jumpPressedTime < jumpPressLeeway){
          velocity = new Vector2(velocity.x, impulse);
          jumpPressedTime = 0;
     }

     lastInput = input;
}

So a couple of things at work here. First, the time we pressed the jump button is recorded only if the previous frame, it wasn't pressed. Every frame we're grounded, we'll test the current time against the pressed jump time, to see if it's less than our jumpPressedLeeway variable (which is usually like 0.1). Then we jump if that's true (if we're grounded).

Setting the lastInput at the end is essential to check if this is the 'JumpDown' frame.

Disclaimer: I haven't tested this code. How fun! Check it, see if it works. Let me know what's wrong!

You can also add other conditions to the jump check. If you have a double jump, your condition may want to read:

if ((grounded || !hasDoubleJumped) && Time.time - jumpPressedTime < jumpPressedLeeway)

This way, grounded and not having double jumped will both satisfy the jump condition - but you'll also have to add:

     if (!grounded)
          hasDoubleJumped = true;

Then when you land, set it to false again.


That's it for now. Soon, I will go into doing slopes with this kind of system. I will also discuss jumping in a little more detail as it pertains to the system of checking downward for stuff. You'll see.

Thanks for reading!

Until next time: stay real, natural numbers.
-mysteriosum(the deranged hermit)

11 comments:

  1. Hi, Mysteryloaf again. I'm back -- just popped-in this code and again as usual I have some feedback and a couple questions. I'm having a problem with the basics of this.

    My issue is that with my preferred jumping height and gravity in place, my player won't leap all the way off the ground most of the time. About 1/10 attempted jumps, he breaks free and completes his jump with the initial jump/gravity/maxFall values I had input.

    I'm trying to figure out why. I thought maybe the player is casting rays during the first frame of the jump, so that the downward rays are connecting with the ground colliders, re-grounding the player and thus preventing him from applying any vertical velocity (only lateral velocity).

    I can decrease gravity/maxFall and increase the jump impulse value, so his first-jump-frame is able to ascend beyond the length that the rays test below the player -- but the workaround gives him a crazy big jump. But sometimes I may want to intentionally reduce the jump power as a "status effect" or something like that, so I don't really want lower gravity and greater jump power than this for all time.

    Maybe it's simple: could we turn the downward rays off during the first moments of the jump?

    Maybe it's even simpler, and I just need to make those assets 20 units in size rather than merely 2 units, and adjust the movement values to complement? ;)

    Maybe it's more complicated or there's something I'm not considering? I don't know if coroutines are appropriate/well-suited for player controls in the first place, but I've had good luck getting them to reliably produce complex behavior before... just not THIS complex.

    Anyway, pardon me for thinking out loud again, here. My primary goal is to clear up my misconceptions, so I have to share them if I can and that takes more words. Thank you yet again for reading.

    I'll continue on to the final part, rework the script into Physics2D and add some improved environment geometry. I'm looking forward to how this all takes shape. The next part looks highly informative.

    ReplyDelete
  2. Hey mysteryloaf, sorry I didn't get to this earlier but I think it is quite simple.

    In your Jump code, include the line 'grounded = false'.
    This way your gravity code knows you're not grounded and won't check down unless your velocity is below 0 (which it's not since you just set it to very high).

    I hope this works for you :)

    ReplyDelete
  3. Hi, travis thanks for the awesome tutorials. I am also making a platformer game with my own physics, but I have a problem, i've posted it here

    http://answers.unity3d.com/questions/667582/fixed-jumping-in-platformer-jump.html

    can you check it out :) ?

    ReplyDelete
    Replies
    1. Hello Ahmed! Thanks for reading! I'm glad you like them.

      Are you using a FixedUpdate() for your platformer script or just Update()?

      Delete
    2. I am using the Update function. I tried FixedUpdate few minutes ago and I am not satisfied by the result. The smoothness that I get in Update is no where near in FixedUpdate :( .

      Is it possible to get same jump height in Update?
      have you ever faced a problem where the jump height is slightly different when the FPS changes?

      Thanks

      Delete
    3. Well, the thing about Update is that Time.deltaTime, by definition, will be different each frame. Let's say your jump velocity is 150. This frame Time.deltaTime might be 0.015, but another time it might be 0.02. In these cases the initial jump velocity will be 2.25 u/s and 3.0 u/s, respectively. So you just end up with a different value. That's the reason your jump is consistent.

      If you just tell the system "I want my jump impulse to be 2.25", without multiplying it by Time.deltaTIme, then you will get the same result each jump.

      This is of course provided that you apply Time.deltaTime to the Gravity you're applying.

      What I do is I never multiply my velocity by Time.deltaTime until I use Translate. So jumping looks like:

      velocity = new Vector2(velocity.x, jumpImpulse)

      Gravity looks like:

      velocity -= Vector2.up * gravity;

      And at the end of the Update I write:

      transform.Translate(velocity * Time.deltaTime);

      I hope this works for you!

      -mysteriosum

      Delete
    4. Still won't this give you different result when deltaTime is 0.02 and 0.015?

      Delete
    5. Yes, but it will send you the appropriate distance for that time frame. If Time.deltaTime is higher one frame than the other, you'll want to send your character further. However, you always want the Jump value to be the same.

      Think of the Jump Impulse value as adding a force, not changing velocity. Velocity changes as a result of that force.

      So keep your jump impulse value constant and apply the Time.deltaTime later.

      Delete
    6. I see, guess I will have to scrap my platformer collision and jumping logic. I was wondering if you have read this article?
      http://gafferongames.com/game-physics/fix-your-timestep/

      Delete
    7. I was wondering if this will work.
      Lets say GRAVITY_CONSTANT = 200

      in Update

      FRAME 1: ( FPS = 60 )
      gravity = GRAVITY_CONSTANT * ( 1/60 ) = 3.33

      we assume that the value of gravity should be 3.33 in all frames.



      FRAME 2: ( FPS = 50)
      gravity = GRAVITY_CONSTANT * ( 1/50 ) = 4


      Gravity difference Frame 2 and 1 = 4 - 3.33 = 0.67

      What if we adjust the value of GRAVITY_CONSTANT every frame such that the gravity is always 3.33?

      the equation will be

      (GRAVITY_CONSTANT ) * deltaTime = 3.33
      GRAVITY_CONSTANT = 3.33 / deltaTime

      Will this cause any issues in movement smoothness or anything?

      Delete
    8. Well... I am not 100% sure I understand the purpose of this equation. What are you doing with GRAVITY_CONSTANT? How do you apply it to your velocity?

      I wouldn't know if it causes any smoothness issues off the bat without having tried it. I've detailed the method that works for me. I use FixedUpdate and apply a flat value to my Velocity vector, which I then multiply by Time.deltaTime at the end of the FixedUpdate to determine how much I move in that frame.

      It's worth noting that, if you're using a script that has the camera following the Avatar, its movement should also be in FixedUpdate so it doesn't get out of sync and look hella buggy.

      Delete