#AbilitySystem Design

1 messages ยท Page 1 of 1 (latest)

clear cloak
#

So I'm thinking of splitting the Ability ScriptableObject to be contained within an IAbilityState that tracks the state, and extract all state handling logic out of the Ability SO class.

The State itself will keep a reference to the Ability and will simply track its state. The annoying thing in this case is that it shouldn't be presumed that the IAbilityState would be containing the Ability. It makes more sense for the opposite to be the case, but I still would like to lean out the Ability Scriptable Object.

So having the TriggerController build its own AbilityState wrapper for the Ability seems to make some sense in that regards.

warm hill
#

This seems to be more about the terms? At least the way i read it is that you think having an (IAbility)State shouldnt contain the ability? But interfaces are more like Can/Has/Does instead of Is. I did separate the core ability data from the runtime data with a RuntimeAbility wrapper which is just a plain c# class (and in my case receives ticks from the managing component, you'd have to adjust that to your system) that grabs/polls the core data in whatever way neccessary

woeful oyster
#

I would change the logic for Ability. I would have two different classes for handling the abilities. One of them abstract class as MonoBehaviour Ability that would have IAbilityState, plain C# like the colleague over there mentioned, with the logic for handling the states, and AbilitySO with some control variables for the abilities handling themselves, damage, cooldown, etc.

clear cloak
# warm hill This seems to be more about the terms? At least the way i read it is that you th...

I already have this split functionality through the AbilityManager (MonoBehaviour), it is coupled with the underlying Controllers in two ways: Forwards the Update() call and for the Trigger it handles the OnUserInput (InputAction.CallbackContext ctx). So that way it can join the loop and also handle user input without caring where it's coming from.

The IAbilityState, as you mentioned, is a stupid implementation the way I described it. I basically threw away the concept of an Interface.

Instead the TriggerController has an AbilityState class that is used around the Ability as a wrapper. This way the Ability is just what it should be (no state handling logic, just initialization and abstract methods to be implemented by deriving abilities). It bothers me that the AbilityState is doing what it should (tracking states) but also acting as a wrapper. I've yet to wrap my head around interfaces, embarrassingly I've had very little exposure to them at work over the past few years.

Thanks a lot for the reply @warm hill , it's given me some context within which to work!

clear cloak
# woeful oyster I would change the logic for Ability. I would have two different classes for han...

This is sort of what I was thinking along the lines of, with the exception of binding to Mono. I don't think I need to directly access the Mono cycle, nor do I plan to have any ability directly attached to a GameObject.

The biggest issue I had was binding State within the Ability which was messing me up completely. Having refactored that out of the Ability to be handled by the TriggerController mentioned above it seems that now this structure is making some sense:

- AbilityManager : MonoBehavior { } // Attacahed to anything that can use abilities or have them used on it.
  - TriggerController { } // Allows AbilityManager owner to use Abilities
  - EffectController { } // Allows AbilityManager to handle incoming ability effects

TriggerController // mentioned above
- Dictionary <String, AbilityState>; // triggerSequence -> AbilityState
  - AbilityState { } // A wrapper with State logic used by the Trigger to track Abilities available
    - Ability : ScriptableObject { } // An abstract class exposing some methods which are ultimately run by EffectController.
      - DodgeAbility : Ability { } // An example of a self cast Ability implementation.
      - FireBallAbility : Ability { } // An example of a targetted Ability implementation.
        // These deriving abilities should be able to access an interface that the AbilityManager derives to make ApplyAbility() calls to.

EffectController // mentioned above, receives incoming abilities applied to the owning Manager.
  - List <Ability>; // active abilities, probably needs to be wrapped around an AbilityEffect
    // Methods here are cycling through the abilities present and applying / upkeeping and fading the ability effects on the gameObject the controller is attached to
#

While writing the above I sort of realized where an interface would be necessary. The AbilityManager itself which is the Mono should own an Interface that accepts incoming abilities from the derived abilities themselves. This way the Abilities can make calls to a generic Manager and they're then handled whichever way is necessary.

Not sure if what I wrote above is bust or makes sense. It does seem to better depict what I'm trying to implement currently.

woeful oyster
#

I like one thing u said but not what u did. U did an example of self casted ability but used as generic script like DodgeAbility. I created another day some sort of Ability system using interfaces and scriptables and decided that each of one them should be a SO for design managing purposes of simplicity. If I want to create a new one just right click and it's done, right? In order to do that I needed some kind of generic class to handle Ice spike and Fire ball abilities since their code would be pretty the same so I created SingleTargetAbility: Ability, MultiTargetAbility: Ability an so on. Hope this guide u with the idea of abilities hierarchy.

#

Another thing, besides the AbiltiyManager I think u will still need to have each Ability as MonoBehaviour. I dunno ur game and ur architecture but I don't think it's a good idea let one controller handled a trigger for, let use as example, one hundred abilities in the game. Imagine a game like Spell break.

clear cloak
#

Cheers for the expansion on this topic @woeful oyster Can you elaborate what Spell break would be a reference to?

#

And very good points raised recarding the Single, Multi etc

woeful oyster
#

It's a mage battle royale game with a mechanic that spells can trigger an event when hitting another spell. Example is a fire tornado when a fireball hits a tornado, or poison cloud when a poison ball hits a tornado

#

Imagine all this spells across the battle royale map being dicted from this AbilityManager

#

I don't think that would be a great handler

#

But a game like Magicka 1, 2 I think that can work well with a Compostable architecture

#

That happens almost the same, spells can triggers another's spells from different players, but in Magicka it's all close to the players that plays together. So a manager here can work like a charm

clear cloak
#

I see what you mean. The scope of the Ability System would basically make the game implementing such probably purely focus on such dynamics

#

Having thought a bit about this I suppose what I'm creating is more of a cause and effect system rather than the creation of "Abilities" as a singular source. It's more along the lines of

"This GameObject triggered this ability"
"This same GameObject received its effects"
"These other GameObjects received such effects"

#

What you're suggesting goes a step further in between triggering and effects that adds

"An Ability GameObject has been created in the game. It now applies its effects"

#

So you'd extract EffectController from the AbilityManager and make that into the Ability's Mono. It does make perfect sense for what you're suggesting

#

I'll have a think on this once more, thanks @woeful oyster

woeful oyster
#

Well, what u described now work as a logger, right?

clear cloak
#

No just giving an example of class behavior. The printing was just the pseudo behavior implemented

#

Well one question would be, say you cast Dodge. Would that ability create a "Dodge Ability" GameObject or somehow bind itself as a temporary script to the gameobject that's doing the dodge?

woeful oyster
#

U could do callback readings for each event u described.

This game object triggered this ability

// Code in component that casts the ability
public delegate void OnAbilityCasted (Ability ability);
public OnAbilityCasted onAbilityCasted;

void CastAbility(Ability ability) {
  // Cast ability
  onAbilityCasted?.Invoke(ability);
}
// Code in the MonoBehaviour "logger" for each control u want
class AbilityLogger {
  Transform caster;
  Ability casted;
  Transform hit;
  Int castAmount;
}

Dictionary<Ability, AbilityLogger> logs;

Start() {
  // Read the components that can trigger the event u want to listen
  component.onCastAbility += OnCastAbility;
}

OnDestroy () {
  // Don't forget to unsubscribe the method
  component.onCastAbility -= OnCastAbility;
}

void OnCastAbility (Ability ability) {
  // Wrap your logic here.
  logs[ability] = new AbilityLogger (ability.caster, ability, null, 0);
}
clear cloak
#

And if it's a FireStorm it would instead generate a new gameObject and fire that way

#

oh wait let me read ๐Ÿ˜›

woeful oyster
# clear cloak Well one question would be, say you cast Dodge. Would that ability create a "Do...

No. I don't think so. It would be better think in the abilities as spells. When u cast a spell like "Berserker" what should happen? I gain move speed, extra damage and ignore collision for an amount of time, let's say that. Berserker: SelfcastAbility so it triggers in the game obejct that called it, it modifies their stats temporarily, it doesn't need to add some script or do anything else. But when u think about a fireball or a tornado that is a spell that needs to check for collision with things around u will need to cast a game object with some collision attached to check that, right? Can u see what I mean?

clear cloak
#

Delegates still make my head hurt. But yeah if I'm understanding it right you have a delegate definition and right below it you declare it. Then a public CastAbility method is called and when that happens you're invoking your delegate (firing the event) and passing it the ability which it's been defined to expect?

#

I'm just going to rip open my code after a quick commit and start shoving this logic in and see how it will take shape. No loss in trying, nobody is gonna whip me if I botch this ๐Ÿ˜›

woeful oyster
#

Haha, please do keep in mind that the first think u should consider in all this logic is "What behaviours u want to have."

clear cloak
#

It initially started as a soulsborne style of gameplay with a bit of spells mixed in, thus the centralized AbilityManager (no expectation to combine spells or anything like that)

#

Still generalizing an AbilitySystem that can be adapted for future use has no harm in it and teaches plenty in integrating with Unity

woeful oyster
#

This talk is awesome. I've watched this many and many times

#

Consider watching then both

clear cloak
#

Thanks a lot, will do so after Work

#

Ooh Firefly reference ๐Ÿ˜„

#

So I had this:

_activationAbilityQueue.ForEach(abilityState => abilityState.TriggerAbility());

extracting the method turned it into an action, and within the action method I can't refer to the onAbilityCasted class field:

        private static Action<AbilityState> TriggerAbility()
        {
            OnAbilityCasted?.invoke  // Intellisense doesn't allow this
            return abilityState => abilityState.TriggerAbility();
        }
#

Oh just realized it's static lol

#
        public delegate void OnAbilityCasted (Ability ability);
        public OnAbilityCasted onAbilityCasted;
        // Activate any abilities in activationQueue
        void Update()
        {
            if (_activationAbilityQueue?.Count > 0)
            {
                _activationAbilityQueue.ForEach(ability => TriggerAbility(ability));
                _activationAbilityQueue.Clear();
            }
        }

        private void TriggerAbility(AbilityState abilityState)
        {
            onAbilityCasted?.Invoke(abilityState.ability);
            abilityState.TriggerAbility();
        }

This is the current layout. I'm not sure if it makes sense still having this in TriggerController. This is the Ability part which is not a Mono but is driving the ability triggers. For now I'll leave as is and push on the rest

woeful oyster
#

This probably would add another layer of complexity in having to subscribe methods from "variable components"

#

I mean, I did not quite understand why are u using this code

#

Why would u put and Update call in this code if it's not derived from MonoBehaviour? And why would have a trigger ability queue? Wouldn't that be better handled if the skill itself knows when it can be called?

#

If you're making a turn based game then it's ok having this kind of control to trigger the abilities in the correct time

clear cloak
#

This is the Trigger Controller, it's offloading the MonoBehavior's update call from the Manager above

#

The name should probably be changed but I'll need a regular check to trigger and apply effects