◀ 6.16.26.3 ▶

6.2 — Ground Checks: No Infinite Jumps

Your jump works. That is the problem. Tap the key in mid-air and you jump again, and again — a jump with no ground check is not a jump, it is a jetpack with infinite fuel. Today you teach the script to ask a question it has never asked: am I standing on something?

In this lesson you will be able to answer“why can I jump forever in mid-air?”Guided build
Break It Down~10 minGuided build

Your jump works — in mid-air, over and over, forever. "Why can I jump forever in mid-air?" is really "how does the script know it is standing on something?" Nothing on the Rigidbody2D means "grounded", and no message fires when a foot touches a floor or leaves one. So the whole feature is: decide what counts as ground, ask about it, and stop the jump until the answer is yes.

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

  1. Make a bucket the physics engine can filter onA Ground layer in Edit ▸ Project Settings ▸ Tags and Layers, assigned to the tilemaps you can land on. A layer is not a tag: this cave's floor is already tagged Ground, and the second solid tilemap beside it is untagged — which is the whole argument for a bucket over a name.Read: the tag-versus-layer callout in the In the Editor steps above
  2. Choose the point you will ask fromAn empty child object at the bottom edge of your collider, wired into a slot. Not the Player root: read Assets/Prefabs/Player and you will find the art on a child called Sprite at local position (0, 0.6, -1), so the root is not the middle of the character you can see.Read: the [SerializeField] private Transform slot pattern from 1.3, and the Sprite child you read in the Editor half
  3. write this partRead: the Physics2D.Raycast(origin, direction, distance, mask) page in the Unity scripting reference, plus the four-technique table above
  4. Stop the jump until the answer is yesThe jump line you wrote in 6.1 with one term added in front of it: it now needs the key and the ground. Two conditions in one if — the folded && guard from 5.2, pointed at the world instead of at an object.Read: the jump you wrote in 6.1, and the folded condition in TryInteract() from 5.2
  5. Draw the question so you can see itA line from the check point, in the check direction, as long as the check distance — one colour when the answer is true and another when it is false. Four lines that turn "my jump does not work" into "my whisker is too short".Read: the Gizmos.DrawLine(...) and OnDrawGizmosSelected() entries in the Unity scripting reference, and the Debug.Log(...) habit from 3.3

Four of those five parts are work you have already done: a [SerializeField] slot is 1.3, the jump line is 6.1, the folded && guard is 5.2, and a gizmo is a Debug.Log(...) you can see. Only one part has no precedent, and it is the whole lesson: every line this script has ever contained tells the engine to do something, and a raycast is the first that asks. A question nothing acts on changes nothing, which is why the part after it is an if.

The test: could you look this part up on its own, right now, in one search? "Stop the infinite jump" cannot be looked up. "Send a ray straight down and find out what is on one layer" can.

Your turn

Two of the four techniques in the table hand you a distance as well as a yes-or-no. Say what a game would do with that distance, and say whether your cave does it.

The answer your check produces is thrown away and computed again sixty times a second. Name the piece of state from 3.3 that worked the same way — and the piece in this lesson that does not.

In the Editor ~25 min
  1. Project window → open Assets/_Scenes/Platformer.unity, then Game view → press Play and keep tapping the jump key in mid-air. You climb, and the Console says nothing — your code is doing exactly what it says: if the key went down, add an impulse. There is no rule about where.
  2. Hierarchy → Grid → Collision, then Inspector → a TilemapCollider2D, a CompositeCollider2D and a Rigidbody2D whose Body Type is Static — which is why you land on it. Its Gravity Scale reads 1 and is greyed out, and both are correct: only a Dynamic body is moved by the engine, so there is nothing for that number to act on. Do not "fix" the 1 — switch this tilemap to Dynamic and the ground falls.
  3. Hierarchy → Grid → Cave Exit: the same three components and the same Static body, and Untagged, because it was built for a different job — SceneSwitch.cs sits on it and touching it sends you back to the Overworld. Then Hierarchy → No Collision: a renderer and no collider at all. It is paint, and no ray can hit it.
  4. Project window → Project Settings ▸ Tags and Layers, and type Ground into the first empty User Layer. No object in your project changed: a layer is an empty bucket until something is put in it, and it is project-wide. Then Hierarchy → Inspector → Layer = Ground on Collision and on Cave Exit.
  5. Project window → Assets/Prefabs/Player, and read the Sprite child's Transform: local position (0, 0.6, -1), scale 2. The art is not on the root — so "fire the ray from the object's position" is a guess about a point you never looked at.
  6. Hierarchy → the Player, right-click ▸ Create Empty, name it GroundCheck, make it a child, then Scene view → drag it onto the bottom edge of your collider: centred in x, on the line in y, and outside the green outline — in this project a query that begins inside a collider reports that collider, so a point inside your own body reports the Player forever.
  7. Once the walkthrough is done — Project window → Assets/Prefabs/Player (the prefab, not one Player in a scene), Inspector → Player Controller: drag GroundCheck into Ground Check, set Ground Layer to Ground and nothing else, and type your measured distance into Ground Check Distance.
Two solid surfaces, one tag between them. Only the floor is tagged Ground — and you can stand on the cave mouth too. That is the difference between a tag and a layer, sitting in your own Hierarchy: a tag answers "is this that one particular thing?" and a layer answers "is this the kind of thing I care about?". Only the second question still gives the right answer when somebody adds a third floor tilemap next week. You can compare a tag to a string; you can hand a layer to the physics engine and let it filter.

Socratic: Asking Versus Remembering

  1. A tag and a layer are both just names attached to an object. Why can a raycast be told to ignore everything except one layer, but not "everything except one tag"?
  2. The Collision tilemap is tagged Ground and the Cave Exit tilemap beside it is not, and both of them are solid. If you wrote a ground check that compared tags, would it answer yes everywhere you can stand — and what would it cost you every frame you ran it?
  3. Your character is standing still on the floor. Is "grounded" a fact you could store when you land, or a fact you should re-check every frame? What could change it without any code of yours running?
In the Code ~20 min

Every platformer ever shipped uses one of these four. Read all of them — the differences are in what each one can tell you and where each one lies to you. The walkthrough below builds the first row, because that is the check 7.2 expects you to own.

TechniqueThe question you are askingWhat it needsWhere it lies to you
Physics2D.Raycast(origin, Vector2.down, distance, mask) "Is there a ground-layer collider directly below this exact point?" An origin (your feet, not your centre), a distance and a LayerMask. One infinitely thin line: stand with a pixel of your foot over the edge of a tile and it reports air. It returns a RaycastHit2D — collider, distance and contact point — so it is also the only row that can tell you how far.
Physics2D.OverlapCircle(point, radius, mask) "Is there a ground-layer collider anywhere inside this circle at my feet?" A point, a radius and a LayerMask. A wall beside your feet is inside the circle too, so you can be "grounded" against a vertical surface. It returns the collider only — no direction, no distance.
Collider2D.IsTouchingLayers(mask) "Is my own collider touching anything on this layer, right now?" Your Collider2D and a LayerMask — no origin, no distance, no ray. It returns a single true or false, and only sees contacts that exist: a Kinematic body reports false while standing on the floor unless Use Full Kinematic Contacts is ticked.
OnCollisionStay2D(Collision2D c) with a layer check "What am I in contact with during this physics step?" A callback method, plus a rule to stop it re-firing your jump every step. It fires continuously, so a naive jump fires continuously. And for a Kinematic body against static colliders it gives you the message without the collision — hearing a floor is never the same as standing on one.
In the CodeGuided build

The question this answers“why can I jump forever in mid-air?”

You are editing Assets/Scripts/PlayerController.cs. The layer, the check point and the Inspector slots are done — that was the In the Editor half. What is left is four fields, one method that asks a question every frame, one term added to your jump from 6.1, and a line you can see. The raycast is the one line with nothing earlier in the course to copy.

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 point, the reach, the filter and the answer

Four lines in the class body, beside moveSpeed and the jump field from 6.1. Three are [SerializeField] knobs a designer can see in the Inspector; the fourth is the answer itself.

    [SerializeField] private Transform groundCheck;
    [SerializeField] private float groundCheckDistance = 0.1f;
    [SerializeField] private LayerMask groundLayer;

    private bool isGrounded;
  1. 1The slot that holds the empty child from the Editor half — a position in the world, and a reference to another object. The same kind of slot as the waypoints in 4.1.Wire it before you press Play. An empty slot does not complain when the scene loads; it throws a NullReferenceException at the first line that reads it, and this script reads it sixty times a second.
  2. 2How far down the ray reaches. 0.1 is a starting guess; the honest value is measured — the gap from the bottom of your collider to the tile surface, plus a hair.This number is the difference between a check and a lie. Too short and the ray misses the floor by a hair while your feet are visibly on it, so the jump fails at random; too long and you are "grounded" standing over thin air beside a ledge.
  3. 3What the ray is allowed to notice. The type is LayerMask, not int, and the Inspector draws it as a dropdown of layer names rather than a number.A LayerMask is an integer whose bits mark which layers you care about, which is why "Everything" is not a special case in the code — it is every bit switched on. Empty means Nothing, and a ray allowed to hit nothing reports nothing.
  4. 5The answer, and the only thing in the file that changes because of it. Named for the question rather than the technique, so it reads correctly whichever technique you chose.In the class body — not inside the method that computes it. That is the scope rule from 1.3, load-bearing twice over here: declared inside the method it would be thrown away sixty times a second, and the jump would read a name that does not exist. The check writes it and the jump reads it, and a value two methods share is state.

Step 2Ask the question every frame

One call added to Update(), and one method that does nothing but ask. This is the part of the lesson with no precedent: every other line in this file tells the engine what to do.

        CheckGround();

    void CheckGround()
    {
        isGrounded = ____;
    }
  1. 1Called from Update(), beside your movement and your animation — the same shape as UpdateAnimation() and CheckAttack() from 1.6. It runs on every frame, and that is not an accident."Am I grounded" is a fact about right now, not an event that happened. Nothing sends you a message when a foot touches a floor and another when it leaves one. The collision callbacks from 2.1 report contacts, not standing — a character leaning on a wall is in contact with it. So the honest value is the one you re-derive.
  2. 3The method that asks. Empty parameter list, void, nothing handed back — everything it needs is a field from step 1. Same shape as UpdateAnimation() in 1.6: one idea, one method.
  3. Your turn5The whole lesson in one line, and the only line in this file that asks the engine a question instead of giving it an order. Fire the question down from the point you made in the Editor half, far enough to reach the floor, asking only about the layer you filled — and store the yes-or-no answer in the field from step 1. All four inputs are in the four-technique table above, in the order they are written there. The method hands back a result object that converts to true when it hit and false when it did not, which is why this line can end in a bool with no if in it.You want the Physics2D method that sends an infinitely thin line from a point, in a direction, for a fixed distance, and reports only what sits on the layers you hand it — the one row of the table with all four inputs. Pass them in the order the table writes them: the point, then straight down, then the distance, then the mask. A Vector3 position is accepted where the method asks for a Vector2, so groundCheck.position goes in as it is.Two things make this a check rather than a trick. It sits in a method Update() calls every frame — not fired once at the moment you land, because nothing would fire at the moment you stop landing. And it cannot change the game on its own: it writes a bool and touches nothing else, which is why the next step is where the jump stops working in mid-air.
  4. 6Closes CheckGround(). Press Play now and the jump still fires in mid-air, because nothing reads the answer yet.

Step 3Make the jump wait for the answer

One term added to a line you already wrote in 6.1, and the infinite jump is over.

        if (isGrounded && Input.GetKeyDown(KeyCode.Space))
        {
            body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
        }
  1. 1The jump condition, now compound: two questions in one if. isGrounded is the new term; the key check is the one from 5.2, and it has to stay GetKeyDown — the Down is what makes one press one jump.Read the && in English before you trust it: "fire only if I am standing on something AND the key went down this frame". C# short-circuits, so on most frames isGrounded settles it. And notice what this means for the player: a jump pressed in mid-air is not refused, it is thrown away. 6.3 stops throwing it away.
  2. 3The jump you wrote in 6.1, unchanged — your key, your force, your force mode. Nothing about how a jump is performed changes today. Only when it is allowed to happen.This is the line the whole hour protects. One extra term in front of it, and a character who could climb the sky leaves the ground exactly once per landing: the whole difference between a jump and a jetpack, for one bool and an ampersand.

Step 4Draw the question

A check you cannot see is a check you will debug by guessing. Four lines of gizmo code turn that around.

    private void OnDrawGizmosSelected()
    {
        if (groundCheck == null) { return; }
        Gizmos.color = isGrounded ? Color.green : Color.red;
        Gizmos.DrawLine(groundCheck.position, groundCheck.position + Vector3.down * groundCheckDistance);
    }
  1. 1A method Unity calls in the editor, not at runtime — nothing you write in here ships, and nothing in here can affect the game. It draws into the Scene view when this object is selected.
  2. 3The guard. In the frames before you wire the slot — and in any scene where you have not — groundCheck is nothing, and a gizmo that reads a position off nothing throws into the Console every frame.
  3. 4One colour while the answer is true and another while it is false. The ? : is the conditional expression — the same yes/no branch as an if, written as a value.Colour is the point, and it is not decoration. "My jump does not work" and "my ray is too short" look identical from the player's seat and completely different from the Scene view. A red line hanging two centimetres above a green floor is a diagnosis.
  4. 5From the check point, straight down, exactly as far as the check reaches — the same inputs the ray uses, drawn. If the drawn line and the real ray ever disagree, one of them is not reading the field you think it is.
  5. 6Closes the gizmo method.
Check your work: the finished filetry every gap first — then open this to compare

Compare against your own file rather than copying it. The gap is the raycast assignment: a point, a direction, a distance and a mask, in that order. This version assigns the result straight into the bool, because a RaycastHit2D converts to true or false on its own — writing .collider != null on the end is equally correct. What has to match is the shape: the answer is computed in exactly one place, every frame rather than stored when you land, and read by the jump before it fires.

// ------------------------------------------------------------------
//  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 at the end of 6.2 — everything from 1.4 through 6.1,
// plus the one question this script has never asked: am I standing on
// something? Every other line here TELLS the engine what to do. CheckGround()
// ASKS it something, and CheckJump() is the only line that acts on the answer.
public class PlayerController : MonoBehaviour
{
    [SerializeField] private float moveSpeed = 5f;
    [SerializeField] private float jumpForce = 7f;          // yours, from 6.1

    // Today's three knobs, all visible in the Inspector (1.3).
    [SerializeField] private Transform groundCheck;              // where the ray starts
    [SerializeField] private float groundCheckDistance = 0.1f;   // how far down it reaches
    [SerializeField] private LayerMask groundLayer;              // what it may notice

    private Rigidbody2D body;

    // The answer. In the class body, because the check writes it and the jump
    // reads it — and re-derived every frame, because nothing ever tells you it
    // changed.
    private bool isGrounded;

    void Start()
    {
        // Looked up once, never inside Update() — the rule from 1.6 and 2.2.
        body = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // ... your input, movement, gravity and animation lines from
        // 1.4, 1.6, 2.2 and 6.1, unchanged and left out here ...

        CheckGround();   // ask
        CheckJump();     // act on the answer
        UpdateAnimation();
    }

    // The whole lesson. A ray from the point under your feet, one direction,
    // one distance, one mask — and the result assigned straight into the bool,
    // because a RaycastHit2D converts to true when it hit and false when it
    // did not. Nothing here moves anything: a question is not an action.
    void CheckGround()
    {
        isGrounded = Physics2D.Raycast(groundCheck.position, Vector2.down, groundCheckDistance, groundLayer);
    }

    // The jump from 6.1 with one term in front of it. Leave the term out and
    // the character can climb the sky.
    void CheckJump()
    {
        if (isGrounded && Input.GetKeyDown(KeyCode.Space))
        {
            body.AddForce(Vector2.up * jumpForce, ForceMode2D.Impulse);
        }
    }

    // The same three inputs the ray uses, drawn. Keep the Player selected in
    // the Hierarchy while you play, and watch this go red the frame you leave
    // the ground.
    private void OnDrawGizmosSelected()
    {
        if (groundCheck == null)
        {
            return;
        }

        Gizmos.color = isGrounded ? Color.green : Color.red;
        Gizmos.DrawLine(groundCheck.position, groundCheck.position + Vector3.down * groundCheckDistance);
    }

    // ... UpdateAnimation(), CheckAttack(), the pickups and the damage, unchanged ...
}

// Edit ▸ Project Settings ▸ Tags and Layers:
//   the first empty User Layer  =  Ground
//
// Inspector, on Assets/Prefabs/Player ▸ PlayerController:
//   Ground Check            =  GroundCheck   <- an empty child sitting on the
//                                               BOTTOM EDGE of your collider
//   Ground Check Distance   =  0.1           <- measured, not guessed
//   Ground Layer            =  Ground        <- Ground, and nothing else
//
// The layer goes on BOTH solid tilemaps under the cave's Grid — Collision and
// Cave Exit. The third one, No Collision, has no collider at all, so no ray can
// ever hit it whatever layer it is on.
Build Lab: Prove It, Then Break It ~25 min

Step 1 — the honesty test. Press Play with the Player selected so the gizmo draws, and watch it through four actions: stand still (true), jump (false the moment you leave the ground), land (true), walk off a ledge (false). If any is wrong, your distance or your origin is wrong — not your logic. Move the empty child or change the number, and run the four again.

Step 2 — the mask experiment. Set your mask to Everything and press Play: if your origin sits inside your own collider the ray hits you at distance zero and reports ground forever — the jetpack is back, wearing a disguise. Put it back to Ground, then set it to Nothing and watch the opposite failure: a character who cannot jump at all, silently.

Step 3 — tune by measurement. Read the real gap from the bottom of your collider to the tile surface in the Scene view and set the number from that. Then halve it, play, and watch the jump start failing on ground you can see. Guessing produces exactly that failure.

Step 4 — walk the whole cave with the gizmo on. Every place the line is red while your feet are on something solid is a surface missing from your Ground layer — and there is at least one, because this cave has two solid tilemaps.

Architectural specification: the check.
  • Which technique did you choose, and what are its exact inputs — origin, direction, distance, mask? If you chose anything other than the raycast the walkthrough built, map those four inputs onto yours.
  • Where does the origin sit, and how did you find out rather than assume? The Player root is not the centre of the character you can see.
  • Where does the mask come from — a serialized LayerMask field, or a hard-coded layer name? Which one lets a teammate fix your level without opening your script?
  • Is the jump condition now compound? Write the whole sentence in English before you write it in C#.
Architectural specification: making the invisible visible. Before you trust a check you cannot see, draw it: OnDrawGizmosSelected() runs in the editor and can draw a line from your origin along your check direction, scaled by your distance, in one colour when the check is false and another when it is true. Drawing it costs your game nothing, because nothing in a gizmo method ships.

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

Reference Docs

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

Next: 6.3 — Jump Feel: Fall Speed & Coyote Time, where a jump that works starts to feel like a jump that is yours.

0/5 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.