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 anEnvironment
item. Most items will likely take damage differently, but will share a common (yet extendable) death. Colliding with anEntity
would trigger the entities subclass method, while colliding with anEnvironment
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);
}
Entity
s 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);
}
}
Enemy
s 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.