0
\$\begingroup\$

I am wondering how games like dota, or Magic Arena implements their hero/ card special ability system, and more specifically how do they "Hook" their special ability triggers to the game code, without making a mess.

I want to make a simpler system than magic (no stack, as each player can't interact during the other player turn). I have read this: TCG special card effects. I am more looking into specific advice for the code provided below

Here is a concrete example (that I use in one of my games, which is turn-based card game in Csharp in Unity). The Unit class is what could be compared to the Heros in dota or the creatures card in Magic.

First is the data structure for storing the ability datas:

public abstract class AbilityData : ScriptableObject
{
    public abstract Ability CreateAbility();
}

An example ability:

[CreateAssetMenu(fileName = "ResistanceModifierTriggerData", menuName = "ScriptableObject/Ability/ResistanceModifierTriggerData")]
public class ResistanceModifierTriggerData : AbilityData
{
    public int Resistance { get { return value; } }
    public Game.Phase Phase { get { return phase; } }

    [SerializeField] private int value = 0;
    [SerializeField] private Game.Phase phase;

    public override Ability CreateAbility()
    {
        return new ResistanceModifierTriggerAbility(this);
    }
}

Then The ability abstract class:

public abstract class Ability
{
    protected bool isActive = true;

    public bool IsActive { get { return isActive; } }
}

With the matching ability:

public class ResistanceModifierTriggerAbility : TriggerAbility
{
    private int value;
    private Game.Phase phase;

    public int Value { get { return value; } }

    public ResistanceModifierTriggerAbility(ResistanceModifierTriggerData resistanceModifierTriggerData)
    {
        value = resistanceModifierTriggerData.Resistance;
        phase = resistanceModifierTriggerData.Phase;
    }

    public override bool IsTriggerAtPhase(Game.Phase phase)
    {
        if (phase == this.phase)
            return true;
        return false;
    }

    public override void Trigger(Unit owner)
    {
        owner.ModifyResistance(value);
    }
}

For now I have a class called UnitAbilityController that is owned by each units, and that handles the unit abilities (creation, trigger etc):

using System.Collections.Generic;
using System.Text;

public class UnitAbilityController
{
    List<Ability> abilities = new List<Ability>();
    Unit owner = null;

    public UnitAbility(Unit owner, List<AbilityData> abilityDatas)
    {
        this.owner = owner;

        foreach (AbilityData abilityData in abilityDatas)
        {
            abilities.Add(abilityData.CreateAbility());
        }
    }

    public void TriggerAtPhase(Game.Phase phase)
    {
        foreach (Ability ability in abilities)
        {
            TriggerAbility triggerAbility = ability as TriggerAbility;
            if (ability.IsActive && 
                triggerAbility != null &&
                triggerAbility.IsTriggerAtPhase(phase))
            {
                triggerAbility.Trigger(owner);
            }
        }
    }

    public void ModifyDamage(ref int modifiedDamage)
    {
        foreach (Ability ability in abilities)
        {
            IDamageModifier damageModifier = ability as IDamageModifier;
            if (ability.IsActive && damageModifier != null)
            {
                damageModifier.ModifyDamage(ref modifiedDamage);
            }
        }
    }
}

And here is where the abilities are being called in the unit class (which owns an instance of UnitAbilityController):

using System.Text;
using UnityEngine;

public class Unit
{
    private UnitData unitData;
    private Zone zone;
    private bool playedThisTurn = true;

    public int currentResistance;
    public int currentPower;
    public Player owner;
    public UnitAbilityController unitAbilityController = null;
    public int Range { get { return unitData.range; } }
    public Zone Zone { get { return zone; } }
    public bool PlayedThisTurn { get { return playedThisTurn; } set { playedThisTurn = value; } }

    public enum Activation
    {
        DoNothing,
        Attack,
        ActivateAbility
    }

    public Unit(UnitData unitData, Player player)
    {
        this.unitData = unitData;
        currentResistance = unitData.resistance;
        currentPower = unitData.power;
        this.owner = player;
        unitAbilityController = new UnitAbilityController(this, unitData.abilities);
    }

    public void StartTurn()
    {
        unitAbilityController.TriggerAtPhase(Game.Phase.StartOfTurn);
    }

    public void Activate()
    {
        unitAbilityController.TriggerAtPhase(Game.Phase.Activation);
    }

    public void Attack(Unit target)
    {
        target.ReceiveAttack(this);
    }

    public void ReceiveAttack(Unit source)
    {
        int modifiedDamage = source.currentPower;

        unitAbilityController.ModifyDamage(ref modifiedDamage);

        ModifyResistance(-modifiedDamage);
    }

    /// <summary>
    /// Change the unit current resistance by the given amount. Note that amount should be negative for damage
    /// and positive for heal. the currentResistance can't go under 0.
    /// </summary>
    /// <param name="amount"> the amount of resistance to modify the current resistance by.</param>
    public void ModifyResistance(int amount)
    {
        currentResistance = Math.Max(0, currentResistance + amount);
    }
}

There are a few things that I don't like in this design:

  1. The triggerAtPhase and the modifyDamage of the UnitAbilityController must cast each ability to determine whether it is a TriggeredAbility, a IDamageModifier ability and whatnot. Ideally I shouldn't break encapsulation and shouldn't have to make this cast.

  2. The fact that the TriggerAbility have the Unit that is owning it as a reference and can modify its resistance feels like a bad things. I would much rather prefer that the ability doesn't know about its owner, but how can it apply its effect then (which is tied to the resistance properties of the owning class)?

  3. I have to add a lot of special function in the Unit class to trigger potential special abilities (StartTurn for abilities that trigger at the start of the turn, Activate for ability that trigger during the activation phase of unit, ReceiveAttack for abilities that trigger when a unit receive attacks). This is the most problematic point for me as every time an ability will do something at a specific time, I will need to add function and special case for it.

I don't know if it is clear, if it's not don't hesitate to comment and I'll post relevant code/ info to better explain what I am trying to do.

\$\endgroup\$
3
  • \$\begingroup\$ If you have a certain game turn order (startRoundPhase, placePhase, fightPhase endRoundPhase...) could your card not register each ability to a phase where one of it's trigger happens? That way you do not need to check all cards. You would call the phase and this triggers all cards that registered to this phase. On the card itself on its trigger you can resolve the effect. This allows you to come up with new cards easily, you just need to define one all possible phases your turn could go through. \$\endgroup\$
    – Zibelas
    Commented Dec 14, 2020 at 9:50
  • \$\begingroup\$ Perhaps C# Events could improve your software architecture? \$\endgroup\$
    – Philipp
    Commented Dec 14, 2020 at 10:50
  • \$\begingroup\$ Yep, events seems like the way to go for the 1st question I feel like the 3rd one though can't be avoided, after checking how it was done in github.com/magarena/magarena. \$\endgroup\$
    – downstroy
    Commented Dec 15, 2020 at 15:25

0

You must log in to answer this question.