Possible Lazy Value Implementation Issue w/ Leveling System?

After implementing the Lazy Value stuff, I just can’t seem to fix my leveling system. It was working before and now the character gets experience but doesn’t level up.

Now I get the following error:

NullReferenceException: Object reference not set to an instance of an object
RPG.Attributes.Experience.GainExperience (System.Single experience) (at Assets/OurScripts/Level and Stats/Experience.cs:21)
RPG.Attributes.Health.AwardExperience (UnityEngine.GameObject instigator) (at Assets/OurScripts/Attributes/Health.cs:86)
RPG.Attributes.Health.TakeDamage (UnityEngine.GameObject instigator, System.Single damage) (at Assets/OurScripts/Attributes/Health.cs:49)
RPG.Combat.Projectile.OnTriggerEnter (UnityEngine.Collider other) (at Assets/OurScripts/Combat/Projectile.cs:62)

The section it most directly points to seems fine when I check it:

[SerializeField] float experiencePoints = 0;

        Health playerHealth;
   
     public event Action onExperienceGained;

        private void Awake()
        {
            playerHealth = GameObject.FindWithTag("Player").GetComponent<Health>();
        }

        public void GainExperience(float experience)
        {
            experiencePoints += experience;
            onExperienceGained();
            playerHealth.GetCurrentMaxHealth();
        }

I could use some help fixing this issue. I’m pretty new to coding so I don’t really know how to go through every potential issue on my own.

I don’t think it’s directly related to LazyValue, as there’s not really a LazyValue in this version of the Experience script.

Most likely the error is occuring with the onExperienceGained(); call. If nothing has subscribed to the event, then a null reference error will occur.
With events, it’s always best to null-check them or use null coalescence, either

if(onExperienceGained!=null)
{
    onExperienceGained();
}

or simply

onExperienceGained?.Invoke();

That being said, there must be some reason that onExperienceGained is not being called. Make sure that it is being subscribed to in BaseStats if the Experience component is present.

I had thought I already subscribed right in BaseStats, doing this:

        private void Awake()
        {
            Experience experience = GetComponent<Experience>();
            currentLevel = new LazyValue<int>(CalculateLevel);
        }

        private void Start()
        {
            currentLevel.ForceInit();
        }

        private void OnEnable()
        {
            if (experience != null)
            {
                experience.onExperienceGained += UpdateLevel;
            }
        }

        private void OnDisable()
        {
            if (experience != null)
            {
                experience.onExperienceGained -= UpdateLevel;
            }
        }

But it must be some element of update level not working, right? Because I am getting experience added to my total still. Not sure if it's relevant but Update Level looks like this:

private void UpdateLevel()

        {

            int newLevel = CalculateLevel();

            if(newLevel > currentLevel.value)//will need tweaking if deleveling is ever introduced

            {

                currentLevel.value = newLevel;

                LevelUpEffect();

                onLevelUp();

            }

        }

The error is on line 21 of Experience.cs. Check what is there, and make sure that whatever can be null is not.

Line 21 of that file just reads: onExperienceGained();

As far as I can tell, my Experience script is identical to the one from the lessons besides my adding a link to Health that honestly probably isn’t necessary and which I believe I commented out already while checking this error. I don’t think the actual problem is in the Experience.cs, although I am not certain about that.

The following is my entire Experience script, with the weird part of this problem being that everything used to work until the Lazy Value lesson. That doesn’t mean it directly caused the issue, it could be something I did while trying to readjust for that script. In fact, it must be that if I’m the only one who has had this problem:

using System;
using System.Collections;
using System.Collections.Generic;
using RPG.Saving;
using UnityEngine;

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

        Health playerHealth;
        public event Action onExperienceGained;

        private void Awake()
        {
            playerHealth = GameObject.FindWithTag("Player").GetComponent<Health>();
        }

        public void GainExperience(float experience)
        {
            experiencePoints += experience;
            onExperienceGained();
            playerHealth.GetCurrentMaxHealth();
        }

        public float GetExperience()
        {
            return experiencePoints;
        }

        public object CaptureState()
        {
            return experiencePoints;
        }

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

The exception tells you where the error occurred. That is ... (at Assets/OurScripts/Level and Stats/Experience.cs:21)

You say it’s the onExperienceGained() line so what that suggests is that nothing is listening to that event - like @Brian_Trotter mentioned. You responded with

But your code only subscribes to it if experience is not null when BaseStats.OnEnable() was called. Perhaps it was - start by checking that.

As a rule, though, it’s best to always check if there are listeners to an event. This is why onExperienceGained?.Invoke() is such a handy pattern. It won’t attempt to execute if there are no listeners

Can you run this again and show the error? The code you pasted here does not have anything on line 21 that can raise the NRE. I just want to make sure we follow the correct path, here.

It’s either the event without listeners, or you didn’t get a health component from the player

I quit out and reloaded everything and the error still occurred:

NullReferenceException: Object reference not set to an instance of an object
RPG.Attributes.Experience.GainExperience (System.Single experience) (at Assets/OurScripts/Level and Stats/Experience.cs:21)
RPG.Attributes.Health.AwardExperience (UnityEngine.GameObject instigator) (at Assets/OurScripts/Attributes/Health.cs:86)
RPG.Attributes.Health.TakeDamage (UnityEngine.GameObject instigator, System.Single damage) (at Assets/OurScripts/Attributes/Health.cs:49)
RPG.Combat.Projectile.OnTriggerEnter (UnityEngine.Collider other) (at Assets/OurScripts/Combat/Projectile.cs:62)

I kill an enemy and the player’s experience total goes up, so at least the counter works right. But the level counter doesn’t go up and none of the level up related events seem to occur.

Here is my whole BaseStats script:
using System.Reflection.Emit;

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

using System;

using GameDevTV.Utils;

namespace RPG.Attributes

{

public class BaseStats : MonoBehaviour//Chracter's base stats before item bonuses and penalties

{

    [Range(1, 99)][SerializeField] int startingLevel = 1;//Max level is set at 99. Shouldn't be an issue in most games.

    [SerializeField] CharacterClass characterClass;

    [SerializeField] Progression progression = null;

    [SerializeField] GameObject levelUpParticleEffect = null;

    [SerializeField] Transform levelTarget = null;

    [SerializeField] bool shouldUseModifiers = false;//By default, entities ignore modifiers, since only the player really needs them and it makes tuning easier

    public event Action onLevelUp;

    LazyValue<int> currentLevel; //Intentionally invalid by default

    Experience experience;

    private void Awake()

    {

        Experience experience = GetComponent<Experience>();

        currentLevel = new LazyValue<int>(CalculateLevel);

    }

    private void Start()

    {

        currentLevel.ForceInit();

    }

    private void OnEnable()

    {

        if (experience != null)

        {

            experience.onExperienceGained += UpdateLevel;

        }

    }

    private void OnDisable()

    {

        if (experience != null)

        {

            experience.onExperienceGained -= UpdateLevel;

        }

    }

    private void UpdateLevel()

    {

        int newLevel = CalculateLevel();

        if(newLevel > currentLevel.value)//will need tweaking if deleveling is ever introduced

        {

            currentLevel.value = newLevel;

            LevelUpEffect();

            onLevelUp();

        }

    }

    private void LevelUpEffect()

    {

        Instantiate(levelUpParticleEffect, levelTarget.transform);

    }

    public float GetStat(Stats stat)

    {

        return (GetBaseStat(stat) + GetAdditiveModifier(stat)) * (1 + GetPercentageModifier(stat)/100);

    }

    private float GetBaseStat(Stats stat)

    {

        return progression.GetStat(stat, characterClass, GetLevel());

    }

    public int GetLevel()//helps avoid calculating level a bunch of times each frame

    {

        return currentLevel.value;

    }

    public int CalculateLevel()

    {

        Debug.Log("Calculate Level Attempted");

        Experience experience = GetComponent<Experience>();

        if (experience == null) return startingLevel;

        float currentXP = experience.GetExperience();

        int penultimateLevel = progression.GetLevels(Stats.ExperienceToLevelUp, characterClass);

        for (int level = 1; level <= penultimateLevel; level++)

        {

            float XPToLevelUp = progression.GetStat(Stats.ExperienceToLevelUp, characterClass, level);

            if (XPToLevelUp > currentXP)

            {

                return level;

            }

        }

        return penultimateLevel + 1;

    }

    private float GetAdditiveModifier(Stats stat)

    {

        if(!shouldUseModifiers) return 0;

        float total = 0;

        foreach(IModifierProvider provider in GetComponents<IModifierProvider>())

        {

            foreach (float modifier in provider.GetAdditiveModifiers(stat))

            {

                total += modifier;

            }

        }

        return total;

    }

    private float GetPercentageModifier(Stats stat)

    {

        if(!shouldUseModifiers) return 0;

        float total = 0;

        foreach(IModifierProvider provider in GetComponents<IModifierProvider>())

        {

            foreach (float modifier in provider.GetPercentageModifiers(stat))

            {

                total += modifier;

            }

        }

        return total;

    }

}

}

Hehehe. I hate when this happens… Your BaseStats.experience is null, so it never subscribes to the event

private void Awake()
{
    Experience experience = GetComponent<Experience>(); // <-- The problem is here
    currentLevel = new LazyValue<int>(CalculateLevel);
}

can you see it?

You are setting a local variable instead of the class variable. It is lost as soon as Awake completes. It should just be

private void Awake()
{
    experience = GetComponent<Experience>(); // <-- Set the class variable
    currentLevel = new LazyValue<int>(CalculateLevel);
}
1 Like

Your issue is in BaseStats.Awake()

You have a global Experience experience; which is what OnEnable() is checking when it determines whether or not to subscribe to experience.onExperienceGained. The problem is that in Awake, you’re declaring a new Experience experience by specifying the type in the statement

Experience experience = GetComponent<Experience>(); 

This becomes a new local variable that only exists within Awake() and completely hides the global experience variable.

Simply remove the Experience from the beginning of the line in Awake()

experience = GetComponent<Experience>();

And this should resolve the issue.

1 Like

Thanks to both you and @bixarrio! This seems to have totally solved the issue.

1 Like

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

Privacy & Terms