Trying to Make a ShowDefenses Script

I am currently trying to generate a “ShowDefenses” script after working with @BrianTrotter to make an elemental damage and defenses system but have hit a bit of a roadblock in terms of my skill set.

Some relevant scripts include this ElementalDamage script:

using System.Collections.Generic;

namespace RPG.Attributes
{
    [System.Serializable] public struct ElementalDamage
    {
        public DamageType damageType;
        public float amount;
    }

    public enum DamageType
    {
        Physical,
        Fire,
        Lightning,
        Acid,
        Divine,
        Myth
    }

    public interface IElementalResistanceProvider
    {
        public IEnumerable<float> GetElementalResistance(DamageType damageType);
    }

    public interface IElementalDamageProvider
    {
        public IEnumerable<float> GetElementalDamageBoost(DamageType damageType);
    }
}

The Fighter script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using RPG.Movement;
using RPG.Core;
using System;
using RPG.Saving;
using RPG.Attributes;
using RPG.Saving.Utils;
using RPG.Saving.Inventories;

namespace RPG.Combat
{
    public class Fighter : MonoBehaviour, IAction//Not Dark Souls combat; more like Baldur's Gate or Diablo
    {
        [SerializeField] float timeBetweenAttacks = 1f;
        [SerializeField] float autoattackRange = 4f;
        [SerializeField] Transform rightHandTransform = null;
        [SerializeField] Transform leftHandTransform = null;
        [SerializeField] Transform spellbookTransform = null;
        [SerializeField] WeaponConfig defaultWeapon = null;
        Health target;
        Equipment equipment;
        public WeaponConfig currentWeaponConfig;
        float timeSinceLastAttack = Mathf.Infinity;//Number is infinitely high, so you always start ready to attack
        LazyValue<Weapon> currentWeapon;
        private void Awake()
        {
            currentWeaponConfig = defaultWeapon;
            currentWeapon = new LazyValue<Weapon>(SetupDefaultWeapon);
            equipment = GetComponent<Equipment>();
            if(equipment)
            {
                equipment.equipmentUpdated += UpdateWeapon;
            }
        }

        private void Start()
        {                           
            currentWeapon.ForceInit();
        }
        private void Update()
        {
            timeSinceLastAttack += Time.deltaTime;

            if(target == null) return;
            if(target.IsDead()) 
            {
                target = FindNewTargetInRange();//needed for autoattacking
                if(target == null) return;
            }

            if (!GetIsInRange(target.transform))
            {
                GetComponent<Mover>().MoveTo(target.transform.position, 1f);//1f for 100% max speed
            }
            else
            {
                GetComponent<Mover>().Cancel();
                AttackBehavior();
            }
        }

        private void UpdateWeapon()
        {
            var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig;
            if(weapon == null)
            {
                EquipWeapon(defaultWeapon);
            }
            else
            {
                EquipWeapon(weapon);
            }
        }

        private Weapon SetupDefaultWeapon()
        {
            return AttachWeapon(defaultWeapon);
        }
        public void EquipWeapon(WeaponConfig weapon)
        {
            currentWeaponConfig = weapon;
            currentWeapon.value = AttachWeapon(weapon);
        } 

        public Weapon AttachWeapon(WeaponConfig weapon)
        {
            Animator animator = GetComponent<Animator>();
            return weapon.SpawnWeapon(rightHandTransform, leftHandTransform, spellbookTransform, animator);
        }
        private void AttackBehavior()
        {
            transform.LookAt(target.transform);
            if(timeSinceLastAttack > timeBetweenAttacks)
            {
                TriggerAttack();
                timeSinceLastAttack = 0f;
            }
        }
        private Health FindNewTargetInRange()
        {
            Health best = null;
            float bestDistance = Mathf.Infinity;
            foreach (var candidate in FindAllTargetsInRange())
            {
                float candidateDistance = Vector3.Distance(
                    transform.position, candidate.transform.position);
                if (candidateDistance < bestDistance)
                {
                    best = candidate;
                    bestDistance = candidateDistance;
                }
            }
            return best;
        }

        private IEnumerable<Health> FindAllTargetsInRange()
        {
            RaycastHit[] raycastHits = Physics.SphereCastAll(transform.position, autoattackRange, Vector3.up, 0);
            foreach(var hit in raycastHits)
            {
                Health health = hit.transform.GetComponent<Health>();
                if(health == null) continue;
                if(health.IsDead()) continue;
                if(health.gameObject == gameObject) continue;
                yield return health;
            }
        }
        private void TriggerAttack()
        {
            GetComponent<Animator>().ResetTrigger("StopAttack");
            GetComponent<Animator>().SetTrigger("Attack");//Triggers hit event, which deals damage
        }

        void Hit() // Anim Event
        {
            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
            BaseStats targetBaseStats = target.GetComponent<BaseStats>();
            if (targetBaseStats != null)
            {
                float defense = targetBaseStats.GetStat(Stats.PhysicalDefense);
                damage /= 1 + defense / damage;
            }

            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;
                    
                    //Line below changed to add damage type for elemental damage.
                    target.TakeDamage(gameObject, damage, elementalDamage.damageType.ToString());
                }
            }
        }
        void Shoot() // Anim Event, probably unneeded
        {
            Hit();
        }
        private bool GetIsInRange(Transform targetTransform)
        {
            return Vector3.Distance(transform.position, targetTransform.transform.position) < currentWeaponConfig.GetRange();
        }
        public bool CanAttack(GameObject combatTarget)
        {
            if(combatTarget == null) {return false;}
            if(GetComponent<Mover>().CanMoveTo(combatTarget.transform.position) == false && !GetIsInRange(combatTarget.transform))
                {
                    return false;
                }
            Health targetToTest = combatTarget.GetComponent<Health>();
            return targetToTest != null && !targetToTest.IsDead();
        }

        public void Attack(GameObject combatTarget)
        {
            GetComponent<ActionScheduler>().StartAction(this);
            target = combatTarget.GetComponent<Health>();
        }

        public Health GetTarget()
        {
            return target;
        }
        public Transform GetHandTransform(bool isRightHand)
        {
            if(isRightHand)
            {
                return rightHandTransform;
            }
            else{
                return leftHandTransform;
            }
        }

        public void Cancel()
        {
            StopAttack();
            target = null;
            GetComponent<Mover>().Cancel();
        }

        private void StopAttack()
        {
            GetComponent<Animator>().ResetTrigger("Attack");
            GetComponent<Animator>().SetTrigger("StopAttack");
        }
    }
}

And my attempt so far at a ShowDefenses script (which is busted right now):

using System.Collections;
using System.Collections.Generic;
using RPG.Attributes;
using RPG.Combat;
using RPG.Saving.Inventories;
using TMPro;
using UnityEngine;

public class ShowDefenses : 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;
    StatsEquipment currentEquipment;
    float damage;
    void Start()
    {
        player = GameObject.FindWithTag("Player");
        currentEquipment = player.GetComponent<StatsEquipment>();
        
    }

    void Update()
    {
        //currentEquipment = currentEquipment;
        elementalDamages = currentEquipment.GetElementalResistance();

        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 = currentWeaponConfig.GetDamage();
        fire = currentEquipment.
        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;
    }
}

I don’t really know what you are trying to achieve so it may be best to wait for @Brian_Trotter because he has context. I would however recommend that you don’t do this

That’s a lot of Linq happening in the update loop.

And this is broken

I don’t much about the Linq thing since I worked with some people slightly better at coding elsewhere to make that in the first place but both Check Damages and SetNumbers are barely started; they’re mostly just leftover code from my script meant to show offensive stats.

Linq creates garbage that will eventually need to be cleaned up by the garbage collector. Calling it on every update creates garbage real often and when the garbage collector kicks in there will be a noticeable pause because garbage collection is expensive.

1 Like

Is that true even for what would seem like “minor” things like pulling numbers? Or would that just mean it clears garbage less often?

Garbage collection runs when specific conditions are met. From the MS documentation

This could be at any time, but if there’s a lot of garbage to collect you will notice it.

Garbage Collection is a massive topic. I just wanted to point out that all those Linq expressions in the update loop is not ideal

1 Like

Every clause (.Where, .FirstOrDefault, .OrderBy, etc) creates a new copy of the collection in the heap and flags it as garbage in need of disposal at some future point. Even with Unity’s new Incremental Garbage Collector, if the garbage piles up faster than the collector can sweep it away then at a certain point the old Garbage Collector steps in and blocks your code while it cleans things up. Nothing requiring System.Linq; within an Update Loop.

1 Like

I’m at work, so have little in the way of code but there’s a bit of overhaul we’d need to do for this…
First, this needs to be event driven, not update driven. Not just so we can use Linq, but because getting these values is EXPENSIVE if you do it every frame. In fact, virtually all UI should be event driven, i.e. using the observer pattern.

Next, I think you’ll get better results by putting a script on each of the texts responsible for pulling just that element…

For a framework reference (and I’ll work more on this later when I have a code editor), something along the lines of

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

public class ElementalDamageUI : MonoBehaviour
{
     [SerializeField] TextMeshProUGUI text;
     [SerializeField] DamageType damageType;
     
     GameObject player;

      void Awake()
      {
            player = GameObject.FindWithTag("Player");
            // This event is likely mispelled, I don't have the code on me
            player.GetComponent<Equipment>().EquipmentUpdated+=UpdateUI; 
            player.GetComponent<BaseStats>().OnLevelUp+=UpdateUI;
      }
      void Start()
      {
            UpdateUI();
       }
       void UpdateUI()
      {
            float damage = 0;
            foreach(var provider in player.GetComponent<IElementalDamageProvider>())
                  foreach(var amount in provider.GetElementalDamageBoost())
                       damage+=amount;
            text.text = $"{amount}";
      }
}

Not bad for typing this out at work on my lunch break with no error checking… Here’s the script once I’ve put it in Rider and cleared the misspellings and mistakes:

using GameDevTV.Inventories;
using RPG.Stats;
using TMPro;
using UnityEngine;

namespace RPG.Attributes
{
    public class ElementalDamageUI : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI text;
        [SerializeField] DamageType damageType;
     
        GameObject player;

        void Awake()
        {
            player = GameObject.FindWithTag("Player");
            player.GetComponent<Equipment>().equipmentUpdated+=UpdateUI; 
            player.GetComponent<BaseStats>().onLevelUp+=UpdateUI;
        }
        void Start()
        {
            UpdateUI();
        }
        void UpdateUI()
        {
            float damage = 0;
            foreach(var provider in player.GetComponents<IElementalDamageProvider>())
            foreach(var amount in provider.GetElementalDamageBoost(damageType))
                damage+=amount;
            text.text = $"{damage}";
        }
    }
}

Make a prefab with a text for a label and a Text (TextMeshPro) component for the value on it. Put this script on it. For your overall UI, make a copy of the prefab for each individual element and be sure to set the element you want in the inspector and of course change the label.

1 Like

So this is a fix for my elemental damage script, not one that can help with showing defenses, correct?

This seems to work much better for what I was trying to do with my attack stats script, so thank you. I didn’t even realize the issue before, but I do still need a way to show defenses.

I think I ended up doing offense because that was how it was looking in your script.

That being said, the elemental defense stat should display very much like an elemental offense stat. In fast, here’s your challenge for this lecture :stuck_out_tongue:

Using the ElementalDamageUI as a starting point, make an ElementalResistanceUI script

Hint: Most of the script will be the same, but we need to get the IElementalResistanceProvider, and iterate over it’s method.

I shall do my best! I’ve definitely been learning a lot doing this stuff but I also constantly feel over my head. I suppose that’s a lot of learning to program though.

It sure is. It never ends, really. I’ve been working through understanding Unity’s Gaming Services so that saves can be done in the cloud. Been at this coding stuff for 40 years and sometimes things still look complicated. That’s good, though, because constantly learning is good for the mind.

I’ve left this one pretty easy… You really only need to change code in two places to account for the interface and the interface method. Otherwise the scripts should be pretty much identical.

Is it this simple?

using RPG.Saving.Inventories;
using TMPro;
using UnityEngine;

namespace RPG.Attributes
{
    public class ElementalDefensesUI : MonoBehaviour
    {
        [SerializeField] TextMeshProUGUI text;
        [SerializeField] DamageType damageType;
     
        GameObject player;

        void Awake()
        {
            player = GameObject.FindWithTag("Player");
            player.GetComponent<Equipment>().equipmentUpdated+=UpdateUI; 
            player.GetComponent<BaseStats>().onLevelUp+=UpdateUI;
        }
        void Start()
        {
            UpdateUI();
        }
        void UpdateUI()
        {
            float damage = 0;
            foreach(var provider in player.GetComponents<IElementalResistanceProvider>())
            foreach(var amount in provider.GetElementalResistance(damageType))
                damage+=amount;
            text.text = $"{damage}";
        }
    }
}

(It’s that simple) :slight_smile:

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms