Hit and Damage System with interfaces

I have been trying to use the Single Responsibility Principle, interfaces, and abstract classes to write code with good structure.

The situation I am trying to code is as follows

Objects can be hit and not damaged (ex. a wall is hit but not damaged).

Objects can be hit and damaged (ex. player is hit and damaged).

So I created two interfaces.

public interface IHittable
{
    void Hit();
}

public interface IDamageable
{
    void Damage(int damageTaken);
}

So, in the player’s case he can be hit and damaged.

Therefore, I thought a hit should drive the damage.

Which led to the following implementations of my interfaces.

public class PlayerHit : MonoBehaviour, IHittable
{
    IDamageable damageable;
    
    void Start()
    {
        damageable = (IDamageable)GetComponent(typeof(IDamageable));
        if (damageable == null)
        {
            throw new MissingComponentException("Requires an implementation of IDamageable")
        }
    }
    
    public void Hit()
    {
        damageable.Damage(/*how should I pass this in?*/);
    }
}

public class PlayerDamage : MonoBehaviour, IDamageable
{
    Stats stats;
    
    void Start()
    {
        stats = GetComponent<Stats>();
    }
    
    public void Damage(int damageTaken)
    {
        stats.health -= damageTaken;
    }
}

However, since everything that is hit isn’t also damaged I didn’t think it would be right to pass the damageTaken as an argument of Hit().

Therefore, how should I pass damageTaken to Damage()?

I thought about consolidating these two interfaces into one interface and only applying the damage if the object had health.

But I was hoping there was a better way?

I do not see any problem with your setup.

Your player is both hittable and damageable. Both interfaces are not relying on each others.

The Hit method is using the IDamageable object because it is meant to be damaged.

In the case of a wall, that can be hit but not damaged, your hit method will simply do something different.

public void Hit(){
    // Apply hit texture but no damage
    // Anyway we have no IDamageable object in here
}

what if you have a taser that deals no damage but still causes the player to play a flinching animation? what about if the player is in a poison cloud that doesn’t stunlock the player but constantly damages them over time? by having Hit call Damage you’re locking down the code.

what you would actually want to do is just have both Hit and Damage as separate calls. and even better in separate scripts. Hit() shouldn’t call Damage(), nor should Damage() call Hit(). you’ll want to keep them decoupled if you can.

have the triggering class just check if the script is IHittable and is IDamageable in separate if checks.

public class Bullet :MonoBehaviour
 {
    int damage;
    void OnTriggerEnter(Collider other)
    {
        IHittable hitScript = other.GetCompnentInParent<IHittable>();
        IDamagable damageScript = other.GetCompnentInParent<IDamagable >();

        if(hitScript != null)
        {
            hitScript.Hit();
        }

        if(damageScript != null)
        {
            damageScript .Damage(damage);
        }

        Destroy(gameObject);
    }
 }

here the bullet is fully decoupled yet can still pass the info that’s needed for the scripts catching it. if the bullet has a noticable effect on what it collided with, then it can use the Hit(). if the bullet can damage the target then it can call Damage(). they are two calls exclusive to each other and are only linked due to being called in the same onTriggerEnter event. while on the other hand the player script isn’t programed specifically to handle the bullet, the two effects have been decoupled allowing all sorts of cool scenarios to come into play.

if you want your character be even more flexible then you’d write the handler scripts for IHittable and IDamageable into separate scripts as separate components. that way you can, during runtime, remove/disable/ or decorate(cool concept, look up decorator pattern when making buffs/debuffs) the IDamageable component to make the player invincible cause he picked up a power star. and when the powerup fades you re-enable/re-add/revert that IDamageable script back to normal.

I tend to think SRP in terms of game jobs I.E. Handing damage related functionality because I'm a bit lazy and because I tend to code with YAGNI. I'd just have one abstract class that deals with Health related stuff because that is as specific as I generally need to get. What I mean by this is that when you are dealing with Damage and hitting you are generally going to have to deal with much of the same logic. The use cases when hitting and damaging aren't overlapping is generally pretty small so I'd have them fall under the same responsibility.You could just possibly rejigger have Damageable inherit from the interfaces if you like but I really don't see a need to go that deep.


    public abstract class Damageable
    {
    	public int HP;
    	public Animator Anim;
    	AudioSource audio;
    	public AudioClip LameHitSound;
    	public AudioClip HealSound;
    	public AudioClip DamageSound;
    	
    	//maybe some cooldown timer logic, particle effects, damage sprites etc.. 
    	
    	public virtual void TakeDamage(int amt)
    	{
    		HP-=amt;
    		if(HP<=0)
    		{
    			Death();
    		}
    	}
    	
    	public virtual void Hit(int amt)
    	{
    		if(amt==0)
    		{
    			//play sound effect for lame damage
    			audio.PlayOneShot(LameHitSound, 0.7F);
    			Anim.SetTrigger("TakeLittleDamage");
    		}
    		else if(amt<0)
    		{
    			//play effects and such for healing
    			audio.PlayOneShot(HealSound, 0.7F);
    			Anim.SetTrigger("Heal");
    		}
    		else
    		{
    			//shake screen, preform screen shake and other effects for damage
    			audio.PlayOneShot(DamageSound, 0.7F);
    			Anim.SetTrigger("TakeSolidDamage");
    		}
    	}
    	
    	public abstract void Death();
    }
    
    //and Inherit from it 
    public class PlayerHealth : Damageable
    {
    	public override void Death()
    	{
    		//Call Game Over logic
    	}
    	
    } 
    //and override as you need
    public class EnemyHealth : Damageable
    {
    	public override void Death()
    	{
    		//play death anim, set death variables
    	}
    }
    
    public class BoxHealth : Damageable
    {
    	void Start(){
    		HP = 1;
    	}
    	public override void Death()
    	{
    		//Provide player with Item;
    	}
    	public override void Hit(int amt)
    	{
    		if(amt>0)
    		{
    			audio.PlayOneShot(DamageSound, 0.7F);
    			Anim.SetTrigger("TakeSolidDamage");	
    		}
    	}
    }

I hope this helps.

Another option is to pass in an IDamageDealer (from the weapon) to the Hit function. Passing the damage amount alone maybe doesn’t make sense conceptually, but passing in the thing that hit it does.

public void Hit(IDamageDealer damageDealer)
{
    damageable.Damage(damageDealer.Amount);

    Debug.Log(damageDealer.Name + "hit me for " + damageDealer.Amount + " damage");
}

Also, look into the new UnityEvent variable. And sometimes it’s just easier to use components for this type of thing.

Late answer, but an answer nonetheless.