Double Jump
An extra jump in mid-air. Press jump again before landing to get additional height or change direction. The most common platformer upgrade.
The Feel
Double jump adds aerial control and recovery. It lets players correct mistakes, reach higher platforms, and feel more expressive in the air.
It's also a psychological safety net - players feel less committed to their first jump because they have a second chance.
Exposed Variables
| Variable | Type | Default | What it does |
|---|---|---|---|
extraJumps |
int | 1 | Number of air jumps allowed |
airJumpForce |
float | 10.0 | Force of air jump (often less than ground) |
resetVelocity |
bool | true | Cancel downward velocity before air jump |
Tuning Guide
For same-height double jump:
airJumpForce= same asjumpForceresetVelocity= true
For diminishing jumps:
airJumpForce= 70-80% ofjumpForce- Encourages using first jump wisely
Reference: Hollow Knight, Metroidvanias
For boosted air jump:
airJumpForce>jumpForce- First jump is setup, air jump is powerful
Perspective Scripts
DoubleJump2D.cs
2D double jump with configurable air jumps and distinct feedback for ground vs air jumps.
Setup:
- Add
Rigidbody2Dto your player - Create a child "GroundCheck" at the feet
- Add this script and configure in Inspector
- Optional: assign airJumpEffect particle prefab
DoubleJump2D.cs
using UnityEngine;
/// <summary>
/// DOUBLE JUMP 2D - Air jump with resource management
/// VG101 Code Bank - Requires InputReader from Input Setup
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
public class DoubleJump2D : MonoBehaviour
{
[Header("Ground Jump")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 12f;
[Header("Air Jump")]
[Tooltip("Number of air jumps (1 = double jump, 2 = triple)")]
[Range(1, 5)]
[SerializeField] private int extraJumps = 1;
[Range(5f, 25f)]
[SerializeField] private float airJumpForce = 10f;
[Tooltip("Cancel downward velocity before air jump")]
[SerializeField] private bool resetVelocity = true;
[Header("Gravity")]
[Range(20f, 80f)]
[SerializeField] private float gravity = 40f;
[Range(1f, 4f)]
[SerializeField] private float fallGravityMultiplier = 1.5f;
[Header("Ground Detection")]
[SerializeField] private Transform groundCheckPoint;
[Range(0.05f, 0.5f)]
[SerializeField] private float groundCheckRadius = 0.2f;
[SerializeField] private LayerMask groundLayer;
[Header("Air Jump Effect (Optional)")]
[SerializeField] private GameObject airJumpEffect;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
[SerializeField] private bool debugMode = false;
private Rigidbody2D rb;
private float verticalVelocity;
private bool isGrounded;
private bool wasGrounded;
private int jumpsRemaining;
public int JumpsRemaining => jumpsRemaining;
public bool CanJump => isGrounded || jumpsRemaining > 0;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
rb.gravityScale = 0f;
jumpsRemaining = extraJumps;
}
private void OnEnable()
{
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()
{
wasGrounded = isGrounded;
isGrounded = Physics2D.OverlapCircle(
groundCheckPoint.position,
groundCheckRadius,
groundLayer
);
if (isGrounded && verticalVelocity < 0)
{
verticalVelocity = 0f;
}
// Refresh jumps on landing
if (isGrounded && !wasGrounded)
{
jumpsRemaining = extraJumps;
if (debugMode) Debug.Log($"Landed! Air jumps: {jumpsRemaining}");
}
}
private void HandleJump()
{
if (isGrounded)
{
// Ground jump
verticalVelocity = jumpForce;
if (debugMode) Debug.Log("Ground jump!");
}
else if (jumpsRemaining > 0)
{
// Air jump
if (resetVelocity)
{
verticalVelocity = 0f;
}
verticalVelocity = airJumpForce;
jumpsRemaining--;
SpawnAirJumpEffect();
if (debugMode) Debug.Log($"Air jump! Remaining: {jumpsRemaining}");
}
}
private void SpawnAirJumpEffect()
{
if (airJumpEffect != null)
{
GameObject effect = Instantiate(airJumpEffect, transform.position, Quaternion.identity);
Destroy(effect, 2f);
}
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (verticalVelocity < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
verticalVelocity -= gravityThisFrame * Time.fixedDeltaTime;
}
}
private void ApplyVelocity()
{
Vector2 velocity = rb.linearVelocity;
velocity.y = verticalVelocity;
rb.linearVelocity = velocity;
}
// Call from external scripts (e.g., wall touch)
public void RefreshAirJumps()
{
jumpsRemaining = extraJumps;
}
private void OnDrawGizmos()
{
if (showGroundCheck && groundCheckPoint != null)
{
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(groundCheckPoint.position, groundCheckRadius);
}
}
}
DoubleJump3D.cs
3D double jump for third-person, first-person, and isometric. Works with CharacterController.
Setup:
- Add
CharacterControllerto your player - Add this script and configure in Inspector
- Optional: assign airJumpEffect particle prefab
DoubleJump3D.cs
using UnityEngine;
/// <summary>
/// DOUBLE JUMP 3D - Air jump for all 3D perspectives
/// VG101 Code Bank - Requires InputReader from Input Setup
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class DoubleJump3D : MonoBehaviour
{
[Header("Ground Jump")]
[Range(5f, 25f)]
[SerializeField] private float jumpForce = 10f;
[Header("Air Jump")]
[Range(1, 5)]
[SerializeField] private int extraJumps = 1;
[Range(5f, 25f)]
[SerializeField] private float airJumpForce = 8f;
[SerializeField] private bool resetVelocity = true;
[Header("Gravity")]
[Range(20f, 80f)]
[SerializeField] private float gravity = 30f;
[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 (Optional)")]
[SerializeField] private float moveSpeed = 8f;
[Range(0f, 1f)]
[SerializeField] private float airControl = 0.5f;
[SerializeField] private bool useBuiltInMovement = true;
[Header("Air Jump Effect (Optional)")]
[SerializeField] private GameObject airJumpEffect;
[Header("Debug")]
[SerializeField] private bool showGroundCheck = true;
[SerializeField] private bool debugMode = false;
private CharacterController controller;
private Vector3 velocity;
private bool isGrounded;
private bool wasGrounded;
private int jumpsRemaining;
private Transform cameraTransform;
public int JumpsRemaining => jumpsRemaining;
public bool CanJump => isGrounded || jumpsRemaining > 0;
private void Awake()
{
controller = GetComponent<CharacterController>();
cameraTransform = Camera.main?.transform;
jumpsRemaining = extraJumps;
}
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();
if (useBuiltInMovement)
{
HandleMovement();
}
ApplyGravity();
ApplyVelocity();
}
private void CheckGrounded()
{
wasGrounded = isGrounded;
Vector3 checkPosition = transform.position + groundCheckOffset;
isGrounded = Physics.CheckSphere(checkPosition, groundCheckRadius, groundLayer);
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f;
}
// Refresh jumps on landing
if (isGrounded && !wasGrounded)
{
jumpsRemaining = extraJumps;
if (debugMode) Debug.Log($"Landed! Air jumps: {jumpsRemaining}");
}
}
private void HandleMovement()
{
Vector2 input = InputReader.Instance != null ? InputReader.Instance.MoveInput : Vector2.zero;
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 = isGrounded ? moveSpeed : moveSpeed * airControl;
velocity.x = moveDirection.x * currentSpeed;
velocity.z = moveDirection.z * currentSpeed;
if (moveDirection.magnitude > 0.1f)
{
Quaternion targetRotation = Quaternion.LookRotation(moveDirection);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, 10f * Time.deltaTime);
}
}
private void HandleJump()
{
if (isGrounded)
{
velocity.y = jumpForce;
if (debugMode) Debug.Log("Ground jump!");
}
else if (jumpsRemaining > 0)
{
if (resetVelocity)
{
velocity.y = 0f;
}
velocity.y = airJumpForce;
jumpsRemaining--;
SpawnAirJumpEffect();
if (debugMode) Debug.Log($"Air jump! Remaining: {jumpsRemaining}");
}
}
private void SpawnAirJumpEffect()
{
if (airJumpEffect != null)
{
GameObject effect = Instantiate(airJumpEffect, transform.position, Quaternion.identity);
Destroy(effect, 2f);
}
}
private void ApplyGravity()
{
if (!isGrounded)
{
float gravityThisFrame = gravity;
if (velocity.y < 0)
{
gravityThisFrame *= fallGravityMultiplier;
}
velocity.y -= gravityThisFrame * Time.deltaTime;
}
}
private void ApplyVelocity()
{
controller.Move(velocity * Time.deltaTime);
}
public void SetHorizontalVelocity(Vector3 horizontalVelocity)
{
velocity.x = horizontalVelocity.x;
velocity.z = horizontalVelocity.z;
}
public void RefreshAirJumps()
{
jumpsRemaining = extraJumps;
}
private void OnDrawGizmos()
{
if (showGroundCheck)
{
Vector3 checkPosition = transform.position + groundCheckOffset;
Gizmos.color = isGrounded ? Color.green : Color.red;
Gizmos.DrawWireSphere(checkPosition, groundCheckRadius);
}
}
}
Key Implementation Details
- Counter management: Track
jumpsRemaining, decrement on air jump, reset on landing - Velocity reset: Optional - makes air jump height consistent regardless of fall speed
- Distinct feedback: Air jump MUST look/sound different from ground jump
- RefreshAirJumps(): Call from wall touch or other refresh triggers
Common Issues
"Double jump doesn't feel different"
Add distinct feedback: particle effect, different sound, animation change. Players need to know they used their resource.
"Infinite jumps"
Make sure you only reset jumpsRemaining on landing (wasGrounded check), not every frame.
"Air jump barely helps when falling"
Set resetVelocity to true for consistent air jump height.
Design Decisions
| Question | Options |
|---|---|
| Coyote jump uses air jump? | Most videogames: no. Coyote is grace, not air jump. |
| Reset on wall touch? | Yes for wall-jump videogames (enables infinite climbing) |
| Reset on taking damage? | Generous design - prevents unfair deaths |
Combine With
- Variable Jump - both jumps can be variable height
- Coyote Time - coyote shouldn't consume air jump
- Dash - dash + double jump for maximum air mobility
Why Teach This
Double jump teaches ability management: tracking state (jumps remaining), reset conditions (landing), and feedback (air jump effects). More complex than basic jump but still approachable.
Teaching Sequence
- Start with basic jump working
- Add counter for remaining jumps
- Allow jump when jumpsRemaining > 0 and in air
- Add distinct visual/audio feedback
- Tune the feel (force, velocity reset)
The Feedback Lesson
Air jump needs different feedback than ground jump so players know they've used their resource:
- Different sound effect
- Different particle effect (wings, sparkles)
- Different animation
Assessment Questions
- Why might air jump be weaker than ground jump?
- What feedback tells the player they've used double jump?
- How does double jump change level design?
Heritage Notes
Double jump has murky origins but became codified in the 1980s-90s. Dragon Buster (1984) is an early example. It spread through action-platformers and Metroidvanias.
Now so common that players expect it. Many videogames start without it and grant it as an upgrade - the Metroidvania tradition of ability-gated progression.
The Abstraction Question
Double jump is physically impossible. You can't push off nothing. But videogames aren't physics simulations. They need to be internally consistent, not realistic.
Permission and Progression
From a Permissions perspective, double jump expands what's allowed. Before: certain heights forbidden. After: reachable. New abilities = new permissions = new areas.
Triple Jump and Beyond
Why stop at two? Diminishing returns:
- More jumps = more to track mentally
- More jumps = harder level design
- More jumps = less meaningful each jump becomes
Double is the sweet spot for most videogames.
Related
- Basic Jump - the foundation
- Variable Jump - can combine with double jump
- Input Setup - required InputReader system
- Dash - another air mobility option