Card game prototype
The game
The card game is a prototype that I made for the Game Mechanics class during the first period of my second year in school. The player is supposed to explore an evil world. At first by avoiding dangerous enemies. And after the beginning phase by fighting them, while adding more – and better cards to their deck.
Card code
For the cards I used Scriptable objects. The card attributes, such as cardName, cardArt and cardDescription, are stored in a CardData Scriptable object.
[CreateAssetMenu(menuName = "Card")] public class CardData : ScriptableObject { public string Name; public string Description; public int Cost; public Sprite Art; public BehaviourType BehaviourType; public int Amount; public CardTarget TargetType; private List<CombatStats> targets; public void PlayCard() { targets = TargetType.SetTarget(); foreach(CombatStats target in targets) { BehaviourType.PerformBehaviour(target, Amount); //play particle effect } } }
The card behaviour is setup in a modular way. I used delegate Scriptable objects, where you override an abstract method to define unique behavior in the derived classes.
I created a TargetType and BehaviourType ‘module’. The TargetType has variations like: Single enemy, All enemies, Random enemy, Self cast. The BehaviourTypes are: Damage, Heal and Block.
public abstract class CardTarget : ScriptableObject { public abstract List<CombatStats> SetTarget(); } public class AllEnemies : CardTarget { public override List<CombatStats> SetTarget() { List<CombatStats> targets = new List<CombatStats>(); EnemyStats[] potentialTargets = FindObjectsOfType<EnemyStats>(); for (int i = 0; i < potentialTargets.Length; i++) { if(potentialTargets[i].IsInCombat) targets.Add(potentialTargets[i]); } return targets; } } public class SelfTarget : CardTarget { public override List<CombatStats> SetTarget() { List<CombatStats> targets = new List<CombatStats> { FindObjectOfType<PlayerStats>() }; return targets; } }
public abstract class BehaviourType : ScriptableObject { public abstract void PerformBehaviour(CombatStats target, int amount); } public class DamageBehaviour : BehaviourType { public override void PerformBehaviour(CombatStats target, int amount) { target.TakeDamage(amount); } }
I created a Card prefab, which contains a monobehaviour, named Card. The Card script has a reference to a CardData scriptable object, as well as the UI elements that are used to present the CardData values to the player.
The Card prefab also has a CardInteraction script, where the mouse interaction with the card is handled.
Player and enemy combat code
The player and enemies share some of their behaviour. They can both take damage, get healed and gain block. However, they also have unique behaviour. For example, the player has mana, whereas the enemy simply deals a random amount of damage each turn.
The shared behaviour is stored in the CombatStats class.
public class CombatStats : MonoBehaviour { public event Action OnDeath; [SerializeField] protected int maxHealth = 20, startBlock = 5; [SerializeField] protected GameObject UIPanel = null; [SerializeField] protected TextMeshProUGUI healthText = null, blockText = null; protected float health, block; protected BattleManager battleManager; protected virtual void Awake() { ResetStats(); } protected virtual void ResetStats() { health = maxHealth; block = startBlock; } protected virtual void SetEvents() { battleManager = FindObjectOfType<BattleManager>(); battleManager.OnBattleStart += ResetStats; battleManager.OnBattleStart += UpdateUI; battleManager.OnBattleStart += () => UIPanel.SetActive(true); battleManager.OnBattleEnd += () => UIPanel.SetActive(false); } public virtual void UpdateUI() { healthText.text = health.ToString() + "/" + maxHealth.ToString(); blockText.text = block.ToString(); } public void TakeDamage(int amount) { block -= amount; if (block <= 0) { health += block; //subtract remaining damage from the health block = 0; } if (health <= 0) { health = 0; OnDeath?.Invoke(); } UpdateUI(); } public void AddBlock(int amount) { block += amount; UpdateUI(); } public void Heal(int amount) { health += amount; if (health > maxHealth) health = maxHealth; UpdateUI(); } }
The PlayerStats and EnemyStats both derive from the CombatStats class. The unique behaviour is added in the PlayerStats and EnemyStats themselves.
public class PlayerStats : CombatStats { public int Mana { get; private set; } public int MaxMana { get => maxMana; private set => maxMana = value; } [SerializeField] private int maxMana = 0; [SerializeField] private TextMeshProUGUI manaText = null; protected override void Awake() { base.Awake(); UpdateUI(); SetEvents(); } protected override void ResetStats() { base.ResetStats(); Mana = maxMana; } protected override void SetEvents() { base.SetEvents(); FindObjectOfType<HandManager>().OnCardPlayed += UpdateUI; battleManager.OnBattleStart += (() => block = startBlock); } public override void UpdateUI() { base.UpdateUI(); manaText.text = Mana.ToString() + "/" + maxMana.ToString(); } public void UsedMana(int amount) { Mana -= amount; UpdateUI(); } public void ResetMana() { Mana = MaxMana; UpdateUI(); } }
Something I’ve learned since this project is to use events to update the UI. That way the UI code is separated from the game logic making the code loosely coupled.