0
\$\begingroup\$

I've been working with my game's settings to get the perfect jump height, gravity and movement that suits the player, however my problem is my player essentially floats back down to the ground, when I want them to fall much faster (it's a 3D game) but I don't want to tamper the gravity anymore solely for the player.

What I've done is created 2 extra Booleans alongside my Grounded Boolean, one for In Air and another for Extra Gravity, and this is where my problem comes in.

I have no idea how to set a tag to the empty space in the air, and I was wondering if anyone knew how to do this, or if there is a better method for detecting when my player is in the air?

This is what my script currently looks like:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerScript : MonoBehaviour
{
    public float movementSpeed = 5f;
    public float jumpForce = 7;
    public float jumpSpeed = 7;
    public Rigidbody rigidbody;
    private Vector3 input;
    public GameObject Player;
    public bool grounded;
    public bool inAir;
    public float ExtraGravity;


    // Use this for initialization
    void Start()
    {
        var rigidbody = GetComponent<Rigidbody>();
        var col = GetComponent<CapsuleCollider>();
        Player = GameObject.FindWithTag("Player");
    }
    void FixedUpdate()
    {
        if (Input.GetKeyDown("space"))
        {

            if (grounded)
            {
                Debug.Log("Spacebar pressed");
                rigidbody.AddForce(0, jumpForce, 0);
            }

            if(inAir)
            {
                Vector3 vel = rigidbody.velocity;
                vel.y -= ExtraGravity * Time.deltaTime;
                rigidbody.velocity = vel;
            }

        }
    }

    // Update is called once per frame
    void Update()
    {

        if (Input.GetKey("w"))
        {
            transform.position += transform.TransformDirection(Vector3.forward) * Time.deltaTime * movementSpeed * 5.5f;
        }
        else if (Input.GetKey("s"))
        {
            transform.position -= transform.TransformDirection(Vector3.forward) * Time.deltaTime * movementSpeed * 4.5f;
        }
        else if (Input.GetKey("d"))
        {
            transform.position += transform.TransformDirection(Vector3.right) * Time.deltaTime * movementSpeed * 5.5f;
        }
        else if (Input.GetKey("a"))
        {
            transform.position -= transform.TransformDirection(Vector3.right) * Time.deltaTime * movementSpeed * 5.5f;
        }


        if (Input.GetKey(KeyCode.LeftShift))
        {
            transform.position += transform.TransformDirection(Vector3.forward) * Time.deltaTime * movementSpeed * 20.5f;
        }

    }

    void OnCollisionStay(Collision collision)
    {
        if (collision.gameObject.CompareTag("Floor"))
        {
            grounded = true;
        }
    }

    void OnCollisionExit(Collision collision)
    {
        if (collision.gameObject.CompareTag("Floor"))
        {
            grounded = false;
        }
    }

}
\$\endgroup\$
4
  • \$\begingroup\$ I'm not convinced your chosen method is the best one — perhaps adding to/subtracting from velocity.y according to a carefully-created animation curve is better, though I've never tried it — but if you're set on going through with it, you could use raycasts instead of colliders. Pick a max distance and cast a ray (or rays) down. If there are no hits, you're in the air. If you only want it to kick in during descent, check whether velocity.y < 0. \$\endgroup\$ – verified_tinker Nov 14 '19 at 8:49
  • \$\begingroup\$ What is in air if not "not grounded"? Why would you need two separate booleans for this? \$\endgroup\$ – DMGregory Nov 14 '19 at 12:25
  • \$\begingroup\$ @DMGregory Because I'm trying to set a Boolean to trigger ExtraGravity, so that whenever the player is in the air, the extra gravity is applied to them, to make them fall faster, without adding more gravity to the world itself. With the gravity settings in Unity, for some reason, it has to have a lot of 0's otherwise the model either won't budge, or fly through the sky like a rocket. I'm not sure why this is and why the settings have to be so finicky, but I thought this would be the best solution to the current problem. \$\endgroup\$ – CraftyMaelyss Nov 14 '19 at 12:30
  • 1
    \$\begingroup\$ @CraftyMaelyss Basically, what you want to do is make the player fall faster, right? Then DMGregory's right; if velocity.y < 0 && !grounded, you can add your ExtraGravity. \$\endgroup\$ – verified_tinker Nov 14 '19 at 20:34
0
\$\begingroup\$

There's a lot we can improve here:

  • Don't use a force to jump. Forces are for a gradual push over time, like gravity or wind. A jump is a single sharp change in motion, so we should apply it with an impulse or velocity change.

    • This ensures we won't have to go change our tuning numbers if we later change the project's fixed timestep, because the whole jump is occurring instantaneously, not integrated over the duration of the step like a force.
  • Keep your tuning parameters in intuitive units.

    • If you have a variable called movementSpeed, make it actually the speed of movement in units per second. Don't add extra fudge factors into the equation that make this parameter only one fifth of the actual runtime speed. These arbitrary factors make our numbers harder to reason about, and require looking at both the code and the Inspector values together to work out the true speed.

    • Trying to find the right force/impulse/velocity value for a jump is hard - we don't have a good intuition for what the numbers mean in practical terms, so it ends up being a lot of trial & error. Instead, let's express our jump in terms of the height we want to hit, and how long it should last. We can reason about the effect of those numbers much more easily. Converting those intuitive parameters to the required physics values is the computer's job. ;)

    • You're shadowing your rigidbody variable in Start - effectively declaring a new one-off variable only for that one method, populating it with GetComponent, then throwing away the result. The rigidbody you've assigned in the inspector is the one you're currently using.

  • Instead of using hard-coded keys to detect movement direction, you should use axes. By default, Unity already maps WASD to horizontal & vertical axes, so this will give you similar behaviour out of the box, with a bunch of extra wins:

    • Simpler, less repetitive code
    • The ability to support gamepad input with no code changes
    • The ability to synonymize controls (so if a player prefers using the arrow keys instead of WASD, that will "just work" without needing to change settings)
    • The ability for players to remap the controls in the launch dialog, for comfort and accessibility
  • Don't mix transform movement with rigidbody movement. If you're using one or the other, use it exclusively. Mixing these two means control is constantly being ripped out of the physics engine's hands, and it needs to clean up the resulting mess on the next frame, leading to unwanted behaviour like jittering, tunnelling, and inaccurate collisions.

  • Don't clear your grounded flag using OnCollisionExit - this gets called when you walk off one floor object, even if you're still standing on another floor object. A short raycast or shapecast is much more reliable.

    • If you use a physics layer and layer mask to mark what's ground vs not, then you don't need to compare tags at all. Stuff that isn't ground will not even be checked, making the query more efficient.
  • Checking for key/button down events in FixedUpdate can lead to undesired behaviour, because FixedUpdate can sometimes be called multiple times in one frame (leading to double-handling input), or not at all for a frame (leading to missed input). Holds are OK, since by nature we expect these to persist across several frames, But presses we need to handle in update

Here's what a player movement controller with these changes might look like:

// This script needs a body to work, so let's ensure the engine knows.
[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement : MonoBehaviour {

    // Set this in the Inspector to filter to physics layers the
    // player can jump off of. (And put your platforms on those layers)
    public LayerMask whatIsGround;

    // Speeds in world units per second.
    public float movementSpeed = 5f;
    public float dashSpeed = 20f;

    // Peak height of the jump, in world units.
    public float jumpHeight = 3f;
    // Duration of the rising and falling phases of the jump.
    public float secondsToPeak = 0.3f;
    public float secondsToFall = 0.2f;

    // Physics quantities calculated from the above.
    [SerializeField, HideInInspector]
    float _jumpVelocity;
    [SerializeField, HideInInspector]
    float _ascentGravity;
    [SerializeField, HideInInspector]
    float _descentGravity;

    // References to our physics components.
    [SerializeField, HideInInspector]
    Rigidbody _body;
    [SerializeField, HideInInspector]
    CapsuleCollider _collider;

    // This flag does double-duty as both on-ground and not-in-air. ;)
    bool _isGrounded;

    // OnValidate gets called when we change stuff in the editor.
    // So this keeps our components & values up-to-date.
    void OnValidate() {
        _body = GetComponent<Rigidbody>();
        _collider = GetComponentInChildren<CapsuleCollider>();

        // Total gravity on the ascent & descent phases.
        _ascentGravity = -2f * jumpHeight / (secondsToPeak * secondsToPeak);
        _descentGravity = -2f * jumpHeight / (secondsToFall * secondsToFall);

        // Vertical velocity needed to reach desired peak height.
        _jumpVelocity = -_ascentGravity * secondsToPeak;

        // Remove gravity already accounted for by the built-in physics,
        // leaving only the extra we need to apply while rising/falling.
        var defaultGravity = Physics.gravity.y;
        _ascentGravity -= defaultGravity;
        _descentGravity -= defaultGravity;
    }

    // This check assumes your character's origin is at the bottom of its collider.
    // You can adjust the origin of the query if that's not true for your case.
    void UpdateGroundedState() {
        Vector3 origin = transform.position;
        origin.y += _collider.radius + Physics.defaultContactOffset * 2f;

        Ray ray = new Ray(origin, Vector3.down);

        // We fire a sphere a tiny bit smaller than our capsule
        // straight down a tiny distance to see if it hits anything.
        _isGrounded = Physics.SphereCast(
                ray,
                _collider.radius - Physics.defaultContactOffset * 2f,
                Physics.defaultContactOffset * 6f,
                whatIsGround
            );
    }

    void FixedUpdate() {
        UpdateGroundedState();

        // Apply the extra gravity if we're in the air,
        // ie. not on ground.
        if (_isGrounded == false) {
            // Use different gravity for the up & down phases.
            float extraGravity = _body.velocity.y > 0f ?
                _ascentGravity : _descentGravity;

            // Apply the extra gravity as an acceleration (ignore mass).
            _body.AddForce(extraGravity * Vector3.up, ForceMode.Acceleration);
        }

        // Gather input as a vector from our axes.
        Vector3 input = Vector3.zero;
        input.x = Input.GetAxis("Horizontal");
        input.z = Input.GetAxis("Vertical");

        // Make sure we don't exceed full speed diagonally.
        input = Vector3.ClampMagnitude(input, 1f);

        // Construct a desired velocity from input and our speed.
        Vector3 velocity = input * movementSpeed;

        // Add the dash (keeping it similar to your existing version).
        if (Input.GetButton("Dash")) {
            velocity += dashSpeed * transform.forward;
        }

        // Compute the net change in velocity we want,
        // and cancel out the vertical effect 
        // so we don't interfere with jumping & falling.
        Vector3 deltaV = velocity - _body.velocity;
        deltaV.y = 0f;

        // Apply the velocity change.
        _body.AddForce(deltaV, ForceMode.VelocityChange);
    }

    void Update() {
        UpdateGroundedState();

        // Handle our jump in Update so we never miss/double a press.
        if (_isGrounded && Input.GetButtonDown("Jump")) {
            _body.AddForce(_jumpVelocity * Vector3.up, ForceMode.VelocityChange);
            _isGrounded = false;
        }
    }
}
\$\endgroup\$
0
\$\begingroup\$

First of do a raycast facing downwards from the player to check for ground, Using oncollision methods are a terrible idea. For example if you are jumping at a box in midair and then collide with said box the way you have it setup will tell your player that you are currently grounded.

\$\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.