No drop or Null reference and projectiles errors

Hello.
In this course I encounter strange happening with the changes of Random Drop and Drop library.
There is no loot at all but the null reference doesn’t pop at each try.
I have an error message with my projectiles when it wants to destroy them…
Sometime the enemy has 0 point and die only after 2 or 3 more hits…
I precise if I don’t add the line with randomDropper in onDie event, everything works fine and I haven’t error message with my projectiles…
Here are the error messages:

NullReferenceException: Object reference not set to an instance of an object
RPG.Inventories.DropLibrary.GetRandomDrop (System.Int32 level) (at Assets/Scripts/Inventories/DropLibrary.cs:70)
RPG.Inventories.DropLibrary+d__6.MoveNext () (at Assets/Scripts/Inventories/DropLibrary.cs:50)
RPG.Inventories.RandomDropper.RandomDrop () (at Assets/Scripts/Inventories/RandomDropper.cs:22)
UnityEngine.Events.InvokableCall.Invoke () (at <30adf90198bc4c4b83910c6fb1877998>:0)
UnityEngine.Events.UnityEvent.Invoke () (at <30adf90198bc4c4b83910c6fb1877998>:0)
RPG.Attributes.Health.TakeDamage (UnityEngine.GameObject instigator, System.Single damage) (at Assets/Scripts/Attributes/Health.cs:51)
RPG.Combat.Projectile.OnTriggerEnter (UnityEngine.Collider other) (at Assets/Scripts/Combat/Projectile.cs:67)

I’ve check a lot of time the scripts but didn’t find any mistake.
Here are them.


using GameDevTV.Inventories;

using RPG.Stats;

using UnityEngine;

using UnityEngine.AI;

namespace RPG.Inventories

{

    public class RandomDropper : ItemDropper

    {

        //CONFIG DATA

        [Tooltip("A quelle distance les pickups peuvent ils être dispersés")]

        [SerializeField] float scatterDistance = 1;

        [SerializeField] DropLibrary dropLibrary;

        //CONSTANT

        const int ATTEMPTS  = 30;

        public void RandomDrop()

        {

            var baseStats = GetComponent<BaseStats>();

           

            var drops = dropLibrary.GetRandomDrops(baseStats.GetLevel());//On choisit unobjet à looter dans la liste des objets lootables

            foreach (var drop in drops)

            {

                DropItem(drop.item, drop.number);//On lache un objet (pour le moment)}          

            }

        }

        protected override Vector3 GetDropLocation()

        {

            //On devrait avoir à tester plus d'une fois d'avoir le navmesh

            for (int i = 0; i < ATTEMPTS; i++)

            {          

                Vector3 randomPoint = transform.position + Random.insideUnitSphere * scatterDistance;

                NavMeshHit hit;

                if(NavMesh.SamplePosition(randomPoint, out hit,0.1f, NavMesh.AllAreas))//Pour poser le loot sur le navmaesh (Allarea)

                {

                    return hit.position;

                }

            }

            return transform.position;      

        }

    }

}
using System.Collections.Generic;

using GameDevTV.Inventories;

using UnityEngine;

namespace RPG.Inventories

{

    [CreateAssetMenu(menuName = ("RPG/Inventory/Drop Library"))]

    public class DropLibrary : ScriptableObject

    {

        [SerializeField]

        DropConfig[] potentialDrops;

        [SerializeField] float[] dropChancePercentage;

        [SerializeField] int[] minDrops;

        [SerializeField] int[] maxDrops;

        [System.Serializable]

        class DropConfig

        {

            public InventoryItem item;

            public float[] relativeChance;

            public int[] minNumber;

            public int[] maxNumber;

            public int GetRandomNumber(int level)

            {

                if(!item.IsStackable())

                {

                    return 1;

                }

                int min = GetByLevel(minNumber, level);

                int max = GetByLevel(maxNumber, level);

                return UnityEngine.Random.Range(min, max + 1);

            }

        }

        public struct Dropped

        {

            public InventoryItem item;

            public int number;

        }

        public IEnumerable<Dropped> GetRandomDrops(int level)

        {

            if(!ShouldRandomDrop(level))

            {

                yield break;

            }

            for (int i = 0; i < GetRandomNumberOfDrops(level); i++)

            {

                yield return GetRandomDrop(level);

            }

        }

        bool ShouldRandomDrop(int level)

        {

            return Random.Range(0,100) < GetByLevel(dropChancePercentage, level);

        }

        int GetRandomNumberOfDrops(int level)

        {

            int min = GetByLevel(minDrops, level);

            int max = GetByLevel(maxDrops, level);

            return Random.Range(min, max);

        }

        Dropped GetRandomDrop(int level)

        {

            var drop = SelectRandomItem(level);

            var result = new Dropped();

            result.item = drop.item;

            result.number = drop.GetRandomNumber(level);

            return result;

        }

       

        DropConfig SelectRandomItem(int level)

        {

            float totalChance = GetTotalChance(level);

            float randomRoll = Random.Range(0 , totalChance);

            float chancetotal = 0;

            foreach (var drop in potentialDrops)

            {

                chancetotal += GetByLevel(drop.relativeChance, level);

                if(chancetotal > randomRoll)

                {

                    return drop;

                }

            }

            return null;

        }

        float GetTotalChance(int level)

        {

            float total = 0;

            foreach (var drop in potentialDrops)

            {

                total += GetByLevel(drop.relativeChance, level);

            }

            return total;

        }

        static T GetByLevel<T>(T[] values, int level)

        {

            if (values.Length == 0)

            {

                return default;

            }

            if (level > values.Length)

            {

                return values[values.Length - 1];

            }

            if (level <=0)

            {

                return default;

            }

            return values [level - 1];

        }

    }

}
using UnityEngine;

using GameDevTV.Saving;

using RPG.Stats;

using RPG.Core;

using GameDevTV.Utils;

using UnityEngine.Events;

namespace RPG.Attributes

{

    public class Health : MonoBehaviour, ISaveable

    {

        [SerializeField] float regeneratePercentage = 75;

        [SerializeField] UnityEvent<float> takeDamageEvent;//Le float est là pour signifier à Unity que l'event va effectuer une opération (addition, multiplication etc...)

        [SerializeField] UnityEvent onDie;

        LazyValue<float> healthPoints;//Pour corriger le bug de resurection au 2eme rappel de sauvegarde        

        private bool isDead = false;

        private void Awake()

        {

            healthPoints = new LazyValue<float>(GetInitialHealth);

        }

        private float GetInitialHealth()

        {

            return GetComponent<BaseStats>().GetStat(Stat.Health);//Pour les pv au démarrage on récupère l'info health via getStat

        }

        private void Start()

        {

            healthPoints.ForceInit();

        }

        private void OnEnable()

        {

            GetComponent<BaseStats>().onLevelUp += RegenerateHealth;//On ajoute à la liste d'écoute l'évènement onLevelUp la méthode RegenerateHealth

        }

        private void OnDisable()

        {

            GetComponent<BaseStats>().onLevelUp -= RegenerateHealth;//On retire à la liste d'écoute l'évènement onLevelUp la méthode RegenerateHealth

        }

        //Création d'une bool public qui renseignera les autres script de la mort ou non du personnage.

        public bool IsDead()

        {

            return isDead;

        }

        public void TakeDamage(GameObject instigator, float damage)

        {

            print(gameObject.name + " est touché et perd " + damage + " PV");

            healthPoints.value = Mathf.Max(healthPoints.value - damage, 0);//Cela borne la santé au fur et à mesure quelle chute en health et 0.

           

            //print(healthPoints);

            if(healthPoints.value == 0)

                {

                    onDie.Invoke();

                    Die();

                    AwardExperience(instigator);

                }

            else

            {

                takeDamageEvent.Invoke(damage);

            }

        }

        public void Heal(float healToRestore)

        {

            healthPoints.value = Mathf.Min(healthPoints.value + healToRestore, GetMaxHealthPoints());

        }

        public float GetHealthPoints()//Méthode pour récupérer les pv actuel du porteur de script.

        {

            return healthPoints.value;

        }

        public float GetMaxHealthPoints()

        {

            return GetComponent<BaseStats>().GetStat(Stat.Health);//PV max du niveau actuel

        }

        public float GetPercentage()

        {

            return 100 * GetFraction();

        }

        public float GetFraction()

        {

            return healthPoints.value / GetComponent<BaseStats>().GetStat(Stat.Health); //PV actuels divisé par PV de départ

        }

        private void Die()

        {

            if (isDead) return;//Si la booléenne isDead est dans le statut fixé au départ dans le script (false) alors on ne lit pas la suite du code.

            isDead = true;

            GetComponent<Animator>().SetTrigger("die");

            GetComponent<ActionScheduler>().CancelCurrentAction();//Si le héro est mort, les IA arrêteront leurs actions.

        }

        private void AwardExperience(GameObject instigator)

        {//On récupère le composant expèrience

            Experience experience = instigator.GetComponent<Experience>();

            if(experience == null) return;

            experience.GainExperience(GetComponent<BaseStats>().GetStat(Stat.ExperienceReward));

        }

        private void RegenerateHealth()

        {

            float regenHealthPoints = GetComponent<BaseStats>().GetStat(Stat.Health)*(regeneratePercentage/100);

            healthPoints.value = Mathf.Max(healthPoints.value, regenHealthPoints);//On borne les pv entre les pv actuels et le pourcentage de pv regagné

        }

        public object CaptureState()

        {

            return healthPoints.value;

        }

        public void RestoreState(object state)

        {

            healthPoints.value =(float)state;

            //On vérifie si à la sauvegarde un des PNJ est mort, si oui pv=0 alors lors du chargement de la sauvegarde on lui assigne le statut de mort via Die(). Avant, la seul raison pour laquelle on lançait Die() c'était quand le pnj était frappé et ses pv tombés à 0.

            if(healthPoints.value == 0)

            {

                Die();

            }

        }

    }

}
using RPG.Attributes;

using UnityEngine;

using UnityEngine.Events;

namespace RPG.Combat

{

    public class Projectile : MonoBehaviour

{

    [SerializeField] private float speed = 1;

    [SerializeField] private float projectileDamage = 0;

    [SerializeField] private bool isHoming = true;//Tête chercheuse.

    [SerializeField] private GameObject hitEffect = null;

    [SerializeField] private float MaxLifeTime = 5;//Pour détruire les missiles qui ont manqués leur cible.

    [SerializeField] private GameObject[] destroyOnHit = null;//Création d'une liste d'objet qu'on souhaite détruire à l'impact.

    [SerializeField] private float lifeAfterImpact = 2f;

    [SerializeField] private UnityEvent onHit;

   

    private Health target = null;

    GameObject instigator = null;

    void Start()

    {

        if(target != null)

        {

            transform.LookAt(GetAimLocation());//Dés le départ on vise la cible.Annule l'effet missile chercheur.

        }

    }

    void Update()

    {

        if (target == null) return;

        if(isHoming && !target.IsDead())//Si le missile est à tête chercheuse, il va suivre la cible. sauf s'il est mort, il continuera son chemin

        {

            transform.LookAt(GetAimLocation());

        }

        transform.Translate(Vector3.forward * speed * Time.deltaTime);

    }

    public void SetTarget(Health target, GameObject instigator, float projectileDamage)

    {

        Debug.Log($"Arrow fired for {projectileDamage} damage");

        this.target = target; // La santé de la cible est celle du porteur de ce script ciblé.

        this.projectileDamage += projectileDamage; //Les dommages infligés par le porteur du script (le projectile) sont les dommages du projectile.

        this.instigator = instigator;

        Destroy(gameObject, MaxLifeTime);

    }

    private Vector3 GetAimLocation()

    {

        BoxCollider targetBox =target.GetComponent<BoxCollider>();

        if(targetBox == null)

        {

            return target.transform.position;

        }

        return target.transform.position + Vector3.up * targetBox.size.y /1.5f;

    }

    private void OnTriggerEnter(Collider other)

    {

        if(other.GetComponent<Health>() != target) return;//Si l'objet touché n'a pas de composant Health, on ne lit pas la suite du code.

        if(target.IsDead()){return;}//Si la cible est morte, la fleche continue son chemin.

        Debug.Log($"Hitting {target} for {projectileDamage}");

       

        onHit.Invoke();

        //Création de l'efffet d'impact

        if(hitEffect != null)

        {

        Instantiate(hitEffect, GetAimLocation(), transform.rotation);

        }

        target.TakeDamage(instigator,projectileDamage);

        speed = 0.5f;//On arrete le projectile

        //Destruction à l'impact de la liste d'objet qu'on a choisi.

        foreach (GameObject toDestroy in destroyOnHit)

        {

            Destroy(toDestroy);

        }

        Destroy(gameObject , lifeAfterImpact);

    }

}

}

My enemies have RandomDropper Script:
image

Here is my DropLibrary scriptableobject:

And as I say in start, if I delete the randomDropper event in OnDie event, everything work fine (no drop of course but no error) in the best of the worlds.
I can kill every people I want :slight_smile:

I lost HP and win XP normally without the new script…
I precise I haven’t had this bug in the preview lesson with fixed number of drop and drop library … :sweat_smile: :crazy_face:
Does somebody have any idea what happen ?
Thanks a lot.
Take care.

François

Edit 2024-05-09
Hello, If put the debug.log that Brian advice in the random.dropper and the DropLibrary:

DropLibrary:

public IEnumerable<Dropped> GetRandomDrops(int level)

        {

            if(!ShouldRandomDrop(level))

            {

                Debug.Log($"Player's roll to get loot failed.  No loot will drop.");

                yield break;

            }

            int numberOfDrops = GetRandomNumberOfDrops(level);

            Debug.Log($"Success! {numberOfDrops} items will drop!");

            for (int i = 0; i < GetRandomNumberOfDrops(level); i++)

            {

                yield return GetRandomDrop(level);

            }

        }

        bool ShouldRandomDrop(int level)

        {

            float playerRoll = Random.Range(0,100);

            Debug.Log($"Player rolled {playerRoll}. Drop Chance Percentage is" + GetByLevel(dropChancePercentage, level));

            return playerRoll < GetByLevel(dropChancePercentage, level);

            /*return Random.Range(0,100) < GetByLevel(dropChancePercentage, level);*/

        }

and RandomDropper

public void RandomDrop()

        {

            var baseStats = GetComponent<BaseStats>();

            var drops = dropLibrary.GetRandomDrops(baseStats.GetLevel());//On choisit unobjet à looter dans la liste des objets lootables

            Debug.Log($"{name} has died and will try to drop items.");

           

            foreach (var drop in drops)

            {

                DropItem(drop.item, drop.number);//On lache un objet (pour le moment)}      

                Debug.Log($"{name} has dropped {drop.number} {drop.item}(s)");    

            }

        }

I’ve tweak my Drop Percentage Chance in droplibrary:
image

And Strangely, the enemy only die when the roll is over the min percentage and the loot fail…

image

As long as the roll is a success and the loot sequence is launch, the enemy refuse to die (their hitpoints are already under 0 but still fighting).
The roll is a success but nothing spawn and the enemy refuse his destiny an go to Valhala :slight_smile:

well… I don’t understand at all what happens…
If I put the “old” random script of lesson 61 with the fixed number of loot and library in the enemy, every thing works fine.
The Enemies loot their items, they die at the good time and I haven’t null references with my arrows when the try to desapear…
If any one have an idea ?
Thank,
Have a good day.
François


This doesn’t look right to me. It looks like you filled in values where the size of the arrays are. I could be wrong, though.

Expand all these (marked in red) and send a new screenshot, please

Hello Bixarrio.
Thanks for your feedback.
I must admit the concept of min and max number versus min drops and max drops is a little but blurry for me. I don’t see too much the difference. If you can enlight me ? :slight_smile:
Here is the field within all the “red” fields.
There are all empty but I don’t remember Sam filled them in the course?
image
The same for the bow
image

The more I writte this reply the more I must say I’m a little but confuse with the fact there is so much element. What are they do ?
What the point all this fields?
I think I must miss something in the spirit lesson :sweat_smile:
Thanks
François

Those are the fields you added.
image
These here are not values, they are how many values you want to specify. These fields are meant to match your levels. You have 0 for everything on every level, so the GetRandomDrop seems to never find a match and returns default (which - for an object - is null)


This is from the lecture. The red is the size (the bits you filled in) and the blue is the actual values that are being used. The inspector changed a little since the course was recorded

Hello Bixarrio.
Thank to spend time for my problem.
I rebuilt the drop library and filled the empty field, simplify it with only 2 items but nothing change…

I’ve find this topic of another people in the forum where Brian Explain more accuratly the differents fields :
here

But my problem doesn’t change at all :grimacing:
The enemy refuse to die even when his life reach 0 and if he dies, nothing loot…
I totally don’t understand why I haven’t this result with the other randomDropper script those before the one with the DefaultDropLibray.
I precise I had the itemDropper script on the enemy.
I don’t remember if Sam made it but nothing change…
Waiting to resolve this problem I go back to the “old” ItemDropper Script and attack Quest and dialog Courses;)
Sorry for the boring…
François

Initially the code didn’t complete because you had an error. You have now fixed your drop library but it’s still not completing suggesting you still have an error. Please post the error

Arff… :blush:
Forget it…
The noob, I 've forget to fill the relative chance field …
They were to 0…
:sweat_smile:
Sorry, everything works fine…
The enemy die normally and loot normally ! :heartpulse:
Have a good day :slight_smile:
François

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

Privacy & Terms