Component Based Ability Part 3 : Examples

For the final part of this blog I will go through a couple of abilities that I made using the component system we have discussed till now. Keep in mind I will be using a few extra components in these abilities that I haven’t discussed. You may or may not want these in your system.

Ability : Detonate

Overview

This is one of more simpler abilities that I made. When executed, it creates an explosion effect around the player, damaging it and any other units around it. It also pushes away any units caught within the blast radius.

public class Detonate : Ability {

    public Detonate(Unit owner)
    {
        Owner = owner;
    }

    [SerializeField]
    private float innerRadius;

    [SerializeField]
    private float outerRadius;

    [SerializeField]
    private float innerDamage;

    [SerializeField]
    private float outerDamage;

    [SerializeField]
    private float innerForce;

    [SerializeField]
    private float outerForce;

    public override void Execute()
    {
        base.Execute();

        if (!CanExecute)
            return;

        if (cooldown.IsOnCoolDown)
        {
            Debug.Log("ON COOLDOWN!");
            return;
        }
        else
        {
            cooldown.IsOnCoolDown = true;

            cast.castHandler.OnSkillCast.AddListener(DetonateAllWithinRange);
            cast.castHandler.OnSkillCast.AddListener(effect.PlayEffect);

            Owner.Owner.GetComponent<PlayerController>().RemoveMovementControl();

            cast.StartAnimation(cast.CastTime);
        }
    }

    void DetonateAllWithinRange()
    {
        List<Collider> hitCollidersOuter = new List<Collider>(Physics.OverlapSphere(Owner.transform.position, outerRadius));

        List<Collider> hitCollidersInner = new List<Collider>(Physics.OverlapSphere(Owner.transform.position, innerRadius));

        cast.ClearCallback();

        if (SkillCastEventBus.Instance.OnSkillCast != null)
            SkillCastEventBus.Instance.OnSkillCast.Invoke(Owner, this);

        // Remove all common units from outer list
        foreach (Collider collider in hitCollidersInner)
        {
            hitCollidersOuter.Remove(collider);
        }

        //Apply inner force and damage
        ExplodeAllUnits(hitCollidersInner, innerDamage, innerForce);

        //Apply outer force and damage
        ExplodeAllUnits(hitCollidersOuter, outerDamage, outerForce);

        Owner.Owner.GetComponent<PlayerController>().ResetMovementControl();
    }

    void ExplodeAllUnits(List<Collider> i_unitColliders, float i_damage, float i_force)
    {
        foreach (Collider collider in i_unitColliders)
        {
            Unit detectedUnit = collider.GetComponent<Unit>();
            Health attachedHealth = collider.GetComponent<Health>();
            UnitController unitController = collider.GetComponent<UnitController>();

            if (attachedHealth != null)
            {
                if (attachedHealth.IsInvincible)
                    continue;

                damage.ApplyDamage(attachedHealth, i_damage);
                if (KillAttributionEventBus.Instance.onDamageEvent != null)
                    KillAttributionEventBus.Instance.onDamageEvent.Invoke(Owner.Owner, detectedUnit);
            }

            if (unitController != null && detectedUnit != Owner)
            {
                unitController.ApplyForce(Owner.transform.position, i_force);
                unitController.Interrupt();
            }    
        }
    }

    private void Start()
    {
        AbilityName = "Detonate";
    }

    [SerializeField]
    private Cooldown cooldown;
    [SerializeField]
    private Damage damage;
    [SerializeField]
    private SkillCast cast;
    [SerializeField]
    private SkillEffect effect;
}

Before I go through and explain each function, lets look at the variables in this class.

The variables at the top represent the various properties of this ability. The radius is the area of effect for the ability, force represents the how far back the enemies will be pushed and damage represents the amount of damage they take. There are inner and outer versions of these variables where the inner variation does more damage than the outer one (same for force). These variables even though private are visible in the editor via the [serialize field] tag, thus can be accessed by a designer editing these values but not by another class.

At the very bottom of the page are my component variables. I put them at the very end because no one should be touching these. You can see the Cooldown and SkillEffect components I discussed in the previous blog. Besides these I have two more components. The Damage component is responsible for dealing damage. It is a seperate component because quite a few abilities deal damage whereas an ability isn’t the only way a player can take damage. The SkillCast component is responsible for playing the units cast animations and syncing the the ability with it. For this I use Unity’s Event messaging system (as discussed below)

Execute()

This is the virtual function we defined before. As can be seen it calls its base implementation first. This enables us to make any necessary checks for whether or not the ability can be cast. After that I check whether or not the ability is on cooldown. If its not, I add listeners to the OnSkillCast event as defined in the SkillCast component. I do this because I want the ability to be cast only when the unit has played its cast animation. After this I disable player movement until the ability is cast. Finally I start the cast animation.

void DetonateAllWithinRange()

This function detects enemies within the inner and outer radius via the OverlapSphere function defined in the Unity physics library. I then separate the common entities so that they don’t take damage twice and call another function which actually does the damage.

void ExplodeAllUnits(List i_unitColliders, float i_damage, float i_force)

This function basically goes through the list of colliders detected, checks if that collider is attached to a player unit and does damage to it. The functionality here is highly specific to my implementation and you may see components and classes that I haven’t discussed. You will probably have a completely different setup.

Here is the ability in action

Detonate Ability

Teleport

Overview

As the name suggests, this ability teleports the player a short distance from its initial position within a certain radius.

public class Teleport : Ability
{
    float MaxDistance { get { return maxDistance; } }

    [SerializeField]
    private float maxDistance = 20.0f;

    public Teleport(Unit owner)
    {
        Owner = owner;
    }

    public override void Execute()
    {
        base.Execute();

        if (!CanExecute)
            return;

        if (cooldown.IsOnCoolDown)
        {
            Debug.Log("ON COOLDOWN!");
            return;
        }
        else
        {
            if (pointTargeting.IsTargeting)
            {
                // Do nothing!
            }
            else
            {
                pointTargeting.IsTargeting = true;

                // set up callbacks
                pointTargeting.controller.OnClickedCallback.AddListener(TeleportToLocation);
                cast.castHandler.OnSkillInterrupted.AddListener(pointTargeting.ResetTargeting);

            }
        }   
    }

    private void TeleportToLocation()
    {
        if (pointTargeting.CheckForValidInput())
        {
            teleportLocation = CalculateTeleportLocation(pointTargeting.Location);

            if (teleportLocation.magnitude == 0.0f)
            {
                // location out of range!
                Debug.Log("OUT OF RANGE!");

                return;
            }
            else
            {
                if (!cooldown.IsOnCoolDown)
                {
                    cast.castHandler.OnSkillCast.AddListener(CastSkill);
                    cast.castHandler.OnSkillCast.AddListener(effect.PlayEffect);
                    cast.StartAnimation(cast.CastTime);

                    //make player look towards point
                    Vector3 targetDir = (teleportLocation - Owner.transform.position).normalized;

                    Owner.transform.rotation = Quaternion.LookRotation(targetDir, Vector3.up);

                    //put ability on cooldown
                    cooldown.IsOnCoolDown = true;
                }
            }
        }
        else
        {
            // clicked invalid terrain
            Debug.Log("invalid location!");
            return;
        }
    }

    private void CastSkill()
    {
        Owner.transform.position = teleportLocation + new Vector3(0.0f, 2.0f, 0.0f);

        // After ability is cast reset callbacks
        pointTargeting.IsTargeting = false;
        cast.ClearCallback();

        if (SkillCastEventBus.Instance.OnSkillCast != null)
            SkillCastEventBus.Instance.OnSkillCast.Invoke(Owner, this);
    }

    private void Start()
    {
        AbilityName = "Teleport";
    }

    private Vector3 CalculateTeleportLocation(Vector3 i_targetedLocation)
    {
        float distance;

        // Check whether point is within teleport range
        distance = (i_targetedLocation - Owner.transform.position).magnitude;

        Debug.Log(distance);

        if (distance > maxDistance)
        {
            return new Vector3(0.0f, 0.0f, 0.0f);
        }
        else
        {
            return i_targetedLocation;
        }
    }

    private Vector3 teleportLocation;

    [SerializeField]
    private Cooldown cooldown;
    [SerializeField]
    private PointTargeting pointTargeting;
    [SerializeField]
    private SkillCast cast;
    [SerializeField]
    private SkillEffect effect;
}

Here is where my component system shines. If you look at the setup for this ability, it is very similar to how I set up the Detonate ability save for a few differences.

I only have one adjustable variable here called maxDistance, which is the furthest the player can teleport from his initial position. As for the components, instead of the Damage component, there is another component called PointTargeting. This is a more complicated component which modifies the player controls while the ability is being cast. Because the basic controls of this game involve point and click movement, when casting an ability like teleport, this control has to change temporarily so that the player can point and click to cast the ability and not move. Once the ability is cast the controls return back to normal.

The functions are similar to those discussed previously save for a few changes. Besides Execute the helper functions have different name for readability. More over inside the Execute function, instead of the ability being cast when the cast animation finishes, it is cast when the player clicks somewhere in the game world. As such the listeners belong to the PointTargeting component. Of course once clicked, the ability still waits for the cast animation to finish.

Teleport Ability

And this is basically it for my ability system! This probably isn’t the best method out there for setting up a system for your abilities and there is definitely room for improvement. However, sometimes it is better to build on top of something rather than start from scratch. If nothing else it should give you ideas about how you could go about setting up something similar, what to avoid and what to implement etc

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s