Basic Jump
A simple, fixed-height jump. Press button, go up, come down. The foundation of character movement across all perspectives.
The Feel
Basic jump is committed and consistent. Same height every time. The player knows exactly what they'll get.
Compare to Variable Jump: less expressive, but more predictable. Some videogames want that predictability.
Exposed Variables
| Variable | Type | Default | What it does |
|---|---|---|---|
jumpForce |
float | 12.0 | Initial upward velocity |
gravity |
float | 40.0 | Downward acceleration |
fallGravityMultiplier |
float | 1.5 | Extra gravity when falling |
groundCheckRadius |
float | 0.2 | Size of ground detection sphere/circle |
Tuning Guide
For a floaty feel:
- Lower
gravity(20-30) - Lower
fallGravityMultiplier(1.0-1.2) - Higher
jumpForce
Reference: Kirby, early Mario
For a snappy feel:
- Higher
gravity(50-70) - Higher
fallGravityMultiplier(2.0-3.0) - Moderate
jumpForce
Reference: Super Meat Boy, modern precision platformers
For a weighty feel:
- High
gravity - Low
jumpForce - Minimal
fallGravityMultiplier(1.0)
Reference: Castlevania, simulation-leaning videogames
Perspective Scripts
BasicJump2D.cs
Classic 2D platformer jump. Uses Rigidbody2D and Physics2D.OverlapCircle for ground detection.
Setup:
- Add
Rigidbody2Dto your player (Freeze Rotation Z) - Add a
Collider2D(BoxCollider2D or CapsuleCollider2D) - Create an empty child GameObject at the feet, name it "GroundCheck"
- Add this script and configure in Inspector
BasicJump2D.cs
using UnityEngine;
/// <summary>
/// BASIC JUMP 2D - Fixed height jump for 2D platformers
/// VG101 Code Bank - Requires InputReader from Input Setup
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class BasicJump2D : MonoBehaviour
{
[Header("Jump Feel")]
[Tooltip("How hard we push off the ground. Higher = taller jump.")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 12f;
[Tooltip("How fast we fall. Higher = snappier, less floaty.")]
[Range(20f, 80f)]
[SerializeField] private float gravity = 40f;
[Tooltip("Extra gravity when falling. Makes descent faster than ascent.")]
[Range(1f, 4f)]
[SerializeField] private float fallGravityMultiplier = 1.5f;
[Header("Ground Detection")]
[Tooltip("Empty GameObject at character's feet")]
[SerializeField] private Transform groundCheckPoint;
[Tooltip("Size of the ground check circle")]
[Range(0.05f, 0.5f)]
[SerializeField] private float groundCheckRadius = 0.2f;
[Tooltip("Which layers count as ground")]
[SerializeField] private LayerMask groundLayer;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
private Rigidbody2D rb;
private float verticalVelocity;
private bool isGrounded;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
rb.gravityScale = 0f; // We handle gravity ourselves
}
private void OnEnable()
{
// Subscribe to InputReader jump event
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed += HandleJump;
}
}
private void OnDisable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed -= HandleJump;
}
}
private void FixedUpdate()
{
CheckGrounded();
ApplyGravity();
ApplyVelocity();
}
private void CheckGrounded()
{
isGrounded = Physics2D.OverlapCircle(
groundCheckPoint.position,
groundCheckRadius,
groundLayer
);
if (isGrounded && verticalVelocity < 0)
{
verticalVelocity = 0f;
}
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (verticalVelocity < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
verticalVelocity -= gravityThisFrame * Time.fixedDeltaTime;
}
}
private void HandleJump()
{
if (isGrounded)
{
verticalVelocity = jumpForce;
}
}
private void ApplyVelocity()
{
Vector2 velocity = rb.linearVelocity;
velocity.y = verticalVelocity;
rb.linearVelocity = velocity;
}
private void OnDrawGizmos()
{
if (showGroundCheck && groundCheckPoint != null)
{
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(groundCheckPoint.position, groundCheckRadius);
}
}
}
BasicJump3D.cs
Third-person 3D jump. Uses CharacterController for smooth movement integration. Works with camera-relative movement.
Setup:
- Add
CharacterControllerto your player - Add this script and configure in Inspector
- Set Ground Layer to your environment's layer
BasicJump3DThirdPerson.cs
using UnityEngine;
/// <summary>
/// BASIC JUMP 3D - Fixed height jump for third-person 3D games
/// VG101 Code Bank - Requires InputReader from Input Setup
/// Uses CharacterController for movement integration
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class BasicJump3D : MonoBehaviour
{
[Header("Jump Feel")]
[Tooltip("How hard we push off the ground. Higher = taller jump.")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 10f;
[Tooltip("How fast we fall. Higher = snappier, less floaty.")]
[Range(20f, 80f)]
[SerializeField] private float gravity = 30f;
[Tooltip("Extra gravity when falling. Makes descent faster than ascent.")]
[Range(1f, 4f)]
[SerializeField] private float fallGravityMultiplier = 2f;
[Header("Ground Detection")]
[Tooltip("Offset from center for ground check sphere")]
[SerializeField] private Vector3 groundCheckOffset = new Vector3(0, -0.9f, 0);
[Tooltip("Size of the ground check sphere")]
[Range(0.1f, 0.5f)]
[SerializeField] private float groundCheckRadius = 0.3f;
[Tooltip("Which layers count as ground")]
[SerializeField] private LayerMask groundLayer;
[Header("Movement (Optional)")]
[Tooltip("Movement speed on ground")]
[SerializeField] private float moveSpeed = 8f;
[Tooltip("How much control in the air (0-1)")]
[Range(0f, 1f)]
[SerializeField] private float airControl = 0.5f;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
private Transform cameraTransform;
private void Awake()
{
controller = GetComponent<CharacterController>();
cameraTransform = Camera.main?.transform;
}
private void OnEnable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed += HandleJump;
}
}
private void OnDisable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed -= HandleJump;
}
}
private void Update()
{
CheckGrounded();
HandleMovement();
ApplyGravity();
ApplyVelocity();
}
private void CheckGrounded()
{
Vector3 checkPosition = transform.position + groundCheckOffset;
isGrounded = Physics.CheckSphere(checkPosition, groundCheckRadius, groundLayer);
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f; // Small downward force to stay grounded
}
}
private void HandleMovement()
{
Vector2 input = InputReader.Instance != null ? InputReader.Instance.MoveInput : Vector2.zero;
// Camera-relative movement
Vector3 forward = cameraTransform != null ? cameraTransform.forward : transform.forward;
Vector3 right = cameraTransform != null ? cameraTransform.right : transform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
Vector3 moveDirection = (forward * input.y + right * input.x);
float currentSpeed = moveSpeed;
if (!isGrounded)
{
currentSpeed *= airControl;
}
velocity.x = moveDirection.x * currentSpeed;
velocity.z = moveDirection.z * currentSpeed;
// Rotate to face movement direction
if (moveDirection.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, 10f * Time.deltaTime);
}
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (velocity.y < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
velocity.y -= gravityThisFrame * Time.deltaTime;
}
}
private void HandleJump()
{
if (isGrounded)
{
velocity.y = jumpForce;
}
}
private void ApplyVelocity()
{
controller.Move(velocity * Time.deltaTime);
}
private void OnDrawGizmos()
{
if (showGroundCheck)
{
Vector3 checkPosition = transform.position + groundCheckOffset;
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(checkPosition, groundCheckRadius);
}
}
}
BasicJumpFPS.cs
First-person jump with classic FPS movement. Includes mouse look integration. The camera is the player's viewpoint.
Setup:
- Add
CharacterControllerto your player - Create a child Camera (or use Main Camera as child)
- Add this script to the player root
- Assign the Camera reference in Inspector
BasicJumpFPS.cs
using UnityEngine;
/// <summary>
/// BASIC JUMP FPS - First-person jump with integrated mouselook
/// VG101 Code Bank - Requires InputReader from Input Setup
/// Complete FPS controller with jumping
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class BasicJumpFPS : MonoBehaviour
{
[Header("Jump Feel")]
[Tooltip("How hard we push off the ground.")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 8f;
[Tooltip("How fast we fall.")]
[Range(20f, 80f)]
[SerializeField] private float gravity = 25f;
[Tooltip("Extra gravity when falling.")]
[Range(1f, 4f)]
[SerializeField] private float fallGravityMultiplier = 2f;
[Header("Ground Detection")]
[SerializeField] private Vector3 groundCheckOffset = new Vector3(0, -0.9f, 0);
[SerializeField] private float groundCheckRadius = 0.3f;
[SerializeField] private LayerMask groundLayer;
[Header("Movement")]
[Tooltip("Walking speed")]
[SerializeField] private float moveSpeed = 6f;
[Tooltip("Air control multiplier")]
[Range(0f, 1f)]
[SerializeField] private float airControl = 0.3f;
[Header("Mouse Look")]
[Tooltip("The camera to rotate for vertical look")]
[SerializeField] private Transform playerCamera;
[Tooltip("Mouse sensitivity for gamepad")]
[SerializeField] private float gamepadLookSpeed = 150f;
[Tooltip("Mouse sensitivity multiplier")]
[SerializeField] private float mouseSensitivity = 0.15f;
[Tooltip("Maximum up/down look angle")]
[Range(45f, 90f)]
[SerializeField] private float maxLookAngle = 85f;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
private float cameraPitch;
private void Awake()
{
controller = GetComponent<CharacterController>();
// Lock and hide cursor for FPS
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void OnEnable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed += HandleJump;
}
}
private void OnDisable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed -= HandleJump;
}
}
private void Update()
{
CheckGrounded();
HandleLook();
HandleMovement();
ApplyGravity();
ApplyVelocity();
}
private void CheckGrounded()
{
Vector3 checkPosition = transform.position + groundCheckOffset;
isGrounded = Physics.CheckSphere(checkPosition, groundCheckRadius, groundLayer);
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f;
}
}
private void HandleLook()
{
if (InputReader.Instance == null) return;
Vector2 lookInput = InputReader.Instance.LookInput;
// Apply different sensitivity for mouse vs gamepad
float lookSpeedX, lookSpeedY;
if (InputReader.Instance.IsUsingGamepad)
{
lookSpeedX = gamepadLookSpeed * Time.deltaTime;
lookSpeedY = gamepadLookSpeed * Time.deltaTime;
}
else
{
// Mouse input is already deltaTime-independent
lookSpeedX = mouseSensitivity;
lookSpeedY = mouseSensitivity;
}
// Horizontal rotation - rotate the whole player
transform.Rotate(Vector3.up * lookInput.x * lookSpeedX);
// Vertical rotation - rotate just the camera
cameraPitch -= lookInput.y * lookSpeedY;
cameraPitch = Mathf.Clamp(cameraPitch, -maxLookAngle, maxLookAngle);
if (playerCamera != null)
{
playerCamera.localRotation = Quaternion.Euler(cameraPitch, 0f, 0f);
}
}
private void HandleMovement()
{
Vector2 input = InputReader.Instance != null ? InputReader.Instance.MoveInput : Vector2.zero;
// Movement relative to player facing direction
Vector3 moveDirection = transform.forward * input.y + transform.right * input.x;
float currentSpeed = moveSpeed;
if (!isGrounded)
{
currentSpeed *= airControl;
}
velocity.x = moveDirection.x * currentSpeed;
velocity.z = moveDirection.z * currentSpeed;
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (velocity.y < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
velocity.y -= gravityThisFrame * Time.deltaTime;
}
}
private void HandleJump()
{
if (isGrounded)
{
velocity.y = jumpForce;
}
}
private void ApplyVelocity()
{
controller.Move(velocity * Time.deltaTime);
}
private void OnDrawGizmos()
{
if (showGroundCheck)
{
Vector3 checkPosition = transform.position + groundCheckOffset;
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(checkPosition, groundCheckRadius);
}
}
}
BasicJumpIsometric.cs
Isometric/top-down jump. Movement is world-axis aligned (or rotated 45 degrees for true isometric). Jump adds vertical motion separate from XZ movement.
Setup:
- Add
CharacterControllerto your player - Set up isometric camera (typically 45-degree Y rotation, 30-45 degree X tilt)
- Add this script and configure in Inspector
- Choose rotation mode: World Axis or Isometric (45-degree rotated)
BasicJumpIsometric.cs
using UnityEngine;
/// <summary>
/// BASIC JUMP ISOMETRIC - Jump for isometric/top-down games
/// VG101 Code Bank - Requires InputReader from Input Setup
/// Movement on XZ plane with jump on Y axis
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class BasicJumpIsometric : MonoBehaviour
{
public enum MovementMode
{
WorldAxis, // Up = +Z, Right = +X (standard top-down)
Isometric45 // Up = +Z+X, Right = +X-Z (true isometric)
}
[Header("Jump Feel")]
[Tooltip("How hard we push off the ground.")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 10f;
[Tooltip("How fast we fall.")]
[Range(20f, 80f)]
[SerializeField] private float gravity = 35f;
[Tooltip("Extra gravity when falling.")]
[Range(1f, 4f)]
[SerializeField] private float fallGravityMultiplier = 1.5f;
[Header("Ground Detection")]
[SerializeField] private Vector3 groundCheckOffset = new Vector3(0, -0.5f, 0);
[SerializeField] private float groundCheckRadius = 0.3f;
[SerializeField] private LayerMask groundLayer;
[Header("Movement")]
[Tooltip("How movement input maps to world space")]
[SerializeField] private MovementMode movementMode = MovementMode.Isometric45;
[Tooltip("Movement speed on ground")]
[SerializeField] private float moveSpeed = 7f;
[Tooltip("Air control multiplier")]
[Range(0f, 1f)]
[SerializeField] private float airControl = 0.5f;
[Tooltip("Rotate character to face movement direction")]
[SerializeField] private bool rotateToMovement = true;
[Tooltip("How fast character rotates to face movement")]
[SerializeField] private float rotationSpeed = 720f;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
// Isometric conversion matrix (45-degree rotation)
private static readonly Matrix4x4 isoMatrix = Matrix4x4.Rotate(Quaternion.Euler(0, 45, 0));
private void Awake()
{
controller = GetComponent<CharacterController>();
}
private void OnEnable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed += HandleJump;
}
}
private void OnDisable()
{
if (InputReader.Instance != null)
{
InputReader.Instance.OnJumpPressed -= HandleJump;
}
}
private void Update()
{
CheckGrounded();
HandleMovement();
ApplyGravity();
ApplyVelocity();
}
private void CheckGrounded()
{
Vector3 checkPosition = transform.position + groundCheckOffset;
isGrounded = Physics.CheckSphere(checkPosition, groundCheckRadius, groundLayer);
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f;
}
}
private void HandleMovement()
{
Vector2 input = InputReader.Instance != null ? InputReader.Instance.MoveInput : Vector2.zero;
// Convert 2D input to 3D movement direction
Vector3 inputDirection = new Vector3(input.x, 0, input.y);
// Apply isometric rotation if needed
Vector3 moveDirection;
if (movementMode == MovementMode.Isometric45)
{
moveDirection = isoMatrix.MultiplyVector(inputDirection);
}
else
{
moveDirection = inputDirection;
}
float currentSpeed = moveSpeed;
if (!isGrounded)
{
currentSpeed *= airControl;
}
velocity.x = moveDirection.x * currentSpeed;
velocity.z = moveDirection.z * currentSpeed;
// Rotate to face movement direction
if (rotateToMovement && moveDirection.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
targetRotation,
rotationSpeed * Time.deltaTime
);
}
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (velocity.y < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
velocity.y -= gravityThisFrame * Time.deltaTime;
}
}
private void HandleJump()
{
if (isGrounded)
{
velocity.y = jumpForce;
}
}
private void ApplyVelocity()
{
controller.Move(velocity * Time.deltaTime);
}
private void OnDrawGizmos()
{
if (showGroundCheck)
{
Vector3 checkPosition = transform.position + groundCheckOffset;
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(checkPosition, groundCheckRadius);
}
}
}
Common Issues
"It feels floaty"
Increase fallGravityMultiplier. Most good-feeling jumps have faster descent than ascent.
"It's too short/tall"
Adjust jumpForce and gravity together. Higher force + higher gravity = same height, different speed.
"I can jump in the air"
Ground check isn't working. Check groundCheckRadius and your collision layers.
"InputReader.Instance is null"
Make sure you have the InputReader prefab in your scene. See Input Setup.
Combine With
- Coyote Time - grace period makes basic jump more forgiving
- Input Buffer - catches jumps pressed slightly before landing
- Squash Stretch - visual feedback for the jump (2D)
- Variable Jump - height control alternative
When to Use Basic vs. Variable
| Use Basic Jump when... | Use Variable Jump when... |
|---|---|
| Predictability matters (puzzle platformers) | Expression matters (action platformers) |
| Jump height is carefully designed per obstacle | Player should adapt jump to situation |
| Simplicity is a virtue | Depth is a virtue |
| Retro aesthetic | Modern platformer aesthetic |
Why Start Here
Basic jump is the ideal first scaffold because it's simple enough to understand completely but expressive enough to teach tuning. Four variables create hundreds of distinct feels.
Teaching Sequence
- Have students play with defaults first - establish baseline
- Change ONE variable dramatically - notice the difference
- Try to match a reference videogame's jump feel
- Document what they changed and why
Perspective Choice
Let students pick their perspective based on the videogame they're studying:
- 2D: Celeste, Hollow Knight, Mario
- 3D Third-Person: Dark Souls, Zelda, platforming collectathons
- 3D First-Person: Half-Life, Titanfall, classic FPS
- Isometric: Hades, Diablo, action RPGs with verticality
Common Student Mistakes
Changing too many variables at once: They can't isolate what's causing the feel. Enforce single-variable changes first.
Only adjusting jumpForce: Students often miss that gravity is equally important. Same height can feel completely different with different gravity/force ratios.
Ignoring fallGravityMultiplier: This is the secret ingredient that makes platformer jumps feel good. Pure physics feels wrong.
Assessment Questions
- Why does increasing both jumpForce and gravity maintain the same height but change the feel?
- What real-world physics does fallGravityMultiplier violate, and why is that violation important?
- When would you choose basic jump over variable jump for a design?
Heritage Notes
The basic jump predates videogames - it's how jumping works in reality. But videogame jumps have always been exaggerated:
- Donkey Kong (1981): Fixed arc jump, limited air control
- Super Mario Bros. (1985): Introduced variable jump, but basic enemies still use fixed jump
- Modern videogames: Variable is default, but basic persists in certain contexts
The Physics Lie
Real physics would produce symmetrical parabolas. Videogame jumps almost universally use asymmetrical arcs - faster falling than rising. This started as a technical necessity (faster gameplay on limited hardware) and became an aesthetic expectation.
When students ask "why doesn't it use real physics?" the common answer is: real physics feels wrong because we've been trained by decades of videogame physics. The heritage has shaped our expectations.
But there's another answer: the more reality-related a videogame experience or feel is, the less surprise and exploration of that videogame's feel and systems is necessary. It's just not surprising, interesting, maybe even fun.
The Jump as Gesture
A basic jump is a complete Gesture - it has Action (the input and resulting movement), Art (the visual arc, any animation), Arc (the duration of the jump), and Atmosphere (the feel of weightiness or lightness). Even this "simple" mechanic contains all four A's.
Aesthetic Heritage of Jump Parameters
When you choose a floaty jump, you're inheriting from Kirby's tradition. When you choose a snappy jump, you're inheriting from Super Meat Boy. These aren't just technical choices - they're Aesthetic Heritage choices that connect your videogame to lineages of feel.
Related
- Variable Jump - more expressive alternative
- Coyote Time - makes any jump more forgiving
- Input Setup - required InputReader system
- Gesture - the jump as gesture
Glossary Terms
- Coyote Time - grace period for jump input
- Input Buffer - remembering inputs for grace
- Juice - feedback and feel techniques