A dash is one of the best feeling moves you can give a player, and one of the easiest to get wrong. Done badly, the player teleports through walls, keeps dashing forever, or slides to a stop like they’re on ice. This guide builds a Unity dash ability with a cooldown, invincibility frames and a trail, with versions for a Rigidbody (2D or 3D) and a CharacterController, and shows how to stop dashes passing through walls.
What a Good Dash Needs
Before code, decide what your dash is. Most games want:
- A fixed distance in a fixed time. For example 4 units in 0.15 seconds. That makes the dash predictable, which players need for platforming and dodging.
- A direction. Usually the movement input direction, falling back to the facing direction if there’s no input.
- A cooldown. So it can’t be spammed. Typically 0.4 to 1 second.
- Control lockout. Normal movement and gravity are ignored during the dash so it moves in a straight line.
- Optional invincibility. A dodge-roll style dash ignores damage while it’s active.
The cleanest approach for all of these is to override velocity for the duration of the dash rather than apply one big force. A single impulse gets eaten by drag and friction and travels a different distance every time.
Dash With Rigidbody2D
This version works with the movement script from the 2D platformer movement guide. The movement script just needs to skip its own velocity writes while IsDashing is true.
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class Dash2D : MonoBehaviour
{
[SerializeField] InputActionReference moveAction;
[SerializeField] InputActionReference dashAction;
[SerializeField] float dashDistance = 4f;
[SerializeField] float dashDuration = 0.15f;
[SerializeField] float cooldown = 0.6f;
[SerializeField] bool resetOnGround = true;
[SerializeField] TrailRenderer trail;
public bool IsDashing { get; private set; }
public float CooldownRemaining => Mathf.Max(0f, nextDashTime - Time.time);
Rigidbody2D rb;
float nextDashTime;
float facing = 1f;
void Awake() => rb = GetComponent<Rigidbody2D>();
void OnEnable() { moveAction.action.Enable(); dashAction.action.Enable(); }
void OnDisable() { moveAction.action.Disable(); dashAction.action.Disable(); }
void Update()
{
Vector2 input = moveAction.action.ReadValue<Vector2>();
if (Mathf.Abs(input.x) > 0.1f) facing = Mathf.Sign(input.x);
if (dashAction.action.WasPressedThisFrame() && !IsDashing && Time.time >= nextDashTime)
{
Vector2 dir = input.sqrMagnitude > 0.01f ? input.normalized : new Vector2(facing, 0f);
StartCoroutine(DoDash(dir));
}
}
IEnumerator DoDash(Vector2 dir)
{
IsDashing = true;
nextDashTime = Time.time + cooldown;
float oldGravity = rb.gravityScale;
rb.gravityScale = 0f;
if (trail) trail.emitting = true;
float speed = dashDistance / dashDuration;
float end = Time.time + dashDuration;
while (Time.time < end)
{
rb.linearVelocity = dir * speed;
yield return new WaitForFixedUpdate();
}
rb.linearVelocity = dir * Mathf.Min(speed, 6f) * 0.5f; // carry a little momentum
rb.gravityScale = oldGravity;
if (trail) trail.emitting = false;
IsDashing = false;
}
}
Speed is worked out from distance and duration, so designers tweak the two numbers they care about. Waiting on WaitForFixedUpdate writes the velocity on every physics step, which beats drag and stops other scripts from steering mid-dash. The exit velocity keeps a little momentum so the dash doesn’t end in a dead stop.
The same code works for a 3D Rigidbody: switch types to Rigidbody and Vector3, and toggle rb.useGravity instead of gravityScale. For 3D, build the direction from the camera’s forward and right vectors flattened onto the ground plane, like you do for normal movement.
Dash With a CharacterController
A CharacterController has no velocity of its own. You move it with Move, so the dash is a timer that adds a large horizontal motion each frame:
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class DashCC : MonoBehaviour
{
[SerializeField] float dashDistance = 5f;
[SerializeField] float dashDuration = 0.18f;
[SerializeField] float cooldown = 0.7f;
public bool IsDashing => dashTimeLeft > 0f;
CharacterController cc;
Vector3 dashDir;
float dashTimeLeft;
float nextDashTime;
void Awake() => cc = GetComponent<CharacterController>();
public bool TryDash(Vector3 worldDir)
{
if (IsDashing || Time.time < nextDashTime) return false;
worldDir.y = 0f;
dashDir = worldDir.sqrMagnitude > 0.01f ? worldDir.normalized : transform.forward;
dashTimeLeft = dashDuration;
nextDashTime = Time.time + cooldown;
return true;
}
void Update()
{
if (!IsDashing) return;
float step = Mathf.Min(Time.deltaTime, dashTimeLeft);
cc.Move(dashDir * (dashDistance / dashDuration) * step);
dashTimeLeft -= step;
}
}
Call TryDash from your player controller when the dash input fires, and skip your normal movement and gravity while IsDashing is true. Because Move does its own collision sweep, a CharacterController dash will slide along walls instead of passing through them.
Stopping Dashes Through Walls
A fast Rigidbody can skip past a thin wall between physics steps. At 4 units in 0.15 seconds the player moves about 0.5 units per step at the default 50 Hz, which is more than many wall colliders are thick. Fixes, in order:
- Collision Detection: Continuous. Set it on the player’s Rigidbody or Rigidbody2D. This is the main fix. Use Continuous Dynamic in 3D if the walls also move.
- Interpolate. Turn on interpolation for smooth visuals during the dash. It doesn’t affect collision, but it removes stutter.
- Thicker colliders. Walls thinner than the per-step movement are risky even with continuous detection on some setups.
- Clamp with a cast. Before dashing, cast the player’s shape in the dash direction (
Rigidbody2D.CastorPhysics.CapsuleCast) and shorten the dash to stop at the first hit.
The Rigidbody jitter guide covers interpolation and why the camera might shake during fast moves.
Invincibility Frames
For a dodge, make the player ignore damage while dashing. The simplest version checks the flag in your health script:
public void TakeDamage(int amount)
{
if (dash != null && dash.IsDashing) return;
current -= amount;
}
If you want enemies to physically pass through the player during a dash, switch the player to a “Dashing” layer for the duration and turn off collisions between that layer and Enemy in Project Settings > Physics (or Physics 2D) > Layer Collision Matrix. Switch back at the end. If an enemy is overlapping when the dash ends, push the player out or extend the dash slightly until the overlap clears.
Many games make the invincible window a little shorter than the dash itself, so timing still matters.
Trail, Sound and Feedback
- TrailRenderer. Add one to the player, set Time to around 0.15, set
emittingto false by default, and switch it on for the dash as the script does. - Afterimages. Spawn a few faded copies of the sprite along the path. Pool them so you’re not instantiating every dash.
- Sound and shake. A short whoosh and a tiny camera impulse sell the speed. See the screen shake guide.
- Cooldown UI. Expose
CooldownRemainingand feed it to an icon, so players can see when the dash is ready.
Tuning Values
Starting points that feel good in most games:
- 2D platformer air dash: 3 to 4 units, 0.12 to 0.18 s, reset on landing.
- Top-down dodge roll: 4 to 6 units, 0.2 to 0.3 s, 0.6 to 1 s cooldown, i-frames for most of the roll.
- 3D action dash: 5 to 8 units, 0.15 to 0.2 s, 0.5 to 1 s cooldown.
If you want “one dash per jump” like Celeste, track a canAirDash flag that’s cleared when you dash in the air and refilled when grounded, alongside or instead of the timer cooldown.
🔧 Skip the boilerplate
Character controller packs on the Asset Store ship dashes, i-frames and feedback out of the box. Here are the Asset Store tools we’d actually buy.
Troubleshooting
Dash distance changes every time
You’re applying a one-off force that’s affected by drag, friction or existing velocity. Set velocity every fixed step for the dash duration instead.
Player dashes through walls
Set Collision Detection to Continuous on the Rigidbody, and check the wall colliders aren’t paper thin. Or clamp the dash with a shape cast first.
Dash stops immediately
Another script, usually the movement script, is writing velocity every frame and overwriting the dash. Skip normal movement while IsDashing is true.
Dash works once, then never again
The coroutine was stopped (for example by disabling the object) before IsDashing was reset. Reset the flag and gravity in OnDisable.
Player falls during the dash
Gravity is still on. Set gravity scale to 0 (2D) or useGravity false (3D) for the dash, or skip your gravity code with a CharacterController.
FAQ
Should a dash use AddForce?
You can, but setting velocity for a fixed time gives a consistent distance. AddForce impulses vary with drag and current speed.
Can I dash diagonally?
Yes. Use the normalized movement input as the direction. If you want 8-way only, snap the input angle to 45 degree steps first.
How do I let the dash go through enemies but not walls?
Move the player to a temporary layer that ignores the Enemy layer in the collision matrix, but still collides with Ground and Walls.
Does this work with the old Input Manager?
Replace the action checks with Input.GetButtonDown, but the Input System is the default for new Unity 6 projects.
Why use WaitForFixedUpdate in the coroutine?
It writes velocity in step with physics, so the Rigidbody moves exactly as intended every step, regardless of frame rate.
Reader questions
Asked by readers of this article and answered by AI. Behavior can differ between Unity versions, so test in yours.




Leave a Reply