Search Unity

How to handle multiple components that implements the same interface? (HP, Armor, and Shield)

Discussion in 'Scripting' started by Saucyminator, Sep 15, 2018.

  1. Saucyminator

    Saucyminator

    Joined:
    Nov 23, 2015
    Posts:
    61
    Hi!

    I'm working on scripts for characters to be able to have multiple life meters (HP, Armor, Shield). Each separated into component-based scripts. Basically layers of health. You might recognize this from Mass Effect games.

    The problem I have is how TakeDamage would work.
    The HP, Armor, Shield-scripts should work by themselves but also with each other. All scripts implements a IHittable-interface which has a TakeDamage method.

    Problem:
    Currently my IHittable.TakeDamage() method calls the first script that implements IHittable. In this case it's my UnitHealth-script. So I've setup UnitHealth to call the other scripts (UnitArmor and UnitShield) and reduce their values (health) before reducing the HP.
    This isn't very modular in my eyes because I want the scripts to be independent of each other, somehow.

    Turning off scripts as they are dead/depleted isn't working either. First script still intercepts the interface call, plus the players might find some armor that will up the Armor again. Also, how do I call the next component with TakeDamage()?

    I'm thinking this order of taking damage when they are combined: Shield -> Armor -> HP. But I'd love to just move the components above/below to set the order.

    (Later I want to implement a Armor/Shield-penetration & Armor/Shield-absorption systems. Their scripts can only absorb % of the incoming damage, rest is passed on to the next component, etc, etc.)

    Examples:
    Characters:
    - Boss: HP and Armor scripts. Armor is to be reduced first, then HP. Is killed when HP is zero.
    - NPC: HP script. Is killed when HP is zero.
    - Turret: Armor and Shield scripts. Shield is to be reduced first, then Armor. Is destroyed if Armor is zero.

    Flow:
    - A player has 100 HP and 50 Armor.
    - Takes damage (100).
    - Armor is first in the order, so it takes 50 damage then the remaining 50 damage is passed to the next component.
    - HP takes damage and is now 50.

    What I got so far:
    Let's start with what I got so far. These scripts are simplified versions of what I have because my classes saves their values out to ScriptableObject assets so other elements (UI for example) can easily use it. I want to keep it simple and readable for you guys.

    Code (CSharp):
    1. public interface IHittable {
    2.   bool TakeDamage (float _amount, bool _isPercent = false, float _armorPenetration = 0f, float _shieldPenetration = 0f);
    3. }

    This is slapped on a bullet-gameobject/prefab to be able to do damage.
    Code (CSharp):
    1. public class DamageDealer : MonoBehaviour {
    2.   public float amount;
    3.   public bool isPercent;
    4.   [Range(0f, 1f)] public float armorPenetration;
    5.   [Range(0f, 1f)] public float shieldPenetration;
    6.  
    7.   void OnTriggerEnter (Collider _other) {
    8.     IHittable hittable = _other.gameObject.GetComponent<IHittable>();
    9.  
    10.     if (hittable != null) {
    11.       hittable.TakeDamage(damage, isPercent, armorPenetration, shieldPenetration);
    12.     }
    13.   }
    14. }

    UnitArmor and UnitShield are basically the same script. Here's UnitArmor:

    Code (CSharp):
    1. public class UnitArmor : MonoBehaviour, IHittable {
    2.   public float RemainingDamage { get { return remainingDamage; } private set { remainingDamage = value; } }
    3.   public float GetValue { get { return currentValue; } }
    4.  
    5.   [SerializeField] private float maxValue;
    6.   private float currentValue;
    7.   private float remainingDamage;
    8.  
    9.   private void Start () {
    10.     currentValue = maxValue;
    11.   }
    12.  
    13.   public bool TakeDamage (float _amount, bool _isPercent = false, float _armorPenetration = 0f, float _shieldPenetration = 0f) {
    14.     // No armor or shield penetration calculations yet
    15.     // Skipping percent based damage as well
    16.  
    17.     RemainingDamage = 0f;
    18.  
    19.     if (_amount <= 0f || currentValue <= 0f) {
    20.       return false;
    21.     }
    22.  
    23.     currentValue -= _amount;
    24.  
    25.     if (currentValue <= 0f) {
    26.       // depleted
    27.       RemainingDamage = Mathf.Abs(currentValue);
    28.       currentValue = 0f;
    29.     }
    30.  
    31.     return true;
    32.   }
    33. }

    UnitHealth is similar but it calls TakeDamage on Shield and Armor to get the leftover damage. Not very modular if I want a character to only have HP (Armor and Shield-scripts is unnecessary in this case).

    Code (CSharp):
    1. [RequireComponent(typeof(UnitArmor))]
    2. [RequireComponent(typeof(UnitShield))]
    3. public class UnitHealth : SerializedMonoBehaviour, IHittable {
    4.   public float RemainingDamage { get { return remainingDamage; } private set { remainingDamage = value; } }
    5.  
    6.   [SerializeField] private bool useArmor;
    7.   [SerializeField] private bool useShield;
    8.  
    9.   [SerializeField] private float maxValue;
    10.   private float currentValue;
    11.   private float remainingDamage;
    12.   private bool isAlive;
    13.   private UnitArmor armorScript;
    14.   private UnitShield shieldScript;
    15.  
    16.   private void Awake () {
    17.     armorScript = GetComponent<UnitArmor>();
    18.     shieldScript = GetComponent<UnitShield>();
    19.   }
    20.  
    21.   private void Start () {
    22.     currentValue = maxValue;
    23.     isAlive=true;
    24.   }
    25.  
    26.   public bool TakeDamage (float _amount, bool _isPercent = false, float _armorPenetration = 0f, float _shieldPenetration = 0f) {
    27.     remainingDamage = _amount;
    28.  
    29.     if (_amount <= 0f || currentValue <= 0f) {
    30.       return false;
    31.     }
    32.  
    33.     if (useShield && shieldScript.GetValue > 0f) {
    34.       shieldScript.TakeDamage(remainingDamage, _isPercent, _armorPenetration, _shieldPenetration);
    35.       remainingDamage = shieldScript.RemainingDamage;
    36.     }
    37.  
    38.     if (useArmor && armorScript.GetValue > 0f) {
    39.       armorScript.TakeDamage(remainingDamage, _isPercent, _armorPenetration, _shieldPenetration);
    40.       remainingDamage = armorScript.RemainingDamage;
    41.     }
    42.  
    43.     currentValue -= RemainingDamage;
    44.  
    45.     if (currentValue <= 0f) {
    46.       // die
    47.       RemainingDamage = Mathf.Abs(currentValue);
    48.       currentValue = 0f;
    49.       isAlive=false;
    50.     }
    51.  
    52.     return true;
    53.   }
    54. }
     
  2. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Try composition rather than each component handling the logic. Remove the Hittable interface from the actual Health, Armor, and Shield components to turn them into data buckets. Only implement the interface on a new DamageReceiver component which will collate the data from the 3 other components and apply the necessary ordering logic.

    Code (csharp):
    1.  
    2. class DamageReceiver : MonoBehaviour, IHittable
    3. {
    4.    private Health health; // GetComponent in Awake or [SerializeField] or whatever
    5.    private Shield shield;
    6.    private Armour armour;
    7.  
    8.    public void TakeDamage(int amount)
    9.    {
    10.        if(armour != null)
    11.            amount = armour.Remove(amount); // have it return any remainder
    12.  
    13.        if(shield != null)
    14.            amount = shield.Remove(amount);
    15.  
    16.        if(amount > 0) // any left over?
    17.        {
    18.            health.Remove(amount);
    19.  
    20.            if(health.hp <= 0)
    21.                KillMe();
    22.        }
    23.    }
    24. }
    25.  
     
    Last edited: Sep 15, 2018
  3. Saucyminator

    Saucyminator

    Joined:
    Nov 23, 2015
    Posts:
    61
    Interesting! I'll have a go at it later tonight. Thanks GroZZleR.
     
  4. Saucyminator

    Saucyminator

    Joined:
    Nov 23, 2015
    Posts:
    61
    Worked great!
     
    GroZZleR likes this.