docs.unity3d.com
Search Results for

    Show / Hide Table of Contents

    Creating a Dash Ability

    This tutorial guides you through creating a Dash ability for your character. The Dash ability gives players a quick burst of speed, allowing them to dodge obstacles or cross gaps. We will split this into two parts: the core Dash Ability logic and the Dash Addon that handles input and resources (stamina).

    Part 1: The Dash Ability Script

    First, we need a script that defines how the dash works physically. This script implements IMovementAbility so it can hook into the character's movement system.

    We will build this script in 3 Steps:

    1. The Setup: Implementing the empty interface.
    2. The Core Logic: Adding basic movement without extra features.
    3. The Full Implementation: Adding cooldowns, effects, and polish.

    Step 1: The Setup (The Interface)

    Create a new script named DashAbility.cs. First, we just want to ensure it connects to the system correctly.

    The IMovementAbility interface requires:

    1. Priority: Determines which ability wins if multiple try to run (e.g., getting hit vs. dashing).
    2. StaminaCost: How much energy it uses.
    3. Initialize: Where we get references to the character motor.
    4. Process: Where the physics logic happens.
    5. TryActivate: The function we call to start the ability.

    Copy this skeleton code:

    using UnityEngine;
    using Blocks.Gameplay.Core;
    
    namespace Blocks.Gameplay.Platformer
    {
        public class DashAbility : MonoBehaviour, IMovementAbility
        {
            // High priority (20) means this overrides standard walking/running.
            public int Priority => 20;
    
            // We'll calculate this later, but the system needs to know it.
            public float StaminaCost => 10f; 
    
            public void Initialize(CoreMovement motor)
            {
                // Called when the ability is added to the character
            }
    
            public MovementModifier Process()
            {
                // Called every frame to apply movement forces
                // Return an empty modifier for now (no effect)
                return new MovementModifier();
            }
            
            // Custom method to trigger the ability
            public bool TryActivate()
            {
                return false;
            }
        }
    }
    

    Step 2: The Core Logic (Basic Movement)

    Now let's make it actually move the character. We will add a timer and apply velocity.

    • Variables: dashForce for speed, dashDuration for time.
    • TryActivate: Sets m_IsDashing to true and starts the timer.
    • Process: Checks if m_IsDashing is true. If so, it overrides gravity and moves the character forward.

    Update your script to this version:

    using UnityEngine;
    using Blocks.Gameplay.Core;
    
    public class DashAbility : MonoBehaviour, IMovementAbility
    {
        [Header("Dash Settings")]
        [SerializeField] private float dashForce = 50f;
        [SerializeField] private float dashDuration = 0.2f;
    
        public int Priority => 20;
        public float StaminaCost => 10f;
    
        private bool m_IsDashing;
        private float m_DashTimer;
        private Transform m_Transform;
        private Vector3 m_DashDirection;
    
        public void Initialize(CoreMovement motor)
        {
            m_Transform = motor.RotationTransform;
        }
    
        public MovementModifier Process()
        {
            var modifier = new MovementModifier();
    
            // If dashing, override movement
            if (m_IsDashing)
            {
                m_DashTimer -= Time.deltaTime;
    
                if (m_DashTimer <= 0)
                {
                    m_IsDashing = false;
                }
                else
                {
                    // Apply velocity in the dash direction
                    modifier.ArealVelocity = m_DashDirection * dashForce;
    
                    // Disable gravity during dash for consistent distance
                    modifier.OverrideGravity = true;
                }
            }
    
            return modifier;
        }
    
        public bool TryActivate()
        {
            // Simple activation: Dash forward relative to the character's facing direction
            m_IsDashing = true;
            m_DashTimer = dashDuration;
            m_DashDirection = m_Transform.forward;
            return true;
        }
    }
    
    

    Step 3: The Full Implementation

    Finally, we adding the polish:

    • Cooldowns: Prevent spamming.
    • Air Dashes: Check RequireGrounded or allow limited air dashes.
    • Direction: Calculate direction based on Input (WASD) instead of just facing forward.
    • Effects: Play particles and sound.

    Replace the code with this final version.

    Full Code: DashAbility.cs

    using UnityEngine;
    using Blocks.Gameplay.Core;
    
    public class DashAbility : MonoBehaviour, IMovementAbility
    {
        [Header("Dash Settings")]
        [SerializeField] private float dashForce = 50f;
        [SerializeField] private float dashDuration = 0.2f;
        [SerializeField] private float dashCooldown = 1.0f;
        [SerializeField] private float staminaCost = 15f;
        [SerializeField] private bool requireGrounded = false;
        [SerializeField] private bool allowAirDash = true;
        [SerializeField] private int maxAirDashes = 1;
    
        [Header("Effects")]
        [SerializeField] private GameObject dashStartEffect;
        [SerializeField] private SoundDef dashStartSound;
    
        // Higher priority overrides standard movement
        public int Priority => 20;
        public float StaminaCost => staminaCost;
    
        // Internal State
        private bool m_IsDashing;
        private float m_DashTimer;
        private CoreMovement m_Motor;
        private float m_CooldownTimer;
        private Vector3 m_DashDirection;
        private int m_RemainingAirDashes;
    
        public void Initialize(CoreMovement motor)
        {
            m_Motor = motor;
            m_Motor.OnGroundedStateChanged += OnGroundedStateChanged;
            m_RemainingAirDashes = maxAirDashes;
        }
    
        // The physics logic applied every frame
        public MovementModifier Process()
        {
            var modifier = new MovementModifier();
    
            // Handle Cooldown
            if (m_CooldownTimer > 0)
            {
                m_CooldownTimer -= Time.deltaTime;
            }
    
            // Handle Active Dash
            if (m_IsDashing)
            {
                m_DashTimer -= Time.deltaTime;
    
                if (m_DashTimer <= 0)
                {
                    EndDash();
                }
                else
                {
                    // Apply Dash Velocity
                    modifier.ArealVelocity = m_DashDirection * dashForce;
                    // Disable gravity during dash
                    modifier.OverrideGravity = true;
                }
            }
    
            return modifier;
        }
    
        public bool TryActivate()
        {
            // Validation Checks
            if (m_CooldownTimer > 0 || m_IsDashing) return false;
            if (requireGrounded && !m_Motor.IsGrounded) return false;
            if (!m_Motor.IsGrounded && !allowAirDash) return false;
            if (!m_Motor.IsGrounded && m_RemainingAirDashes <= 0) return false;
    
            // Calculate Direction
            Vector3 dashDir = CalculateDashDirection();
    
            // Fallback if no input: dash forward
            if (dashDir.magnitude < 0.1f)
            {
                dashDir = m_Motor.RotationTransform != null
                    ? m_Motor.RotationTransform.forward
                    : m_Motor.transform.forward;
            }
    
            // Start Dash
            StartDash(dashDir.normalized);
            return true;
        }
    
        private Vector3 CalculateDashDirection()
        {
            // If there is movement input, dash in that direction relative to camera/character
            if (m_Motor.MoveInput.magnitude > 0.1f)
            {
                Vector3 inputDirection = new Vector3(m_Motor.MoveInput.x, 0.0f, m_Motor.MoveInput.y);
                switch (m_Motor.directionMode)
                {
                    case CoreMovement.MovementDirectionMode.CharacterRelative:
                        return m_Motor.transform.rotation * inputDirection;
                    case CoreMovement.MovementDirectionMode.CameraRelative:
                        return Quaternion.Euler(0.0f, m_Motor.TargetRotationY, 0.0f) * inputDirection;
                    default:
                        return inputDirection;
                }
            }
    
            // Otherwise default to forward
            Transform rotationTransform = m_Motor.RotationTransform != null
                ? m_Motor.RotationTransform
                : m_Motor.transform;
    
            return rotationTransform.forward;
        }
    
        private void StartDash(Vector3 direction)
        {
            m_IsDashing = true;
            m_DashTimer = dashDuration;
            m_CooldownTimer = dashCooldown;
            m_DashDirection = new Vector3(direction.x, 0f, direction.z).normalized;
    
            if (!m_Motor.IsGrounded)
            {
                m_RemainingAirDashes--;
            }
    
            // Reset vertical velocity for a snappy dash feel
            m_Motor.SetVerticalVelocity(0f);
    
            // Play Effects
            if (dashStartEffect != null)
            {
                CoreDirector.CreatePrefabEffect(dashStartEffect)
                    .WithPosition(m_Motor.transform.position)
                    .WithRotation(Quaternion.LookRotation(m_DashDirection))
                    .WithName("DashStart")
                    .WithDuration(dashDuration + 0.5f)
                    .Create();
            }
    
            if (dashStartSound != null)
            {
                CoreDirector.RequestAudio(dashStartSound)
                    .AttachedTo(m_Motor.transform)
                    .Play();
            }
        }
    
        private void EndDash()
        {
            m_IsDashing = false;
            m_DashTimer = 0f;
        }
    
        private void OnGroundedStateChanged(bool isGrounded)
        {
            if (isGrounded)
            {
                // Reset air dashes
                m_RemainingAirDashes = maxAirDashes;
            }
        }
    
        private void OnDestroy()
        {
            if (m_Motor != null)
            {
                m_Motor.OnGroundedStateChanged -= OnGroundedStateChanged;
            }
        }
    }
    
    

    Part 2: The Dash Addon Script

    Now we need a "Manager" script to handle the player input and resources. This separates the logic of movement from the control of the player.

    Create a new script named DashAddon.cs.

    Key Components

    • GameEvent Integration: We listen for a GameEvent. For this example, we'll use an existing input GameEventOnPrimaryActionPressed since we haven't set up a custom "Dash" input yet. We will cover custom inputs in Part 2.
    • Stamina Check: Before calling dashAbility.TryActivate(), we check if the player has enough stamina using StatKeys.Stamina.
    • Sound Feedback: If stamina is too low, we play a warning sound.

    Full Code: DashAddon.cs

    using UnityEngine;
    using Unity.Netcode;
    using Blocks.Gameplay.Core;
    
    public class DashAddon : NetworkBehaviour, IPlayerAddon
    {
        [Header("References")]
        [SerializeField] private DashAbility dashAbility;
        [SerializeField] private GameEvent onPrimaryActionPressed;
    
        [Header("Feedback")]
        [SerializeField] private SoundDef insufficientStaminaSound;
        [SerializeField] private float warningCooldown = 2.0f;
    
        private CorePlayerManager m_PlayerManager;
        private CoreStatsHandler m_StatsHandler;
        private float m_LastWarningTime;
    
        public void Initialize(CorePlayerManager playerManager)
        {
            m_PlayerManager = playerManager;
            m_StatsHandler = playerManager.CoreStats;
            if (dashAbility == null)
            {
                dashAbility = GetComponent<DashAbility>();
            }
        }
    
        // Enable input listening when player spawns
        public void OnPlayerSpawn()
        {
            if (!m_PlayerManager.IsOwner) return;
            if (onPrimaryActionPressed != null)
            {
                onPrimaryActionPressed.RegisterListener(HandleDashInput);
            }
        }
    
        // Cleanup when player despawns
        public void OnPlayerDespawn()
        {
            if (!m_PlayerManager.IsOwner) return;
            if (onPrimaryActionPressed != null)
            {
                onPrimaryActionPressed.UnregisterListener(HandleDashInput);
            }
        }
    
        public void OnLifeStateChanged(PlayerLifeState previousState, PlayerLifeState newState) { }
    
        private void HandleDashInput()
        {
            if (dashAbility == null)
            {
                Debug.LogWarning("[DashAddon] DashAbility component not found.", this);
                return;
            }
    
            float staminaCost = dashAbility.StaminaCost;
            // Get current stamina from stats system
            float currentStamina = m_StatsHandler.GetCurrentValue(StatKeys.Stamina);
    
            // Check Stamina
            if (currentStamina < staminaCost)
            {
                HandleInsufficientStamina();
                return;
            }
    
            // Attempt Dash
            if (dashAbility.TryActivate())
            {
                // Consume Stamina
                m_StatsHandler.ModifyStat(
                    StatKeys.Stamina,
                    -staminaCost,
                    m_PlayerManager.OwnerClientId,
                    ModificationSource.Consumption
                );
            }
        }
    
        private void HandleInsufficientStamina()
        {
            // Prevent spamming the warning sound
            if (Time.time - m_LastWarningTime < warningCooldown)
            {
                return;
            }
    
            m_LastWarningTime = Time.time;
            if (insufficientStaminaSound != null)
            {
                CoreDirector.RequestAudio(insufficientStaminaSound)
                    .AttachedTo(transform)
                    .Play(0.5f);
            }
        }
    }
    
    

    Setup Instructions

    1. Add Components: Add both DashAbility and DashAddon scripts to your Player Prefab (or a child object where other abilities reside).
    2. Assign References:
      • In DashAddon, assign the Dash Ability field to the component you just added.
      • Assign the On Primary Action Pressed GameEvent for now. In Part 2, we will create a dedicated Dash input.
    3. Configure: Adjust Dash Force and Stamina Cost in the inspector to tune the gameplay feel.
    4. Here's how it should look
    In This Article
    Back to top
    Copyright © 2026 Unity Technologies — Trademarks and terms of use
    • Legal
    • Privacy Policy
    • Cookie Policy
    • Do Not Sell or Share My Personal Information
    • Your Privacy Choices (Cookie Settings)