Looking for a better solution than using strings within Scriptable Objects to call specific methods elsewhere

442 Views Asked by At

I was asked to describe my use case in a previous version of my question. As such this post is quite detailed and goes into specifics about what I'm trying to do. If you arrived at this question hoping it would help you with your own problem, here is a quick TL:DR to see if it's worth delving any further:

TL:DR: I use scriptable objects in my project, and want to find the best way to attach scripts to those objects. My idea of using strings to call methods elsewhere is possible, but considered expensive and error prone. Here is a link to my original question if you would like to read why it is considered as such.

FULL QUESTION

I am currently creating my own card game (single player, so I don't need to worry about multiplayer or server stuff). I've created a scriptable object (CardAsset) to handle some fields that all my populated cards will need. Stuff like:

  • Card Name (string)
  • Cost (int)
  • Image (image)
  • Description Text (string)

I'm now at the point all the other stuff for my prototype is complete (playing field, deck, I can instantiate cards populated from my CardAsset, etc), and I need to start actually making the effects for my cards and programming them.

Since every card is different but some of the effects are the same ("draw more cards" and "deal damage to a character" are very common for instance), I figured I could save a lot of time by altering my scriptable object CardAsset to include some basic scripts and variables:

  • Card Name (string)

  • Cost (int)

  • Image (image)

  • Description Text (string)

  • Effect1 (script)

  • Effect1Amount (int)

  • Effect2 (script)

  • Effect2Amount (int)

  • Effect3 (script)

  • Effect3Amount (int)

This way, I could create CardAssets with up to 3 effects on them (again such as "deal X damage to a character" or "draw Y cards") and then use something in my game to call those scripts with the associated variables when the player plays that card (leaving any unneeded slots as null). While any card that is truly unique would likely need it's own script, I figured this would cut down on time significantly (rather than programming each card individually).

My issue is that I cannot attach scripts to my CardAsset, so I figured a suitable workaround would be for me to type in the name of a method (DealDamage, DrawCards) in the Effect1/Effect2/Effect3 slot on the CardAsset (changing them to a string), and then have something read them and call the associated method elsewhere.

I understand that this solution is prone to errors and could be painful to maintain/change, so is there a better way I could do it in this specific project? Unfortunately as a beginner I currently lack the metalanguage to easily find a solution to this issue, so any pointers at all would be most beneficial. I am more than happy to do additional research on my own, I just need to be pointed in the right direction.

2

There are 2 best solutions below

0
On

I would suggest an abstract class like

public abstract class CardEffect : ScriptableObject
{
    // The GameManager reference might be useful later when you need e.g. StartCoroutine
    // or a transform reference where to spawn objects to etc
    // or simply to Invoke your different behaviours on 
    public abstract void RunEffect(GameManager behaviour);
}

Then you can derive different effects and their behavior like e.g.

[CreateAssetMenu]
public class DamageCardEffect : CardEffect
{
    [SerializeField] private float damageAmount = 1.5f;

    public override void RunEffect(GameManager gameManager)
    {
        gameManager.SelectCard(DealDamageToSelectedCard);
    }

    private void DealDamageToSelectedCard (Card card)
    {
        Card.DealDamage(damageAmount);
    }
}

Or in the same way

[CreateAssetMenu]
public class DrawCardsEffect : CardEffect
{
    [SerializeField] private int drawAmount = 3;

    public override void RunEffect (GameManager gameManager)
    {
        gameManager.DrawCards(drawAmount);
    }
}

And so on ...

Then in your card you can have

[CreateAssetMenu]
public class Card : ScriptableObject
{
    [SerializeField] private CardEffect [] effects;

    public void PlayCardEffects(GameManager gameManager)
    {
        foreach(var effect in effects)
        {
            effect.RunEffect(gameManager);
        }
    }
}

And finally in your GameManager main controller class you would implement according methods and reference the different Card assets as a deck like e.g.

public class GameManager : MonoBehaviour
{
    // Reference assets via Inspector or edit it on runtime
    public List<Card> deck;

    public List<Card> hand;

    public void DrawCards(int amount)
    {
        for(var i = 0; i < amount; i ++)
        {
            if(deck.Count <= 0) return;

            var card = deck[0];
            deck.RemoveAt(0);

            hand.Add(card);
        }
    }

    public void SelectCard(Action<Card> onSelected)
    {
        StartCoroutine(SelectCardRoutine (onSelected));
    }

    private IEnumerator SelectCardRoutine(Action<Card> onSelected)
    {
        // Somehow wait until User selects a Card

        // then Invoke the callback
        onSelected?.Invoke(theSelectedCard);
    }
}
2
On

You can use abstract class for that (with only one or two functions i suggest).

Like:

public abstract class Effect
{
    public virtual void runeffect(Card target)
    {
    
    }

    public virtual void runeffect()
    {
    
    }
}

Then for every effect you want you will write a script which use your abstract Effect class (inheritance) and add the effects you want to the main script. So your main card script will be something like that:

public Effect[] cardeffects; //add effects you want here
//like --> public Effect cardeffects={new Drawcard(2)};
public void playthecard()
{
    foreach(Effect e in cardeffects)
        e.runeffect;
}

And one effect example:

public class DrawCard : Effect
{
    public override void runeffect()
    {
    
    }
}