- Home /
Using interfaces as composeable plugins for a health system
I am trying to build up a health system that makes it easy to plug in standard damage behaviour like adding/removing health. I want to make it so that when the objects health reaches 0 or below then the generic health script calls custom Kill functionality. So for example, if we kill the player then the scene reloads but if we kill an enemy then I can start up an explosion particle effect. I am trying to achieve this by providing a pluggable IKillable interface to my generic health management system. I know that I can't serialize an interface and have it pluggable in the inspector so my question is what is the generally accepted way of composing objects in this way? Do I have to create a script for each killable component whose sole responsibility is to initialize the health system with custom kill functionality? Am I better off removing the kill functionality from the health system and have each killable component manage their own death trigger?
Health system for reference:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Health : MonoBehaviour
{
[SerializeField] int maxHealth;
[SerializeField] int currentHealth;
[SerializeReference] IKillable deathHandler;
void Start(){
currentHealth = maxHealth;
}
void DecreaseHealthBy(int amount){
SetHealth(currentHealth - amount);
}
void IncreaseHealthBy(int amount){
SetHealth(currentHealth + amount);
}
void SetHealth(int newCurrentHealth){
currentHealth = newCurrentHealth;
if(deathHandler != null && currentHealth <= 0){
deathHandler.Kill();
}
}
}
Answer by andrew-lukasik · May 06, 2021 at 12:01 AM
I suggest playing to Unity strengths and staying away from it's weaknesses. In other words: drop interface fields and say hi to gameObject.GetComponent<interface_name>()
(yup, it works).
IHealth.cs
public interface IHealth
{
int current { get; }
int max { get; }
void Set ( int value );
void Modify ( int change );
}
PawnType1.cs
using UnityEngine;
public class PawnType1 : MonoBehaviour, IHealth
{
[SerializeField] int _currentHealth = 100;
[SerializeField] int _maxHealth = 100;
[SerializeField] GameObject _spawnOnDeath = null;
#if UNITY_EDITOR
void OnValidate () => _currentHealth = _maxHealth;
#endif
/// PawnType2 can implement this differently
void OnDeath () => Instantiate( _spawnOnDeath , transform.position , transform.rotation );
#region ICanHasHealth
int IHealth.current => _currentHealth;
int IHealth.max => _maxHealth;
void IHealth.Set ( int value ) => _currentHealth = value;
void IHealth.Modify ( int value )
{
_currentHealth += value;
if( _currentHealth<=0 ) OnDeath();
}
#endregion
}
So then you can call these, for example, like this:
public class Spikes : MonoBehaviour
{
void OnCollisionEnter ( Collision collision )
{
var health = collision.collider.GetComponentInParent<IHealth>();
if( health!=null )
health.Modify( -10 );
}
}
That's definitely helpful and seems like a good approach thank you. So if I understand correctly you would have the OnCollisionEnter piece in its own separate script which would then be attached to the the Pawn, the Player, or anything else damageable that would provide generic collision+damage functionality?
I am still wondering about how to avoid code replication for the health management component though. It seems that with this strategy each component would have to replicate the health management code and then know to explicitly call the death function. I know that health management is a pretty basic example but I could see myself encountering this issue many times with more complicated processes as I progress with my first project. What if I had a complex algorithm that I want to attach to different enemies that varies slightly depending on the enemy? For example, a generic path finding system that requires the speed of the entity it is attached to in order to properly decide what to do?
So if I understand correctly you would have the OnCollisionEnter piece in its own separate script which would then be attached to the the Pawn, the Player, or anything else damageable that would provide generic collision+damage functionality?
No, no. This method I would put on something that does the damaging, like spikes etc:
public class Spikes : MonoBehaviour
{
void OnCollisionEnter ( Collision collision )
{
var health = collision.collider.GetComponentInParent<IHealth>();
if( health!=null )
health.Modify( -10 );
}
}
I am still wondering about how to avoid code replication (...) but I could see myself encountering this issue many times with more complicated processes as I progress with my first project. (...)
I know modularity seems very fancy and code repetition seems like the worst thing... but component-based atomic-like modularity (which requires scene cross-references) will also increase your project complexity ten times very fast and bring your productivity to a grind halt when you'll need it the most (late in the project) - the actual worst thing ever.
What I'm trying to say here is: be pragmatic and value simplicity, don't solve the problems you don't have. That's my humble advice.
That all makes a lot of sense thanks very much!
Your answer
Follow this Question
Related Questions
How to create a script reference in the inspector that can accept any class of script? 1 Answer
Getting a Reference to a Prefab Dragged into Scene 1 Answer
Unable to drop GameObject in Inspector of other GameObject 0 Answers
Script forgets assigned Inspector variables 2 Answers
Best Practices: How to handle inspector's lack of interface support? 1 Answer