#AbilitySystem Design
1 messages ยท Page 1 of 1 (latest)
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.
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
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.
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!
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.
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.
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
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
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
Well, what u described now work as a logger, right?
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?
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);
}
And if it's a FireStorm it would instead generate a new gameObject and fire that way
oh wait let me read ๐
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?
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 ๐
Haha, please do keep in mind that the first think u should consider in all this logic is "What behaviours u want to have."
And, yeah, that's right
Wel truthfully it's already way beyond the capabilities of what I need, but as an educational attempt there's no harm in seeking it to some sort of conclusion
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
Get the assets here: https://github.com/richard-fine/scriptable-object-demo
This session goes over ScriptableObject class in detail, compares it to the MonoBehaviour class and works through many examples of how it might be applied in a project.
Richard Fine - Unity Technologies
00:00 Intro
1:34 The MonoBehaviour Tyranny
5:58 Uninstantiated pr...
This talk is awesome. I've watched this many and many times
Scriptable Objects are an immensely powerful yet often underutilized feature of Unity. Learn how to get the most out of this versatile data structure and build more extensible systems and data patterns. In this talk, Schell Games shares specific examples of how they have used the Scriptable Object for everything from a hierarchical state machine...
Consider watching then both
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
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