What is the most effective way to structure Card Effects in a Single Player game?
Hi,
I'm working on a single player card game idea, in the same vein as Slay the Spire, Meteorfall: Journey, or Dream Quest. I'm trying to figure out how to best structure the code for the Effects the Cards will have. The Effects will need to follow certain criteria in the game:
Can have their values modified in game; ex. "Deal 5 Damage" can become "Deal 10 Damage".
Effects can be added/removed from cards during gameplay; ex. "Deal 5 Damage" becomes "Deal 5 Damage AND Heal 5 Damage".
Effect values should be easily modified in-editor for easy balancing by anyone on the team.
Effects are independent from other stats like Card Level or Mana Cost.
The structure should scale well; we plan on having 320 or so cards in the final product.
The structure shouldn't clutter up the project if at all possible.
Our first pass at tackling this problem was based off a reply by the Meteorfall developer in this thread. We're doing a similar setup, where each individual card will have its own script, though we're using Scriptable Objects rather than Monobehaviour Objects.
Our Card.cs script handles all of the visuals, as well as holding a List of CardEffects that it will cycle through and trigger when the card is played.
public class Card : MonoBehaviour
{
public List<CardEffect> cardEffects = new List<CardEffect>();
public void PlayCard()
{
foreach (CardEffect ce in cardEffects)
{
ce.ApplyEffect();
}
GameplayManager.Instance.activeCharacter.DiscardCard(this);
}
}
Then we have a CardEffect class that inherits from Scriptable Object, and has a couple basic functions in it in order to do some functionality when called upon:
public class CardEffect : ScriptableObject
{
public virtual void ApplyEffect()
{
}
public virtual string CardDescription()
{
return "";
}
}
And finally, we have a script per card essentially, that inherits from the CardEffect.cs:
[CreateAssetMenu(fileName ="DealDamage", menuName ="CardEffects/DamageEffects")]
public class DealDamage : CardEffect
{
public int damage;
public override void ApplyEffect()
{
GameplayManager.Instance.DamageCharacter(damage, false);
}
public override string CardDescription()
{
return "Deal " + damage + " Damage.";
}
}
However, there's a few problems with this implementation. First of all, as far as I know, modifying values of a Scriptable Object in game would modify the Asset itself, thus affecting all cards that use that Scriptable Object, as well as changing the value for future sessions as well. This method also creates quite a bit of clutter, as even for a Damage Effect on multiple cards, we'd need a unique Scriptable Object for each different integer value we want, so it makes a lot of clutter in the project. And finally, this is going to create a whole lot of scripts, assuming I make a unique .cs file for each card.
So I'm wondering what other options I have available to me for accomplishing the goals listed above. I've attempted to create an abstract CardEffect class that doesn't inherit from Scriptable Object or MonoBehaviour. However, I can't seem to access the values of the CardEffect and classes that inherit from it in a list of CardEffects in the Editor, even if I make the class Serializable. I've tried to check out various other threads and tutorials on how to tackle this problem, but nothing seems to be quite the right fit for our game. Any help would be greatly appreciated!
Answer by DanielBatoff · Oct 30, 2018 at 09:00 PM
For anyone interested, I've come up with another method to tackle this problem. It may not be the optimal code structure, but it meets all the criteria I listed above, and is easy enough to understand.
Step 1 is to separate the card visuals from any of the card logic.
public class Card : MonoBehaviour
{
public TextMeshProUGUI manaCostTMP;
public TextMeshProUGUI levelTMP;
public TextMeshProUGUI descriptionTMP;
public GameObject cardBack;
public GameObject cardFront;
public CardData cardData;
}
This is really useful because it means you only need a single prefab for the visuals of your card. So if you want to change the layout of the image or anything, you only need to do it once, not 300 times.
Then we've got our CardData class that the Card class takes in to display all the card-specific data, and give the cards functionality.
public class CardData : MonoBehaviour
{
public int manaCost;
public int cardLevel;
public List<CardEffect> cardEffects = new List<CardEffect>();
}
The CardData is what you will be making all your prefabs out of. The CardData takes in a list of CardEffects, which are the classes that describe all the effects the cards will have.
public abstract class CardEffect : MonoBehaviour
{
[TextArea(2, 5)]
public string effectText;
public bool targetsActiveCharacter;
public virtual void ApplyEffect()
{
}
public virtual string CardDescription()
{
return "";
}
}
The CardEffect class is abstract, as you will never have a base CardEffect, it'll always be a child of the CardEffect class with the real functionality. They're also MonoBehaviours rather than ScriptableObjects so it's less clutter in the Assets, and it allows you to modify the values.
Here's an example of a class that inherits from CardEffects, DealDamage.cs.
public class DealDamage : CardEffect
{
public int damage;
public override void ApplyEffect()
{
GameplayManager.Instance.DamageCharacter(damage, targetsActiveCharacter);
}
public override string CardDescription()
{
return string.Format(effectText, damage);
}
}
It has its own public variables for whatever values you need to change. Then it overrides the CardEffect class' ApplyEffect function with whatever specific functionality you need. And you can call the CardDescription function to return a string, which can include variables inside the string.
Here's an example of the Damage card prefab in the Hierarchy:
As you can see, the Designers can fill out all the Card Data values in the Hierarchy easily, then they add any CardEffect scripts they want, and fill those out as well. The Effect Text is nice too, as you can use the {0} to include any variables in the actual card string. So in this example, although it reads "Deal {0} Damage", in-game it'll actually read "Deal 8 Damage", due to the line in the CardDescription function "return string.Format(effectText, damage);". You can pass in multiple variables if you want too.
Hope this helps!
Your answer
Follow this Question
Related Questions
Problem when acessing a list from another script? (ArgumentOutOfRangeException) 0 Answers
Changing an objects location 1 Answer
dash till puff game 0 Answers
uNet. Spawning objects over the network. 0 Answers