NullExceptionError

when i kill an enemy and get xp i get a nullExceptionError pointing to the onExperienceGained() in the experience script.

its definitely the delegate that causes the problems because if i comment out onExperienced gained and call UpdateLevel as a public function i dont get this error. also if i get the component experience inside the onenable function the error goes away and i update the level. this is strange since i get the experience component in the awake function. isnt onenable called after awake?

NullReferenceException: Object reference not set to an instance of an object
RPG.Stats.Experience.GainExperience (System.Single experience) (at Assets/Scripts/Stats/Experience.cs:21)
RPG.Attributes.Health.AwardExperience (UnityEngine.GameObject instigator) (at Assets/Scripts/Attributes/Health.cs:81)
RPG.Attributes.Health.TakeDamage (UnityEngine.GameObject instigator, System.Single damage) (at Assets/Scripts/Attributes/Health.cs:70)
RPG.Combat.Fighter.Hit () (at Assets/Scripts/Combat/Fighter.cs:183)

Is your BaseStats subscribing to experience.GainExperience?

Paste in both your BaseStats and Experience scripts and we’ll take a look.

namespace RPG.Stats
{
    public class BaseStats : MonoBehaviour
    {
        // Range will add a slider in unity where we can set our starting level. 
        [Range(1,99)]
        [SerializeField] int startingLevel = 1;
        // the character of our class, we choose our class from the enum, inside unity.
        [SerializeField] CharacterClass characterClass;
        // start it as null, if there is none, we dont get any compilation problems. we add the progression scriptable object.
        [SerializeField] Progression progression = null;
        // the particle effect when we level up, prefab set in unity.
        [SerializeField] GameObject levelUpParticleEffect = null;
        // enemies should not use modifiers.
        [SerializeField] bool shouldUseModifiers = false;

        // lazyvalue, we dont set it to a number since its a container for initialization.
        LazyValue<int> currentLevel;
        Experience experience;

        // delegate that has no arguments.
        public event Action onLevelUp;

        // set up state in awake and not start. want state to be set by the time other start
        // methods might be calling into basestats.
        private void Awake()
        {
            Experience experience = GetComponent<Experience>();
            currentLevel = new LazyValue<int>(CalculateLevel);
            
        }

        private void Start()
        {
            
            // make sure we initialize healhpoints if its not called before this start.
            currentLevel.ForceInit();
            

        }
        // when basestats is enabled we suscribe to update level.
        // register callbacks in onenable function, its best practise, dont do it in start. gets called about same time as awake.
        // but always after it. Dont get race condition with awake. same rules as for awake
        // you cannot set up external functions because you dont know if their state has been set up yet.
        // here we just register a call back, its not an external function since we are not updating any states.
        private void OnEnable()
        {
            if (experience != null)
            {
                // add update level to delegate list.
                experience.onExperienceGained += UpdateLevel;
            }
        }
        // if basestats is disabled we remove the subscription to updatelevel.
        // if something disables basestats it could still continue to get notifications from experience when experience
        // is gained. so we dont get callbacks when basestats is disabled. 
        private void OnDisable()
        {
            if (experience != null)
            {
                // remove update level from delegate list.
               experience.onExperienceGained -= UpdateLevel;
            }
        }

        // update our level.
        public void UpdateLevel()
        {
            int newLevel = CalculateLevel();
            if(newLevel > currentLevel.value)
            {
                currentLevel.value = newLevel;
                LevelUpEffect();
                // set health points to max when we level up. we add regeneration function in health script.
                // If there are no listeners to an event, it is null. When invoking events you should always check if they are null first.
                // so we dont get a null refference error.
                if (onLevelUp != null)
                {
                    onLevelUp();
                }

            }
        }

        // instantiate level up effect around player.
        private void LevelUpEffect()
        {
            Instantiate(levelUpParticleEffect, transform);
        }

        public int GetLevel()
        {
            // will initialize it if it healthpoins isnt already initialized
            return currentLevel.value;
        }
        
        // Gets basestat + added modifiers * percentage bonus(if 10% we make it 1.1 = 110 percent). 
        public float GetStat(Stat stat)
        {
            return (GetBaseStat(stat) + GetAdditiveModifier(stat)) * (1 + GetPercentageModifier(stat) / 100);
        }


        // returns stats level value from progression.
        private float GetBaseStat(Stat stat)
        {
            return progression.GetStat(stat, characterClass, GetLevel());
        }

        // add upp all the modifiers that we have stored for a particular stat in the GetAdditiveModifier function
        // in the IModifierProvider.
        private float GetAdditiveModifier(Stat stat)
        {
            // enemies dont get modifiers.
            if (!shouldUseModifiers) return 0;

            float total = 0;
            foreach (IModifierProvider provider in GetComponents<IModifierProvider>())
            {
                foreach (float modifier in provider.GetAdditiveModifiers(stat))
                {
                    total += modifier;
                }
            }
            return total;
        }

        // add up all percentage modifiers.
        private float GetPercentageModifier(Stat stat)
        {
            // enemies dont get modifiers.
            if (!shouldUseModifiers) return 0;

            float total = 0;
            foreach (IModifierProvider provider in GetComponents<IModifierProvider>())
            {
                foreach (float bonus in provider.GetPercentageModifiers(stat))
                {
                    total += bonus;
                }
            }
            return total;
        }

        // get players xp level.
        private int CalculateLevel()
        {
            Experience experience = GetComponent<Experience>();
            // if its an enemy just return its startinglevel which is its level.
            if (experience == null) return startingLevel;

            float currentXP = experience.GetPoints();
            int penultimateLevel = progression.GetLevels(Stat.ExperienceToLevelUp, characterClass);
            // loops over all the levels we have in experiencetolevelup stat. starts at level 1.
            // start at 1 since we use level - 1 in getstat to reach level arrays 0 index.
            for (int level = 1; level <= penultimateLevel ; level++)
            {
                // get xp you need to level up for each level in experiencetolevelup.
                float xpToLevelUp = progression.GetStat(Stat.ExperienceToLevelUp, characterClass, level);
                // if we find a level with xp greater than our current xp we return this level.
                if (xpToLevelUp > currentXP)
                {
                    return level;
                }
            }
            // else if we have xp greater than the penultimatelevel, add 1 to reach max level, starts at 0.
            return penultimateLevel + 1;

        }
    }
}
namespace RPG.Stats
{
    public class Experience : MonoBehaviour, ISaveable
    {
        [SerializeField] float experiencePoints = 0;

        // Delegate, cant be overwritten since its an event.
        public event Action onExperienceGained;

        public void GainExperience(float experience)
        {
            experiencePoints += experience;
            // call delegate when we gain experience, calls UpdateLevel that we 
            // added to the delegate in basestats.
            onExperienceGained();
            //GetComponent<BaseStats>().UpdateLevel();
            
            
        }

        public float GetPoints()
        {
            return experiencePoints;
        }

        public object CaptureState()
        {
            return experiencePoints;
        }

        public void RestoreState(object state)
        {
            experiencePoints = (float)state;
        }
    }
}

Take a closer look at your BaseStats.Awake() method.

We have a global variable

Experience experience;

but in Awake(), you’re assignment is also

Experience experience = GetComponent<Experience>();

What happens here is that a new local variable experience that only the Awake() method can see is created, and this is assigned the value of GetComponent<Experience>();

Remove the Type from the Awake line and this should be fixed

experience = GetComponent<Experience>();
1 Like

ahh i didnt see that. Thnx!