Extending Attack Movements - Air Dash in Any Direction

 The following tutorial expects users to have a basic understanding of Platformer PRO including movements, hit boxes and attacks. 

 

Problem Statement

Create an attack that allows you to dash attack in any direction (based on mouse direction).

 Video: https://www.youtube.com/watch?v=o659NvTxurQ

 

Stage 1 - Analysis

There's already a DashAttack we could consider extending it but our dash in any direction is actually quite different. So instead lets use so lets use the existing Dash as a template by copying it and renaming it. We can then changing the parts we need to.

To get the aim direction we can use the existing ProjectileAimer.

Because we want the dash to happen in any direction we also are going to need to rotate the character sprite. We could rotate the whole character, bur rotating the sprite is much simpler as we don't have to worry too much about the affect of the rotation on other movements.

 

Stage 2 - Dash in the Air

First lets duplicate and rename DashAttack and DashAttackInspector scripts:

 

1. Select DashAttack script and duplicate it by pressing Alt-D/Command-D

2. Rename the script to AnyDirectionAirDashAttack

3. Open the script and change the class name to AnyDirectionAirDashAttack

 

public class AnyDirectionAirDashAttack : BasicAttacks 

 

4. Select DashAttackInspector script and duplicate it by pressing Alt-D/Command-D

5. Rename the script to AnyDirectionAirDashAttackInspector

6. Open the script and change the class name to AnyDirectionAirDashAttackInspector

7. Change all references in the new inspector to that reference DashAttack to reference AnyDirectionAirDashAttack

    /// <summary>
    /// Editor for air dash attacks.
    /// </summary>
    [CustomEditor (typeof(AnyDirectionAirDashAttack), false)]
    public class AnyDirectionAirDashAttackInspector : BasicAttacksInspector
    {
        /// <summary>
        /// Draw the inspector GUI.
        /// </summary>
        public override void OnInspectorGUI()
        {
            // Always maintain control with a dash
            bool maintainControl = true;
            if (maintainControl != ((BasicAttacks)target).attackSystemWantsMovementControl)
            {
                ((BasicAttacks)target).attackSystemWantsMovementControl = maintainControl;
                EditorUtility.SetDirty(target);
            }

            // Draw one attack
            if (((BasicAttacks)target).attacks == null)
            { 
                ((BasicAttacks)target).attacks = new List<BasicAttackData> ();
                ((BasicAttacks)target).attacks.Add(new BasicAttackData());
                ((BasicAttacks)target).attacks[0].name = "Air Dash";
                EditorUtility.SetDirty(target);
            }

            DrawBasicAttackEditor(((BasicAttacks)target).attacks[0]);

            float speed = EditorGUILayout.FloatField(new GUIContent("Dash Speed""How fast the dash attack is"), ((AnyDirectionAirDashAttack)target).dashSpeed);
            if (speed != ((AnyDirectionAirDashAttack)target).dashSpeed && speed > 0.0f)
            {
                ((AnyDirectionAirDashAttack)target).dashSpeed = speed;
                EditorUtility.SetDirty(target);
            }
        }
    }


Note: you may also want to update the comments and change the attack name from "Dash to "Air Dash".

 

Basic Setup 

You also need to create a CharacterHitBox (withCollider2D and non-kinematic Rigidbody2D):

Note: You would usually add your hitbox on a child GameObject of the character sprite. This is particularly important for this sample as we are later going to rotate the sprite in the attack direction.

 

With this done you can now add the movement to your character. Lets set some basic settings such as a location of AIRBORNE and an Attack length of 1, and assign the HitBox:

Note: We set the attack to Reset X and Y Velocity as thats usually how these kind of air dash attacks work.

 

Hit Play...

You should be able to jump in the air and press the action button in order to trigger an air dash attack in the facing direction.

 

Stage 3 - Adding Direction

 

Lets update the code so that the attack moves in the direction of the mouse cursor.

1. Open the code for AnyDirectionAirDashAttack and find the dashDirection variable. Change it to a Vector2 and rename it dashVelocity:

        /// <summary>
        /// Vector 2 representing direction and speed of travel.
        /// </summary>
        protected Vector2 dashVelocity;

 

2. Find the Attack() method and update it to set the dashVelocity based on the direction of the attached ProjectileAimer (noting that the base class BasicAttacks already has a reference to the attached ProjectileAimer):

        /// <summary>
        /// Do whichever attack is available.
        /// </summary>
        /// <returns>true if this movement wants to main control of movementfalse otherwise</returns>
        override public bool Attack()
        {
            // Early out for grounded
            bool result = base.Attack ();
            if (result
            {
                dashVelocity = projectileAimer.GetAimDirection(character).normalized * dashSpeed;
                return true;
            }
            return false;
        }

 

3. Find the DoMove() method and update it to move in both X and Y:

        /// <summary>
        /// Moves the character.
        /// </summary>
        override public void DoMove()
        {
            character.SetVelocityX(dashVelocity.x);
            character.SetVelocityY(dashVelocity.y);
            character.Translate(dashVelocity.x * TimeManager.FrameTimedashVelocity.y * TimeManager.FrameTimefalse);
        }

Note: Its always a good idea to accurately set character Velocity in your custom movements. This is used by collission!

 

Add a Projectile Aimer

Add a ProjectileAimer component to the same GameObject you added your attack to. Set the projectile aiming type to MOUSE:

 

Hit Play...

You should now be able to attack in the direction of the mouse (you might want to change your action button to be a MouseButton).

 

Stage 4 - Cancelling the Attack when we Collide

When we hit a wall, the floor or the ceiling its probably a good idea to cancel our DashAttack. We will do this by checking for collisions and calling InterruptAttack() when we find them.

 

1. Update DoMove() to check for collisions:

        /// <summary>
        /// Moves the character.
        /// </summary>
        override public void DoMove()
        {
            if (CheckForCancel ())
            {
                InterruptAttack();
            }
            else
            {
                character.SetVelocityX(dashVelocity.x);
                character.SetVelocityY(dashVelocity.y);
                character.Translate(dashVelocity.x * TimeManager.FrameTimedashVelocity.y * TimeManager.FrameTimefalse);
            }
        }

 

2. And add a collision check function like so:

        /// <summary>
        /// Checks if we hit something
        /// </summary>
        /// <returns><c>true</c> if we hit something and should cancel the attack<c>false</c> otherwise.</returns>
        virtual protected bool CheckForCancel() 
        {
            // Check for grounded only when moving downwards (grounded often has a large look ahead if we don't do this it might cancel too early)

            if (dashVelocity.y <= 0 && character.Groundedreturn true;
            if (CheckSideCollisions(character1, (dashVelocity.x > 0 ? RaycastType.SIDE_RIGHT : RaycastType.SIDE_LEFT))) return true;
            if (CheckSideCollisions(character1RaycastType.HEAD)) return true;
            return false;
        }

 

A Quick Clean-up

If we interrupt an attack during DoMove the BasicAttacks set the animation state to NONE. Lets clean that up by always setting the attack animation state.:

        /// <summary>
        /// Gets the animation state that this movement wants to set.
        /// </summary>
        /// <value>The state of the animation.</value>
        override public AnimationState AnimationState
        {
            get 
            {
                return attacks[0].animation;
            }
        }

 

Stage 5 - Rotating the Character Sprite

To add a little flair to our attack lets rotate the character in the direction they are attacking.

We will do this with a new component we call RotateToAttackDirection. This script will be placed on a GameObject which we will call RotationPoint. The RotationPoint GameObject will be a child of the Character and the sprite will be a child of the new GameObject.

1. Set up the new structure:

 

 

The new script will  simple rotate to the angle that corresponds to the characters velocity. It will also only rotate when a specific attack is active (it does this using the AttackName property of the character.

We don't describe this script in detail, as other than the previous two points its not particularly tied to Platformer PRO. There are inline comments.

 

2. Create a new script called RotateToAttackDirection:

using UnityEngine;
using System.Collections;

namespace PlatformerPro
{
    /// <summary>
    /// A little class to rotate towars the direction of a jump.
    /// </summary>
    public class RotateToAttackDirection : MonoBehaviour 
    {
        /// <summary>
        /// name of attack to rotate for.
        /// </summary>
        public string attackName = "Air Dash";

        /// <summary>
        /// Speed we rotate at when attacking.
        /// </summary>
        public float rotateToSpeed = 720.0f;
        
        /// <summary>
        /// Speed we rotate back to the standard movement.
        /// </summary>
        public float resetRotationSpeed = 360.0f;

        /// <summary>
        /// Point we rotate around.
        /// </summary>
        public Vector3 rotationPoint;
        
        /// <summary>
        /// Cached reference to character.
        /// </summary>
        protected IMob character;
        
        /// <summary>
        /// Desired rotation.
        /// </summary>
        protected float targetRotation;
        
        void Start()
        {
            character = GetComponentInParent<Character> ();
            if (character == nullcharacter = GetComponentInParent<Enemy> ();
            if (character == nullDebug.LogWarning ("Unable to find a character or enemy for RotateToAttackDirection");
        }
        
        void Update()
        {
            SetTargetRotation ();
            RotateTowardsTarget ();
        }
        
        protected void SetTargetRotation()
        {
            targetRotation = 0.0f;
            if (character is Character && ((Character)character).AttackName == attackName)
            {
                // We are using x,y not y,x as we are going to subtract the difference from the characters rotation
                targetRotation = Mathf.Atan2(character.Velocity.xcharacter.Velocity.y) * Mathf.Rad2Deg;
            }
        }
        
        /// <summary>
        /// Rotates towrds target rotation.
        /// </summary>
        protected void RotateTowardsTarget()
        {
            // Grounded - instantly set rotation
            if (character.Grounded || (character is Character && ((Character)character).CurrentWall != null))
            {
                transform.localRotation = Quaternion.identity;
            }
            // Otherwise rotate to desired target
            else 
            {
                Vector3 rotateAround = transform.position + rotationPoint;
                float difference  = -targetRotation - transform.localEulerAngles.z;

                // Limit difference to -180 / 180
                if (difference > 180difference = difference - 360;
                if (difference < -180difference = difference + 360;

                // Make sure we don't rotate too fast
                if (targetRotation == 0 && difference > resetRotationSpeed * TimeManager.FrameTimedifference = resetRotationSpeed * TimeManager.FrameTime;
                if (targetRotation == 0 && difference < -resetRotationSpeed * TimeManager.FrameTimedifference = -resetRotationSpeed * TimeManager.FrameTime;
                if (targetRotation != 0 && difference > rotateToSpeed * TimeManager.FrameTimedifference = rotateToSpeed * TimeManager.FrameTime;
                if (targetRotation != 0 && difference < -rotateToSpeed * TimeManager.FrameTimedifference = -rotateToSpeed * TimeManager.FrameTime;

                // Do rotation
                transform.RotateAround(rotateAroundnew Vector3(0,01), difference);
            }
        }
    }
}

 

Note: Its not absolutely neccessary to add the new GameObject for the rotation point, but it does make it easier to control the rotation and separate it from the animation system.

 

Hit play... 

We are done!

Have more questions? Submit a request

1 Comments

  • 0
    Avatar
    Roman Fuhrer

    Does any of the available examples use this?

Please sign in to leave a comment.
Powered by Zendesk