◀ 6.26.36.4 ▶

6.3 — Jump Feel: Fall Speed & Coyote Time

The jump works, the ground check is honest, and the character still comes down like a balloon. "Feel" is four numbers with names, and every platformer you have ever called tight is using all four.

In this lesson you will be able to answer“how do I make my jump feel good?”Guided build
Break It Down~10 minGuided build

Your jump works and your ground check is honest, and the character still feels like something operated through a spreadsheet: it floats down like a balloon, it refuses a press you made a step past the lip of a ledge, and it throws away a press you made a moment before you landed. "Make my jump feel good" is that whole complaint — and every knob that fixes it is a number somebody chose and wrote down.

First, before any code: cut that into parts small enough that you could look each one up on its own.

  1. Decide which way the body is moving verticallyRead the vertical half of the body's velocity and take the sign: positive is rising, negative is falling. The only part of this lesson that is not about time, and the switch the fall-speed trick hangs off.Read: the rb.linearVelocity row of the quick reference, for the Unity 6 rename.
  2. Make the fall heavier than the riseWrite a bigger gravity multiplier onto the body while it is falling, and the body's own value back while it is rising. The "normal" number is read off the body once in Start(), so the Inspector keeps ownership of it.Read: the rb.gravityScale row of the quick reference, and the Gravity Scale table you read in 6.1.
  3. write this partRead: the Time.time row of the quick reference, and the field the turret stamped on every shot in 3.3. The reading for "is that moment still recent enough?" is the same three pieces with the operator turned around.
  4. Spend the remembered moment the instant the jump uses itWhen the jump fires, overwrite the remembered moment with a value so far in the past that no subtraction can bring it back inside the window. The same stamp line as 3.3, written with a different value and for the opposite reason.Read: the Cooldown row of the pattern table (act, set last = Time.time) and your own stamp line from 3.3 — then ask why that line cannot close this window by itself.
  5. Gate the jump on two windows instead of on the groundA press is on file and the body is either standing on something or still inside the grace window. && and || in one condition, and the only line in the file that launches anything.Read: the && you combined the turret's range and cooldown checks with in 3.3, and the isGrounded bool your own 6.2 check produces.
  6. Cut the rise when the key comes back upThe smallest change of the four: while the body is still rising, the player's release multiplies the upward velocity down. The release becomes an input, not just the press.Read: the hold-longer-go-higher row of 6.1's four-technique table, and the rb.linearVelocity row of the quick reference for writing a velocity back.

Four of those six parts are things you have already typed. Reading one half of a velocity is 6.1; putting a gravity multiplier on a body is the Gravity Scale field you turned on in 6.1; and cutting a rise is the hold-longer-go-higher row of 6.1's table. What is left is the timer. Every memory this game has kept so far was kept for good — a coin count, health, the moment of the turret's last shot. This memory is different in two ways at once: it is only meant to be true for a fraction of a second, and it has to be thrown away on purpose the moment it is used.

The test: could you look this part up on its own, right now, in one search? "Make my jump feel good" cannot be looked up — it is four unrelated numbers, two of them timers. "Compare a timestamp against the clock with a tolerance" can, and you have already written that subtraction once, in 3.3.

Your turn

Two of the four numbers are windows measured in seconds and two are multipliers. Which half of that split would you hand to a teammate as a spreadsheet, and which half do they have to feel for themselves?

Coyote time and the jump buffer both let a press count when a strict reading of the rules says it should not. Pick the one you would set longer than the other, and say what the player would think had gone wrong if you set it to zero.

In the Editor ~25 min
  1. Project window → open Assets/_Scenes/Platformer.unity, then Game view → press Play and play for thirty seconds with your attention on your hands rather than the screen. Three complaints come up, and they are the three every playtester in the world reports:
    • "I float down like a balloon." One gravity number, doing two jobs.
    • "It ate my jump at the ledge." A press made one step past the lip is refused.
    • "It ignored my press just before I landed." The press arrives while you are airborne, and nothing is listening.
  2. Hierarchy → the Player → Inspector → Rigidbody2D → Gravity Scale, and keep that row visible while you jump in Game view →. Watch it: it never changes while you play.
  3. Game view → walk off the lip of a ledge and press jump as late as you dare, then jump a fraction of a second before you land. Nothing happens either time. Both fixes are timers that remember a moment rather than counting anything.
  4. Once the walkthrough is done — Project window → Assets/Prefabs/Player → Inspector → Player Controller: four new rows. Tune them one at a time, in play mode, one sentence of feel written down for each.
    • Fall Multiplier = 1 first, as your control — the balloon you started with. Then 2.5, then 3.
    • Coyote Time = 0.1, then 0.5 — at 0.5 you can leave a ledge, decide about it, and jump anyway.
    • Jump Buffer Time = 0.12, then 1 — at 1 a press made a full second early still fires, and every fall becomes a jump you did not ask for.
    • Rise Cut = 0.5, then 0 — tap and hold side by side. 0.5 gives two visibly different jumps; 0 gives a tap that does nothing at all, which is a bug rather than a mechanic.
  5. Prove the windows are state, not luck. Put Debug.Log("grounded at " + lastGroundedTime + " now " + Time.time); in Update() for one run and watch the two numbers as you walk off a ledge and land: the gap between them is your coyote window, live. Then delete the print — a log that survives into the next lesson is noise you will read past.

Socratic: Forgiving Intent Versus Playing For Them

  1. Coyote time and the jump buffer both let a press count when it "should not". Why does that make the player feel more in control, when technically it makes the game more permissive?
  2. Set coyote time long enough and you can jump from thin air a metre past a ledge. What is the difference between forgiving the player's intent and playing the game for them?
  3. Asymmetric gravity is a lie about physics, told by a multiplier on a law of the universe. What other lie does your character already tell — and what does each lie buy the player?
  4. Which of the four knobs needs to know which way the body is moving, and which of them only need to know about time?
In the Code ~10 min
Both mercies are the same subtraction you met in 3.3, read in the opposite direction — the same clock, the same remembered moment, the same knob, with the comparison turned around. Step 3's annotation works the operator out.
A window is not a countdown — pick one idiom per field. 3.3 named two honest timers: a timestamp compared against Time.time, and a countdown with Time.deltaTime subtracted from it every frame. Both work. What does not work is one field doing both jobs: a number that means "when it happened" on the frame you write it and "how long is left" everywhere else has no correct comparison, and the bug looks like the window closing at the wrong moment. Today's two fields are both timestamps, because both of them answer questions about when.
In the CodeGuided build

The question this answers“how do I make my jump feel good?”

You are editing Assets/Scripts/PlayerController.cs. Four numbers, one gravity line on the physics clock, and two fields that hold a moment in time. Your launch from 6.1 and your ground check from 6.2 are untouched, and one line here is yours to write: the window that reads the first of those two moments back.

This build leaves 1 line for you to write. Everything around it is given, so you always know the shape of what you are producing.

Step 1Declare the four numbers and the two moments

Seven lines in the class body, beside moveSpeed and your jump force. Four are knobs a designer turns; the other three are this script's own bookkeeping — the same split as range and lastAttackTime on the turret in 3.3.

    [SerializeField] private float fallMultiplier = 2.5f;
    [SerializeField] private float riseCut = 0.5f;
    [SerializeField] private float coyoteTime = 0.1f;
    [SerializeField] private float jumpBufferTime = 0.12f;
    private float lastGroundedTime;
    private float lastJumpPressedTime;
    private float baseGravity;
  1. 1The fall-speed knob. A multiplier on the body's own gravity, applied only while it is falling. 1 is the balloon you have now, 2.5 is what most platformers use.A multiplier, not a gravity value. The cave's gravity lives on the body. Type 9 here and you own two gravity numbers that have to agree, and the day either one changes, the jump is quietly wrong.
  2. 2The fourth number. How much of the rise survives when the player lets go of the key — 0.5 halves it. 1 switches the knob off entirely, which is where your game is now.
  3. 3The grace window, in seconds. How long after your feet leave the ground a jump is still accepted — about six frames in the cave, roughly the width of the mistake a player makes at the lip of a ledge.Seconds, not frames. A frame counter is six frames on your machine and a different amount of time on a laptop running at half the speed.
  4. 4How long a press is remembered, in seconds. And deliberately longer than the coyote window: a player pressing early is watching their landing, where their timing error tends to be the bigger one.
  5. 5The first memory. The moment, on the game clock, when the feet were last known to be on something. private, because it is this script's own bookkeeping — the same declaration, for the same reason, as the turret's lastAttackTime.Declared in the class body. A timestamp kept inside a method would be rebuilt every time that method ran, and could never remember anything — the scope rule from 1.3.
  6. 6The second memory: the moment the jump key was last pressed down. A separate field on purpose — two timers, two names, two jobs, nothing shared.
  7. 7Where the "normal" gravity is kept. It is read off the body once, in Start(), so the multiplier has something honest to multiply.

Step 2Make the fall heavier than the rise

One branch, on the physics clock, in FixedUpdate(). It reads the body's vertical velocity and hands the body a gravity scale — and that is the whole of the "it floats" complaint.

    void FixedUpdate()
    {
        if (body.linearVelocity.y < 0f)
        {
            body.gravityScale = baseGravity * fallMultiplier;
        }
        else
        {
            body.gravityScale = baseGravity;
        }
    }
  1. 1The physics step. gravityScale is a property the physics engine reads, so it belongs on the clock the physics engine runs on.FixedUpdate() and not Update(). FixedUpdate runs fifty times a second in this project whatever rate the machine draws frames at. On a 144 Hz monitor Update() runs about three times per physics step and writes the same number three times for nothing.
  2. 3The switch, and the only line in this lesson about direction instead of time. linearVelocity is the body's velocity as a Vector2; .y is the vertical half; below zero means it is on its way down.Read it, never re-derive it. The sign of the velocity is the engine's own answer to "am I falling", and it is already correct — on the way up after a jump it is positive even though gravity is pulling the whole time. A boolean of your own would need setting true and false in the right places, and the frame it is wrong is a frame the fall speed is wrong.
  3. 5The fall is now heavier. Notice what is not here: no Time.deltaTime, no Time.fixedDeltaTime. Gravity Scale is a multiplier, not a rate, so time is not part of it anywhere.Multiply, never add. baseGravity + fallMultiplier looks plausible at 2.5 and is nonsense the moment somebody sets the knob to 0.5.
  4. 9The branch everybody leaves out. While the body is rising — and standing still, and on the first frame after a jump — gravity goes back to the body's own value.Without this line the multiplier is permanent. Write only the falling branch and every jump after the first is crushed by heavy gravity: the first looks fine and the rest barely leave the floor. Build exactly that once, on purpose, and watch it happen.
  5. 11Closes FixedUpdate(). Your Update() is still the only place anything else happens.

Step 3Remember where you last stood — and write the window that reads it back

Two given lines and one gap. The line that writes the moment down is the turret's stamp from 3.3; the line you write is the same subtraction with the question turned around.

        if (isGrounded)
        {
            lastGroundedTime = Time.time;
        }
        bool stillInGrace = ______________________________________;
  1. 1Where the memory is written. isGrounded is the bool your 6.2 check leaves behind — and this block is the only place it is read for this purpose, because a second ground check in this file would be a second answer to one question.One ground check, one place. If your 6.2 check lives in a method of its own, this block goes directly underneath the call that runs it. Record the moment anywhere else and the file asks "am I standing on something?" twice, with nothing keeping the answers in step.
  2. 3The memory, refreshed on every frame the feet are down. This is 3.3's stamp line — the clock that only ever counts up, written down at the moment the interesting thing happened.
  3. Your turn5Your line — the whole right-hand side. It answers one question: is the moment you just wrote down still recent enough to count as standing? The declaration on the left is fixed — step 4 consumes stillInGrace, so do not rename it — and everything from the = onward is yours.Three pieces go into it: the moment you recorded on the line above, the window you declared as a knob in step 1, and the clock that only ever counts up — the row of the quick reference described as the seconds since the scene started, the row the turret stamped on every shot in 3.3. Put all three into one comparison, then ask which way round it runs: 3.3 asked whether enough time had passed, and this asks the mirror question. == is exactly as wrong here as it was there.The same subtraction, read backwards. 3.3 asked clock >= moment + gap. This asks whether too little time has passed — same three pieces, opposite operator, and the answer is a bool rather than a jump. Keeping it in a named bool is what lets step 4 ask about standing and pressing in the same breath.

Step 4Remember the last press, and open the gate that uses both

The mirror of step 3, written by the key instead of by the floor. Then one condition that needs both memories to be true.

        if (Input.GetKeyDown(KeyCode.Space))
        {
            lastJumpPressedTime = Time.time;
        }
        bool pressedRecently = Time.time - lastJumpPressedTime <= jumpBufferTime;

        if (pressedRecently && (isGrounded || stillInGrace))
        {
            Jump();
        }
  1. 1The single-frame key check from 6.1. It is not the jump — it only writes the moment down. In 6.2 this check was the second half of if (isGrounded && Input.GetKeyDown(...)), so a press was only ever considered while you were standing: made in mid-air, it was not rejected, it was never seen at all.And the held-key version would refresh the memory every frame. Lean on the jump key and you would have a press on file for as long as you held it, so the buffer would fire the instant you touched the floor. The one-frame version is the whole point of a press.
  2. 3The same stamp, on the other field. If it looks like the line above, that is the lesson: two timers, written by two events, read by two questions.
  3. 5The buffer's window — the line you wrote in step 3, with the other field and the other knob in it. Nothing about the shape changed, which is exactly why step 3 asked you to produce it.
  4. 7The gate, and the only line in the file that launches anything. Read it as a sentence: a press is on file, and the body is either standing on something or still inside the grace window.This is the line that keeps two mercies from becoming a jetpack. Drop stillInGrace and you have 6.2's plain ground check — correct, and it refuses the press a player made one step past the lip. Drop pressedRecently and the jump fires the instant you touch down whether or not anybody asked. Drop the isGrounded half and you can jump from anywhere at all.
  5. 9The method from 6.1, called from exactly one place — and that place now has a rule about when. Whatever your launch is, nothing about it changes today except the company it keeps.

Step 5Spend both windows the moment the jump fires

Two lines, and they are the difference between a mercy and a jetpack. Without them the window you just used is still open on the next frame.

        body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
        lastGroundedTime = -999f;
        lastJumpPressedTime = -999f;
    }
  1. 1Your launch from 6.1, unchanged. If you picked one of the other Dynamic-body rows instead — writing linearVelocity directly — keep that one: it is the same jump by a different route.One of 6.1's four rows cannot be used with this lesson at all. MovePosition() on a Kinematic body: a Kinematic body ignores gravity, so it never falls, which means there is no velocity for the heavy branch to read and no gravity for it to fight. If that is the row you wrote in 6.1, switch it now — it is one line.
  2. 2The spend, and the new idea in this lesson. A moment far enough in the past that no subtraction can bring it back inside the window. The jump you just made has used the grace up; the next frame must not be able to use it again.Why not one cooldown-length back, the way 3.3 seeded its turret? Because the question is different. 3.3 asked "has enough time passed since last", so pushing last back by one cooldown closed the gate. This window asks whether too little has passed, so pushing the moment back by exactly one window leaves it sitting on the boundary, ready to open again on frame one.
  3. 3The same spend, on the other window. Both have to be closed: the press you just honoured must not be honoured again when you leave the ground, and the grace you just used must not survive into the next frame.
  4. 4Closes the jump. One more } closes the class.

Step 6The fourth number: cut the rise when the key comes back up

Three lines, and the player's release becomes an input for the first time. Press for a full jump, tap for a small one.

        if (Input.GetKeyUp(KeyCode.Space) && body.linearVelocity.y > 0f)
        {
            body.linearVelocity = new Vector2(body.linearVelocity.x, body.linearVelocity.y * riseCut);
        }
  1. 1The key coming up, and the body still rising. Two conditions, because a release while you are already falling must not do anything — there is no rise left to cut.And 6.1's own proof leaves an edge case here. AddForce(...) does not change linearVelocity until the physics engine next runs, so a player who presses and releases inside a single frame lets go before the rise exists. Nothing to cut, nothing errors — which is why this line asks the body whether it is rising instead of assuming a jump just happened.
  2. 3Multiply the upward half of the velocity down instead of setting it to zero. A hard 0 kills the rise dead and reads as a collision; a multiplier reads as the character choosing to come down.This is where the buffer and the rise cut collide. A buffered jump fires the instant you land, from a press made before the landing — and if the player has already released the key, this block cuts that jump on its very first frame, so it arrives already half spent. Test the two together before you decide the numbers.
Check your work: the finished filetry every gap first — then open this to compare

Compare against your own file rather than copying it. Three things have to match and nothing else does: the fall multiplier is written on the physics clock and restored on the way up, the jump is gated by two windows rather than by the ground check alone, and both windows are spent the instant the jump fires. Your launch, your ground check, your names and all four of your numbers are yours.

// ------------------------------------------------------------------
//  REFERENCE FILE — Assets/Scripts/PlayerController.cs
//  Written to be read, not pasted. Build it with the walkthrough
//  above, one step at a time, then open this and compare.
//  If it is sitting in your project unchanged, that is the thing
//  to go and do now — the walkthrough is still above you.
// ------------------------------------------------------------------
using UnityEngine;

// PlayerController.cs — the jump, and the four numbers that make it feel like
// yours. Movement 1.4, animation 1.6, the velocity rule 2.2, the launch from
// 6.1, the ground check from 6.2 — and the two mercies.
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float jumpForce = 7f;          // yours already — 6.1's step 1

    // ---- the four numbers this lesson is about --------------------------
    [SerializeField] private float fallMultiplier = 2.5f;   // heavier going down
    [SerializeField] private float riseCut = 0.5f;          // release shortens the rise
    [SerializeField] private float coyoteTime = 0.1f;       // grace after the ledge
    [SerializeField] private float jumpBufferTime = 0.12f;  // grace before the floor

    // ---- your 6.2 check, unchanged. This file writes the raycast row; if you
    // picked a different one, CheckGround() is the only place that changes. ---
    [SerializeField] private float groundDistance = 0.6f;
    [SerializeField] private LayerMask groundMask;

    private Rigidbody2D body;
    private float baseGravity;          // read once, so the Inspector still owns it

    private bool isGrounded;
    private float lastGroundedTime;     // a MOMENT, not a count
    private float lastJumpPressedTime;  // the other moment, a different field

    void Start()
    {
        body = GetComponent<Rigidbody2D>();

        // ... your 6.1 gravity line, if you wrote one for the scene ...
        baseGravity = body.gravityScale;    // read it LAST — after your 6.1 gravity line above
    }

    void Update()
    {
        // ... your input, movement and animation from 1.4, 1.6 and 2.2 ...
        CheckGround();

        // The memory: refreshed on every frame the feet are down.
        if (isGrounded)
        {
            lastGroundedTime = Time.time;
        }

        // The other memory: written by the key, on the frame it goes down.
        if (Input.GetKeyDown(KeyCode.Space))
        {
            lastJumpPressedTime = Time.time;
        }

        // The same subtraction twice, asked in opposite directions.
        bool stillInGrace = Time.time - lastGroundedTime <= coyoteTime;
        bool pressedRecently = Time.time - lastJumpPressedTime <= jumpBufferTime;

        if (pressedRecently && (isGrounded || stillInGrace))
        {
            Jump();
        }

        // The fourth number: let go early and the rise is cut short.
        if (Input.GetKeyUp(KeyCode.Space) && body.linearVelocity.y > 0f)
        {
            body.linearVelocity = new Vector2(body.linearVelocity.x, body.linearVelocity.y * riseCut);
        }
    }

    // Gravity is a value the physics engine reads, so it is written on the
    // physics clock: fifty times a second here, whatever the frame rate is.
    void FixedUpdate()
    {
        if (body.linearVelocity.y < 0f)
        {
            body.gravityScale = baseGravity * fallMultiplier;
        }
        else
        {
            body.gravityScale = baseGravity;
        }
    }

    private void Jump()
    {
        // Your launch from 6.1: one flick. Writing linearVelocity directly
        // works identically here — the Kinematic MovePosition row does not,
        // because a Kinematic body has no gravity for the fall branch to fight.
        body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);

        // Spend both windows. A window that is never closed is a jetpack —
        // and unlike the turret's cooldown, this one cannot close itself.
        lastGroundedTime = -999f;
        lastJumpPressedTime = -999f;
    }

    private void CheckGround()
    {
        // A RaycastHit2D is true when the ray hit something and false when it
        // did not, so it stores straight into the bool. Your 6.2 technique,
        // whichever of the four you chose, belongs on this line.
        isGrounded = Physics2D.Raycast(transform.position, Vector2.down, groundDistance, groundMask);
    }
}
Build Lab: Tune the Four Numbers ~30 min

The walkthrough built the reference version of all four. What it cannot give you is the numbers, because they are the design — and each one is now an Inspector row.

Architectural specification: the fall multiplier.
  • Where does the "normal" gravity number live in your project — a literal in your own code, a serialized field, a scene-instance value, or a value you read off the body? Whatever the answer, you now need it in a second place, and that is a design smell worth fixing rather than living with.
  • Is the value you multiply your own field or the body's gravityScale? Which choice survives a teammate retuning the cave in the Inspector?
  • What happens in the top-down Overworld if this branch runs there? Before you answer, look at the Player in that scene and read the Gravity Scale that is actually there rather than the one you remember setting.
Architectural specification: the two mercies.
  • What exact moment does each field record, and which event writes it? Say it in one sentence per field, and use the word "moment" in both, not "timer".
  • How long is each window, in seconds? Write your two numbers down beside a sentence of feel for each, the way you did for the jump force in 6.1.
  • What is the "spend" rule — and can the same jump fire twice off one remembered press if you get it wrong? Which of the two windows does the double-fire come from?
  • What is the initial value of each field before anything has happened — before the feet have touched the ground and before the key has ever been pressed? If it starts at 0 and the scene clock also starts near 0, what does your first frame of play do?
Architectural specification: the fourth number.
  • When is the cut allowed? Only while rising, or on the way down too? What would a cut on the way down even mean?
  • How does the cut interact with a buffered jump the player never held? Describe the two knobs fighting, then decide which one you would weaken.
Two timers and one jump key is how you build an infinite jump by accident. Watch the number you printed in the In the Editor steps. If it keeps resetting while you are nowhere near the floor, one of the two remembered moments is being refreshed when it should not be, or one of them is not being spent.

Checkpoint — before moving on, you must be able to:

Reference Docs

Need a deeper explanation? These pages cover the same concepts:

Next: 6.4 — Boots: Double Jump, where one pickup changes what your level design is allowed to ask for.

0/6 answered

Answers save on this device as you type. At the end of class, click Download and upload the file to the Canvas assignment.

Saved on this device only — the Dashboard tracks your progress.