Forum begins after the advertisement:


[General] Adding Fade and Shrink Effect to HP Bar

Home Forums Video Game Tutorial Series Creating a Rogue-like Shoot-em Up in Unity [General] Adding Fade and Shrink Effect to HP Bar

  • This topic has 3 replies, 2 voices, and was last updated 1 week ago by Kai.
Viewing 4 posts - 1 through 4 (of 4 total)
  • Author
    Posts
  • #16062
    Kai
    Participant
    Helpful?
    Up
    0
    ::

    I am having problems trying to implement fading and shrinking effect to player’s hp bar to show the amount of damage he took by having a white bar between the border and the hp bar.
    When player took damage, the white bar will have its fill amounts alpha increase to 1 then decrease toward 0 after some time, also when player is hit twice in quick succession, the effect will stack and show as a single white bar.

    View post on imgur.com

    #16063
    Kai
    Participant
    Helpful?
    Up
    0
    ::

    The problem is the effect only work if player is hit twice in quick succession not a single hit from an enemy.
    HealthBarFade.cs

    using UnityEngine;
    using UnityEngine.UI;
    
    public class HealthBarFade : MonoBehaviour
    {
        private Image barImage;            // The normal health bar image reference
        private Image damagedBarImage;     // The damaged health bar image reference
        private Color damagedColor;        // Color for the damaged bar
        private const float DAMAGED_HEALTH_FADE_TIMER_MAX = 1f;  // Time before fade starts
        private float damagedHealthFadeTimer;  // Timer for fading effect
    
        private void Awake()
        {
            // Find the two bar image components
            barImage = transform.Find("bar").GetComponent<Image>();
            damagedBarImage = transform.Find("damagedBar").GetComponent<Image>();
    
            // Initialize damaged bar color to be fully visible (alpha = 1)
            damagedColor = damagedBarImage.color;
            damagedColor.a = 0f;
            damagedBarImage.color = damagedColor;
        }
    
        private void Update()
        {
            // Start fading if the damaged bar's alpha is greater than 0
            if (damagedColor.a > 0)
            {
                damagedHealthFadeTimer -= Time.deltaTime;
    
                // Once the fade timer is up, start reducing the alpha value
                if (damagedHealthFadeTimer < 0)
                {
                    float fadeAmount = 1f;  // Fade speed
                    damagedColor.a -= fadeAmount * Time.deltaTime;
                    damagedBarImage.color = damagedColor;
                }
            }
    
            // Sync the damaged bar with the main bar, but make sure it lags behind
            if (damagedBarImage.fillAmount > barImage.fillAmount)
            {
                float shrinkSpeed = 0.05f; // Controls how fast the damaged bar shrinks towards the actual health bar
                damagedBarImage.fillAmount -= shrinkSpeed * Time.deltaTime;
            }
        }
    
        public void SetHealth(float healthNormalized)
        {
            // Update the main health bar to the current health value
            barImage.fillAmount = healthNormalized;
    
            // If the damaged bar is not already fading, sync it with the main bar
            if (damagedColor.a <= 0)
            {
                damagedBarImage.fillAmount = barImage.fillAmount;
            }
    
            // Reset the alpha for the damaged bar (fully visible)
            damagedColor.a = 1f;
            damagedBarImage.color = damagedColor;
    
            // Reset the fade timer
            damagedHealthFadeTimer = DAMAGED_HEALTH_FADE_TIMER_MAX;
        }
    }
    

    SetHealth() is called in TakeDamage() function in PlayerStats.cs

    using System;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using TMPro;
    using System.Collections;
    
    public class PlayerStats : EntityStats
    {
    
        CharacterData characterData;
        public CharacterData.Stats baseStats;
        [SerializeField] CharacterData.Stats actualStats;
    
        public CharacterData.Stats Stats
        {
            get { return actualStats; }
            set
            {
                actualStats = value;
            }
        }
    
        public CharacterData.Stats Actual
        {
            get { return actualStats; }
        }
    
        #region Current Stats Properties
        
        public float CurrentHealth
        {
    
            get { return health; }
    
            // If we try and set the current health, the UI interface
            // on the pause screen will also be updated.
            set
            {
                //Check if the value has changed
    
                if (health != value)
                {
    
                    health = value;
                   UpdateHealthBar();
    
                }
            }
        }
        #endregion
    
        [Header("Visuals")]
        public ParticleSystem damageEffect; // If damage is dealt.
        public ParticleSystem blockedEffect; // If armor completely blocks damage.
        public GameObject levelUpEffect; //If player level up
    
        //Experience and level of the player
        [Header("Experience/Level")]
        public int experience = 0;
        public int level = 1;
        public int experienceCap;
    
        //Class for defining a level range and the corresponding experience cap increase for that range
        [System.Serializable]
        public class LevelRange
        {
            public int startLevel;
            public int endLevel;
            public int experienceCapIncrease;
        }
    
        //I-Frames
        [Header("I-Frames")]
        public float invincibilityDuration;
        float invincibilityTimer;
        bool isInvincible;
    
        public List<LevelRange> levelRanges;
    
        PlayerInventory inventory;
        PlayerCollector collector;
    
        [Header("UI")]
    
        public HealthBarFade healthBarFade;
        public Image healthBar;
        public Image expBar;
        public TMP_Text levelText;
    
        
    
        void Awake()
        {
            characterData = CharacterSelector.GetData();
            if (CharacterSelector.instance)
                CharacterSelector.instance.DestroySingleton();
    
            inventory = GetComponent<PlayerInventory>();
            collector = GetComponentInChildren<PlayerCollector>();
    
            //Assign the variables
            baseStats = actualStats = characterData.stats;
            collector.SetRadius(actualStats.magnet);
            health = actualStats.maxHealth;
    
            //Assign the Animator controller to the existing Animator component
            Animator animator = GetComponent<Animator>();
            if (animator != null)
            {
                animator.runtimeAnimatorController = characterData.AnimatorController;
            }
    
            
            
        }
    
        protected override void Start()
        {
    
            //Spawn the starting weapon
            inventory.Add(characterData.StartingWeapon);
    
            //Initialize the experience cap as the first experience cap increase
            experienceCap = levelRanges[0].experienceCapIncrease;
    
            GameManager.instance.AssignChosenCharacterUI(characterData);
    
            UpdateHealthBar();
            UpdateExpBar();
            UpdateLevelText();
        }
    
        protected override void Update()
        {
            base.Update();
            if (invincibilityTimer > 0)
            {
                invincibilityTimer -= Time.deltaTime;
            }
            //If the invincibility timer has reached 0, set the invincibility flag to false
            else if (isInvincible)
            {
                isInvincible = false;
            }
            
    
            Recover();
        }
    
        public override void RecalculateStats()
        {
            actualStats = baseStats;
            foreach (PlayerInventory.Slot s in inventory.passiveSlots)
            {
                Passive p = s.item as Passive;
                if (p)
                {
                    actualStats += p.GetBoosts();
                }
            }
    
            // Create a variable to store all the cumulative multiplier values.
            CharacterData.Stats multiplier = new CharacterData.Stats
            {
                maxHealth = 1f,
                recovery = 1f,
                armor = 1f,
                moveSpeed = 1f,
                might = 1f,
                area = 1f,
                speed = 1f,
                duration = 1f,
                amount = 1,
                cooldown = 1f,
                luck = 1f,
                growth = 1f,
                greed = 1f,
                curse = 1f,
                magnet = 1f,
                revival = 1
            };
            // We have to account for the buffs from EntityStats as well.
            foreach (Buff b in activeBuffs)
            {
                BuffData.Stats bd = b.GetData();
                switch (bd.modifierType)
                {
                    case BuffData.ModifierType.additive:
                        actualStats += bd.playerModifier;
                        break;
                    case BuffData.ModifierType.multiplicative:
                        multiplier *= bd.playerModifier;
                        break;
                }
            }
            actualStats *= multiplier;
    
            // Update the PlayerCollector's radius.
            collector.SetRadius(actualStats.magnet);
        }
    
        public void IncreaseExperience(int amount)
        {
            experience += amount;
    
            LevelUpChecker();
            UpdateExpBar();
        }
    
        void LevelUpChecker()
        {
            if (experience >= experienceCap)
            {
                //Level up the player and reduce their experience by the experience cap
                level++;
                experience -= experienceCap;
    
                // Play the level up effect if it exists
                if (levelUpEffect != null)
                {
                    // Activate the level up effect GameObject
                    levelUpEffect.SetActive(true);
    
                    Animator levelUpAnimator = levelUpEffect.GetComponent<Animator>();
                    if (levelUpAnimator != null)
                    {
                        // Trigger the level up animation
                        levelUpAnimator.SetTrigger("LevelUp");
                    }
                    
                }
                
                //Find the experience cap increase for the current level range
                int experienceCapIncrease = 0;
                foreach (LevelRange range in levelRanges)
                {
                    if (level >= range.startLevel && level <= range.endLevel)
                    {
                        experienceCapIncrease = range.experienceCapIncrease;
                        break;
                    }
                }
                experienceCap += experienceCapIncrease;
    
                UpdateLevelText();
    
                // Start a coroutine to wait for a few seconds before switching to the StartLevelUp state
                StartCoroutine(DelayedStartLevelUp());
    
                // If the experience still exceeds the experience cap, level up again.
                if (experience >= experienceCap) LevelUpChecker();
            }
        }
    
        IEnumerator DelayedStartLevelUp()
        {
            // Wait for a few seconds to allow the level up effect to play
            yield return new WaitForSeconds(1.5f); // Adjust the duration as needed
    
            // Switch to the StartLevelUp game state
            GameManager.instance.StartLevelUp();
        }
    
        void UpdateExpBar()
        {
            // Update exp bar fill amount
            expBar.fillAmount = (float)experience / experienceCap;
        }
    
        public void UpdateLevelText()
        {
            // Update level text
            levelText.text = $"{level}";
        }
    
        public override void TakeDamage(float dmg)
        {
           
            //If the player is not currently invincible, reduce health and start invincibility
            if (!isInvincible)
            {
                
                dmg -= actualStats.armor;
    
                if (dmg > 0)
                {
                   
                    Debug.Log("Player took damage: " + dmg);
                    // Deal the damage.
                    CurrentHealth -= dmg;
                   
                    healthBarFade.SetHealth(CurrentHealth / actualStats.maxHealth);
    
                    invincibilityTimer = invincibilityDuration;
                    isInvincible = true;
                    // If there is a damage effect assigned, play it.
                    if (damageEffect) Destroy(Instantiate(damageEffect, transform.position, Quaternion.identity), 5f);
    
                    if (CurrentHealth <= 0)
                    {
                        Kill();
                    }
                }
                else
                {
                    // If there is a blocked effect assigned, play it.
                    if (blockedEffect) Destroy(Instantiate(blockedEffect, transform.position, Quaternion.identity), 5f);
                }
    
            }
        }
    
        void UpdateHealthBar()
        {
            //Update the health bar
            healthBar.fillAmount = CurrentHealth / actualStats.maxHealth;
            
        }
    
        public override void Kill()
        {
            if (!GameManager.instance.isGameOver)
            {
                GameManager.instance.AssignLevelReachedUI(level);
                GameManager.instance.GameOver();
            }
        }
    
        public override void RestoreHealth(float amount)
        {
            // Only heal the player if their current health is less than their maximum health
            if (CurrentHealth < actualStats.maxHealth)
            {
                CurrentHealth += amount;
    
                // Make sure the player's health doesn't exceed their maximum health
                if (CurrentHealth > actualStats.maxHealth)
                {
                    CurrentHealth = actualStats.maxHealth;
                }
            }
        }
    
        void Recover()
        {
            if (CurrentHealth < actualStats.maxHealth)
            {
                CurrentHealth += Stats.recovery * Time.deltaTime;
    
                // Make sure the player's health doesn't exceed their maximum health
                if (CurrentHealth > actualStats.maxHealth)
                {
                    CurrentHealth = actualStats.maxHealth;
                }
            }
        }
    }
    #16073
    Terence
    Keymaster
    Helpful?
    Up
    0
    ::

    The issue where the white component of the health bar only triggers when hit twice is likely due to how the SetHealth() function and the alpha logic for the damagedBarImage are interacting.

    In the Update() method, you only start the fading effect if damagedColor.a > 0. However, the fade timer (damagedHealthFadeTimer) only resets and the damaged bar’s alpha value is set to 1 when SetHealth() is called. This happens right after the first health hit, but the fading effect hasn’t had enough time to complete since it’s triggered immediately after.

    The damagedBarImage.fillAmount is only updated when damagedColor.a <= 0, which means it only fully resets after the fade completes. The first time you reduce health, the damaged bar’s alpha is reset to 1 and the fill amount is synced with the normal health bar. However, if you hit the health bar a second time before the fading effect finishes, the second hit applies correctly since the alpha has dropped enough to allow the sync.

    You can adjust the behavior so that the damaged bar triggers correctly on the first hit by modifying the logic to better handle the initial health reduction. You want the damaged bar to shrink but not wait for a second hit to take effect. Here’s an updated approach:

    public void SetHealth(float healthNormalized)
    {
        // Update the main health bar to the current health value
        barImage.fillAmount = healthNormalized;
    
        // Only update the damaged bar if its alpha is currently 0 (meaning it's not visible)
        if (damagedColor.a <= 0)
        {
            damagedBarImage.fillAmount = barImage.fillAmount;
        }
        else if (damagedBarImage.fillAmount > barImage.fillAmount)
        {
            // Sync the damaged bar immediately with the main bar's value on the first hit
            damagedBarImage.fillAmount = Mathf.Max(barImage.fillAmount, damagedBarImage.fillAmount);
        }
    
        // Reset the alpha for the damaged bar (fully visible) and the fade timer
        damagedColor.a = 1f;
        damagedBarImage.color = damagedColor;
        damagedHealthFadeTimer = DAMAGED_HEALTH_FADE_TIMER_MAX;
    }
    

    damagedBarImage.fillAmount is immediately synced with the main health bar during the first hit, so the white damage bar should react on the first hit instead of waiting for a second.

    #16075
    Kai
    Participant
    Helpful?
    Up
    1
    ::

    Hi Terence,
    Thanks for addressing the issue, I managed to fix it by initializing alpha to 1f at the awake function and started from there.

    HealthBarFade.cs

    using UnityEngine;
    using UnityEngine.UI;
    
    public class HealthBarFade : MonoBehaviour
    {
        private Image barImage;            // health bar image reference
        private Image damagedBarImage;     // damaged health bar image reference
        private Color damagedColor;        // color of the damaged bar
        private float damagedHealthFadeTimer;  // timer for fading effect
        [SerializeField] private float fadeSpeed = 1f;  // adjustable fade speed
        [SerializeField] private float shrinkSpeed = 1f;  // adjustable shrink speed for the damaged bar
        [SerializeField] private float damagedHealthFadeDuration = 1f; // adjustable fade duration for the damaged bar
    
        private void Awake()
        {
            // Find the two bar image components
            barImage = transform.Find("bar").GetComponent<Image>();
            damagedBarImage = transform.Find("damagedBar").GetComponent<Image>();
    
            // Initialize damaged bar color to be fully visible (alpha = 1)
            damagedColor = damagedBarImage.color;
            damagedColor.a = 1f;
            damagedBarImage.color = damagedColor;
            // Initialize the fade timer
            damagedHealthFadeTimer = 0f; // ensure it's inactive at the start
        }
    
        private void Update()
        {
            // Start fading only if the fade timer is active 
            if (damagedHealthFadeTimer > 0)
            {
                damagedHealthFadeTimer -= Time.deltaTime;
    
                // Once the fade timer is up, start reducing the alpha value
                if (damagedHealthFadeTimer <= 0)
                {
                    damagedColor.a -= fadeSpeed * Time.deltaTime;
                    damagedColor.a = Mathf.Clamp(damagedColor.a, 0, 1);  // ensure alpha does not go below 0
                    damagedBarImage.color = damagedColor;
                }
            }
    
            // Shrink the damaged bar towards the current health bar
            if (damagedBarImage.fillAmount > barImage.fillAmount)
            {
                damagedBarImage.fillAmount = Mathf.Lerp(damagedBarImage.fillAmount, barImage.fillAmount, shrinkSpeed * Time.deltaTime);
    
            }
        }
    
        public void OnDamaged(float healthNormalized)
        {
            // Check if the current health is less than the damaged bar 
            if (healthNormalized < damagedBarImage.fillAmount)
            {
                // This means damage occurred, so we reset the damaged bar for the fading effect
                damagedHealthFadeTimer = damagedHealthFadeDuration; 
                damagedColor.a = 1f; // ensure the damaged bar is visible after damage
                damagedBarImage.color = damagedColor;
    
                // Update the main health bar to the current health value
                barImage.fillAmount = healthNormalized;
            } else
            {
                // If no damage (healing or first update), just sync the bars instantly
                barImage.fillAmount = healthNormalized;
                damagedBarImage.fillAmount = healthNormalized;
    
                // If the damaged bar is currently showing damage, reset it to be fully invisible
                if (damagedColor.a > 0)
                {
                    damagedColor.a = 0f; // Hide the damaged bar if healing or no damage occurs
                    damagedBarImage.color = damagedColor;
                }
            }
        }
    
    }
    

    View post on imgur.com

Viewing 4 posts - 1 through 4 (of 4 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: