Dash

A burst of movement in a direction. Quick, committed, often with invincibility. The defining gesture of modern action platformers.

Practice - what you do

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: 25
  • dashDuration: 0.12-0.15
  • dashCount: 1
  • dashDirection: EightWay
  • refreshOnGround: true
  • invincibleDuringDash: true (brief)
  • momentumTransfer: 0

One dash per airtime, eight directions, full commitment

Hades-style (combat, spammable):

  • dashSpeed: 18
  • dashDuration: 0.1
  • dashCooldown: 0.3
  • dashCount: unlimited (via cooldown)
  • dashDirection: EightWay
  • invincibleDuringDash: true
  • cancelWindow: 0.05

Faster, shorter dashes you can chain

Mega Man X-style (ground only, forward):

  • dashSpeed: 15
  • dashDuration: 0.3
  • dashDirection: Horizontal
  • dashCooldown: 0
  • groundOnly: 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
Prerequisite: These scripts use the InputReader pattern for device-agnostic input. Set that up first.

Unity Setup (2D)

  1. InputReader Setup: See Input Setup page - ensure you have a "Dash" action (Button type) bound to Left Shift and Right Trigger
  2. Player Setup:
    • Add Rigidbody2D (Gravity Scale: 3, Freeze Rotation Z: checked)
    • Add Collider2D (BoxCollider2D or CapsuleCollider2D)
    • Add the Dash2D.cs script below
  3. Ground Check: Create empty child object at player's feet, assign to groundCheck
  4. 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)

  1. InputReader Setup: See Input Setup page - ensure you have a "Dash" action (Button type)
  2. Player Setup:
    • Add CharacterController component
    • Add the Dash3D.cs script below
  3. Optional - Assign Camera: For camera-relative dashing, assign your main camera to the playerCamera field

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


Related

Glossary Terms