0
\$\begingroup\$

I want to create a jump function that launches a RigidBody2D along a low, long, trajectory. In the following examples, I have set the linear and angular drag to 0, to eliminate any possibility of unwanted friction calculations. Mass is set to 1, and gravity to 3 for the rigidbody. At present, I have tried variations on this:

rb.AddForce(new Vector2(x,y));

or:

rb.velocity = new Vector2(x, y);

These have been implemented in my CharacterController script, in a public void LongJump() function.

As I understand it, AddForce respects physics, while velocity simply sets velocity to a given value, irrespective of physics. Thus, when using AddForce(new Vector2(x,y)), I expect the magnitude of the force to be sqrt(x^2 + y^2) and the direction to be sin(arctan(y/x)), or thereabouts. Given this, I would expect normal, kinematic equations to describe the result (i.e., x(t) = x0 + xt - .5gt^2).

However, instead of this, the rigidbody simply flies in the direction of motion at roughly the correct angle, rises slightly at the end of the frame, and then falls directly downward without preserving the x-direction velocity.

I would like to know how to get correct physics behavior. I can only guess there's something I don't know about unity's handling of physics.

I have found some tutorials online, for creating parabolic motion, but all of them require a known target, which is not helpful. My function is meant to allow the jump from any point to any other point, and I need to respect collisions.

Edit. Here is the Character Controller. It is based on brackley's (from his tutorial series on 2d games). The longjump() as written is just one variation of things I've tried, so think of it as a placeholder:

using UnityEngine;
using UnityEngine.Events;

public class CharacterController2D : MonoBehaviour
{
    
    [SerializeField] private float m_JumpForce = 400f;                          // Amount of force added when the player jumps.
    [Range(0, 1)] [SerializeField] private float m_CrouchSpeed = .36f;          // Amount of maxSpeed applied to crouching movement. 1 = 100%
    [Range(0, .3f)] [SerializeField] private float m_MovementSmoothing = .05f;  // How much to smooth out the movement
    [SerializeField] private bool m_AirControl = false;                         // Whether or not a player can steer while jumping;
    [SerializeField] private LayerMask m_WhatIsGround;                          // A mask determining what is ground to the character
    [SerializeField] private Transform m_GroundCheck;                           // A position marking where to check if the player is grounded.
    [SerializeField] private Transform m_CeilingCheck;                          // A position marking where to check for ceilings
    [SerializeField] private Collider2D m_CrouchDisableCollider;                // A collider that will be disabled when crouching

    const float k_GroundedRadius = .2f; // Radius of the overlap circle to determine if grounded
    private bool m_Grounded;            // Whether or not the player is grounded.
    const float k_CeilingRadius = .2f; // Radius of the overlap circle to determine if the player can stand up
    private Rigidbody2D m_Rigidbody2D;
    private bool m_FacingRight = true;  // For determining which way the player is currently facing.
    private Vector3 m_Velocity = Vector3.zero; 

    //The values below are my additions to this code.
    [SerializeField] private Transform m_SideCheck;//position to check if player is wall sliding (right)
    const float k_SideCheckRadius = .5f; //Radius for determining if a wall jump is possible. 
    private bool m_WallSliding;//boolean to check if side jump is allowed. 
    private bool m_WallSlideValidation;//Bool to ensure we can jump only once. 
    

    [Header("Events")]
    [Space]

    public UnityEvent OnLandEvent;

    [System.Serializable]
    public class BoolEvent : UnityEvent<bool> { }

    public BoolEvent OnCrouchEvent;
    private bool m_wasCrouching = false;

    public BoolEvent onWallSlideEvent;//An event to check when wallsliding may be happening.


    private void Awake()
    {
        m_Rigidbody2D = GetComponent<Rigidbody2D>();

        if (OnLandEvent == null)
            OnLandEvent = new UnityEvent();

        if (OnCrouchEvent == null)
            OnCrouchEvent = new BoolEvent();

        if (onWallSlideEvent == null)
            onWallSlideEvent = new BoolEvent();
    }


    private void FixedUpdate()
    {
        bool wasGrounded = m_Grounded;
        m_Grounded = false;

        // The player is grounded if a circlecast to the groundcheck position hits anything designated as ground
        // This can be done using layers instead but Sample Assets will not overwrite your project settings.
        Collider2D[] colliders = Physics2D.OverlapCircleAll(m_GroundCheck.position, k_GroundedRadius, m_WhatIsGround);
        for (int i = 0; i < colliders.Length; i++)
        {
            if (colliders[i].gameObject != gameObject)
            {
                m_Grounded = true;
                if (!wasGrounded)
                    OnLandEvent.Invoke();
            }
        }

        //this section addresses the wall slide conditions. 
        bool wasWallSliding = m_WallSliding;
        m_WallSliding = false;//Assumes we are not wall sliding. 
        if (m_Grounded == true)
        {
            m_WallSlideValidation = true;//Used to ensure we can only wall jump once.
        }
        Collider2D[] walls = Physics2D.OverlapCircleAll(m_SideCheck.position, k_SideCheckRadius, m_WhatIsGround);//Walls
        for (int i = 0; i < walls.Length; i++)
        {
            if ((walls[i].gameObject != gameObject) && (m_Grounded != true))//Second condition ensures we are not grounded. We only want to wall jump when not grounded.
            {
                m_WallSliding = true;
                //if (!wasWallSliding)//This part needs to be fixed, so that the animation is properly invoked.
                //  onWallSlideEvent.Invoke(true);//this line would invoke the wallslide animation, if there were one. 
            }
        }
    }


    public void Move(float move, bool crouch, bool jump)
    {
        // If crouching, check to see if the character can stand up
        if (!crouch)
        {
            // If the character has a ceiling preventing them from standing up, keep them crouching
            if (Physics2D.OverlapCircle(m_CeilingCheck.position, k_CeilingRadius, m_WhatIsGround))
            {
                crouch = true;
            }
        }
        
        if (!Physics2D.OverlapCircle(m_GroundCheck.position, k_GroundedRadius, m_WhatIsGround))//note the "!"
        {
            //This fixes the issue with crouch movement speed remaining constant even if you crouch walk off a ledge. 
            crouch = false;
        }

        //only control the player if grounded or airControl is turned on
        if (m_Grounded || m_AirControl)
        {
            // If crouching
            if (crouch)
            {
                if (!m_wasCrouching)
                {
                    m_wasCrouching = true;
                    OnCrouchEvent.Invoke(true);
                }
                // Reduce the speed by the crouchSpeed multiplier
                move *= m_CrouchSpeed;
                
                // Disable one of the colliders when crouching
                if (m_CrouchDisableCollider != null)
                    m_CrouchDisableCollider.enabled = false;
            } else
            {
                // Enable the collider when not crouching
                if (m_CrouchDisableCollider != null)
                    m_CrouchDisableCollider.enabled = true;

                if (m_wasCrouching)
                {
                    m_wasCrouching = false;
                    OnCrouchEvent.Invoke(false);
                }
            }

            // Move the character by finding the target velocity and smoothing the movement.
            Vector3 targetVelocity = new Vector2(move * 10f, m_Rigidbody2D.velocity.y);//This sets the x and y directions, but not the z direction. 
            m_Rigidbody2D.velocity = Vector3.SmoothDamp(m_Rigidbody2D.velocity, targetVelocity, ref m_Velocity, m_MovementSmoothing);//ref m_Velocity = zero.

            // If the input is moving the player right and the player is facing left...
            if (move > 0 && !m_FacingRight)
            {
                // ... flip the player.
                Flip();
            }
            // Otherwise if the input is moving the player left and the player is facing right...
            else if (move < 0 && m_FacingRight)
            {
                // ... flip the player.
                Flip();
            }
        }

        // If the player should jump...
        if (m_Grounded && jump)
        {
            // Add a vertical force to the player.
            m_Grounded = false;
            m_Rigidbody2D.AddForce(new Vector2(0f, m_JumpForce));
        }

        //The portion below is my addition to this code. 
        //Implements wall jump
        if (m_WallSliding && jump && m_WallSlideValidation)
        {
            // Add diagonal force to the player.
            WallJump();
            m_WallSliding = false;//ensures we make sure we're still wall sliding if we try this again.
            m_WallSlideValidation = false;//makes it so you can only walljump once. 
        }

    }

    private void WallJump()
    {
        //My function to handle wall-jumping. 
        float newForceY = .75f * m_JumpForce;
        float newForceX = -1f * m_JumpForce * transform.localScale.x;
        //Debug.Log(transform.localScale.x);
        m_Rigidbody2D.velocity = Vector3.zero;
        m_Rigidbody2D.AddForce(new Vector2(newForceX, newForceY));
    }

    //LongJump() should be made private and called by Move(), and the conditions of calling it should be shifted.
    public void LongJump()
    {
        //Trying this code. While loop did not work.
        m_Rigidbody2D.AddForce(new Vector2(0, m_JumpForce / 2));
        m_Rigidbody2D.velocity = new Vector2(100f, 0);
    }

    private void Flip()
    {
        // Switch the way the player is labelled as facing.
        m_FacingRight = !m_FacingRight;

        // Multiply the player's x local scale by -1.
        Vector3 theScale = transform.localScale;
        theScale.x *= -1;
        transform.localScale = theScale;
        //Debug.Log(theScale.x);
    }

    private void OnDrawGizmosSelected()
    {
        //This is for the editor. It simply draws a wire sphere around the transform of a given radius.
        //Basically excellent for debugging. 
        Gizmos.DrawWireSphere(m_SideCheck.position, k_SideCheckRadius);
        Gizmos.DrawWireSphere(m_GroundCheck.position, k_GroundedRadius);
    }
}
\$\endgroup\$
  • \$\begingroup\$ You might want to show us your CharacterController code. Stopping and falling directly downward is not the normal expected behaviour, so it's likely something in your code is causing this to happen. \$\endgroup\$ – DMGregory Aug 11 at 19:14
  • \$\begingroup\$ I have added the controller code. \$\endgroup\$ – Luke Mathwalker Aug 11 at 19:33
  • 1
    \$\begingroup\$ Do you have air control enabled? If so, m_Rigidbody2D.velocity = Vector3.SmoothDamp(m_Rigidbody2D.velocity, targetVelocity, ref m_Velocity, m_MovementSmoothing); will rapidly kill your momentum while you're airborne. \$\endgroup\$ – DMGregory Aug 11 at 20:08
  • \$\begingroup\$ That's exactly it. Thank you so much. Looks like I need to set conditions to switch air control on and off when doing this jump. \$\endgroup\$ – Luke Mathwalker Aug 11 at 20:19
  • \$\begingroup\$ If you've solved your problem, please feel free to write up your solution as an Answer below. \$\endgroup\$ – DMGregory Aug 11 at 20:20
1
\$\begingroup\$

Thanks to DMGregory, the issue was the m_AirControl variable, which caused the velocity to rapidly fall when attempting to do a LongJump().

The full solution is this:

public void LongJump()
    {
        //This is fine
        m_AirControl = false;
        m_Rigidbody2D.AddForce(new Vector2(0, m_JumpForce / 2));
        m_Rigidbody2D.velocity = new Vector2(100f, 0);
    }

And then modify the move() function:

public void Move(float move, bool crouch, bool jump)
    {
        //Debug.Log("Move is :" + move);
        if (move != 0 || crouch == true || jump == true)
        {
            m_AirControl = true;
        }
    //[....]
    }

This will effectively disable air control for the duration of the jump, forcing the jump to follow the set trajectory based on the force inputs. Then the adjustments to move ensure that, when the player is next able to input a valid input, air control turns back on, allowing for control over the jump.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.