0
\$\begingroup\$

I'm new to Unity and C#. For concreteness, I'll phrase the question in terms of building a TCG like Magic: the Gathering or Hearhtstone, although the method seems of more general utility.

Suppose we wish to create cards that have special properties, such as: "when played, does [a thing]", or "Immunity to fire". Having created a card class, here is the pseudo-code that I would like to write, when initializing the 'library' of cards:

Card testCard = new Card( /*generic arguments here like HP, etc*/);

testCard.abilities = [fireImmunity, drawCardOnPlay, ...];

Where I would have defined the abilities fireImmunity, drawCardOnPlay, and all others in some convenient location. Of course, I could manually define these abilities on each card, but that seems like a short route to insanity: many cards will share the abilities.

First question: it seems I need to create a 'container' on each card to hold the abilities of that card; how does one do this, and what's it called? How/ where to write the abilities list that will go into these containers? First thought was the container should be a list, but I don't know what type of objects we should populate it with.

Second question: in order to handle effects that occur at specified times ("when drawn", "at end of turn", etc) I think I need to be able to create custom events, similar to the built in ones such as onCollisionEnter. These custom events would then be referenced in the scripts such as drawCardOnPlay. How to do this and what's it called?

Third question: is this unnecessary? Is there a more obvious way to do these things?

I'm certain this has answers somewhere (here, probably), but without knowing what these things are called I haven't made any progress. I've read some similar questions here but none that have addressed the implementation.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

First, I'd recommend making each card a GameObject. You'll likely want one anyway for the sake of having a visual object with a position/orientation in the scene and a collision representation for the player to click on. Then you can attach behaviours to it as components. This will help you avoid making one Card uberclass that does everything any card could ever do, keeping the code more modular and easier to maintain.

So you might have...

  • A PlayableCard component that specifies how the card may be played, and has an attachment point for "OnPlayed" effects like your "drawCardOnPlayed" example

  • A Health component that tracks the initial and current health of the card, and has an attachment point for "OnDamaged" effects like your "fireImmunity" example and "OnKilled" effects

  • A Cost component that specifies the resources needed to play a card, etc...

You can mix and match these and other components to get a wide variety of card types, while each class stays focused and reasonably simple. It also means you don't pay for what you don't use. Cards that have no cost or no health don't reserve memory for storing resource and health variables you never look at.

Now on to these "attachment points" which seems to be the main point of your question.

We'll often implement these using the Observer Pattern, where each card component exposes an event (or maybe UnityEvent for convenient viewing and editing in the Inspector) for each interesting thing that can happen to it - like the Health component exposing a public UnityEvent<DamageEvent> OnDamaged - then attaching an ability/effect just means subscribing to that event.

But there are some quirks of card games like the ones you reference that will often make us reach for a more custom version of this:

  • Card abilities often stack or modify one another.

    If I have a card that says "Takes double damage from fire" and "When this card takes damage, you gain an equal amount of life" then I could reasonably expect the damage doubling to take effect first, and pass that modified damage value to the life-giving effect, so that I heal double.

    So, you'll likely want to implement your own container of effect objects or event handler delegates. That container can implement a prioritization (like a SortedList<float, CardEffectDelegate>), so you can ensure the effects trigger in the desired order.

    This container can also include a chaining mechanism, so that the outcome of one effect handler can be passed as input into the next handler. This might be as simple as making your argument a mutable reference type, so the changes spill over automatically. Or if you prefer immutability, each handler can take an event and return the next event to pass to the next handler in sequence - returning the input unchanged if nothing special needs to happen.

  • The response to one event often triggers another event.

    I might have a card that says something complicated like "When this card is destroyed, flip a coin. On heads, draw a card. You may choose to play that card immediately at no cost. If you do, your opponent gains 1 life."

    Each step of this rule would need some time to be animated on the screen (flipping the coin, drawing the card), and potentially wait for player input (choosing whether to play the card). That means we can't execute the whole set of card effects synchronously in a single frame. We need to have an ability to partially execute a set of modifiers on a card, then suspend execution to resume later.

    This gets a little easier if you think of your game's "rule interpreting engine" as being a virtual machine. It has a program counter (the current card effect it's evaluating for a given game event), and an execution stack (the card effects/events to resolve after this one has been dealt with). (This is very closely analogous to the Stack in Magic the Gathering, which is a literal pile of cards you use to step through the resolution of its card effects)

    With this model, each card effect handler could be an IEnumerator that yields new card events as needed, or a delegate that takes a reference to the virtual machine, and asks it to add new events onto its stack when needed.

Lastly, it looks like you plan to have a number of effects that might be shared between many cards. For these, it's convenient to set them up as a ScriptableObject. This lets you create each distinct version of an effect as an asset file in your project folder that you can edit in the Inspector. Many cards can reference this effect without duplicating the data in many places, making it easier to tune and keep track of.

For instance, your fire immunity might use code something a bit like this...

// This represents what's common to card effects in general.
public abstract class CardEffect : ScriptableObject {

    [SerializeField, Tooltip("Order of precedence - lower numbers take effect first")]
    protected float _priority;

    // Handle subscribing to any events that are needed for this 
    public abstract void ApplyTo(Card card);
}


// This attribute lets us create new damage multiplier effects in the Unity editor menus.
[CreateAssetMenu(fileName = "NewDamageMultiplier.Asset", 
                 menuName = "Card Effects/Damage Multiplier")]
public class DamageMultiplierEffect : CardEffect {

    [SerializeField]
    DamageType _typesToAffect = DamageType.All;

    [SerializeField]
    float _multiplier = 1.0f;

    public override void ApplyTo(Card card) {
        var health = card.GetComponent<Health>();
        health.OnDamage.Add(ModifyDamage, _priority);
    }

    void ModifyDamage(EffectStack stack) {
        var damage = stack.CurrentEvent as DamageEvent;

        if ((damage.type & _typesToAffect) != DamageType.None) {
            damage.amount *= _multiplier;
        }
    }
}

So fire immunity is a DamageMultiplier asset with _typesToAffect = DamageType.Fire and _multiplier = 0.0f assigned in the Inspector.

Your card archetype can have a List<CardEffect>, containing references to all the effect assets that need to apply to this card. When instantiating the card, you can walk through each effect in the list and call its ApplyTo to wire it up to the events in the card's lifecycle that the effect should respond to. That might be different events on different components for each effect, but we don't need to know that detail - we've delegated the responsibility to each concrete CardEffect subclass, so each one can be highly custom and flexible.

\$\endgroup\$
1
  • \$\begingroup\$ Solid answer. I was going to say use a list with effects that implement an interface but ScriptableObject subclasses are a much more Unity friendly answer, especially for non-programmers that don't want to constantly change code. Also very good catch on the stack resolution detail, that is an easy feature to overlook and difficult to hack in later mid-development. \$\endgroup\$ Commented Jan 16, 2021 at 2:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .