Much simpler alternative for disabling the healthbar

I’m assuming this lecture discussed and overcame floating point imprecision for the sake of teaching it, because this solution to me was unnecessarily complex for such a simple issue.

Some of my code is different from Sam’s but basically have bool that is changed and based on whether the character is alive or not:

        bool _isDead = false;
        public bool IsDead => _isDead;

Then in the healthbar.cs you can use:

  if (_health.IsDead)
            {
                gameObject.SetActive(false);
            }

You can also follow a long my alternative of using an event instead of unity event when taking damage:

And then update the healthbar based on the event:

 public class HealthBar : MonoBehaviour
    {
        [SerializeField] Health _health = null;
        [SerializeField] RectTransform _healthBarForeground = null;

        const int _turnPercentageIntoFraction = 100;
        private void OnEnable()
        {
            _health.OnTakeDamage += UpdateHealthBar;
        }
        private void Start()
        {
            ScaleHealthBarBasedOnHP();
        }
        void UpdateHealthBar(float damage)
        {
            ScaleHealthBarBasedOnHP();
        }
        private void ScaleHealthBarBasedOnHP()
        {
            _healthBarForeground.localScale = new Vector3(
                _health.CurrentPercentageFromMax() / _turnPercentageIntoFraction, 1, 1);
            if (_health.IsDead)
            {
                gameObject.SetActive(false);
            }
        }

        private void OnDisable()
        {
            _health.OnTakeDamage -= UpdateHealthBar;
        }
    }

Hi,

On the HealthComponent there is already a function IsDead, so why not use it like this?

using UnityEngine;

namespace RPG.Attributes
{
    public class HealthBar : MonoBehaviour
    {
        [SerializeField] private Health healthComponent;
        [SerializeField] private RectTransform foreground;
        [SerializeField] private Canvas rootCanvas;

        private void Update()
        {
            if (healthComponent.IsDead()) rootCanvas.enabled = false;
            foreground.localScale = new Vector3(healthComponent.GetFraction(), 1.0f, 1.0f);
        }
    }
}

Bert

Ultimately, we want to take these sorts of things out of the hands of the Update() loop, as too many active update loops can start to have an effect on game performance.
Once we get to Events and Delegates, we can make HealthBar a reactive lightweight script with little or no impact on performance…

Here’s my approach: In Health.cs, I add the following two events:

public event System.Action OnHealthChanged;
public event System.Action OnDeath;

and in TakeDamage(), if the character survives, I call

OnHealthChanged?.Invoke();

and if the character dies, I call

OnDeath?.Invoke();

So here’s my modified HealthBar.cs

using UnityEngine;
namespace RPG.Attributes;
{
    public class HealthBar : MonoBehaviour
    {
        [SerializeField] private Health healthComponent;
        [SerializeField] private RectTransform foreground;
        [SerializeField] private Canvas rootCanvas;
 
        private void Awake()
        {
              healthComponent.OnHealthChanged+=OnHealthChanged;
              healthComponent.OnDeath += OnDeath;
        }
   
        private void OnHealthChanged()
        {
              foreground.localscale = new Vector3(healthComponent.GetFraction(), 1.0f, 1.0f);
        }
        private void OnDeath()
        {
              rootCanvas.enabled=false;
        }
   }
}

Now there is no update loop, no constant calculation of GetFraction() or checking to see if the character is alive. Whenever these things change, the script simply reacts.
Note that you also will need to call OnHealthChanged?.Invoke() when the character heals or levels up as well.

4 Likes

Thanks for this, I much prefer this approach! Just a quick question relating to whether or not it is a good idea to invoke the onHealthUpdated event in Start for Health.cs.

using RPG.Core;
using RPG.Stats;
using RPG.Saving;
using GameDevTV.Utils;
using UnityEngine;
using UnityEngine.Events;
using System;

namespace RPG.Attributes
{
    // Health is placed in Attributes directory to avoid circular dependecy that could occur when placed in Core namespace
    public class Health : MonoBehaviour, ISaveable
    {
        [SerializeField] float regenerationPercentage = 70f; // % of health that is added when this levels up
        [SerializeField] TakeDamageEvent takeDamage;
        [SerializeField] UnityEvent onDied;

        /* This is used because cannot Unity cannot serialize UnityEvent<float> (classes with chevrons). Select Dynamic float in
        Unity Editor to allow the float passed into this event to be used in functions triggered by 
        this event*/ 
        [System.Serializable]
        public class TakeDamageEvent : UnityEvent<float>
        {
        }

        public event Action onHealthUpdated;
        public event Action onNoHealthLeft;

        /* Setting healthpoints as type LazyValue<float> ensures that healthPoints are 
        set to something in Awake so it is not null and set its value when it is first used*/
        LazyValue<float> healthPoints; 
        bool hasDied;
        
        private void Awake() 
        {
            healthPoints = new LazyValue<float>(GetInitialHealth);
        }

        private float GetInitialHealth()
        {
            return GetComponent<BaseStats>().GetStat(Stat.Health);
        }

        private void Start() 
        { 
            healthPoints.ForceInit(); // Initialise healthpoints if it has not already been before Start() is called
            onHealthUpdated.Invoke();
        }

        private void OnEnable() 
        {
            GetComponent<BaseStats>().onLevelUp += RegenerateHealth; // Subscibe to onLevelUp event in OnEnable  
        }

        private void OnDisable() 
        {
            GetComponent<BaseStats>().onLevelUp -= RegenerateHealth;  // Unsubscribe to onLevelUp event in OnDisable  
        }

        public bool IsDead()
        {
            return hasDied;
        }

        /* sourceOfDamage passed in as Parameter to determine how much experience reward
         the sourceOfDamage should receive from this*/
        public void TakeDamage(GameObject sourceOfDamage, float damage)
        {
            healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);
            
            if (healthPoints.value == 0)
            {
                onNoHealthLeft.Invoke();
                onDied.Invoke(); // Trigger all the functions in onDied UnityEvent (e.g audio sound etc.)
                Die();
                RewardExperience(sourceOfDamage);
            }
            else
            {
                onHealthUpdated.Invoke();
                takeDamage.Invoke(damage); // Trigger all the functions in takeDamage UnityEvent (e.g spawn damage text etc.)
            }
        }

        public float GetHealth()
        {
            return healthPoints.value;
        }

        public float GetMaxHealth()
        {
            return GetComponent<BaseStats>().GetStat(Stat.Health);
        }

        public float GetPercentage()
        {
            return (healthPoints.value / GetComponent<BaseStats>().GetStat(Stat.Health)) * 100;
        }

        public float GetFraction()
        {
            return healthPoints.value / GetComponent<BaseStats>().GetStat(Stat.Health);
        }

        private void Die()
        {
            // If target health is already dead, do nothing
            if (hasDied) return;

            hasDied = true;

            // Trigger death animation
            GetComponent<Animator>().SetTrigger("die");

            // Cancel any action (Fighter, Mover etc) that has been set 
            GetComponent<ActionScheduler>().CancelCurrentAction();
        }
        
        private void RewardExperience(GameObject sourceOfDamage)
        {
            Experience experience = sourceOfDamage.GetComponent<Experience>();
            if (experience == null) return;

            experience.GainExperience(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward));
        }

        private void RegenerateHealth()
        {
            float regenHealthPoints = GetComponent<BaseStats>().GetStat(Stat.Health) * (regenerationPercentage / 100);
            healthPoints.value = Mathf.Max(healthPoints.value, regenHealthPoints);
            onHealthUpdated.Invoke();
        }

        public void Heal(float healthToRestore)
        {
             healthPoints.value = Mathf.Min(healthPoints.value + healthToRestore, GetMaxHealth());
             onHealthUpdated.Invoke();
        }

        public object CaptureState()
        {
            return healthPoints.value;
        }

        public void RestoreState(object state)
        {
            healthPoints.value = (float)state;

            if (healthPoints.value == 0)
            {
                Die();
            }
        }
    }
}

I think I have called invoke in all the places where health should be updated. One thing I noticed was that the character health bar will not appear at 100% if you don’t invoke the HealthUpdated event in Start. I tried it in Awake but that did not work either because I am assuming the health value is not set up then (also curious about this)?

The correct place for this is definitely in Start(). I would also add this in RestoreState()

Privacy & Terms