0
\$\begingroup\$

I want to implement a damage system that works across both living items (Entity) and environment items (Environment). The key components of this is that I need to be able to trigger something to take damage from another script, be it a collision with a projectile or a blast radius trigger, etc.

The two approaches I see involve interfaces or inheritance (or maybe a mix). While the inheritance appears necessary to copy the similar code between all items from a base class, it does lead to some issues where only the parent class method is called (defined in collision example). Although interfaces would solve this, I would also wind up duplicating the common code that inheritance helps with.

Obviously this issue is much deeper than a damage system, but it was the best example I could think of that illustrated this problem.

TL;DR: I want to create a system of classes/interfaces that enable the following:

When a projectile collides with an damageable item, it will trigger the "Take Damage" method. This item may be an Entity or an Environment item. Most items will likely take damage differently, but will share a common (yet extendable) death. Colliding with an Entity would trigger the entities subclass method, while colliding with an Environment item would trigger the environment item's subclass method.

Collision (Problem)

This underlying issue (calling specific subclass methods through cast parent class/interface) is most noticeable in the Projectile collision script. As noted in the script comments, I understand that since the Entity class is what implements the IDamageable interface, it is what is returned.

I'm also assuming that if I moved the IDamageable interface to each item that inherited from Entity it would then be called on the subclasses? But in that case I would have to implement the method separately each time, even if there was duplicate code. At the same time, that would enable some entities to take damage differently than others (ie. if wearing armour, etc)...

public void Projectile : MonoBehaviour {
    private float damage = 5f;

    private void OnCollisionEnter(Collision collider) {
        // Check if on layer

        // This properly returns the colliding object, but it currently returns the base
        //   class, "Entity" for example (makes sense).
        IDamageable damageableObject = collider.gameObject.GetComponent<IDamageable>();
        if (damageableObject != null) {
            // Now I totally get why this is calling the "TakeHit" method of "Entity", but I
            //    am searching for a way to call the "TakeHit" of the "Enemy" itself.
            //    Is this only possible by checking the type? If so, isn't that losing the point
            //    of the interface?
            damageableObject.TakeHit(damage, collider);
        }
    }
}

Code

Both entities and environment items can be damageable (ie. entity death or environment destruction), which (to me) requires an interface to make the projectile collision (and other) system easier.

Should the interface be applied to just Entity and Environment, or to each subclass?

public interface IDamageable {
    void TakeHit(float damage, Collision collider);

    // Future method that only deals damage (no collision)
    //void TakeDamage(float damage);
}

Entitys all have health (and some other common attributes), as well as a shared but extendable (through inheritance) death mechanic.

How do I properly handle the relationship between taking damage and dying, if one is an interface and the other an overridable base class method?

public class Entity : MonoBehaviour, IDamageable {
    public float Health = 10f;
    public bool IsAlive = true;

    // Implement IDamageable (this is what gets called if using interface, but I want child class method to be called)
    // Unsure if it should be common method, what if children may check for armor,
    //    etc, to reduce damage taken?
    public void TakeHit(float damage, Collision collider) {
        Health -= damage;

        // It's nice having a shared "base" die method, as I don't have to replicate this
        //    in each class that implements the interface (and then not sure where "Die"
        //    method would be either, probably on interface which means more extending)
        if (Health <= 0 && IsAlive) {
              Die();
        }
    }

    // All entities should set IsAlive to false, which is why it is here
    protected virtual void Die() {
         IsAlive = false;

        // Child objects should handle destroying themselves (particle effects, etc).
        //Destroy(gameObject);
    }
}

Enemys are basically Entity with pathfinding and simple attacks, but also a more customized death.

public class Enemy : Entity {
    // Death should play particle effects, sound, and destroy object
    protected override Die() {
        base.Die();

        // Do particle effects and sound
        // Destroy game object
    }
}

Player is obviously way more complicated than a regular Entity, but also has a more customized death.

public class Player : Entity {
    // Death should end the game, play sound, and destroy object
    protected override Die() {
        base.Die();

        // Play sound
        // End game
        // Destroy game object
    }
}

Please note, I am struggling with how to represent this best, let me know if it needs clarity in an area.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

In Unity, I'd generally recommend working with the principle of composition over inheritance.

In this case, if player characters, enemies, and environment objects all need to take damage, then damage-ability doesn't say much about what the thing is, it's a behaviour each of these things has. So we can model it as a component attached to GameObjects of all these types.

This component can handle the shared needs of tracking HP and dying, and expose hooks to attach more specialized behaviours. Here I've shown an example using Unity Events, so you can use the Inspector or scripts to wire up a receiver to act on damage or death events, regardless of the type of script it's defined in.

Attached on its own, this will let you create destructible objects, and it can be freely mixed with other components to create more specialized behaviours without duplicating the core HP/death logic.

public class Damageable : MonoBehaviour {
    [System.Serializable]
    public class DamageEvent : UnityEvent<float> {}

    public float Health = 10f;
    public bool IsAlive = true;

    public DamageEvent OnDamage;
    public UnityEvent OnDeath;    


    public void TakeDamage(float damageAmount) {
        if(!IsAlive)
            return;

        if(OnDamage != null)
            OnDamage.Invoke(damageAmount);

        Health -= damageAmount;
        if(Health <= 0f)
            Die();
    }

    public void Die() {
        if(!IsAlive)
            return;

        Health = 0f;
        IsAlive = false;

        if(OnDeath != null)
           OnDeath.Invoke();

        Destroy(gameObject);
    }
}
\$\endgroup\$
8
  • \$\begingroup\$ Interesting, how would another component in the same object react? Would it be something like get the damage able component if it exists, then subscribe to the event? Is there anyway of guaranteeing (other than require component)? Additionally, could I move the health to a Vitals component and have it react through these events (when taking damage)? \$\endgroup\$
    – Kendall
    Commented Dec 16, 2018 at 0:55
  • \$\begingroup\$ Yes, if the other script needs a Damageable component then it makes sense to use [RequireComponent]. You can either wire-up your handler routines in Start() or do it at edit time in the Inspector so there's no runtime fiddling to do. What's great is you can even have multiple listeners not necessarily on the same object - so your level manager script that sits outside the player character can also subscribe to its death event and trigger the game over behaviour accordingly, freeing up your scripts on the character itself to focus on character control, not game mode logic. \$\endgroup\$
    – DMGregory
    Commented Dec 16, 2018 at 0:59
  • \$\begingroup\$ You can certainly define health wherever you want to define it, though I'd argue that the concepts of "has health" and "can take damage" are so linked that they do well being one shared script. Are there specific cases you can see where you need divergent behaviour between health and damage-taking? \$\endgroup\$
    – DMGregory
    Commented Dec 16, 2018 at 1:01
  • \$\begingroup\$ Yeah, I've seen references to this pattern but never understood (may not now either, but something to go on). Using the inspector I haven't heard of, not a big fan of that but may look into it. I'll give a go and see what happens (will come back to question then) \$\endgroup\$
    – Kendall
    Commented Dec 16, 2018 at 1:04
  • \$\begingroup\$ It may grow on you. Leveraging the Unity editor features like the Inspector can help you make behaviours more modular & flexible, so you need less special-case code, and can wring more use & variety out of the code you have. If you're working on a team - particularly with non-coders - this can help more members of the team work efficiently in parallel, without getting bottlenecked on code changes or shared dependencies. It lets the data do data's job (specifying the particulars) while the code does code's job (defining the rules). \$\endgroup\$
    – DMGregory
    Commented Dec 16, 2018 at 1:09

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .