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:
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.
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)?
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.