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:
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
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 …
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:
And Strangely, the enemy only die when the roll is over the min percentage and the loot fail…
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