- Home /
Does polymorphism apply here?
I am brushing up on my OOP knowledge and want to employ good practices in my Unity project. Let's think about an example scenario:
Damage dealing objects have a component that inherits from an OnHit class.
A fire sword has a OnHitFireSword component, that burns the enemies for example.
The enemies will have a function that takes an OnHit (the base class) object as an argument.
This way I only have one simple function in my enemy script, and each damage dealing object has their own component that handles specific interactions. Does this approach make sense?
In my previous project I had different tags attached to my damage dealing objects, and the enemy script had a separate if statement for each collider tag, which got out of hand very quickly.
Answer by Xarbrough · Mar 10, 2018 at 02:44 AM
I would think about the system like this:
There are damageable objects, which store data to indicate their health or related properties, e.g. players, NPCs or destructible environment objects. These do not know anything about weapons.
There are weapons ('damage dealers' maybe?), which apply damage points or related effects to the damageable objects. Hence, weapons know about their damaging effects and the objects which to affect.
In a simple case, this looks like this:
public interface IDamageable
{
void ChangeHealth(int amount);
}
public class PlayerHealth : MonoBehaviour, IDamageable
{
private int healthPoints;
public void ChangeHealth(int amount)
{
healthPoints += amount;
}
}
public class DamageDealer : MonoBehaviour
{
public int damage = 5;
private void OnTriggerEnter(Collider other)
{
IDamageable damageable = other.GetComponent<IDamageable>();
if (damageable != null)
{
damageable.ChangeHealth(-damage);
}
}
}
A lot of situations can be modelled this way. If the weapon applies fire damage over time, it could either store a reference to all affected entities and apply more damage to them at every tick, or it could add a special kind of object to the affected IDamageable which ticks on its own (think poison). These decisions depend on the needs of your game.
A fire sword know how to apply its effects, this makes sense, however enemies should not know about damage dealers. It may be ok to abstract this by using only a single abstract base class, but then why write this code in the enemy class if it's only ever going to call onHit.DoSomething()? Instead, let the weapon call whatever code it needs through the interface of the affect IDamageable. This gives more flexibility.
For example:
public interface IDamageable
{
void ChangeHealth(int amount);
Transform GetEffectAttachPoint();
}
public class PlayerHealth : MonoBehaviour, IDamageable
{
private int healthPoints;
public void ChangeHealth(int amount)
{
healthPoints += amount;
}
public Transform GetEffectAttachPoint()
{
return transform;
}
}
public class FireSword : MonoBehaviour
{
public int damagePerSecond = 1;
public float burnDuration = 3f;
public GameObject particleFXPrefab;
private List<IDamageable> affectedEntities = new List<IDamageable>();
private void OnTriggerEnter(Collider other)
{
IDamageable damageable = other.GetComponent<IDamageable>();
if (damageable != null)
{
damageable.ChangeHealth(-damagePerSecond);
Instantiate(particleFXPrefab, damageable.GetEffectAttachPoint());
affectedEntities.Add(damageable);
}
}
private void Update()
{
// This is totally wrong, but you can do this correctly
// by looking through all entities and counting down a timer for each one
// then applying damage every x seconds or a small portion of the damage each frame etc.
foreach (var entity in affectedEntities)
entity.ChangeHealth(-damagePerSecond);
// Remove the entities from the list when their timers are done.
}
}
If this works, no reason to change it. However, your game might need to separate each component into even more components. For example, if the sword gets destroyed, the burning effect should continue. In this case, we need to separate a StatusEffect object from the weapon:
public interface IDamageable
{
void ChangeHealth(int amount);
Transform GetEffectAttachPoint();
}
public class StatusEffect : MonoBehaviour
{
public int damage;
// per second etc.
public GameObject particleFXPrefab;
private void Update()
{
// every x seconds
GetComponent<IDamageable>().ChangeHealth(-damage);
// if timer is done
Destroy(this);
}
}
public class PlayerHealth : MonoBehaviour, IDamageable
{
private int healthPoints;
public void ChangeHealth(int amount)
{
healthPoints += amount;
}
public Transform GetEffectAttachPoint()
{
return transform;
}
}
public class FireSword : MonoBehaviour
{
public StatusEffect statusEffectPrefab;
private void OnTriggerEnter(Collider other)
{
IDamageable damageable = other.GetComponent<IDamageable>();
if (damageable != null)
{
GameObject go = damageable.GetEffectAttachPoint().gameObject;
// Apply the effect.
// Here, I simply add a new component and copy the values from a prefab.
// Later, the effect can remove itself.
var effect = go.AddComponent<StatusEffect>();
effect.damage = statusEffectPrefab.damage;
effect.particleFXPrefab = statusEffectPrefab.particleFXPrefab;
// Alternatively, you can create an EffectRunner component, which
// takes some e.g. ScriptableObject data as input to add, update and remove after a set time.
}
}
}
There are a lot of possible variations to these systems, especially if there are multiple effects involved. I've once created a system where each player (PlayerHealth component) also had a StatusEffectRunner, which was a class that simple holds a list of effects which need to be updated. Each effect can remove itself again via a delegate that was passed when the effect was added.
The takeaways are:
Entities hold data, e.g. health or visuals, which represent a state and do not know anything about other system's logic like weapons.
Weapons are active systems which access other entities through an interface (so that a fire sword can potentially deal damage to a brick wall one day).
Interfaces are contracts of functionality. This can be misleading. If you say "Damageable.ApplyStatusEffect(effect)", it looks like the damageable object fullfills a contract of the functionality of being able to have a status effect, but this would mean that the damageable knows what to do with the effect, which it shouldn't, because it would have to change everytime a new effect is added. Instead we can invert the dependency and let the interface provide functionality, which belongs in its domain, e.g. "ChangeHealth" or "GetVisualAttachPoint". You can argue about if those two examples even belong together, or both should be separate, because one is "real state data", whereas the other is only part of the visualisation of such state.
Coming back to the original question: I don't thank that polymorphism is the solution to your problem. Instead, use components and well-designed dependencies. You can still use base classes to share common functionality like StatusEffect - FireEffect/PoisonEffect.
These were just some ideas based on my previous experiences in projects, but hopefully they fit your use-case.