0
\$\begingroup\$

I'm new to using Scriptable Objects, so this might just be a bit of confusion.

Let's say we have a Scriptable Object that defines the abilities a character might have in the game, and a character class that triggers these abilities:

public class Ability : ScriptableObject
{
    public string Name = "New Ability";
    public int ManaCost = 10;
    public Sprite AbilitySprite;

    public void Execute(ref Character character)
    {
        if (character.Mana < ManaCost)
        {
            return;
        }

        // Do something, like instantiating and moving the sprite

        character.Mana -= ManaCost;
    }
}

public class Character : Monobehaviour
{
    public int Health = 100;
    public int Mana = 100;

    public List<Ability> AbilityList = new List<Ability>();

    private void Update()
    {
        // Crude implementation for example purposes 
        if (Input.GetKeyDown(KeyCode.Alpha1)) Abilities[0].Execute(ref this);
        if (Input.GetKeyDown(KeyCode.Alpha2)) Abilities[1].Execute(ref this);
    }
}

Obviously we can make numerous abilities with different mana costs, and add them to each character in the scene as needed; some characters may have a "Bolt" ability, while others might have an ability that casts "Fire." The Execute() method will then evaluate whether or not the character can use these ability based on the character's current mana.

But what if some of these abilities, all of which have similar effects, needed to also check the character's health, or perhaps a collision state, like isGrounded? For instance, maybe a certain ability can only be executed when the character has the appropriate mana value, and is also airborne. Would it be necessary to make many different subclasses of the Ability class to accommodate these different conditions, each with its own logic?

At first I tried changing the Execute() method to take in a bool, like this:

public void Execute(ref Character character, bool canExecute)
{
    if (!canExecute || character.Mana < ManaCost)
    {
        return;
    }
}

The problem with this is that I have to hard code the conditions for canExecute and compare the current ability against its Name, and that sort of defeats the purpose of Scriptable Objects. My goal is to be able to create abilities that might need to check varying conditions while performing the same functionality, and assign them in the inspector to each character as needed.

\$\endgroup\$
1
\$\begingroup\$

It sounds like you might want to pull out your criteria to one (or more) separate types, rather than embed them in the ability itself. Here's a simple example with a condition struct that uses an enum to switch it between multiple modes. You could also just as easily define this as an interface or abstract type with multiple implementations for different criteria types.

[System.Serializable]
public struct CharacterCondition {
    [System.Serializable]
    public enum Criterion {
        isGrounded,
        hasMana,
        hasHealth
    }

    public Criterion criterion;

    // You can use a custom editor / property drawer to style this as a checkbox
    // if and only if a boolean criterion is selected, using 0 = false, 1 = true.
    public float value;

    public bool Evaluate(Character character) {
        switch(criterion) {
            case Criterion.isGrounded:
                return character.isGrounded == (value != 0f);
            case Criterion.hasMana:
                return character.mana >= value;
            case Criterion.hasHealth:
                return character.health >= value;
        }
    }
}

Then you can pair your abilities with any number of conditions - either inside their ScriptableObject (if every character using this ability SO should be subject to the same conditions), or inside the character (if different characters can use the same ability SO under different conditions):

[System.Serializable]
public struct ConditionalAbility {
    public CharacterCondition[] conditions;
    public Ability ability;

    public bool TryExecute(Character character) {
        // When asked to execute the ability, first check all conditions.
        // If any are false, abort!
        // (Note you can leave the array empty for unconditional abilities)
        foreach(var condition in conditions) {
            if(condition.Evaluate(character) == false)
                return false;
        }

        // We don't need "ref" here.
        // (Character is a class, so its instances are always passed by reference)
        ability.Execute(character);
        return true;
    }
}

So for example, your character could contain an array ConditionalAbility[] and attempt to trigger them with TryExecute(this), processing any conditional requirements automatically inside that method.

\$\endgroup\$
  • \$\begingroup\$ This is what I was looking for. Would this also work with more complex conditions? For example, what about something like: (isGrounded ^ isUnderWater) && velocity.x >= 5. (I understand I can expand the Criterion enum forever, but is that sloppy?) \$\endgroup\$ – Ginger and Lavender Mar 6 at 3:29
  • \$\begingroup\$ You'd need to modify it to allow more complex combinations - as shown here, all conditions are implicitly joined by "&&" - but you could make your condition structure something richer than a single array to be able to handle multiple alternative conditions that can still succeed even if one fails. \$\endgroup\$ – DMGregory Mar 6 at 3:35
0
\$\begingroup\$

bool canExecute doesn't make any sense here. How does the character know whether or not to pass true or false when it invokes this method? The character doesn't even know what condition the ability wants to check for!

Considering that the ability is already checking Mana, why can't it also check Health? This is, afterall, a check that the ability needs to make.

public void Execute(ref Character character)
{
    if (character.Mana < ManaCost || character.Health < HealthNeeded)
    {
        return;
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.