Trying to Implement Elemental Damage But Introduced Bug

With a bunch of help, I’ve been adding elemental damage to my weapons but, as it stands, this introduces a bug to my game I’d like to fix where damage text layers over top itself, once for the base damage and once for elemental damage.

I need a way to either get the damage to all total as one number (without ruining the ability to resist or boost elemental damages of certain types) OR to make multiple damage texts give each other some space if they occur together, so that the numbers can be read. I wouldn’t mind the numbers being split if it was at least readable.

I assume the issue is how this is coded:

        {
            if(target == null) {return;}

            float damage = GetComponent<BaseStats>().GetStat(Stats.PhysicalDamage);//eventually can check weapon for type and the modifiers the weapon should pull from via bools

            if(currentWeapon.value != null)
            {
                currentWeapon.value.OnHit();
            }

            if(currentWeaponConfig.HasProjectile() != false)
            {
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, spellbookTransform, target, gameObject, damage);
                return;
            }

            else
            {
                target.TakeDamage(gameObject, damage);
                foreach (ElementalDamage elementalDamage in currentWeaponConfig.GetElementalDamages())
                {
                    damage = elementalDamage.amount;
                    float boosts = 0;
                    foreach (IElementalDamageProvider provider in GetComponents<IElementalDamageProvider>())
                    {
                        foreach (float amount in provider.GetElementalDamageBoost(elementalDamage.damageType))
                        {
                            boosts += amount;
                        }
                    }
                    boosts /= 100f;

                    float resistances = 0;
                    foreach (IElementalResistanceProvider provider in target.GetComponents<IElementalResistanceProvider>())
                    {
                        foreach (float amount in provider.GetElementalResistance(elementalDamage.damageType))
                        {
                            resistances += amount;
                        }
                    }
                
                    resistances /= 100f;
                    damage += damage * boosts;
                    damage -= damage * resistances;
                    if (damage <= 0) continue;
                    
                    target.TakeDamage(gameObject, damage);}
            }
            
            target.TakeDamage(gameObject, damage);
        }```

@Brian_Trotter had previously helped me as seen in this post, Trying to Create a "Show Weapon Damage" script - #11 by Brian_Trotter, with much of his stuff working as intended.

One change I made was making what is admittedly probably a bit of a clunky GetWeaponDamage script:

using System.Collections;
using System.Linq;
using System.Collections.Generic;
using RPG.Combat;
using RPG.Attributes;
using UnityEngine;
using TMPro;
using System;

public class GetWeaponDamage : MonoBehaviour
{
    //public float physical = 0;
    public float fire = 0;
    public float lightning = 0;
    public float acid = 0;
    public float divine = 0;
    public float myth = 0;

    //[SerializeField] TMP_Text physicalText;
    [SerializeField] TMP_Text fireText;
    [SerializeField] TMP_Text lightningText;
    [SerializeField] TMP_Text acidText;
    [SerializeField] TMP_Text divineText;
    [SerializeField] TMP_Text mythText;

    IEnumerable<ElementalDamage> elementalDamages;
    GameObject player;
    Fighter playerFighter;
    WeaponConfig currentWeaponConfig;
    float damage;
    void Start()
    {
        player = GameObject.FindWithTag("Player");
        playerFighter = player.GetComponent<Fighter>();
        
    }

    void Update()
    {
        currentWeaponConfig = playerFighter.currentWeaponConfig;
        elementalDamages = currentWeaponConfig.GetElementalDamages();

        CheckDamages();
        SetNumbers();
    }

    private void SetNumbers()
    {
        //physicalText.SetText(physical.ToString());//should be same as default damage
        fireText.SetText(fire.ToString());
        lightningText.SetText(lightning.ToString());
        acidText.SetText(acid.ToString());
        divineText.SetText(divine.ToString());
        mythText.SetText(myth.ToString());
    }

    public void CheckDamages()
    {
        //physical = elementalDamages.FirstOrDefault(x => x.damageType == DamageType.Physical).amount;
        fire = elementalDamages.FirstOrDefault(x => x.damageType == DamageType.Fire).amount;
        lightning = elementalDamages.FirstOrDefault(x => x.damageType == DamageType.Lightning).amount;
        acid = elementalDamages.FirstOrDefault(x => x.damageType == DamageType.Acid).amount;
        divine = elementalDamages.FirstOrDefault(x => x.damageType == DamageType.Divine).amount;
        myth = elementalDamages.FirstOrDefault(x => x.damageType == DamageType.Myth).amount;
    }
}

This is what I do, with each damage/damage type showing up one after the other with a small gap between them.

To accomplish this, we need a Queue to hold our messages. A Queue is very similar to a List, except that it is highly specialized to allow you to add something to the end of the list (Enqueue()) and Get and remove something from the beginning of the list (Dequeue()).

Our Queue will be storing a list of System.Actions.

private Queue<Action> actions = new Queue<Actions>();

To manage these actions, we’re going to need to have an Update() in our DamageTextSpawner()

        private float last = 0;
        
        private void Update()
        {
            last += Time.deltaTime;
            if (actions.Count > 0 && (last > timeBetweenMessages))
            {
                var action = actions.Dequeue();
                action.Invoke();
                last = 0;            
            }
        }

last is the timer. TimeBetweenMessages will be a Serialized float.

Now for the tricky part, Enqueueing the actions as they come in. This is going to require a slight change to our Spawn routine, we’re going to have to use a Lambda Expression to create an anonymous method.

        public void Spawn(float damageAmount)
        {
            actions.Enqueue(() =>
            {
                DamageText instance = Instantiate<DamageText>(damageTextPrefab, transform);
                instance.SetValue(damageAmount);
            });
        }

So what’s happening here is that we’re creating an anonymous method, sometimes called a Closure. Don’t worry if that link didn’t make much sense just yet, Closures and First Class Functions are an upper-intermediate to advanced topic in C#, but they’re extremely valuable. See, what we just created was a delegate that stores the value of damageAmount inside the method so that even though the next time Spawn is called the damage might be different, the version of the damageAmount inside of our Closure will stay the same (which is why it’s called a Closure, because the information it needs to do it’s job is enclosed inside the anonymous method.

So now, when Spawn() is called, it creates an anonymous function which is essentially what the old Spawn did, and puts it in a queue. Then Update looks at that Queue and one at a time with a delay between executes the anonymous functions.

This, by itself, will space out the messages to make them more readable, but the player might get confused when two damage texts pop up for one hit with just numbers. How do I know? Because one of my games used this script and I got direct feedback from the users in the beta. It’s helpful to label the damage as it comes in. For that, we’re going to need to be able to spawn plain text instead of just a value. We’ll need a new method in DamageText

        public void SetText(string text)
        {
            damageText.text = text;
        }

And another Spawn method in our DamageTextSpawner

        public void SpawnText(string text)
        {
            actions.Enqueue(() =>
            {
                DamageText instance = Instantiate<DamageText>(damageTextPrefab, transform);
                instance.SetText(text);
            });
        }

Like Spawn, it’s enqueuing an Action, this time simply setting the text instead of a value.

Here’s our fully fleshed out DamageTextSpawner.cs and DamageText.cs

DamageTextSpawner.cs
using System;
using System.Collections.Generic;
using UnityEngine;

namespace RPG.UI.DamageText
{
    /// <summary>
    /// Spawns text in 3D space to show damage dealt.
    /// </summary>
    public class DamageTextSpawner : MonoBehaviour
    {
        /// <summary>
        /// Prefab to instantiate when showing damage text.
        /// </summary>
        [SerializeField] DamageText damageTextPrefab = null;

        /// <summary>
        /// The time (in seconds) between each text message spawn.
        /// </summary>
        [SerializeField] private float timeBetweenMessages = .25f;

        /// <summary>
        /// Queue of text spawn actions waiting to be performed.
        /// </summary>
        private Queue<Action> actions = new Queue<Action>();

        /// <summary>
        /// Time since the previous action was performed.
        /// </summary>
        private float last = 0;

        /// <summary>
        /// Updates the spawner. Performs queued actions at intervals defined by 'timeBetweenMessages'.
        /// </summary>
        private void Update()
        {
            last += Time.deltaTime;
            if (actions.Count > 0 && (last > timeBetweenMessages))
            {
                var action = actions.Dequeue();
                action.Invoke();
                last = 0;
            }
        }
        
        /// <summary>
        /// Spawns a new damage text instance, setting its value to damageAmount.
        /// </summary>
        /// <param name="damageAmount">The amount of damage to display.</param>
        public void Spawn(float damageAmount)
        {
            actions.Enqueue(() =>
            {
                DamageText instance = Instantiate<DamageText>(damageTextPrefab, transform);
                instance.SetValue(damageAmount);
            });
        }

        /// <summary>
        /// Spawns a new text message at the spawner's location.
        /// </summary>
        /// <param name="text">The message to spawn.</param>
        public void SpawnText(string text)
        {
            actions.Enqueue(() =>
            {
                DamageText instance = Instantiate<DamageText>(damageTextPrefab, transform);
                instance.SetText(text);
            });
        }
    }
}
DamageText.cs
using UnityEngine;
using UnityEngine.UI;

namespace RPG.UI.DamageText
{
    /// <summary>
    /// Represents text displaying damage dealt in UI.
    /// </summary>
    public class DamageText : MonoBehaviour
    {
        /// <summary>
        /// The Text UI component to display the damage text.
        /// </summary>
        [SerializeField] Text damageText = null;
        
        /// <summary>
        /// Destroys the damage text GameObject.
        /// </summary>
        public void DestroyText()
        {
            Destroy(gameObject);
        }

        /// <summary>
        /// Sets the value of the damage text to the specified amount, formatted as an integer.
        /// </summary>
        /// <param name="amount">The damage amount.</param>
        public void SetValue(float amount)
        {
            damageText.text = $"{amount:F0}";
        }

        /// <summary>
        /// Sets the value of the damage text to the specified string.
        /// </summary>
        /// <param name="text">The text to set.</param>
        public void SetText(string text)
        {
            damageText.text = text;
        }
    }
}

This leaves us with describing the damage in our TakeDamage method.

First, I created a new event in Health

public UnityEvent<string> takeDamageText; //Note:  In Unity2019 or greater, we no longer need to encapsulate UnityEvent<T> in a wrapper class

Then in TakeDamage, I added an optional parameter to describe the damage

        public void TakeDamage(GameObject instigator, float damage, string damageInfo = "")
        {
            healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);
            
            if(IsDead())
            {
                onDie.Invoke();
                AwardExperience(instigator);
            } 
            else
            {
                takeDamage.Invoke(damage);
                takeDamageText.Invoke($"{damage} {damageInfo}");
            }
            UpdateState();
        }

I then removed the entry in Health for the takeDamage (float), and added an entry in takeDamageText.

Now when you do normal damage, call TakeDamage without any string (it’s optional, and will default to nothing). When you do elemental damage, call TakeDamage with the name of the element (passing the DamageType.ToString() will send the enum’s value as a string).

Firstly, wow, this was so much help and does solve the biggest issue. as the numbers no longer overlap. But I do have a few questions and it isn’t working perfectly for me yet. I very well could have missed a few words or something, not sure.

First, you write “Update State” near the end of the TakeDamage code. Is that in one of the courses? Mine doesn’t have that method. Not sure if that is for this particular solve or just something that eventually gets implemented.

Second, I still don’t get the elemental damage label, telling players what type of damage was taken. How do I get the info into there? I know you mention adding DamageType.ToString() but I have to admit a lot of this code is beyond me so I am fumbling a bit. It’s not surprising they didn’t go over this in the course.

I also seem to have a bug right now where enemy damage repeats itself in terms of being shown, even when only one damage type is dealt. Could that be because of this code:

            {
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, spellbookTransform, target, gameObject, damage);
                return;
            }

            else
            {
                target.TakeDamage(gameObject, damage);
                foreach (ElementalDamage elementalDamage in currentWeaponConfig.GetElementalDamages())
                {
                    damage = elementalDamage.amount;
                    float boosts = 0;
                    foreach (IElementalDamageProvider provider in GetComponents<IElementalDamageProvider>())
                    {
                        foreach (float amount in provider.GetElementalDamageBoost(elementalDamage.damageType))
                        {
                            boosts += amount;
                        }
                    }
                    boosts /= 100f;

                    float resistances = 0;
                    foreach (IElementalResistanceProvider provider in target.GetComponents<IElementalResistanceProvider>())
                    {
                        foreach (float amount in provider.GetElementalResistance(elementalDamage.damageType))
                        {
                            resistances += amount;
                        }
                    }
                
                    resistances /= 100f;
                    damage += damage * boosts;
                    damage -= damage * resistances;
                    if (damage <= 0) continue;
                    
                    target.TakeDamage(gameObject, damage);}
            }
            
            target.TakeDamage(gameObject, damage);

That is introduced in the Shops and Abilities course. You can comment that line out or remove it altogether.

You have target.TakeDamage() in the else statement before going into elemental damage (that’s correct), and you have target.TakeDamage at the end of the snippet (that’s incorrect, because you only want to call target.TakeDamage if you’re NOT launching a projectile.

Within the elemental damage section, add elementalDamage.damageType.ToString(), as elementalDamage is the name of the variable holding the damage amount and element.

I’m a little unclear on what you mean by the last two solutions you recommend, as they seem to contradict in my mind, as if you’re saying to remove the TakeDamage call and then add it right back with the ToString. Here is my code at present:

                target.TakeDamage(gameObject, damage);
                foreach (ElementalDamage elementalDamage in currentWeaponConfig.GetElementalDamages())
                {
                    damage = elementalDamage.amount;
                    float boosts = 0;
                    foreach (IElementalDamageProvider provider in GetComponents<IElementalDamageProvider>())
                    {
                        foreach (float amount in provider.GetElementalDamageBoost(elementalDamage.damageType))
                        {
                            boosts += amount;
                        }
                    }
                    boosts /= 100f;

                    float resistances = 0;
                    foreach (IElementalResistanceProvider provider in target.GetComponents<IElementalResistanceProvider>())
                    {
                        foreach (float amount in provider.GetElementalResistance(elementalDamage.damageType))
                        {
                            resistances += amount;
                        }
                    }
                
                    resistances /= 100f;
                    damage += damage * boosts;
                    damage -= damage * resistances;
                    if (damage <= 0) continue;

                    target.TakeDamage(gameObject, damage, elementalDamage.damageType.ToString());
                }
            }
{
                currentWeaponConfig.LaunchProjectile(rightHandTransform, leftHandTransform, spellbookTransform, target, gameObject, damage);
                return;
            }

            else
            {
                target.TakeDamage(gameObject, damage); 
                foreach (ElementalDamage elementalDamage in currentWeaponConfig.GetElementalDamages())
                {
                    damage = elementalDamage.amount;
                    float boosts = 0;
                    foreach (IElementalDamageProvider provider in GetComponents<IElementalDamageProvider>())
                    {
                        foreach (float amount in provider.GetElementalDamageBoost(elementalDamage.damageType))
                        {
                            boosts += amount;
                        }
                    }
                    boosts /= 100f;

                    float resistances = 0;
                    foreach (IElementalResistanceProvider provider in target.GetComponents<IElementalResistanceProvider>())
                    {
                        foreach (float amount in provider.GetElementalResistance(elementalDamage.damageType))
                        {
                            resistances += amount;
                        }
                    }
                
                    resistances /= 100f;
                    damage += damage * boosts;
                    damage -= damage * resistances;
                    if (damage <= 0) continue;
                    //Line below changed to add damage type for elemental damage.
                    target.TakeDamage(gameObject, damage, elementalDamage.damageType.ToString());}
            }
            
           // target.TakeDamage(gameObject, damage);  This is the source of double damage

So I still seem to get two numbers pop up when enemies hit. Elemental damage also doesn’t seem to be labeled.

Could any part of the issue by my using TMPro rather than the default text:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;

namespace RPG.UI
{
    public class DamageText : MonoBehaviour
    {
        [SerializeField] TMP_Text damageText = null;
        /// <summary>
        /// Destroys the damage text GameObject.
        /// </summary>
        public void DestroyText()
        {
            Destroy(gameObject);
        }

        /// <summary>
        /// Sets the value of the damage text to the specified amount, formatted as an integer.
        /// </summary>
        /// <param name="amount">The damage amount.</param>
        public void SetValue(float amount)
        {
            damageText.text = $"{amount:F0}";
        }

        /// <summary>
        /// Sets the value of the damage text to the specified string.
        /// </summary>
        /// <param name="text">The text to set.</param>
        public void SetText(string text)
        {
            damageText.text = text;
        }
    }
}

Show me the inspector for your Health Component with the UnityEvents expanded…

Somehow, I forgot to mention that you’ll need to clear the entry in Take Damage (Single), and instead hook up the DamageTextSpawner to Take Damage Text (string). You’ll call the automatic version of DamageTextSpawner.SpawnText

Like this?

image

If this is what you meant, it now doesn’t produce anything when a hit occurs. Damage and sound still happen and stuff, but no text or numbers.

Actually, hold on, it was an issue with prefab overrides. Let me continue checking if I’ve done it right.

Did you update TakeDamage()?

public void TakeDamage(GameObject instigator, float damage, string damageInfo = "")
        {
            healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);
            
            if(IsDead())
            {
                onDie.Invoke();
                AwardExperience(instigator);
            } 
            else
            {
                takeDamage?.Invoke(damage);
                takeDamageText?.Invoke($"{damage} {damageInfo}");
            }
            UpdateState();
        }

Yes. So right now it appears to be half-working. Enemy damage now doesn’t double appear, which is good, but player damage isn’t showing up at all, which seems very odd. I also don’t see any words signalling damage.

Check your Player’s inspector…

It’s very odd, so this is a picture of the Player Health script:
image

And here is an example enemy’s:
image

I can confirm that text works on enemy damage now, as I changed their weapons to have elemental damage and it went off correctly. So it’s just that enemy’s getting hit aren’t spawning text.

So damage text generates if the Player is unarmed apparently.

But not if this weapon is equipped.

Using the same scripts, I don’t see how that’s happening. Nothing is spawning at all???

Even weirder to me is that this:
image

Generates text as if the base damage and fire damage are put together. I guess because it is a projectile weapon?

Privacy & Terms