Search Results for

    Show / Hide Table of Contents

    Ability System

    This page will serve as an manual to how the Ability System functions in LobsterFramework.

    Contents

    • Intro
    • Basics
      • Ability Manager & Ability Data
      • Defining Abilities & Editing Properties
      • Invoke Ability
    • Ability Instancing
      • Ability Config, Channel & Context
      • Acquire Component Reference
      • Initialization & Finalization Routines
    • Shared Data Between Abilities
      • Edit Multiple Abilities In The Inspector
      • AbilityComponent
      • Enforce the Requirement
    • Customized Running Condition & Signal Handling
      • Ability Condition
      • Prepare & Reset Ability Context
      • Animation Event & Ability Signals
      • Interrupt & Stop Ability
    • Join Abilities
    • Coroutine
    • Utilities
      • Ability Selector
    • Notes
      • Life cycle of Ability Instance
    • Summary
    • What Goes Next

    Intro

    Before we begin, it is necessary to clarify what kind of ability system we're dealing with here. The idea is directly taken from League of Legends, a MOBA game of 10 where each player play as a champion with a couple unique abilities. Here're some observations.

    1. Most abilities have cooldowns which represents the amount of time it'll take for them to be available to cast again.
    2. Some abilities can be interacted with after the initial cast.
    3. Each ability's effect is somewhat unique, but the extent of these effects (i.e. how much health is restored on a healing spell) is governed by a set of champion stats (Attack Damage, Ability Power, etc) that is shared by all champions.
    4. Some abilities feature effects that is affected by stats/resources that only exists on certain champions.
    5. Abilities can be interrupted
    6. Abilities have priorities. When 2 champions each cast a unique ability that is able to cancel the other champion's ability cast on each other, the outcome is the ability with higher priority goes through and cancels the other.
    7. Some abilities can be toggled on/off

    There're some other features I will not go in detail here. Overall, based on these observations, in order to be able to implement these features easily, the design goal is for it to have the following properties:

    • Ability can have unique attributes
    • Ability can have unfixed duration at runtime
    • Data can be shared between abilities
    • Ability can be evaluated in a certain order to avoid race condition
    • Ability can be interrupted
    • Ability can react to events
    • Ability can receive input and produce output while running
    • Configurable ability settings, the configuration can be saved assets and should act like other assets and be used by any entity.
    • Editor Support: Having custom inspector that allows developers to easily configure ability settings
    • Code-Backed & Single Threaded. All abilities and the associated parts should be implemented with code. Although developers can edit abilities settings in the inspector, this system is not intended for creation of abilities inside unity editor.

    Usage

    The following examples will demonstrate how to use the ability system in your project.

    Basics

    Ability Manager & Ability Data

    Start in a simple scene with our character.

    example1-begin

    We want our character to cast abilities, to do that, we'll add the AbilityManager component. This component requires an AbilityData as input, which defines the set of abilities our character will have access to.

    example1-add-abilitymanager

    Defining Abilities & Editing Properties

    Now that there's not much going on as we haven't defined our abilities yet! Let's start with this simple definition of CircleAttack.

    // CircleAttack.cs
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    [AddAbilityMenu]
    public sealed class CircleAttack : Ability
    {
        protected override bool Action()
        {
            Debug.Log("Attack!");
            /* Code to deal damage to the enemy */
            return false; 
        }
    }
    public class CircleAttackConfig : AbilityConfig { }
    public class CircleAttackChannel : AbilityChannel { }
    public class CircleAttackContext : AbilityContext { }
    

    Defining an ability is really simple! The only thing we are required to do is implement Ability.Action() method. This method will be called during ability invokation, which occurs right before the LateUpdate() unity event, and will continously be called every frame until the method returns false. Currently our ability immediately terminates after dealing damage to the enemy.

    Make sure the [AddAbilityMenu] attribute is applied and the bottom 3 classes match the names letter by letter in the example. The former makes the ability definition visible to the ability system and it will then use reflection to search for and validate the definition of these 3 supplementary classes. We'll talk about those later in other examples. We should now be able to see the option to add this ability to the data asset we created moments ago when we open the editor. Also, the ability system requires all abilities that are instantiable to be sealed for safety concerns.

    example1-add-circleattack

    Here inside the inspector we can edit ability settings. Currently the only thing we can edit is the cooldown and execution priorities of the ability. The former determines the frequency the ability can be casted and the latter dictates the order of invokation of this ability. The higher the priority, the earlier the ability will be executed in relation to other abilities. We can also define our own properties of the ability that can be edited in the inspector. Take a look a the extended definition of CircleAttackConfig:

    // CircleAttackConfig.cs
    using LobsterFramework.AbilitySystem;
    
    public class CircleAttackConfig : AbilityConfig {
        public int AttackDamage;
    }
    

    example1-circleattack-config1-inspector The newly added field AttackDamage can now be edited inside the inspector. Note that we have moved CircleAttackConfig out as a separate file from CircleAttack.cs since unity has trouble displaying editor for multiple classes defined in the same file and will throw warnings.

    To access the newly added data field from CircleAttack, we need to use the variable Ability.Config:

    // CircleAttack.cs
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    [AddAbilityMenu]
    public sealed class CircleAttack : Ability
    {
        protected override bool Action()
        {
            CircleAttackConfig config = Config as CircleAttackConfig;
            Debug.Log($"Attack for damage {config.AttackDamage}!");
            /* Code to deal damage to the enemy */
            return false; 
        }
    }
    
    public class CircleAttackChannel : AbilityChannel { }
    public class CircleAttackContext : AbilityContext { }
    

    Invoke Ability

    Time to put it all together! We are going to cast this ability through the AbilityManager component we just added to our character. Let's create a simple player controller component and map it with mouse left click using the unity input system.

    // PlayerControl.cs
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    public class PlayerControl : MonoBehaviour
    {
        [SerializeField] private AbilityManager abilityManager;
        public void Attack() { 
            abilityManager.EnqueueAbility<CircleAttack>();
        }
    }
    

    example1-addControl

    Here we're using AbilityManager.EnqueueAbility<T>(string) for ability invokation, note that this call should only be done during Update() or FixedUpdate() or anywhere before LateUpdate() since the ability execution will take place right before LateUpdate() and the ability will not be runned during the current frame if being enqueued during LateUpdate(). When we enter playmode and spam left clicks, we can see the output of the ability on the console. Additionally, we can use AbilityManager inspector to edit ability data in real time. The changes won't get saved to the disk unless the Save button is clicked. You can also use Save As button to store a copy of the AbilityData somewhere else.

    example1-run

    Ability Instancing

    Ability Config, Channel & Context

    In some cases we want our character to be able to cast the same ability with different settings in 1 play session, such as shooting different bullets from the weapon, or throwing out punches with different strength. We would need to define an ability for each variation that has the same behavior but only differs in parameters, which is not ideal. Here we introduce the concept of ability instance to help us solve this issue.

    An ability is allowed to have multiple instances running at the same time. The number of instances allowed for each ability is equal to the number of configurations defined for the ability. It consists of a configuration of the ability, a communication channel that allows for sending input and reading output from the ability while it's executing and a context object that holds all of the temporary variables during its execution. These directly corresponds to Ability.Config, Ability.Channel and Ability.Context variables that we have access during ability callbacks. At runtime, we can choose any ability instance we have created to run, or multiple of them when necessary as they're independant from each other. Let's take a look how things are put together in action! Continuing from our last example, we have this simple scene setup:

    example2-scene

    In case you're wondering, you can assign icon to your ability script to have it displayed in the inspector for visual clarity. Here we have a particle system attached to our character, we will use it to draw particle effect as our ability effect. We want to be able to choose from 2 different colors to draw the particle effect in play mode. The AbilityData editor allows us to create & edit configurations for ability instances. We will start by creating a new configuration for our ability:

    example2-addconfig

    Now that we have 2 configurations for our ability, it is important to know how to invoke each of them. We'll modify our player control script as follows:

    // PlayerControl.cs
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    using UnityEngine.InputSystem;
    
    public class PlayerControl : MonoBehaviour
    {
        [SerializeField] private AbilityManager abilityManager;
        public void Attack1(InputAction.CallbackContext context) {
            if (context.started) {
                abilityManager.EnqueueAbility<CircleAttack>("default");
            }
        }
    
        public void Attack2(InputAction.CallbackContext context)
        {
            if (context.started)
            {
                abilityManager.EnqueueAbility<CircleAttack>("instance2");
            }
        }
    }
    

    AbilityManager.EnqueueAbility<T>(string) takes in a string parameter as the identifier to the ability configuration you wish to cast the ability with. By default it is 'default' when called without arguments for convenience. Here we have finished the setup of wiring player input mouse left key to Attack1() and mouse right key to Attack2(). Since we want them to differ in the color of the particles being played out, we will add another property to CircleAttackConfig.

    // CircleAttackConfig.cs
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    public class CircleAttackConfig : AbilityConfig {
        public int AttackDamage;
        public Color color; 
    }
    

    Acquire Component Reference

    Now we will need the reference to the particle system component attached to the character. Since we're asking for references to scene object, we cannot serialize them on an asset object. The simpliest way would be to use Component.GetComponent<T>() on Ability.AbilityManager as they're on the same game object. However this may not always be the case and this appoach would fail when the component is located elsewhere. Instead, we will use RequireComponentReferenceAttribute to achieve this goal:

    // CircleAttack.cs
    using LobsterFramework;
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    [AddAbilityMenu]
    [RequireComponentReference(typeof(ParticleSystem))]
    public sealed class CircleAttack : Ability
    {
        protected override bool Action()
        {
            CircleAttackConfig config = Config as CircleAttackConfig;
            Debug.Log($"Attack for damage {config.AttackDamage}!");
            /* Code to deal damage to the enemy */
            return false; 
        }
    }
    
    public class CircleAttackChannel : AbilityChannel { }
    public class CircleAttackContext : AbilityContext { }
    

    The AbilityManager and AbilityData inherits from ReferenceProvider and ReferenceRequester, therefore it is able to utilize editor support for abilities with RequireComponentReferenceAttribute.

    example2-component-reference

    Note that once we apply this attribute the ability system will check if the field is null when entering the play mode. If the any of the required field is null for this ability a warning will be thrown and it may not be casted at runtime. Now we are prepared to modify CircleAttack to change the color of the particle system and play the particle effect. To access this reference, use Ability.GetComponentReference<T>(int):

    // CircleAttack.cs
    using LobsterFramework;
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    [AddAbilityMenu]
    [RequireComponentReference(typeof(ParticleSystem))]
    public sealed class CircleAttack : Ability
    {
        private ParticleSystem particleSystem;
        protected override void InitializeSharedReferences()
        {
            particleSystem = GetComponentReference<ParticleSystem>();
        }
    
        protected override bool Action()
        {
            CircleAttackConfig config = Config as CircleAttackConfig;
            var main = particleSystem.main;
            main.startColor = config.color;
            particleSystem.Play();
            Debug.Log($"Attack for damage {config.AttackDamage}!");
            /* Code to deal damage to the enemy */
            return false; 
        }
    }
    
    public class CircleAttackChannel : AbilityChannel { }
    public class CircleAttackContext : AbilityContext { }
    

    Initialization & Finalization Routines

    Since the component reference is shared among all ability instances, we can declare it as a property inside CircleAttack directly. Ability.InitializeSharedReferences() provides a routine for us to initialize any of these references when AbilityManager is enabled. Conversly, we also have Ability.FinializeSharedReferences() to allow for any clean up operations such as unsubscribing from events. For temporary fields that are specific to each ability instance defined in ability context requiring initialization, we have Ability.InitializeContext() and Ability.FinalizeContext() for this purpose. Here's the result when we run the game:

    example2-result

    Shared Data Between Abilities

    Edit Multiple Abilities In The Inspector

    We need to define another ability for this part of the demonstration. Continuing from our last example, let us define SweepAttack that does almost exactly the same thing as CircleAttack to keep things simple:

    // SweepAttack.cs
    using LobsterFramework;
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    [AddAbilityMenu("Example")]
    [RequireComponentReference(typeof(ParticleSystem), "Particle VFX", "The particle vfx that will be played when the ability is invoked")]
    public sealed class SweepAttack : Ability
    {
        private ParticleSystem particleSystem;
        protected override void InitializeSharedReferences()
        {
            particleSystem = GetComponentReference<ParticleSystem>();
        }
    
        protected override bool Action()
        {
            SweepAttackConfig config = Config as SweepAttackConfig;
            var main = particleSystem.main;
            main.startColor = config.color;
            particleSystem.Play();
            Debug.Log($"Sweep Attack: {config.AttackDamage}!");
            /* Code to deal sweep damage to the enemy */
            return false;
        }
    }
    
    public class SweepAttackChannel : AbilityChannel { }
    public class SweepAttackContext : AbilityContext { }
    
    // SweepAttackConfig.cs
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    public class SweepAttackConfig : AbilityConfig
    {
        public int AttackDamage;
        public Color Color;
    }
    

    example3-add-sweepattack

    The above demonstrates how multiple abilities can be edited using the inspector. Note that you can pass multiple arguments to the RequireComponentReferenceAttribtue to customize the name and tooltip of the exposed property fields.

    AbilityComponent

    As previously stated in the design goals, abilities should be able to have access to a shared resource. A simple way to implment this is by adding a property that stores the reference to a shared data object to each ability. However, this approach can fail if any of the reference is forgotten to be assigned and manually doing these assignments is cumbersome and error prone. We will see later on how editor support can help with alleviating this problem. In LobsterFramework, the sharing of resources between abilities is implemented via AbilityComponent.

    In the previous step, we defined SweepAttack and its config SweepAttackConfig the same as CircleAttack and CircleAttackConfig. Now suppose we want these 2 abilities' damage to be affected by the same data asset, we can do this by defining ChampionStat as follows:

    // ChampionStat.cs
    using LobsterFramework.AbilitySystem;
    
    [AddAbilityComponentMenu("Example")]
    public sealed class ChampionStat : AbilityComponent
    {
        public int AttackDamage;
    }
    

    example3-add-component

    And we remove the field AttackDamage from both CircleAttackConfig and SweepAttackConfig as they're no longer needed. To access ChampionStat, use Ability.GetAbilityComponent<T>():

    // CircleAttack.cs
    using LobsterFramework;
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    [AddAbilityMenu("Example")]
    [RequireComponentReference(typeof(ParticleSystem))]
    public sealed class CircleAttack : Ability
    {
        private ParticleSystem particleSystem;
        private ChampionStat stat;
        protected override void InitializeSharedReferences()
        {
            particleSystem = GetComponentReference<ParticleSystem>();
            stat = GetAbilityComponent<ChampionStat>();
        }
    
        protected override bool Action()
        {
            CircleAttackConfig config = Config as CircleAttackConfig;
            var main = particleSystem.main;
            main.startColor = config.Color;
            particleSystem.Play();
            Debug.Log($"Circle Attack: {stat.AttackDamage}!");
            /* Code to deal damage to the enemy */
            return false; 
        }
    }
    
    public class CircleAttackChannel : AbilityChannel { }
    public class CircleAttackContext : AbilityContext { }
    

    Enforce the Requirement

    The ability system features RequireAbilityComponentAttribute to allow the custom inspector to help developers to enforces the requirements of abilities. When attempting to remove an ability component from ability data, the editor script will check the number of abilities relying on it. The operation will only be carried out once the script has verified that number is 0, otherwise an error will be displayed in the console, indicating which abilities depend on it:

    // CircleAttack.cs
    using LobsterFramework;
    using LobsterFramework.AbilitySystem;
    using UnityEngine;
    
    [AddAbilityMenu("Example")]
    [RequireComponentReference(typeof(ParticleSystem))]
    [RequireAbilityComponent(typeof(ChampionStat))]
    public sealed class CircleAttack : Ability
    {
        /*...*/
    }
    

    example3-remove-component

    Conversely, when attempting to add an ability while its requirements are not satisfied, an error will be displayed:

    example3-add-ability

    If new requirements have been added for the ability, the validation process will automatically add the missing ability components and display warning on the console. This is displayed as warning to notify the developer of any potential asset corruption.

    Customized Running Condition & Signal Handling

    Ability Condition

    The ability system has built in support for verifying ability cooldowns. However, developers are allowed to make their own customized rules about when the ability can be executed. Here's an example of using Ability.ConditionSatisfied() to implement Fireball:

    // Fireball.cs
    [AddAbilityMenu]
    [RequireComponentReference(typeof(Mana))]
    public sealed class Fireball : Ability {
        private Mana manaComponent;
    
        protected override void InitializeSharedReferences(){
            manaComponent = GetComponentReference<Mana>();
        }
    
        protected override bool ConditionSatisfied(){
            // Only allowed to cast fireball if the mana cost can be satisfied
            FireballConfig config = Config as FireballConfig;
            return manaComponent.mana >= config.cost;
        }
    
        protected override bool Action(){
            /* Code to control fireball */
        }
    }
    
    // FireballConfig.cs
    public class FireballConfig : AbilityConfig{
        public int cost;
    }
    
    // Mana.cs
    public class Mana : Monobehavior {
        public int mana;
    }
    

    Prepare & Reset Ability Context

    Sometimes there are actions you want to perform once before and after the ability execution, like playing ability animation and deduct the cost of the ability. These can be done by overriding Ability.OnAbilityEnqueue and Ability.OnAbilityFinish:

    // Fireball.cs
    [AddAbilityMenu]
    [RequireComponentReference(typeof(Mana))]
    public sealed class Fireball : Ability {
        private Mana manaComponent;
    
        protected override void InitializeSharedReferences(){
            manaComponent = GetComponentReference<Mana>();
        }
    
        protected override bool ConditionSatisfied(){
            // Only allowed to cast fireball if the mana cost can be satisfied
            FireballConfig config = Config as FireballConfig;
            return manaComponent.mana >= config.cost;
        }
    
        protected override void OnAbilityEnqueue(){
            FireballConfig config = Config as FireballConfig;
            manaComponent.mana -= config.cost;
            /* Code to play control fireball animation */
        }
    
        protected override bool Action(){
            /* Code to control fireball */
        }
    
        protected override void OnAbilityFinish(){
            /* Code to switch to walk animation */
        }
    }
    

    Animation Event & Ability Signals

    Oftentimes we want to make use of animation events to make sure the game logic is synchronized with the animation, for this use case, use AbilityManager.AnimationSignal(AnimationEvent) and implement Ability.OnSignaled(AnimationEvent). To stop the ability, use AbilityManager.AnimationEnd(). Additionally, you can also use parameterless AbilityManager.Signal<T>() and implement Ability.OnSignaled() to implement signals issued by code. Continue using the example, the Fireball will be more powerful when we signal it:

    // Fireball.cs
    [AddAbilityMenu]
    [RequireComponentReference(typeof(Mana))]
    public sealed class Fireball : Ability {
        private Mana manaComponent;
    
        protected override void InitializeSharedReferences(){
            manaComponent = GetComponentReference<Mana>();
        }
    
        protected override bool ConditionSatisfied(){
            // Only allowed to cast fireball if the mana cost can be satisfied
            FireballConfig config = Config as FireballConfig;
            return manaComponent.mana >= config.cost;
        }
    
        protected override void OnAbilityEnqueue(){
            FireballConfig config = Config as FireballConfig;
            manaComponent.mana -= config.cost;
            /* Code to play control fireball animation */
        }
    
        protected override bool Action(){
            FireballContext context = Context as FireballContext;
            if(context.isEmpowered){
                /* Code to make fireball bigger if not already */
            }
    
            /* Code to control fireball */
        }
    
        protected override void OnSignaled(AnimationEvent){
            FireballContext context = Context as FireballContext;
            context.isEmpowered = true;
        }
    
        protected override void OnAbilityFinish(){
            FireballContext context = Context as FireballContext;
            context.isEmpowered = false;
    
            /* Code to switch to walk animation */
        }
    }
    
    public class FireballContext : AbilityContext {
        bool isEmpowered;
    }
    

    For AbilityManager to properly handle animation event, the animation must be started via Ability.StartAnimation(AnimationClip,float). This allows it to remember which ability instance started the animation and thus will call the correct ability instance when an animation event is received. Calling this method when there's already an active ability instance playing animation will override that animation and that ability instance will receive Ability.OnAnimationInterrupt(Animancer.AnimancerStart) signal if it's not the same ability instance. The default implementation will simply terminate the abiliy instance.

    Interrupt & Stop Ability

    To stop the execution of an ability instance, use AbilityManager.SuspendAbilityInstance<T>(string) or Ability.SuspendInstance(string) while in the context methods. To stop the execution of all instances of an ability, use AbilityManager.SuspendAbility<T>() or Ability.SuspendAll() while in the context methods. To stop the execution of all abilities, use AbilityManager.SuspendAbilities().

    The effect will take place during the before the next LateUpdate() event, Ability.OnAbilityFinish() will be called and the query of the status of the ability instance will indicate the ability instance is no longer running. Here's an example scenario of where stopping ability can be used:

    public class PlayerControl : MonoBehavior {
        [SerializeField] private AbilityManager abilityManager;
        public void Move(Vector3 direction){
            abilityManager.SuspendAbilityInstance<Fireball>();
            /* In this context the following also works: 
                    abilityManager.SuspendAbility<Fireball>() 
                    abilityManager.SuspendAbilities()
            */
    
            /* Code to move in the specified direction */
        }
    }
    

    Join Abilities

    The AbilityManager provides support for one ability to terminate along with another ability. It is called joining abilities under this context. For an ability instance A to join another ability instance B, it would mean that when B terminates, A will be terminated right after B. However, the opposite it not true. A is still allowed to terminate on its own, and when any of A or B terminates, this relationship no longer holds and the next time A or B is runned they will be completely independent as usual.

    To do this, call AbilityManager.EnqueueAbilitiesInJoint<T,V>(string, string) or from ability methods, call Ability.JoinAsSecondary<T>(string)

    Here's an example usecase:

    // BuffAttack.cs
    // Ability that periodically buff user's next weapon attack
    public sealed class BuffAttack : Ability {
        
        protected override OnAbilityEnqueue(){
            /* Code to apply buff */
        }
    
        protected override Action() {
            // Do nothing and wait until the joined ability finishes
            return true;
        }
    
        protected override OnAbilityFinish(){
            /* Code to remove buff */
        }
    }
    
    /* Definition of channel and context types */
    
    
    /* Perform buffed attack if the buff ability is not on cooldown, otherwise perform normal attack */
    public class PlayerControl : MonoBehavior {
        [SerializeField] private AbilityManager abilityManager;
        public void Attack(){
            if(!abilityManager.EnqueueAbilitiesInJoint<Attack, BuffAttack>()){
                abilityManager.EnqueueAbility<Attack>();
            }
        }
    }
    

    Coroutine

    The ability system provides an extended version of Ability named AbilityCoroutine that allows abilities to be executed like coroutines. Of course, with the order of execution by ability priorities still being preserved as it is directly implemented via the ability interface. Inheriting from AbilityCoroutine will allow you to write the ability code that remembers its position before return so that in the next frame the execution will continue from that position. Similar to UnityEngine.YieldInstruction, the coroutine will return a CoroutineOption indicating whether the coroutine will continue, wait for another coroutine, wait for condition, wait for time/unscaled time or reset. Here's a simple example of weapon ability Attack implemented via coroutine:

    // Attack.cs
    
    [AddAbilityMenu]
    public sealed class Attack : AbilityCoroutine {
    
        protected override void OnCoroutineEnqueue(){
            AttackContext context = Context as AttackContext;
            context.isAnimationSignaled = false;
    
            /* Code to start weapon attack animation */
        }
    
        protected override IEnumerable<CoroutineOption> Coroutine(){
            AttackContext context = Context as AttackContext;
    
            // Wait for the end of the attack wind up
            while(!context.isAnimationSignaled){
                yield return CoroutineOption.Continue;
            }
            context.isAnimationSignaled = false;
    
            /* Code to activate weapon hitbox */
    
            // Wait for the end of attack
            while(!context.isAnimationSignaled){
                yield return CoroutineOption.Continue;
            }
    
            /* Code to deactivate weapon hitbox */
    
            AttackConfig config = Config As AttackConfig;
            yield return CoroutineOption.WaitForSeconds(config.recoveryTime);
    
            // End of Execution
        }
    
        protected override void OnCoroutineFinish(){
            /* Code to switch to walk animation */
        }
    
        protected override void OnSignaled(AnimationEvent event){
            AttackContext context = Context as AttackContext;
            context.isAnimationSignaled = true;
        }
    }
    
    public class AttackContext : AbilityCoroutineContext {
        public bool isAnimationSignaled;
    }
    
    // AttackConfig.cs
    
    public class AttackConfig : AbilityConfig {
        public float recoveryTime;
    }
    

    Methods Replacing Default Ability Interface

    The ability interface is changed as follows:

    • Ability.Action() => AbilityCoroutine.Coroutine()
    • Ability.OnAbilityEnqueue() => AbilityCoroutine.OnCoroutineEnqueue()
    • Ability.OnAbilityFinish() => AbilityCoroutine.OnCoroutineFinish()

    When defining context type for your ability, you must inherit from AbilityCoroutineContext instead of AbilityContext according to the rule of ability inheritence.

    In addition, when CoroutineOption.Reset is yielded, the coroutine will restart from the beginning next frame it is invoked. AbilityCoroutine.OnCoroutineReset() is called immediately after receiving this return value to allow context variables to be reset before resuming next frame.

    Utilities

    Ability Selector

    For dynamically loading in and changing the ability instance you wish to call, you'll need to be able to store this information and edit it inside the inspector. AbilitySelector is a serializable ability instance that can be edited in the inspector to support such behavior. In addition, RestrictAbilityTypeAttribute can be used to limit of the set of options you can have when editing the ability instance.

    class PlayerController : Monobehavior {
        [SerializeField] private AbilitySelector abilityToRun;
        [SerializeField] private AbilityManager abilityManager;
    
        /* ... */
    
        public void RunAbility(){
            abilityManager.EnqueueAbility(abilityToRun.AbilityType, abilityToRun.Instance);
        }
    }
    

    Notes

    Life Cycle of Ability Instance

    The ability system injects 2 events into the player loop event system using PlayerLoopEventGroupAttribute in the following order:

    FixedUpdate() => Update() => Execution => Termination => LateUpdate()

    When enqueuing an ability instance, it will be marked as running and Ability.OnAbilityEnqueue() is immediately called if this operation is successful. The ability instance will be added to the execution queue waiting to be executed. It is the programmer's responsibility to ensure the logic inside Ability.OnAbilityEnqueue() is independent of the execution priority of the abilities.

    The Execution event executes enqueued abilitiy instances by calling Ability.Action() ordered by their execution priority. If the return value this method is false, Ability.SuspendInstance(string) will be called for the ability instance and it will be removed from the execution queue. Any ability instance that is marked as suspended via this method will have itself along with any other joined ability instances added to the suspension queue.

    The Termination event goes through the suspension queue to suspend ability instances. Ability.OnAbilityFinish() is called during this process. The abilities are again handled in order by their execution priority. All suspended ability instances will then be removed from the suspension queue and be marked as not running and not suspended. To enforce this order, any ability instance that are later added to the suspension queue during the suspension process will be deferred to the next frame for suspension.

    Summary

    AbilityManager

    Attach this component to the character to enable it to cast abilities. This component takes in an AbilityData as input. Calls to enqueue, query, terminate, send event to and communication with abilities should be done through this component during the Update() and FixedUpdate() unity event. The following section summarizes the set of methods this component has to offer.

    Enqueue Abilities

    • AbilityManager.EnqueueAbility<T>(string)
    • AbilityManager.EnqueueAbility(Type, string)
    • AbilityManager.EnqueueAbilitiesInJoint<T, V>(string, string)

    Suspend Abilities

    • AbilityManager.SuspendAbilityInstance<T>(string)
    • AbilityManager.SuspendAbilityInstance(Type, string)
    • AbilityManager.SuspendAbility<T>()
    • AbilityManager.SuspendAbilities()

    Join Abilities

    • AbilityManager.JoinAbilities(Type, Type, string, string)
    • AbilityManager.JoinAbilities<T,V>(string, string)

    Query

    • AbilityManager.IsAbilityReady(Type, string)
    • AbilityManager.IsAbilityReady<T>(string)
    • AbilityManager.IsAbilityRunning(Type, string)
    • AbilityManager.IsAbilityRunning<T>(string)
    • AbilityManager.IsAnimating
    • AbilityManager.ActionBlocked

    Animation & Signaling

    • AbilityManager.AnimationSignal()
    • AbilityManager.AnimationEnd()
    • AbilityManager.Signal<T>(string)
    • AbilityManager.InterruptAbilityAnimation()

    Fetch Reference

    • AbilityManager.GetAbilityComponent<T>()
    • AbilityManager.GetAbilityChannel<T>()

    Ability Data

    An asset object that defines a set of Abilities and Ability Components. Client code should not interact with objects of this type directly. Can be edited using inspector.

    AbilityComponent

    An asset object that defines a resource shared by all abilities. References can be obtained via AbilityManager.GetAbilityComponent<T>().

    Ability

    An asset object that defines an ability in the Ability System. Client code should not directly interact with ability objects. To create new abilities, you must subclass it and implement the required methods. It comes with 3 complimentary classes that you must define: AbilityConfig, AbilityChannel, AbilityContext.

    Mandatory:

    The following must be implemented

    • bool Action()

    Optional:

    The following can be overriden but are not required to do so

    • void InitializeSharedReferences()
    • void FinalizeSharedReferences()
    • void InitializeContext()
    • void FinalizeContext()
    • bool ConditionSatisfied()
    • void OnAbilityEnqueue()
    • void OnAbilityFinish()
    • void OnSignaled(AnimationEvent)
    • void OnSignaled()
    • void OnAnimationInterrupt()

    Context Methods:

    These methods have access to variables (not null): Ability.Config, Ability.Channel and Ability.Context

    • bool Action()
    • void InitializeContext()
    • void FinalizeContext()
    • bool ConditionSatisfied()
    • void OnAbilityEnqueue()
    • void OnAbilityFinish()
    • void OnSignaled(AnimationEvent)
    • void OnSignaled()
    • void OnAnimationInterrupt()

    Others:

    These methods are provided to support various actions & querying

    • T GetComponentReference<T>()
    • T GetAbilityComponent<T>()
    • AnimancerState StartAnimation(AnimationClip, float)
    • bool SuspendInstance(string)
    • void SuspendAll()
    • bool JoinAsSecondary<T>(string)
    • bool JoinAsSecondary(Type, string)
    • bool HasInstance(string)
    • bool IsReady(string)
    • bool IsRunning(string)

    AbilityConfig

    An asset object that defines the setting of the ability. Only accessible while in the context methods. An instantiable ability needs to define {#NameOfAbility}Config that inherit from its closest ancestor's config type.

    AbilityChannel

    Allows client code to communicate with the ability when it is being runned. Accessible via AbilityManager.GetAbilityChannel<T>(string) where T is the type of the ability this channel type belongs and while in the context methods . An instantiable ability needs to define {#NameOfAbility}Channel that inherit from this type or its closest ancestor's channel type. Do not define constructors for this type as the system uses reflection to call the default parameterless constructor. For custom initialization see Ability.InitializeContext().

    AbilityContext

    Stores context variables use by the ability during its execution. Only accessible while in the context methods. An instantiable ability needs to define {#NameOfAbility}Context that inherit from this type or its closest ancestor's context type. Do not define constructors for this type as the system uses reflection to call the default parameterless constructor. For custom initialization see Ability.InitializeContext().

    What Goes Next

    • Custom Analyzer: Currently the system performs checks to ensure the type and arguments passed in via attributes are valid after compilation. As the codebase grow larger and more attributes are added the compilation time will increase. Custom analyzer can help lifting some of these workload and help developers to discover some of these errors before the scripts are compiled.
    • Improve this Doc
    In This Article
    Back to top LobsterFramework documentation