Improving The Jump in 3rd Person Controller Course

I recently finished the 3rd Person Controller course, and loved the ground work it laid out. I want to improve upon it a bit more, and one of my bigger issues is how the jump works, it doesn’t feel as floaty, and controlled as jumping in some other games, both classic and current, so I was wondering if I could have some help on improving it.

Some goals:
-Having a falling state, and landing state.
-Having it feel more controlled, so you can pull your jumps midair
-giving it a weightier feel.
-Working better if you get stuck on a ledge.
-Basically making it closer to how Unity sets up their 3rd Person Controller Standard Asset. Cause that one’s jumping is pretty fun, and gets the job done.

I am still learning however, and working with the StateMachine feels limiting, when trying to find other “Player Jumping” tutorials online since we did things in a specific way.

so if anyone has any advice or insight, I’d love to get a discussion started, and of course have this here for future students of this course to refer to as well!

Thanks!

I’ve taken the liberty of editing the categories to put this in the 3rd Person Combat and Traversal course, and since I think this might stretch beyond one question into a discussion I converted it into a Talk topic (so our system doesn’t go and close it on us).

I think a bit more definition might be in order for a couple of these… For example, I’m not sure what you mean by “pulling your jumps midair”… (I have an idea). In the course, we strove to keep things fairly realistic… for example, if you do a running jump off of a ledge in real life, we don’t get to turn about or pull the jump once we’ve committed to it. Once your feet have left the ground, the result is a combination of gravity and momentum (unless you happend to also have a parachute, rocket belt, or boots of levitation). That being said, some systems do have a bit more control available, allowing you to turn about in mid jump or to double jump (those tricky Mario Brothers!).

I’ll start things off, however, with a bit of a freebie… the falling state is already in the game, the PlayerFallingState, which we go to from the PlayerJumpingState when the player has reached the apex of the jump…

Now as it happens, there is a handy property on the CharacterController called IsGrounded…
So if you add this to the beginning of your Tick() methods in PlayerFreeLookState() and PlayerTargettingState(), your character will automagically go into the falling state whenever he steps off of a rock, plank, or cliff:

if (!stateMachine.Controller.isGrounded)
        {
            stateMachine.SwitchState(new PlayerFallingState(stateMachine));
            return;
        }

Thanks for changing it to the proper place.

To explain what I mean by pull, and further explain what I’m looking for; in many games when you jump with a third person character you have a small degree of control with the direction, and velocity of the character in the air. Take Elden Ring for example. If you run forward with all your momentum, and jump you get the longest jump you can, but in mid air there are times where you might of over shot a bit, and you have some seconds to readjust, pull back, and possible still land on the desired platform. I know this isn’t realistic, but I think it’s become something players enjoy, adding a kind of super human maneuverability to the character.

Right now our character fires off at the momentum, and cannot shift direction, or lose velocity in anyway. I should reiterate, that these are not drastic control changes once in midair, but subtle. Ideally I’d find a way to make them adjustable for testing/playing.

As for the code you added, that makes alot of sense, however in my game when I added and tested I cam into the issue of any slope I’d fall down (so a rock terrain area) I’d go into my falling state, and it looked really weird, and was hard to control my player.

Is there a way to adjust how isGrounded senses the ground? or a way to sense whether the slope the character is falling off is for instance, a dead 90degree drop or just a subtle slope? I was thinking this was in Character Controller, slope limit? It gets rid of this problem, but seems finicky.

Unfortunately, the IsGrounded property is out of our control. There are some tricks we can pull, though… For example, on a slope, the character loses IsGrounded less often than off of a cliff… so we can put a timing trap on IsGrounded…

I ran some playtesting in the course provided terrain, and found that except for the steepest slopes in the terrain, IsGrounded is never more than about .25f… I did this by creating a private float isGroundedTime, and then whenever the character is grounded, setting that time to zero, and whenever it’s not grounded, adding deltaTime and logging it out. Jumping off the large rock yielded closer to .6f time not grounded…

I then put a field in the PlayerStateMachine

[field: SerializeField] float FallingThreshhold {get; private set;} = .25f;

Then in the PlayerBaseState, I put a simple method to keep track of the time, filtering for short falls, like going down a slope:

    private float isGroundedTime = 0;
    protected bool FilteredGrounded(float deltaTime)
    {
        if (!stateMachine.Controller.isGrounded)
        {
            isGroundedTime += deltaTime;
            //You can uncomment the next line if your still stumbling too much to get a log out of the isGroundedTime
            //Debug.Log($"IsGrounded=false {isGroundedTime} {Time.time}");
            if (isGroundedTime > stateMachine.FallingThreshhold)
            {
                return true;
            }
        }
        else
        {
            isGroundedTime = 0;
        }
        return false;
    }

And finally, the isGrounded trap in the PlayerFreeLookState and PlayerTargetingState becomes:

if(FilteredGrounded(deltaTime))
{
    stateMachine.SwitchState(new PlayerFallingState(stateMachine));
    return;
}

Now this one is a bit trickier, but I have an idea that might work for this…
We don’t want instant control, like it shouldn’t even be possible to leap forward and then return to where you leapt from by pulling back on the controller (or hitting S)… but… that doesn’t mean that we can’t utilize the input…

I’m going to start by noting that at the moment of entering either the PlayerJumpingState or the PlayerFallingState, we get the momentum as the CharacterController’s current velocity. This is effectively the speed and direction the character was moving in the last FixedUpdate(). We don’t need to worry about how that’s calculated, just know that what makes this an ideal starting point is that we’re using the physics engine to get the actual current speed… This gives us the realistic movement. Just like in real life, if you step off of a cliff, you won’t move very far from the cliff before the rocks below make you wish you hadn’t stepped off, if you run off of the cliff, you’ll continue your forward momentum.

There are two ways we can go about adjusting with the controller. You could have the momentum slowly seek the direction of the InputReader movement, or you could have the momentum get the value of the InputReader movement slowly added to the momentum…

Ultimately, this is what I came up with.
First, I need a jump input sensitivity, which I added to the PlayerStateMachine:

[field: SerializeField] float JumpingInputSensitivity {get; private set;} = 2.0f;  //Adjust to taste

Then I added this helper class to the PlayerBaseState

    protected Vector3 ApplyInputToMomentum(Vector3 _momentum, float deltaTime)
    {
        Vector2 movement = stateMachine.InputReader.MovementValue * stateMachine.JumpingInputSensitivity;
        Vector3 movementVector = new Vector3(movement.x, 0, movement.y);
        momentum = stateMachine.transform.TransformVector(momentum); //convert from local to world space
        return Vector3.Lerp(_momentum, movementVector, deltaTime);
    }

Then the first line in PlayerJumpingState and PlayerFallingState will be

momentum = ApplyInputToMomentum(momentum, deltaTime);

With the setting of 2.0f, I was able to achieve a small amount of change. This can easily be adjusted to taste. I do find that if you are going to want to change jump momentum, you may wish to increase the jump force to give the player more time to react (fun times while testing, try setting Jump Force to 10 and Jumping Input Sensitivity to 250.)

Thanks Brian, all makes sense, and this worked pretty well. Nice thinking using the timer to get around the restrictions of the isGrounded bool

Having a slight issue with this bit. momentum is commenting red because it doesn’t exist in the current context (this is in the PlayerBaseState) I noticed there’s the momentum passing into the method, then momentum without the "" I changed it to have all of them have “_momentum” but my character would just jump straight into the air.

I feel like I might be missing something simple here.

Also, and please forgive me as I continue to complicate this, but is it possible to add slight rotation to this as well or would that be complicated. I played a few more games today to see how the jumping felt, and I noticed that the same thing we’re doing here, also happens with rotation, to let the player slightly change direction as they jump too.

Let’s get the momentum sorted first…

There is a typo in my version that I typed in, as I added the line

momentum = stateMachine.transform.TransformVector(momentum);

after I pasted in the original code I’d written, when I realized we needed to convert the Vector from local to world space… the correct method should read:

protected Vector3 ApplyInputToMomentum(Vector3 _momentum, float deltaTime)
    {
        Vector2 movement = stateMachine.InputReader.MovementValue * stateMachine.JumpingInputSensitivity;
        Vector3 movementVector = new Vector3(movement.x, 0, movement.y);
        _momentum = stateMachine.transform.TransformVector(_momentum); //convert from local to world space
        return Vector3.Lerp(_momentum, movementVector, deltaTime);
    }

I was wondering if that was the case and corrected it myself. All the code works without errors, but when I jump it’s pretty wonky, in the air I do a bit of a circular movement, and my camera gets shaky trying to keep up with the movement. I also don’t really go anywhere as compared to the way we had it normally.

I’m putting

momentum = stateMachine.transform.TransformVector(momentum);

At the top of Jump and falling states’ Tick?

No, definitely not! I found another mistake… let me repaste the code directly from my copy instead of typing it:

    protected Vector3 ApplyInputToMomentum(Vector3 _momentum, float deltaTime)
    {
        Vector2 movement = stateMachine.InputReader.MovementValue * stateMachine.JumpingInputSensitivity;
        Vector3 movementVector = new Vector3(movement.x, 0, movement.y);
        movementVector = stateMachine.transform.TransformVector(movementVector);
        return Vector3.Lerp(_momentum, movementVector, deltaTime);
    }

And in Tick() the line should be

momentum = ApplyInputToMomentum(momentum, deltaTime);

Sorry about that.

No worries! It’s working great this time, and you were right. Setting up the sensitivity is quite fun! Like crouching tiger hidden dragon jumps!

Hi! I just found this wonderful thread, read through it and implemented the ApplyInputToMomentum.

It works amazingly! My player is one step closer to making sick midair dashes in between platforms, timing between bursts of fire and avoiding archers from above (I’ve got a whole foundation on paper).

One question remains- how heavy my character feels. I feel that my player still jumps like they’re on the moon, and falls just as slowly. How would I be able to increase my player’s weight and/or gravity?

Look in the ForceReceiver. Right now, we use Physics.Gravity. You could alter the gravity yourself in the Editor|Project Settings|Physics, or you could simply add a force multiplier… like Physics.gravity * 2

1 Like

My jumps are already feeling better after tripling the gravity. Will be playing around with the Jump Force and Physics settings now. Thank you!

Hi, I’m back with another question (when am I not?)

This might be a relatively simple issue, but this one will make or break my jumping controls.

My goal is to allow the player to control their direction in the air (they should be able to rotate 360 degrees in air to dash themselves onto a platform.) So I put the FaceMovementDirection() on my Jumping and Falling States to address this. It worked for what it was.

Then I added the ApplyInputToMomentum and did some more testing.

Unfortunately, it worked too well. When I jump and pull back, instead of pulling back as intended, I launch farther ahead with my character facing the screen. My guess as to why this happens is because ApplyInputToMomentum pulls the character depending on from where they jumped, not where they are in the air (I could be wrong).

My question is: Is there a way to make it so that while the player is in midair, they shift in the direction the character is facing? (I played Blue Fire again to remind myself what its jumping controls were, hence the edit).

It sounds like you need to apply the input translated to stateMachine.transform.forward (much like the movement in PlayerFreelookState) to the momentum…

Easier said than done for me I guess.

I’ve been trying to fit different parts of

protected void FaceMovementDirection(Vector3 movement, float deltaTime)
    {
        stateMachine.transform.rotation = Quaternion.Lerp(
        stateMachine.transform.rotation,
        Quaternion.LookRotation(movement),
        deltaTime * stateMachine.RotationDamping);
    }

and

protected Vector3 CalculateMovement()
    {
        Vector3 forward = stateMachine.MainCameraTransform.forward;
        Vector3 right = stateMachine.MainCameraTransform.right;

        forward.y = 0f;
        right.y = 0f;

        forward.Normalize();
        right.Normalize();

        return forward * stateMachine.InputReader.MovementValue.y + right * stateMachine.InputReader.MovementValue.x;
    } 

into

protected Vector3 ApplyInputToMomentum (Vector3 _momentum, float deltaTime)
    {
        Vector2 movement = stateMachine.InputReader.MovementValue * stateMachine.JumpingInputSensitivity;
        Vector3 movementVector = new Vector3(movement.x, 0, movement.y);
        movementVector = stateMachine.transform.TransformVector(movementVector); //converts from local to world space
        return Vector3.Lerp(_momentum, movementVector, deltaTime);
    }

Will keep trying new combinations to see what works.

Also, I found that when I enabled FaceMovementDirection as before, the character would face the world space front when I jumped without movement input (not sure why that was).

In the Jumping state, I think I would use this CalculateMovement

protected Vector3 CalculateMovement()
{
    Vector3 forward = stateMachine.transform.forward; //You're adding the input relative to the character
    Vector3 right = statemachine.transform.right; //Not the camera
    forward.y = 0f;
    right.y = 0f;
    forward.Normalize();
    right.Normalize();
    momentum += (forward * stateMachine.InputReader.MovementValue.y + right * stateMachine.InputReader.MovementValue.x) * jumpSteeringStrength;
    return momentum;
}

Hi, I’m back

After a few days of recovering from burnout, I tried implementing this CalculateMovement (I renamed it CalculateAirMovement). It provided different results than before, but none as close as I had gotten before. I was able to get the movement right, but when I pulled back the character would spin around constantly. I was also unable to rotate the character with the camera.

I’ll link the video of what I had the first time, and what happens when I substitute in the new CalculateAirMovement class.

In both cases, I also have the problem where if I jump without moving, it automatically faces the character to the world front.

The first half of this video shows my (almost) desired midair rotations that interact with both the camera and move input. The second half demonstrates my jumping problems.

This video demonstrates what jumping is like with the new CalculateAirMovement class.

Here’s the Tick for my Jumping and Falling states.

public override void Tick(float deltaTime)
{
        Vector3 movement = CalculateMovement(); //Added the new CalculateAirMovement class for second video. Left all else the same.
        Move(momentum, deltaTime);
        momentum = ApplyInputToMomentum(momentum, deltaTime);
        FaceMovementDirection(movement, deltaTime);
}

Try testing Momentum to make sure it is not practically zero…

if(momentum.magnitude>0.05f) FaceMovementDirection(movement, deltaTime);

This should leave the character’s direction unchanged when jumping without movement.

Privacy & Terms