Interested in Adding Elemental Damage to Projectiles

@Brian_Trotter and I (mostly him) have been working on code to add elemental damage to attacks. I am interested in adding this type of damage to projectiles and was wondering if anyone had done something similar.

The goal would be to get the projectile to deal the elemental damages from this 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);
    }
}

If an entity is dealt this damage, there’s already systems in place to handle it. I just don’t know how to actually get the projectile to deal these damages.

Very similarly to how we did it in Hit().

In this case, your projectiles will have to get the Elemental damages in their setup in addition to the normal damage.

Our projectiles are spawned in the WeaponConfig, which happens to already have the ElementalDamages array… (edit) First, we need to add an overload method for SetTarget() in Projectile.cs:

        private IEnumerable<ElementalDamage> elementalDamages;

        public void SetTarget(Health target, GameObject instigator, float calculatedDamage, IEnumerable<ElementalDamage> elementalDamages)
        {
            this.projTarget = target;
            this.damage = damage;
            this.instigator = instigator;
            this.elementalDamages = elementalDamages;
            Destroy(gameObject, maxLifetime);
        }

And in WeaponConfig.LaunchProjectile, we want to add our IEnumerable to the parameters:

        public void LaunchProjectile(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform, Health target, GameObject instigator, float calculatedDamage)
        {
            Projectile projectileInstance = Instantiate(projectile, GetTransforms(rightHandTransform, leftHandTransform, spellbookTransform).position, Quaternion.identity);
            projectileInstance.SetTarget(target, instigator, calculatedDamage, GetElementalDamages());
        }

Finally, in the OnTrigger method of Projectile, we need to apply any elementalDamages that were sent to the projectile:

        private void OnTriggerEnter(Collider other) 
        {
            if(other.GetComponent<Health>() != projTarget) return;
            if(projTarget.IsDead()) return;
            projTarget.TakeDamage(instigator, damage);

            foreach (ElementalDamage elementalDamage in elementalDamages)
            {
                damage = elementalDamage.amount;
                float boosts = 0;
                foreach (IElementalDamageProvider provider in instigator.GetComponents<IElementalDamageProvider>())
                {
                    foreach (float amount in provider.GetElementalDamageBoost(elementalDamage.damageType))
                    {
                        boosts += amount;
                    }
                }
                boosts /= 100f;

                float resistances = 0;
                foreach (IElementalResistanceProvider provider in projTarget.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.
                projTarget.TakeDamage(gameObject, damage, elementalDamage.damageType.ToString());
            }
            projSpeed = 0; //Prevents piercing as it is right now, may change later

            if(hitEffect != null)
            {
                Instantiate(hitEffect, GetAimLocation(), transform.rotation);
            }

            if(destroyOnHit == null)
            {
                Destroy(gameObject);
                Destroy(hitEffect);
            }

            foreach(GameObject toDestroy in destroyOnHit)
            {
                Destroy(toDestroy);
            }

            Destroy(gameObject, lifeAfterImpact);
        }

Sorry, that first bit of code goes where? In WeaponConfig?

Yes, that’s where we Launch the projectiles.

So I am trying to implement the first change and am clearly doing something wrong. I’ve put my code here, with the part you talked about added at the end:

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

namespace RPG.Combat
{
    [CreateAssetMenu(fileName = "Weapon", menuName = "Weapons/New Weapon", order = 0)]
    public class WeaponConfig : EquipableItem, IModifierProvider
    {
        [SerializeField] AnimatorOverrideController weaponAnimationOverride;
        [SerializeField] Weapon equippedPrefab = null;
        [SerializeField] float weaponBaseDamage = 5;
        [SerializeField] float percentageBonus = 0;
        [SerializeField] float weaponRange = 2f;
        [SerializeField] bool isRightHanded = true;//most weapons will be right-handed but some may need to be left-handed for simplicity and anims.
        [SerializeField] bool isSpellbook = false;
        [SerializeField] Projectile projectile = null;//Holds object this weapon launches (and will be null for most weapons)
        [SerializeField] ElementalDamage[] elementalDamages;

        const string weaponName = "Weapon";
        public Weapon SpawnWeapon(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform, Animator animator)
        {
            DestroyOldWeapon(rightHandTransform, leftHandTransform, spellbookTransform);

            Weapon weapon = null;

            if (equippedPrefab != null)//Only tries to make prefab if it has one. Mostly for used Unarmed.
            {
                Transform handTransform = GetTransforms(rightHandTransform, leftHandTransform, spellbookTransform);

                weapon = Instantiate(equippedPrefab, handTransform);

                weapon.name = weaponName;
            }

            if (weaponAnimationOverride != null)
            {
            animator.runtimeAnimatorController = weaponAnimationOverride;
            }
            else
            {
                var overrideController = animator.runtimeAnimatorController as AnimatorOverrideController;
                if(overrideController != null)
                {
                    animator.runtimeAnimatorController = overrideController.runtimeAnimatorController;
                }
            }

            return weapon;
        }

        private void DestroyOldWeapon(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform)
        {
            Transform oldWeapon = rightHandTransform.Find("Weapon");
            if(oldWeapon == null) oldWeapon = leftHandTransform.Find("Weapon");
            if(oldWeapon == null) oldWeapon = spellbookTransform.Find("Weapon");
            if(oldWeapon == null) return;

            oldWeapon.name = "DESTROYING WEAPON";
            Destroy(oldWeapon.gameObject);
        }

        private Transform GetTransforms(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform)
        {
            Transform handTransform;
            
            if (isSpellbook == true) 
            {
                handTransform = spellbookTransform;
                return handTransform;
            }
            if (isRightHanded == false) handTransform = leftHandTransform;
            else handTransform = rightHandTransform;
            return handTransform;
        }
        public IEnumerable<ElementalDamage> GetElementalDamages()
        {
            foreach (ElementalDamage elementalDamage in elementalDamages)
            {
                yield return elementalDamage;
            }
        }
        public bool HasProjectile()
        {
            return projectile != null;//returns true if projectile isn't null
        }
        public void LaunchProjectile(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform, Health target, GameObject instigator, float calculatedDamage)
        {
            Projectile projectileInstance = Instantiate(projectile, GetTransforms(rightHandTransform, leftHandTransform, spellbookTransform).position, Quaternion.identity);
            projectileInstance.SetTarget(target, instigator, calculatedDamage);
        }
        public float GetDamage()
        {
            return weaponBaseDamage;
        }
        public ElementalDamage[] GetRawElementalDamages()
        {
            return elementalDamages;
        }

        public float GetRange()
        {
            return weaponRange;
        }

        public float GetPercentageBonus()
        {
            return percentageBonus;
        }

        public IEnumerable<float> GetAdditiveModifiers(Stats stat)
        {
            if (stat == Stats.PhysicalDamage)
            {
                yield return weaponBaseDamage;
            }
        }

        public IEnumerable<float> GetPercentageModifiers(Stats stat)
        {
            if (stat == Stats.PhysicalDamage)
            {
                yield return percentageBonus;
            }
        }

        private IEnumerable<ElementalDamage> elementalDamages;

            public void SetTarget(Health target, GameObject o, float calculatedDamage, IEnumerable<ElementalDamage> elementalDamages)
            {
                this.target = target;
                this.damage = damage;
                this.instigator = instigator;
                this.elementalDamages = elementalDamages;
                Destroy(gameObject, maxLifeTime);
            }
    }
}

Don’t I already define elementalDamages as an array? And most of the terms in SetTarget aren’t defined according to the squigglies.

Oops, I meant to say that first, we need to add an overload method in Projectile.cs for SetTarget().

So projectile should look something like this?

using UnityEngine;
using RPG.Attributes;
using UnityEngine.Events;
using System.Collections.Generic;

namespace RPG.Combat
{
    public class Projectile : MonoBehaviour
    {
        [SerializeField] float speed = 1;
        [SerializeField] bool isHoming = true;
        [SerializeField] GameObject hitEffect = null;
        [SerializeField] float maxLifeTime = 10;
        [SerializeField] GameObject[] destroyOnHit = null;
        [SerializeField] float lifeAfterImpact = 2;
        [SerializeField] UnityEvent onHit;
        private IEnumerable<ElementalDamage> elementalDamages;
        Health target = null;
        Vector3 targetPoint;
        GameObject instigator = null;
        float damage = 0;

        private void Start()
        {
            transform.LookAt(GetAimLocation());
        }

        void Update()
        {
            if (target != null && isHoming && !target.IsDead())
            {
                transform.LookAt(GetAimLocation());
            }
            transform.Translate(Vector3.forward * speed * Time.deltaTime);
        }

        public void SetTarget(Health target, GameObject instigator, float damage)
        {
            SetTarget(instigator, damage, target);
        }

        public void SetTarget(Vector3 targetPoint, GameObject instigator, float damage)
        {
            SetTarget(instigator, damage, null, targetPoint);
        }

        public void SetTarget(GameObject instigator, float damage, Health target=null, Vector3 targetPoint=default)
        {
            this.target = target;
            this.targetPoint = targetPoint;
            this.damage = damage;
            this.instigator = instigator;

            Destroy(gameObject, maxLifeTime);
        }
        public void SetTarget(Health target, GameObject o, float calculatedDamage, IEnumerable<ElementalDamage> elementalDamages)
        {
            this.target = target;
            this.damage = damage;
            this.instigator = instigator;
            this.elementalDamages = elementalDamages;
            Destroy(gameObject, maxLifeTime);
        }

        private Vector3 GetAimLocation()
        {
            if (target == null)
            {
                return targetPoint;
            }
            CapsuleCollider targetCapsule = target.GetComponent<CapsuleCollider>();
            if (targetCapsule == null)
            {
                return target.transform.position;
            }
            return target.transform.position + Vector3.up * targetCapsule.height / 2;
        }

        private void OnTriggerEnter(Collider other)
        {
            Health health = other.GetComponent<Health>();
            if (target != null && health != target) return;
            if (health == null || health.IsDead()) return;
            if (other.gameObject == instigator) return;
            health.TakeDamage(instigator, damage);

            speed = 0;

            onHit.Invoke();

            if (hitEffect != null)
            {
                Instantiate(hitEffect, GetAimLocation(), transform.rotation);
            }

            foreach (GameObject toDestroy in destroyOnHit)
            {
                Destroy(toDestroy);
            }

            Destroy(gameObject, lifeAfterImpact);
        }

    }

}

Except that you need to update OnTriggerEnter as well, to deal the elemental damage.

Right, OnTriggerEnter should look kind of like this:

private void OnTriggerEnter(Collider other)
        {
            Health health = other.GetComponent<Health>();
            if (target != null && health != target) return;
            if (health == null || health.IsDead()) return;
            if (other.gameObject == instigator) return;
            health.TakeDamage(instigator, damage);

            speed = 0;

            foreach (ElementalDamage elementalDamage in elementalDamages)
            {
                damage = elementalDamage.amount;
                float boosts = 0;
                foreach (IElementalDamageProvider provider in instigator.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());
            }

            speed = 0; //Prevents piercing as it is right now, may change later

            if(hitEffect != null)
            {
                Instantiate(hitEffect, GetAimLocation(), transform.rotation);
            }

            if(destroyOnHit == null)
            {
                Destroy(gameObject);
                Destroy(hitEffect);
            }

            foreach(GameObject toDestroy in destroyOnHit)
            {
                Destroy(toDestroy);
            }

            Destroy(gameObject, lifeAfterImpact);
        }
1 Like

Now, additional changes are needed to add damage to ability projectiles, right?

Projectiles seem to be giving me an error that reads "NullReferenceException: Object reference not set to an instance of an object
RPG.Combat.Projectile.OnTriggerEnter (UnityEngine.Collider other) (at Assets/OurScripts/Combat/Projectile.cs:93)
"

You shouldn’t need to add changes to the projectiles because the elemental damage is injected in the SetTarget.

Which line is the error on? (not all scripts are created equal, so 93 tells me nothing)

93 specifically seems to be blank but it’s in this section of code:

 foreach (ElementalDamage elementalDamage in elementalDamages)
            {
                damage = elementalDamage.amount;
                float boosts = 0;

                foreach (IElementalDamageProvider provider in instigator.GetComponents<IElementalDamageProvider>())
                {
                    foreach (float amount in provider.GetElementalDamageBoost(elementalDamage.damageType))
                    {
                        boosts += amount;
                    }
                }
                boosts /= 100f;

It’s right in the middle of boosts and foreach that’s getting pointed to

Odd… not much that should be null there…

Paste in your WeaponConfig.cs on a hunch…

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

namespace RPG.Combat
{
    [CreateAssetMenu(fileName = "Weapon", menuName = "Weapons/New Weapon", order = 0)]
    public class WeaponConfig : EquipableItem, IModifierProvider
    {
        [SerializeField] AnimatorOverrideController weaponAnimationOverride;
        [SerializeField] Weapon equippedPrefab = null;
        [SerializeField] float weaponBaseDamage = 5;
        [SerializeField] float percentageBonus = 0;
        [SerializeField] float weaponRange = 2f;
        [SerializeField] bool isRightHanded = true;//most weapons will be right-handed but some may need to be left-handed for simplicity and anims.
        [SerializeField] bool isSpellbook = false;
        [SerializeField] Projectile projectile = null;//Holds object this weapon launches (and will be null for most weapons)
        [SerializeField] ElementalDamage[] elementalDamages;

        const string weaponName = "Weapon";
        public Weapon SpawnWeapon(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform, Animator animator)
        {
            DestroyOldWeapon(rightHandTransform, leftHandTransform, spellbookTransform);

            Weapon weapon = null;

            if (equippedPrefab != null)//Only tries to make prefab if it has one. Mostly for used Unarmed.
            {
                Transform handTransform = GetTransforms(rightHandTransform, leftHandTransform, spellbookTransform);

                weapon = Instantiate(equippedPrefab, handTransform);

                weapon.name = weaponName;
            }

            if (weaponAnimationOverride != null)
            {
            animator.runtimeAnimatorController = weaponAnimationOverride;
            }
            else
            {
                var overrideController = animator.runtimeAnimatorController as AnimatorOverrideController;
                if(overrideController != null)
                {
                    animator.runtimeAnimatorController = overrideController.runtimeAnimatorController;
                }
            }

            return weapon;
        }

        private void DestroyOldWeapon(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform)
        {
            Transform oldWeapon = rightHandTransform.Find("Weapon");
            if(oldWeapon == null) oldWeapon = leftHandTransform.Find("Weapon");
            if(oldWeapon == null) oldWeapon = spellbookTransform.Find("Weapon");
            if(oldWeapon == null) return;

            oldWeapon.name = "DESTROYING WEAPON";
            Destroy(oldWeapon.gameObject);
        }

        private Transform GetTransforms(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform)
        {
            Transform handTransform;
            
            if (isSpellbook == true) 
            {
                handTransform = spellbookTransform;
                return handTransform;
            }
            if (isRightHanded == false) handTransform = leftHandTransform;
            else handTransform = rightHandTransform;
            return handTransform;
        }
        public IEnumerable<ElementalDamage> GetElementalDamages()
        {
            foreach (ElementalDamage elementalDamage in elementalDamages)
            {
                yield return elementalDamage;
            }
        }
        public bool HasProjectile()
        {
            return projectile != null;//returns true if projectile isn't null
        }
        public void LaunchProjectile(Transform rightHandTransform, Transform leftHandTransform, Transform spellbookTransform, Health target, GameObject instigator, float calculatedDamage)
        {
            Projectile projectileInstance = Instantiate(projectile, GetTransforms(rightHandTransform, leftHandTransform, spellbookTransform).position, Quaternion.identity);
            projectileInstance.SetTarget(target, instigator, calculatedDamage, GetElementalDamages());
        }
        public float GetDamage()
        {
            return weaponBaseDamage;
        }
        public ElementalDamage[] GetRawElementalDamages()
        {
            return elementalDamages;
        }

        public float GetRange()
        {
            return weaponRange;
        }

        public float GetPercentageBonus()
        {
            return percentageBonus;
        }

        public IEnumerable<float> GetAdditiveModifiers(Stats stat)
        {
            if (stat == Stats.PhysicalDamage)
            {
                yield return weaponBaseDamage;
            }
        }

        public IEnumerable<float> GetPercentageModifiers(Stats stat)
        {
            if (stat == Stats.PhysicalDamage)
            {
                yield return percentageBonus;
            }
        }
    }
}

Found it, it was in this section. I autogenerated the overload, and for some reason Rider used o instead of Instigator. The SetTarget() header should read

public void SetTarget(Health, Target, GameObject instigator, float calculatedDamage, IEnumerable<ElementalDamage> elementalDamages);

Sorry about that.

1 Like

Don’t be sorry, I couldn’t figure this stuff out without you, at least not without taking more courses than I have at the moment. I appreciate the help. I’ll implement the change and get back to you about how well it works.

So it seems to mostly work now except it’s ONLY dealing the elemental damage selected, not the weapon’s base damage with the added base damage from the player’s progression.

Figured it out; you had a float listed as “calculatedDamage” but for my code it needed to be “damage.” Projectiles now have elemental damage! Cool, thanks very much.

Privacy & Terms