Overview | Videos | Documentation | Demos | Integrations | API | Support

Doc Home > How To > Create a new Ability

The Third Person Controller is structured in a way that you can easily add new animation states without changing the core Third Person Controller classes. The Third Person Controller uses the term "Ability" to describe these new animation states. Abilities are extremely powerful in that they can give your character completely new and original functionality. There are no limitations in what you can do with abilities - cover, swimming, wall running, and flying is all possible with the ability system. This document will walk you through creating your own ability.

The ability that we are going to create is a Balance ability. The balance ability allows the character to walk carefully over a narrow platform, such as a tightrope. This ability will give a great overview into how the ability system works. It will add a new blend tree which is transitioned to using the implicit transition system. It will also give an example of using the normal Animator Controller transition system (we use the term "explicit transition" to describe this type of transition). The final feature that this ability will demonstrate is overriding a couple of the optional Ability methods.

The first step to adding a new ability is to create a new class derived from the Ability class. This class contains all of the base functionality for all of the abilities, as well as contains some common helper variables and methods. Our new Balance ability will consist of overriding the Ability methods. The Third Person Controller transitions to new abilities using an implicit transition so the first method that we will override is the GetDestinationState method. This method is called when the ability is activated and it specifies which state should play. For this ability we are going to return the "Balance.Movement" state:

using UnityEngine;
using Opsive.ThirdPersonController.Abilities;

public class Balance : Ability
{
    public override string GetDestinationState(int layer)
    {
        return "Balance.Movement";
    }
}

This is the minimum needed for a new ability and with this code you'll be able to select the ability within the RigidbodyCharacterController:

We specified the "Balance.Movement" ability rather than just "Movement" because we want to transition to the "Movement" state within the "Balance" substate. Within your Animator Controller the state will look like:

No explicit transitions are necessary - the AnimatorMonitor will take care of transitioning to the "Balance.Movement" state. With the current GetDestinationState implementation the "Balance.Movement" state will play on each layer within the Animator Controller. For this ability we only want it to play on the lower and upper body layers. The base Ability class caches the AnimatorMonitor component so we can easily prevent the ability from playing on unwanted layers:

    public override string GetDestinationState(int layer)
    {
        if (layer != m_AnimatorMonitor.BaseLayerIndex && layer != m_AnimatorMonitor.UpperLayerIndex && 
            !m_AnimatorMonitor.ItemUsesAbilityLayer(this, layer)) {
            return string.Empty;
        }

        return "Balance.Movement";
    }

With this ability in place we can now test it out to see how it functions. If we hit play within Unity the ability will automatically start because it has an Start Type of Automatic. We'll see that the "Balance.Movement" state plays on the lower and upper body layers, and the remaining layers play their default state.

This works well for what it is, but we now want to add more functionality to it so the ability will only start when the character is over a narrow object that they can balance on. If we look at the list of virtual Ability methods we'll find CanStartAbility. This method is called when the ability is tried to be started, and it will return a true or false value indicating if the ability can be started.

We only want this balance ability to start if the following conditions are true:

  1. The character is on the ground.
  2. There is a drop to the left of the character.
  3. There is a drop to the right of the character.

With those three conditions defined we can implement the CanStartAbility method:

    public override bool CanStartAbility()
    {
        var front = m_Controller.CapsuleCollider.radius * Mathf.Sign(m_Transform.InverseTransformDirection(m_Controller.Velocity).z);

        if (!Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                m_Controller.CapsuleCollider.height / 2 + 0.1f, front), -m_Transform.up, m_MinDropHeight)) {
            return false;
        }

        if (Physics.Raycast(m_Transform.TransformPoint(-m_Controller.CapsuleCollider.radius - m_SidePadding, 
                m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                -m_Transform.up, m_MinDropHeight)) {
            if (Physics.Raycast(m_Transform.TransformPoint(-(m_Controller.CapsuleCollider.radius * 2) - m_SidePadding,
                     m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                    -m_Transform.up, m_MinDropHeight)) {
                return false;
            }
        }

        if (Physics.Raycast(m_Transform.TransformPoint(m_Controller.CapsuleCollider.radius + m_SidePadding, 
                m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                -m_Transform.up, m_MinDropHeight)) {
            if (Physics.Raycast(m_Transform.TransformPoint(m_Controller.CapsuleCollider.radius * 2 + m_SidePadding, 
                    m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                    -m_Transform.up, m_MinDropHeight)) {
                return false;
            }
        }

        if (Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                    m_Controller.CapsuleCollider.height / 2 + 0.1f, front), -m_Transform.right, m_MinDropHeight)) {
                return false;
        }
        if (Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                    m_Controller.CapsuleCollider.height / 2 + 0.1f, front), m_Transform.right, m_MinDropHeight)) {
            return false;
        }

        return true;
    }

The first part of this method ensures the character is on the ground. The second part of this method casts two raycasts to the left of the character. If both of those raycasts hit an object then the character is not on a narrow object and should not balance. Two raycasts are used because the character could be on the edge of a non-narrow object and just one raycast would return success when the object is actually narrow so it should return failure. The third part is the same as the second part except is casts a ray to the right of the character rather than the left. The last part of this method ensures there is not a wall to the left or right of the character. This method uses a couple new variables that we first must define and initialize:

public class Balance : Ability
{
    [SerializeField] private float m_MinDropHeight = 1;
    [SerializeField] private float m_SidePadding = 0.1f;

A fully commented version of this ability is added at the bottom of this page. This ability and animations are also included in the Third Person Controller package. When we test this ability out we will see that the ability only starts when the character is over a narrow object:

Now that the ability starts correctly, it's time to get the ability to stop. By default the Stop Type is set to Manual. This means that the ability must be stopped within code - whether that be through the ability itself or through an event. For the Balance ability we want to set the Stop type to Automatic - this will tell the RigidbodyCharacterController that the ability should be asked if it can be stopped. The RigidbodyCharacterController asks by calling the CanStopAbility method. This method is very similar to the CanStartAbility method, except instead of asking to start the ability it instead asks to stop the ability.

The Balance ability should be stopped when the character is not on a narrow object or not on the ground - which is the opposite of the CanStartAbility. For this we can move the CanStartAbility implementation to a new method, and then call that method from both CanStartAbility and CanStopAbility. This implementation will look like:

    public override bool CanStartAbility()
    {
        return IsOnBalanceObject();
    }

    private bool IsOnBalanceObject()
    {
        var front = m_Controller.CapsuleCollider.radius * Mathf.Sign(m_Transform.InverseTransformDirection(m_Controller.Velocity).z);

        if (!Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                m_Controller.CapsuleCollider.height / 2 + 0.1f, front), -m_Transform.up, m_MinDropHeight)) {
            return false;
        }

        if (Physics.Raycast(m_Transform.TransformPoint(-m_Controller.CapsuleCollider.radius - m_SidePadding, 
                m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                -m_Transform.up, m_MinDropHeight)) {
            if (Physics.Raycast(m_Transform.TransformPoint(-(m_Controller.CapsuleCollider.radius * 2) - m_SidePadding, 
                    m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                    -m_Transform.up, m_MinDropHeight)) {
                return false;
            }
        }

        if (Physics.Raycast(m_Transform.TransformPoint(m_Controller.CapsuleCollider.radius + m_SidePadding, 
                m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                -m_Transform.up, m_MinDropHeight)) {
            if (Physics.Raycast(m_Transform.TransformPoint(m_Controller.CapsuleCollider.radius * 2 + m_SidePadding, 
                    m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                    -m_Transform.up, m_MinDropHeight)) {
                return false;
            }
        }

        if (Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                    m_Controller.CapsuleCollider.height / 2 + 0.1f, front), -m_Transform.right, m_MinDropHeight)) {
                return false;
        }
        if (Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                    m_Controller.CapsuleCollider.height / 2 + 0.1f, front), m_Transform.right, m_MinDropHeight)) {
            return false;
        }

        return true;
    }

    public override bool CanStopAbility()
    {
        return !IsOnBalanceObject();
    }

If we play the game we'll see that the ability correctly starts, plays, and stops itself. This is great and it could be considered a complete ability. However, we want to more to it to show the ability system works. When the character tries to move to the left or the right we want to play a stumble animation. For this animation we are going to add an explicit transition to the stumble left and right states. This transition will then be triggered within the ability.

The Third Person Controller uses a minimal parameter list to keep everything as clean and straight forward as possible. For this ability we are going to use one of those parameters to describe which state the Balance ability should be in. We can do this by overriding the UpdateAnimator method:

    public override bool UpdateAnimator()
    {
        if (m_Controller.InputVector.x > m_StumbleMagnitude) {
            m_BalanceID = BalanceID.StumbleRight;
        } else if (m_Controller.InputVector.x < -m_StumbleMagnitude) {
            m_BalanceID = BalanceID.StumbleLeft;
        } else {
            m_BalanceID = BalanceID.Movement;
        }
        m_AnimatorMonitor.SetStateValue((int)m_BalanceID);

        return true;
    }

There are two new concepts introduced within this method: the return status and the Animator parameters. As you go through the virtual Ability methods you'll noticed that many of the key movement methods return a bool value. This value indicates if the RigidbodyCharacterController should continue its execution of that method. The RigidbodyCharacterController works by going through many methods which update the state of the controller: velocity, rotation, Animator parameters, etc. When an ability returns a true value it tells the RigidbodyCharacterController that it should update its own method corresponding to that ability method. For example, if we return false within the UpdateAnimator method then the RigidbodyCharacterController will not run its UpdateAnimator implementation. In the case of the Balance ability we return true so the RigidbodyCharacterController will execute its UpdateAnimator implementation.

The second concept introduced within this method is setting the Animator paremter. We used a couple new variables: the Balance ID and Stumble Magnitude. The Balance ID is an enum which describes which state the Balance ability should be in. This enum is cast to an int and is then set to the State parameter within the Animator. When we transition between the Movement, Stumble Left, and Stumble Right states we will use this State parameter to determine the transition condition. The Stumble Magnitude is a normalized value indicating how much horizontal input needs to be applied in order for the character to stumble. These variables are defined at the top of the Balance class:

    private enum BalanceID { Movement, StumbleLeft, StumbleRight }

    [SerializeField] private float m_StumbleMagnitude = 0.5f;

    private BalanceID m_BalanceID;

When the ability starts we want to make sure we reset the Balance ID to its default value:

    protected override void AbilityStarted()
    {
        m_BalanceID = BalanceID.Movement;

        base.AbilityStarted();
    }

With this change the character will stumble when the horizontal input is greater than the Stumble Magnitude. If you look at the Animator Controller you'll see it moving through the explicit transitions to the stumble states. The last thing that we want to add to this ability is to prevent the character from carrying an item while the ability is active. This can be accomplished by overriding the CanHaveItemEquipped method:

    public override bool CanHaveItemEquipped()
    {
        return false;
    }

That's all that it takes to add a reasonably complex ability! We tried to make this ability system as easy as possible to use while also making sure your ability can take complete control of the character controller if it wants to. With this ability complete the next step is to go through the Ability class and see what other methods can be overridden. Take a look at the other abilities as well - they provide an excellent example in how to use each method. Every ability is extremely well commented so it should help walk you through how the ability works. This entire ability (with comments) has been copied below:

using UnityEngine;
using Opsive.ThirdPersonController.Abilities;

/// 
/// When on a narrow object the Balance ability will slow the character's movements down and 
/// stretch the character's hands out to balance on the object.
/// 
public class Balance : Ability
{
    // The current Animator state that balance should be in.
    private enum BalanceID { Movement, StumbleLeft, StumbleRight }

    [Tooltip("Any drop more than this value can be a balance object")]
    [SerializeField] private float m_MinDropHeight = 1;
    [Tooltip("Extra padding applied to the left and right side of the character when determining if over a balance object")]
    [SerializeField] private float m_SidePadding = 0.1f;
    [Tooltip("Start stumbing if the horizontal input value is greater than this value")]
    [SerializeField] private float m_StumbleMagnitude = 0.5f;

    // Internal variables
    private BalanceID m_BalanceID;

    /// 
    /// Can the ability be started?
    /// 
    /// True if the ability can be started.
    public override bool CanStartAbility()
    {
        return IsOnBalanceObject();
    }

    /// 
    /// Is the character on a object that they can balance on?
    /// 
    /// True if the character is on a balance object.
    private bool IsOnBalanceObject()
    {
        var front = m_Controller.CapsuleCollider.radius * Mathf.Sign(m_Transform.InverseTransformDirection(m_Controller.Velocity).z);

        // The character is not over a balance object if they are in the air.
        if (!Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                m_Controller.CapsuleCollider.height / 2 + 0.1f, front), -m_Transform.up, m_MinDropHeight)) {
            return false;
        }

        // The character is not over a balance object if there is an object just to the left of the character.
        if (Physics.Raycast(m_Transform.TransformPoint(-m_Controller.CapsuleCollider.radius - m_SidePadding, 
                m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                -m_Transform.up, m_MinDropHeight)) {
            // Do not assume that because there is nothing immediately to the left of the character that they are not on a balance object. 
            // If the object is not narrow then the character is not on a balance object.
            if (Physics.Raycast(m_Transform.TransformPoint(-(m_Controller.CapsuleCollider.radius * 2) - m_SidePadding, 
                   m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                   -m_Transform.up, m_MinDropHeight)) {
                return false;
            }
        }

        // The character is not over a balance object if there is an object just to the right of the character.
        if (Physics.Raycast(m_Transform.TransformPoint(m_Controller.CapsuleCollider.radius + m_SidePadding,
                m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                -m_Transform.up, m_MinDropHeight)) {
            // Do not assume that because there is nothing immediately to the right of the character that they are not on a balance object. 
            // If the object is not narrow then the character is not on a balance object.
            if (Physics.Raycast(m_Transform.TransformPoint(m_Controller.CapsuleCollider.radius * 2 + m_SidePadding, 
                    m_Controller.CapsuleCollider.center.y - m_Controller.CapsuleCollider.height / 2 + 0.1f, front), 
                   -m_Transform.up, m_MinDropHeight)) {
                return false;
            }
        }

        // The character is not over a balance object if there is a wall to the left of the character.
        if (Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                m_Controller.CapsuleCollider.height / 2 + 0.1f, front), -m_Transform.right, m_MinDropHeight)) {
            return false;
        }

        // The character is not over a balance object if there is a wall to the right of the character.
        if (Physics.Raycast(m_Transform.TransformPoint(0, m_Controller.CapsuleCollider.center.y - 
                m_Controller.CapsuleCollider.height / 2 + 0.1f, front), m_Transform.right, m_MinDropHeight)) {
            return false;
        }
        return true;
    }

    /// 
    /// The ability has been started.
    /// 
    protected override void AbilityStarted()
    {
        m_BalanceID = BalanceID.Movement;

        base.AbilityStarted();
    }

    /// 
    /// Returns the destination state for the given layer.
    /// 
    /// The Animator layer index.
    /// The state that the Animator should be in for the given layer. An empty string indicates no change.
    public override string GetDestinationState(int layer)
    {
            // The ability only affects the base, upper, and any layers that the item specifies.
        if (layer != m_AnimatorMonitor.BaseLayerIndex && layer != m_AnimatorMonitor.UpperLayerIndex && 
            !m_AnimatorMonitor.ItemUsesAbilityLayer(this, layer)) {
            return string.Empty;
        }

        return "Balance.Movement";
    }

    /// 
    /// Update the Animator.
    /// 
    /// Should the RigidbodyCharacterController stop execution of its UpdateAnimator method?
    public override bool UpdateAnimator()
    {
        if (m_Controller.InputVector.x > m_StumbleMagnitude) {
            m_BalanceID = BalanceID.StumbleRight;
        } else if (m_Controller.InputVector.x < -m_StumbleMagnitude) {
            m_BalanceID = BalanceID.StumbleLeft;
        } else {
            m_BalanceID = BalanceID.Movement;
        }
        m_AnimatorMonitor.SetStateValue((int)m_BalanceID);

        return true;
    }

    /// 
    /// Can the character have an item equipped while the ability is active?
    /// 
    /// True if the character can have an item equipped.
    public override bool CanHaveItemEquipped()
    {
        return false;
    }

    /// 
    /// Can the ability be stopped?
    /// 
    /// True if the ability can be stopped.
    public override bool CanStopAbility()
    {
        return !IsOnBalanceObject();
    }
}


<- Add the UI to a New Scene
Detect Headshots ->