Search Unity

Difficulty Preserving Momentum While Jumping

Discussion in '2D' started by SMCawein, Apr 26, 2016.

  1. SMCawein

    SMCawein

    Joined:
    May 14, 2015
    Posts:
    5
    Good evening, folks!

    I'm designing a 2D platformer in Unity (version 5.3.1f1), but I'm having a lot of trouble preserving my character's momentum, specifically while airborne. I've done some research and tried to implement it in a few different ways based on older search results, but nothing so far has worked.

    Essentially, my problem is this: the player can move left and right just fine, and they can even jump around a bit, but whenever they do, they always jump straight up. Any speed they'd built going in either direction is immediately cancelled, and emulating the advice in the thread linked above isn't helping. It's still possible to jump sideways by inputting left or right after the jump, but this does not make for fun or precise movement.

    I think I may have narrowed down where the issue is coming from via testing, but I've no idea how to actually solve it. I was hoping somebody here could help me out with it?

    I'll include my player controller script below, along with my hypothesis on where the problem is and what I've tried to do to fix it. I apologize if it ends up kind of longish or I break the forum formatting; it's actually a lot shorter than it looks. I just tend to over-comment (I'm actually an English student, not a programmer, so I need the baby steps). Sorry if that makes it a pain to read!

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using UnityEngine.UI;
    4.  
    5. public class PlayerController : MonoBehaviour {
    6.  
    7.     // Determines player facing, which simplifies animations.
    8.     // Returns true if facing right, else false.
    9.     public bool facingRight = true;
    10.  
    11.  
    12.     // Returns true if player is able to jump, ie has not done so yet.
    13.     public bool jump = false;
    14.  
    15.     // The first checks if the player is trying to double jump, the second keeps track
    16.     // of whether they still have a jump to expend.
    17.     public bool doubleJump = false;
    18.     public bool hasdoubleJumped = false;
    19.  
    20.     // Amount of force exerted on the player when they move.
    21.     // Increase to cause the player to accelerate from idle position faster.
    22.     public float moveForce = 350f;
    23.  
    24.     // Maximum speed, prevents infinite acceleration.
    25.     public float maxSpeed = .5f;
    26.  
    27.     // Amount of force applied to player when they jump.
    28.     public float jumpForce = 700;
    29.     public float doubleJumpForce = 500f;
    30.  
    31.     // I don't know what this is going to do yet, honestly.
    32.     public Transform groundCheck;
    33.  
    34.     // Returns true if player is on a valid, flat surface from which they can jump normally.
    35.     private bool grounded = false;
    36.  
    37.     // This is a dirty hack, you might want to come back to this.
    38.     private bool onPlatform = false;
    39.  
    40.     // This stuff lets the player animate properly and interact with physics objects.
    41.     private Animator anim;
    42.     private Rigidbody2D rb2d;
    43.  
    44.     // Goldfish collected
    45.     private int count;
    46.     public Text countText;
    47.  
    48.     // These are all of the sound effects the player character can make.
    49.     public AudioClip GoldfishPickup;
    50.     public AudioClip NormalHop;
    51.     public AudioClip DoubleJump;
    52.  
    53.  
    54.     void Awake ()
    55.     {
    56.  
    57.         // Just fetching and storing component references.
    58.         // Nothing to see here, move along.
    59.         anim = GetComponent <Animator>();
    60.         rb2d = GetComponent<Rigidbody2D>();
    61.  
    62.         count = 0;
    63.         SetCountText();
    64.  
    65.     }
    66.  
    67.     void Update()
    68.     {
    69.         // Linecast draws a line down and returns a boolean (true) if it collides with
    70.         // groundCheck, at the player's feet. The latter part of the code ensures it is
    71.         // only checking the ground layer, and not other stuff that you might have going on.
    72.         grounded = Physics2D.Linecast(transform.position, groundCheck.position,
    73.             1 << LayerMask.NameToLayer("Ground"));
    74.  
    75.         // This is doing the same thing with platforms.
    76.         onPlatform = Physics2D.Linecast(transform.position, groundCheck.position,
    77.             1 << LayerMask.NameToLayer("Platform"));
    78.  
    79.  
    80.  
    81.         // Checks to see if the jump button is pressed, and also if the player is grounded.
    82.         // If you want to add a double jump later, this is where you need to play with the code!
    83.         if (Input.GetButtonDown("Jump") && (grounded || onPlatform))
    84.             jump = true;
    85.  
    86.         // Allows you to double jump while airborn.
    87.         else if (Input.GetButtonDown("Jump") && (!grounded || !onPlatform))
    88.         {
    89.             doubleJump = true;
    90.         }
    91.      
    92.         // Just animation stuff for jumping.
    93.         else if (Input.GetButtonUp("Jump"))
    94.         {
    95.             anim.ResetTrigger("Jump");
    96.         }
    97.  
    98.     }
    99.     // Physics stuff always goes here.
    100.     void FixedUpdate()
    101.     {
    102.  
    103.         float horizontal = Input.GetAxis("Horizontal");
    104.  
    105.         // Sets animation speed. Should even get faster as the player accelerates!
    106.         // The Mathf.Abs bit is using the absolute value of the above variable, since
    107.         // we're using Flip() to cheat and reorient our sprites by multiplying the
    108.         // X axis by -1. We always want the animation speed to be positive, right?
    109.             anim.SetFloat("Speed", Mathf.Abs(horizontal));
    110.  
    111.         // If the amount of force being applied to the player is less than the
    112.         // maximum allowed, we're going to add some more. This works in both directions,
    113.         // since horizontal can be a negative number.
    114.         if (horizontal * rb2d.velocity.x < maxSpeed)
    115.             rb2d.AddForce(Vector2.right * horizontal * moveForce);
    116.  
    117.  
    118.  
    119.         // If we are going too fast somehow, this will slow us down again.
    120.         // Mathf.Sign is doing something fun; it returns a 1 or -1, corresponding to whether
    121.         // the velocity is currently positive or negative (left or right). This way we can
    122.         // clamp the max speed in either direction with one line of code.
    123.         // We're leaving the y axis alone because we don't want to change the player's jump speed.
    124.         if (Mathf.Abs(rb2d.velocity.x) > maxSpeed && !jump)
    125.             rb2d.velocity = new Vector2(Mathf.Sign(rb2d.velocity.x) * maxSpeed, rb2d.velocity.y);
    126.      
    127.  
    128.         // This just makes sure we're animating in the same direction that we're moving.
    129.         // It does this by comparing the positive or negative value for horizontal, with the
    130.         // boolean for which direction we're currently facing. Positive values should mean we're
    131.         // facing to the right, negative to the left.
    132.         // (I actually had to invert these > < signs and I don't know why it works this way.)
    133.         if (horizontal < 0 && !facingRight)
    134.             Flip();
    135.         else if (horizontal > 0 && facingRight)
    136.             Flip();
    137.  
    138.         // This is making sure you get your double jump back when you land.
    139.         if (grounded)
    140.             hasdoubleJumped = false;
    141.  
    142.         if(jump)
    143.         {
    144.             // Tells the animator we're jumping now.
    145.             AudioSource.PlayClipAtPoint(NormalHop, transform.position);
    146.             anim.SetTrigger("Jump");
    147.             rb2d.AddForce(new Vector2(rb2d.velocity.x, jumpForce));
    148.             jump = false; // This also needs to be changed in the event of a double jump!
    149.         }
    150.  
    151.         // In theory, this triggers if the player CAN double jump (has single jumped), and has not
    152.         // done so already.
    153.         else if (doubleJump && !hasdoubleJumped)
    154.         {
    155.             AudioSource.PlayClipAtPoint(DoubleJump, transform.position);
    156.  
    157.             // This is cancelling out the stacking double jump height. Might want to remove it.
    158.             rb2d.AddForce(new Vector2(rb2d.velocity.x, -1*jumpForce));
    159.  
    160.  
    161.             rb2d.AddForce(new Vector2(rb2d.velocity.x, doubleJumpForce));
    162.             doubleJump = false; // This also needs to be changed in the event of a double jump!
    163.             hasdoubleJumped = true;
    164.         }
    165.     }
    166.  
    167.     // This method flips the orientation of your sprite, by scaling the X axis of the sprite by -1.
    168.     // This reduces the number of animations you need to draw manually. You're welcome, future me.
    169.     void Flip()
    170.     {
    171.         Vector3 spriteScale = transform.localScale;
    172.         spriteScale.x *= -1;
    173.         transform.localScale = spriteScale;
    174.         facingRight = !facingRight;
    175.     }
    176.  
    177.     //OnTriggerEnter2D is called whenever this object overlaps with a trigger collider.
    178.     void OnTriggerEnter2D(Collider2D other)
    179.     {
    180.         //Check the provided Collider2D parameter other to see if it is tagged "PickUp", if it is...
    181.         if (other.gameObject.CompareTag("Pickup"))
    182.         {
    183.             // Plays a sound, deactivates the game object, and then increments total collected.
    184.             AudioSource.PlayClipAtPoint(GoldfishPickup, transform.position);
    185.             other.gameObject.SetActive(false);
    186.             count = count + 1;
    187.             SetCountText();
    188.         }
    189.    
    190.     }
    191.  
    192.     // Method refreshes total of pickups collected in the UI when called.
    193.     void SetCountText()
    194.     {
    195.         countText.text = "x" + count.ToString();
    196.     }
    197. }
    Again, sorry if that was way too much to sift through, but I didn't want to risk not including some method or another that might actually turn out to be the problem.

    I'm fairly certain that the problem is in the bit of code that defines how a player jumps, in line 147. My gut says it's specifically this bit:

    ...which, in theory, should be passing both the old value of the player's velocity on the x axis, with the new force of the jump on the y axis, as arguments. Except, clearly, this isn't happening. I determined this must be the problem because if I replace rb2d.velocity.x with any static value, positive or negative, the character will hop in that direction! For instance, a value of 30 will cause them to hop to the right; -30 will steer them to the left. This is due to the flip function.

    So that's cool, it tells the line of code is definitely working as intended, in that it is allowing the player to jump while moving on the x axis, but that rb2d.velocity.x is not what I think it is.

    A friend of mine took a whack at it, too, and we wrote it a few different ways, but I've got no clue what the solution is. Am I just using it incorrectly? Any help would be much appreciated. After all, I can't imagine a platformer being fun to play if jumping doesn't even feel good!

    Sorry this is such a basic question, and thanks in advance!
     
  2. SMCawein

    SMCawein

    Joined:
    May 14, 2015
    Posts:
    5
    Hey again! I don't mean to pain, but this post spent about three days in limbo since it was my first, and had to be approved. So I'm just bumping it for visibility!

    Thanks again to anyone that can offer some insight~
     
  3. Manny Calavera

    Manny Calavera

    Joined:
    Oct 19, 2011
    Posts:
    205
    The line you mention (147) is indeed strange. You are setting the numerical value of velocity as a force. Think about it.. velocity is measured in m/s while force is measured in N.

    However, you are adding a force in the same direction of the velocity and thus it doesn't explain why the horizontal speed gets canceled. So while line 147 is not correct you have another problem elsewhere.

    You can narrow it down quickly by adding some Debug.Log() lines and watch how and when the velocity changes.
     
  4. SMCawein

    SMCawein

    Joined:
    May 14, 2015
    Posts:
    5
    Thanks for the response! I actually do not know how Debug.Log() works, can you explain to me how to use it? It sounds like it'd be awfully useful a thing to learn, anyway! Then I'll get back to you on what's going on with it.

    And what would you recommend putting in place of rb2d.velocity.x in that case, even if it is not the cause of the problem? I'd like to get into the habit of good form.
     
  5. Manny Calavera

    Manny Calavera

    Joined:
    Oct 19, 2011
    Posts:
    205
    Here is the doc on Debug.Log(): http://docs.unity3d.com/ScriptReference/Debug.Log.html
    For instance, you could print out the current velocity like: Debug.LogFormat("My velocity right now is {0}", rb2d.velocity);

    But anyway, assuming you have a rigidbody with default mass of 1, your force of 700 is far too strong for a max speed of 0.5 which is way too low.. So what's happening is your code is applying a force and the object is accelerating way past your max speed. Next frame it won't apply any force and instead will truncate the speed to 0.5. Then next frame it will apply a force again and so on. This will create a stutter movement. It's probably also the cause of your horizontal speed disappearing.. after you jump, line 125 will truncate the speed to 0.5 which is practically stopping. (Note that the variable 'jump' will only be true during one frame, so soon after the jump force is applied, that code will hit again)
     
  6. SMCawein

    SMCawein

    Joined:
    May 14, 2015
    Posts:
    5
    I've been playing with the debug tool today; thanks again for teaching me how to use it! I knew there had to be a feature like it somewhere and I'm sure I'll get a ton of mileage out of it!

    So, looking at my results, it looks like you're spot on. That max speed code is kicking in pretty much every frame, because .5f is too low compared to the forces being applied to it. But here's my dilemma; while changing the maximum speed to something higher does seem to fix the jumping momentum problem (hooray!) it creates a new one because then the character rockets off of the screen every time they move! I've tried lowering the force applied when the character moves or jumps to compensate, but that just creates new problems with their acceleration being too low, or not getting off of the ground, respectively.

    I think this might be because of the size of the sprites I'm working with. The player is maybe like, 20x20 pixels? So even a little bit of force results in quite a bit of movement. We're talking this scale:





    Are there any values in Unity that I can play with, such as its mass or the force of gravity, that would allow me to implement this fix without firing off into the distance? Or is there something else I'm missing? If I can, I'd like to avoid having to upscale all of the assets (which won't look as nice, and also necessitate me starting completely over). But in any case, this is actually a lot of progress.

    [Edit: Sorry about the half-post, hit enter too quickly.]
     
    Last edited: May 3, 2016
  7. Manny Calavera

    Manny Calavera

    Joined:
    Oct 19, 2011
    Posts:
    205
    A lot of games with tight platforming controls don't use physics at all. If you implement everything from scratch you have more control.

    But it can be achieved with physics too. I certainly use it.

    It's tough to get it just right. It will take a lot of iterations, trial and errors, workarounds..

    Some next steps for you:

    - The overall gravity can be found here: http://docs.unity3d.com/Manual/class-Physics2DManager.html
    - You can also change the gravity for specific objects individually with the gravity scale: http://docs.unity3d.com/ScriptReference/Rigidbody2D-gravityScale.html
    - The size of your sprite in pixels doesn't matter. But the size in meters matter. Try to keep your character around 1 or 2 m height. Scale the world around him accordingly.
    - You can also 'jump' by assigning an initial Y velocity. That will make the character jump instantly.
    - Some games let you jump a little higher if you keep holding the jump button. That's achieved with a mix of initial velocity and an additional force that is applied and decays over time.

    Like I said, it's not easy to get this right and you definitely want to get this right before designing levels. Otherwise later you decide that the character should jump higher and suddenly you have to readjust a ton of levels... been there, done that.
     
  8. SMCawein

    SMCawein

    Joined:
    May 14, 2015
    Posts:
    5
    Thanks so much for all the links! I'll definitely take your advice and start playing with all of these settings until it's perfect!

    Also I forgot to mention but I approve of your username; Grim Fandango is great, and by extension, so are you.
    Anyway, thanks again, you're the best!
     
    Manny Calavera likes this.