Dash
A burst of movement in a direction. Quick, committed, often with invincibility. The defining gesture of modern action platformers.
The Feel
Dash is explosive and committed. You press, you move, no take-backs. The commitment creates tension; the burst creates power.
Exposed Variables
| Variable | Type | Default | What it does |
|---|---|---|---|
dashSpeed |
float | 25.0 | How fast you move during dash |
dashDuration |
float | 0.15 | How long the dash lasts (seconds) |
dashCooldown |
float | 0.0 | Time before you can dash again |
dashDirection |
enum | EightWay | Which directions are valid |
dashCount |
int | 1 | Dashes available per air period |
refreshOnGround |
bool | true | Does landing restore dash? |
invincibleDuringDash |
bool | false | Ignore damage while dashing? |
cancelWindow |
float | 0.0 | When can you cancel into other actions? |
momentumTransfer |
float | 0.0 | How much dash speed carries into normal movement |
Tuning Guide
Celeste-style (precise, resource-based):
dashSpeed: 25dashDuration: 0.12-0.15dashCount: 1dashDirection: EightWayrefreshOnGround: trueinvincibleDuringDash: true (brief)momentumTransfer: 0
One dash per airtime, eight directions, full commitment
Hades-style (combat, spammable):
dashSpeed: 18dashDuration: 0.1dashCooldown: 0.3dashCount: unlimited (via cooldown)dashDirection: EightWayinvincibleDuringDash: truecancelWindow: 0.05
Faster, shorter dashes you can chain
Mega Man X-style (ground only, forward):
dashSpeed: 15dashDuration: 0.3dashDirection: HorizontaldashCooldown: 0groundOnly: true (separate variable)
Longer duration, ground-based
Pseudocode
On Dash Press (if dashCount > 0):
- Record dash direction from input
- Set dash state
- Start dash timer
- If invincible: enable invincibility frames
During Dash:
- Override normal movement with dashSpeed in dashDirection
- Ignore gravity (optional)
- Check for cancel window inputs
On Dash End:
- Return to normal movement
- Apply momentum transfer if any
- Start cooldown timer if applicable
- Decrement dashCount
On Ground Contact:
- If refreshOnGround: restore dashCount
Unity Setup (2D)
- InputReader Setup: See Input Setup page - ensure you have a "Dash" action (Button type) bound to Left Shift and Right Trigger
- Player Setup:
- Add
Rigidbody2D(Gravity Scale: 3, Freeze Rotation Z: checked) - Add
Collider2D(BoxCollider2D or CapsuleCollider2D) - Add the
Dash2D.csscript below
- Add
- Ground Check: Create empty child object at player's feet, assign to
groundCheck - Layers: Create "Ground" layer, assign to ground objects, set in script's Inspector
The Script (2D)
Dash2D.cs
// ============================================================
// Dash2D.cs
// 2D dash with direction options, resource management,
// invincibility, and momentum transfer.
// Uses InputReader for device-agnostic input (gamepad + KB/M).
// Unity 6.x + New Input System
// ============================================================
using UnityEngine;
public enum DashDirectionMode2D
{
EightWay, // All 8 cardinal + diagonal directions
FourWay, // Up, Down, Left, Right only
Horizontal, // Left and Right only
FacingDirection // Dash the way sprite is facing
}
[RequireComponent(typeof(Rigidbody2D))]
public class Dash2D : MonoBehaviour
{
[Header("Dash Settings")]
[Tooltip("Speed during dash (units/second)")]
[SerializeField] private float dashSpeed = 25f;
[Tooltip("Duration of the dash (seconds)")]
[SerializeField] private float dashDuration = 0.15f;
[Tooltip("Cooldown before next dash (0 = none)")]
[SerializeField] private float dashCooldown = 0f;
[Header("Direction")]
[Tooltip("Which directions are valid")]
[SerializeField] private DashDirectionMode2D directionMode = DashDirectionMode2D.EightWay;
[Tooltip("Default direction if no input (1=right, -1=left)")]
[SerializeField] private int facingDirection = 1;
[Header("Resource Management")]
[Tooltip("Dashes available per air period")]
[SerializeField] private int maxDashCount = 1;
[Tooltip("Landing restores dashes?")]
[SerializeField] private bool refreshOnGround = true;
[Header("Combat")]
[Tooltip("Invincible while dashing?")]
[SerializeField] private bool invincibleDuringDash = false;
[Tooltip("Momentum carried after dash (0-1)")]
[SerializeField] [Range(0f, 1f)] private float momentumTransfer = 0f;
[Header("Ground Detection")]
[SerializeField] private Transform groundCheck;
[SerializeField] private float groundCheckRadius = 0.1f;
[SerializeField] private LayerMask groundLayer;
// Components
private Rigidbody2D rb;
// Dash state
private bool isDashing;
private float dashTimer;
private float cooldownTimer;
private int dashesRemaining;
private Vector2 dashDirection;
private float storedGravityScale;
// Ground state
private bool isGrounded;
private bool wasGrounded;
// Public properties
public bool IsDashing => isDashing;
public bool IsInvincible => isDashing && invincibleDuringDash;
public int DashesRemaining => dashesRemaining;
public bool CanDash => !isDashing && dashesRemaining > 0 && cooldownTimer <= 0f;
void Awake()
{
rb = GetComponent<Rigidbody2D>();
storedGravityScale = rb.gravityScale;
dashesRemaining = maxDashCount;
}
void OnEnable()
{
InputReader.Instance.OnDashPressed += TryDash;
}
void OnDisable()
{
InputReader.Instance.OnDashPressed -= TryDash;
}
void Update()
{
if (cooldownTimer > 0f)
cooldownTimer -= Time.deltaTime;
CheckGroundState();
}
void FixedUpdate()
{
if (isDashing)
ProcessDash();
}
private void TryDash()
{
if (!CanDash) return;
dashDirection = CalculateDashDirection();
if (dashDirection == Vector2.zero) return;
StartDash();
}
private Vector2 CalculateDashDirection()
{
Vector2 input = InputReader.Instance.MoveInput;
switch (directionMode)
{
case DashDirectionMode2D.EightWay:
if (input.magnitude > 0.1f)
return input.normalized;
return new Vector2(facingDirection, 0);
case DashDirectionMode2D.FourWay:
if (Mathf.Abs(input.x) > Mathf.Abs(input.y))
return new Vector2(Mathf.Sign(input.x), 0);
else if (Mathf.Abs(input.y) > 0.1f)
return new Vector2(0, Mathf.Sign(input.y));
return new Vector2(facingDirection, 0);
case DashDirectionMode2D.Horizontal:
if (Mathf.Abs(input.x) > 0.1f)
return new Vector2(Mathf.Sign(input.x), 0);
return new Vector2(facingDirection, 0);
case DashDirectionMode2D.FacingDirection:
return new Vector2(facingDirection, 0);
default:
return new Vector2(facingDirection, 0);
}
}
private void StartDash()
{
isDashing = true;
dashTimer = dashDuration;
dashesRemaining--;
storedGravityScale = rb.gravityScale;
rb.gravityScale = 0f;
rb.linearVelocity = dashDirection * dashSpeed;
}
private void ProcessDash()
{
dashTimer -= Time.fixedDeltaTime;
rb.linearVelocity = dashDirection * dashSpeed;
if (dashTimer <= 0f)
EndDash();
}
private void EndDash()
{
isDashing = false;
rb.gravityScale = storedGravityScale;
rb.linearVelocity *= momentumTransfer;
cooldownTimer = dashCooldown;
}
private void CheckGroundState()
{
wasGrounded = isGrounded;
if (groundCheck != null)
{
isGrounded = Physics2D.OverlapCircle(
groundCheck.position,
groundCheckRadius,
groundLayer
);
}
if (refreshOnGround && isGrounded && !wasGrounded)
dashesRemaining = maxDashCount;
}
// Public API
public void RefreshDashes() => dashesRemaining = maxDashCount;
public void SetFacingDirection(int dir) => facingDirection = dir > 0 ? 1 : -1;
public void CancelDash() { if (isDashing) EndDash(); }
void OnDrawGizmosSelected()
{
if (groundCheck != null)
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(groundCheck.position, groundCheckRadius);
}
}
}
Unity Setup (3D - Third Person, First Person, or Isometric)
- InputReader Setup: See Input Setup page - ensure you have a "Dash" action (Button type)
- Player Setup:
- Add
CharacterControllercomponent - Add the
Dash3D.csscript below
- Add
- Optional - Assign Camera: For camera-relative dashing, assign your main camera to the
playerCamerafield
The Script (3D)
Dash3D.cs
// ============================================================
// Dash3D.cs
// 3D dash for third-person, first-person, or isometric.
// Uses CharacterController and InputReader for device-agnostic
// input (gamepad + KB/M). Works with camera-relative movement.
// Unity 6.x + New Input System
// ============================================================
using UnityEngine;
public enum DashDirectionMode3D
{
EightWay, // 8 directions on XZ plane
FourWay, // Cardinal directions only
Forward, // Always forward
CameraRelative // Based on camera facing
}
[RequireComponent(typeof(CharacterController))]
public class Dash3D : MonoBehaviour
{
[Header("Dash Settings")]
[Tooltip("Speed during dash (units/second)")]
[SerializeField] private float dashSpeed = 25f;
[Tooltip("Duration of the dash (seconds)")]
[SerializeField] private float dashDuration = 0.15f;
[Tooltip("Cooldown before next dash (0 = none)")]
[SerializeField] private float dashCooldown = 0f;
[Header("Direction")]
[Tooltip("Which directions are valid")]
[SerializeField] private DashDirectionMode3D directionMode = DashDirectionMode3D.CameraRelative;
[Tooltip("Camera for relative movement (optional)")]
[SerializeField] private Transform playerCamera;
[Header("Resource Management")]
[Tooltip("Dashes available per air period")]
[SerializeField] private int maxDashCount = 1;
[Tooltip("Landing restores dashes?")]
[SerializeField] private bool refreshOnGround = true;
[Header("Combat")]
[Tooltip("Invincible while dashing?")]
[SerializeField] private bool invincibleDuringDash = false;
[Tooltip("Momentum carried after dash (0-1)")]
[SerializeField] [Range(0f, 1f)] private float momentumTransfer = 0f;
[Header("Vertical Behavior")]
[Tooltip("Ignore gravity during dash?")]
[SerializeField] private bool suspendGravity = true;
[Tooltip("Allow vertical dashing (up/down)?")]
[SerializeField] private bool allowVerticalDash = false;
// Components
private CharacterController controller;
// Dash state
private bool isDashing;
private float dashTimer;
private float cooldownTimer;
private int dashesRemaining;
private Vector3 dashDirection;
private Vector3 dashVelocity;
// Ground state
private bool wasGrounded;
// Public properties
public bool IsDashing => isDashing;
public bool IsInvincible => isDashing && invincibleDuringDash;
public int DashesRemaining => dashesRemaining;
public bool CanDash => !isDashing && dashesRemaining > 0 && cooldownTimer <= 0f;
// For external movement scripts to read
public Vector3 DashVelocity => isDashing ? dashVelocity : Vector3.zero;
void Awake()
{
controller = GetComponent<CharacterController>();
dashesRemaining = maxDashCount;
if (playerCamera == null)
playerCamera = Camera.main?.transform;
}
void OnEnable()
{
InputReader.Instance.OnDashPressed += TryDash;
}
void OnDisable()
{
InputReader.Instance.OnDashPressed -= TryDash;
}
void Update()
{
if (cooldownTimer > 0f)
cooldownTimer -= Time.deltaTime;
CheckGroundState();
if (isDashing)
ProcessDash();
}
private void TryDash()
{
if (!CanDash) return;
dashDirection = CalculateDashDirection();
if (dashDirection == Vector3.zero) return;
StartDash();
}
private Vector3 CalculateDashDirection()
{
Vector2 input = InputReader.Instance.MoveInput;
Vector3 direction = Vector3.zero;
switch (directionMode)
{
case DashDirectionMode3D.CameraRelative:
if (playerCamera != null && input.magnitude > 0.1f)
{
Vector3 camForward = playerCamera.forward;
Vector3 camRight = playerCamera.right;
if (!allowVerticalDash)
{
camForward.y = 0;
camRight.y = 0;
camForward.Normalize();
camRight.Normalize();
}
direction = (camForward * input.y + camRight * input.x).normalized;
}
else
{
direction = transform.forward;
}
break;
case DashDirectionMode3D.EightWay:
if (input.magnitude > 0.1f)
direction = new Vector3(input.x, 0, input.y).normalized;
else
direction = transform.forward;
break;
case DashDirectionMode3D.FourWay:
if (Mathf.Abs(input.x) > Mathf.Abs(input.y))
direction = new Vector3(Mathf.Sign(input.x), 0, 0);
else if (Mathf.Abs(input.y) > 0.1f)
direction = new Vector3(0, 0, Mathf.Sign(input.y));
else
direction = transform.forward;
break;
case DashDirectionMode3D.Forward:
direction = transform.forward;
break;
}
return direction;
}
private void StartDash()
{
isDashing = true;
dashTimer = dashDuration;
dashesRemaining--;
dashVelocity = dashDirection * dashSpeed;
}
private void ProcessDash()
{
dashTimer -= Time.deltaTime;
Vector3 movement = dashVelocity * Time.deltaTime;
if (!suspendGravity && !controller.isGrounded)
movement.y -= 9.81f * Time.deltaTime;
controller.Move(movement);
if (dashTimer <= 0f)
EndDash();
}
private void EndDash()
{
isDashing = false;
dashVelocity *= momentumTransfer;
cooldownTimer = dashCooldown;
}
private void CheckGroundState()
{
bool isGrounded = controller.isGrounded;
if (refreshOnGround && isGrounded && !wasGrounded)
dashesRemaining = maxDashCount;
wasGrounded = isGrounded;
}
// Public API
public void RefreshDashes() => dashesRemaining = maxDashCount;
public void CancelDash() { if (isDashing) EndDash(); }
void OnDrawGizmosSelected()
{
if (isDashing)
{
Gizmos.color = Color.cyan;
Gizmos.DrawRay(transform.position, dashDirection * 2f);
}
}
}
// ============================================================
// INTEGRATION WITH YOUR MOVEMENT SCRIPT:
//
// If you have a separate movement script using CharacterController,
// check for IsDashing before applying your own movement:
//
// void Update()
// {
// if (dash.IsDashing) return; // Let dash handle movement
//
// // Your normal movement code here...
// }
//
// Or combine velocities by reading DashVelocity:
//
// Vector3 totalVelocity = myVelocity + dash.DashVelocity;
// controller.Move(totalVelocity * Time.deltaTime);
// ============================================================
Common Issues
"The dash feels weak"
Increase dashSpeed. Dash should feel noticeably faster than running.
"I can't control where I'm going"
Check dashDirection. If set to four-way, you might expect eight. Also check input handling for diagonals.
"The dash is too short/long"
Adjust dashDuration. 0.1-0.2 is typical. Shorter = snappier; longer = more distance.
"It feels like I have infinite dashes"
Check refreshOnGround and dashCount. If grounded resets dash instantly, add a brief window.
"I'm taking damage during dash"
invincibleDuringDash needs to be true, and your damage system needs to check for invincibility frames.
Direction Systems
| System | Directions | Use Case |
|---|---|---|
| FourWay | Up, Down, Left, Right | Simple, classic |
| EightWay | + Diagonals | Precise, modern platformers |
| Horizontal | Left, Right only | Ground-focused videogames |
| Aim-Based | Follows mouse/stick direction | Twin-stick, shooters |
| Movement-Based | Follows current velocity | Momentum-focused |
Combine With
- Coyote Time - brief dash window after leaving platform
- Double Jump - dash extends air mobility
- Hitstop - freeze when dash-attack connects
- Screen Shake - on dash-attack impact
- Squash & Stretch - visual stretch during dash
Why Teach Dash
Dash introduces students to resource management in movement. Unlike jump (which is almost always available), dash often has a count or cooldown. This creates decisions: "Should I dash now or save it?"
Teaching Sequence
- Start with unlimited dashes, no cooldown - feel the basic mechanic
- Add dashCount = 1 with refreshOnGround - introduces resource thinking
- Add invincibility - changes how dash is used (evasion vs. movement)
- Experiment with direction systems
Common Student Mistakes
Making dash too similar to running: If dash speed is only 1.5x run speed, it doesn't feel special. It needs to feel like a distinct mode.
Forgetting cancel windows: Students implement dash that can't be interrupted. This feels unresponsive. Even committed dashes usually have late cancel windows.
Ignoring the resource: When dashCount is treated as infinite, students miss the design opportunity. One dash per airtime creates meaningful choices.
Assessment
- Can they articulate why Celeste uses one dash per airtime?
- Can they explain how invincibility changes the dash's role?
- Can they design a dash that fits a specific videogame genre?
Exercise: Dash Role Analysis
Have students play three videogames with dashes (Celeste, Hades, Mega Man X). For each:
- What role does the dash serve? (Traversal? Evasion? Combat?)
- How does the resource system support that role?
- What would change if you removed dash invincibility?
Heritage Notes
The dash evolved from several sources:
Mega Man X (1993) - Ground dash that changed the pace of action platformers. Made movement itself feel aggressive.
Fighting games - Air dashes in Marvel vs. Capcom, Guilty Gear created aerial mobility that felt distinct from jumping.
Modern platformers - Celeste refined the single-dash-per-air model into a precise tool.
The dash is now so standard that its absence is notable. A platformer without dash feels deliberately retro.
Dash as Commitment
Dash represents a design philosophy: powerful but costly. Unlike jump (low commitment, low power), dash trades control for speed. You're locked into a direction.
This connects to Permissions: dash allows burst movement but requires commitment. It forbids changing direction mid-dash. The permissions shape the feel.
The Invincibility Question
Adding invincibility to dash transforms it from movement tool to survival tool. This changes everything:
- Players dash through attacks, not around them
- Dash timing becomes a core skill
- Enemy design must account for i-frames
The decision to include invincibility is one of the most impactful in action videogame design.
The Celeste Model
Celeste made the dash-per-airtime model canonical for precision platformers. Key insights:
- One dash per air period creates meaningful scarcity
- Eight directions enables precise positioning
- Refreshing on ground makes failure recoverable
- Brief invincibility forgives close calls
This model is now the default expectation. Deviating from it is a deliberate choice.
Unresolved Questions
- How much should momentum carry after dash? Full stop vs. smooth transition creates very different feels.
- Should dash refresh on other surfaces (walls, enemies)? Each refresh point changes level design possibilities.
- Is the Celeste model becoming too dominant? Are we losing variety in dash design?
Related
- Gesture - dash as canonical gesture
- Aesthetic Heritage - dash lineage
- Trail Effect - common visual pairing
Glossary Terms
- Invincibility Frames - damage immunity period
- Cancel Window - when you can interrupt an action
- Juice - feedback techniques that make dash feel powerful