Forum begins after the advertisement:
[part 15] NullReferenceException on treasure chest
Home › Forums › Video Game Tutorial Series › Creating a Rogue-like Shoot-em Up in Unity › [part 15] NullReferenceException on treasure chest
- This topic has 12 replies, 3 voices, and was last updated 1 week ago by
Alp Apustaja.
-
AuthorPosts
-
August 19, 2025 at 1:44 am #18692::
ok so im back again and almost finished with part 15(it’s quite dense and a bit difficult to troubleshoot as we only test the game at the end). when i play the game now everything apparently works (with a couple of errors on the console) except for the chest not being destroyed when my character collides with it
when i start the game i get both this errors:
NullReferenceException: Object reference not set to an instance of an object Projectile.Start () (at Assets/Scripts/Weapons/Weapon Effects/Projectile.cs:17)
NullReferenceException: Object reference not set to an instance of an object Projectile.FixedUpdate () (at Assets/Scripts/Weapons/Weapon Effects/Projectile.cs:70)
it happens anyways if I use the aura or the projectile characters, but the game plays normally. it just shows this messages on the console
and then when i try to pick the chest i get this error and the chest dont get destroyed (my chest is called xenovault):
NullReferenceException: Object reference not set to an instance of an object PlayerInventory.Remove (WeaponData data, System.Boolean removeUpgradeAvailability) (at Assets/Scripts/Player/PlayerInventory.cs:112) Item.AttemptEvolution (ItemData+Evolution evolutionData, System.Int32 levelUpAmount) (at Assets/Scripts/Passive Items/Item.cs:86) XenoVault.OpenXenoVault (PlayerInventory inventory, System.Boolean isHigherTier) (at Assets/Scripts/Pickups/XenoVault.cs:33) XenoVault.OnTriggerEnter2D (UnityEngine.Collider2D col) (at Assets/Scripts/Pickups/XenoVault.cs:13)
here’s my scripts:
Projectile:
using UnityEngine; [RequireComponent(typeof(Rigidbody2D))] public class Projectile : WeaponEffect { public enum DamageSource { projectile, owner }; public DamageSource damageSource = DamageSource.projectile; public bool hasAutoAim = false; public Vector3 rotationSpeed = new Vector3(0, 0, 0); protected Rigidbody2D rb; protected int piercing; protected virtual void Start() { rb = GetComponent<Rigidbody2D>(); Weapon.Stats stats = weapon.GetStats(); if (rb.bodyType == RigidbodyType2D.Dynamic) { rb.angularVelocity = rotationSpeed.z; rb.linearVelocity = transform.right * stats.speed; } //prevent the area from being 0, as it hides the projectile float area = stats.area == 0 ? 1 : stats.area; transform.localScale = new Vector3( area * Mathf.Sign(transform.localScale.x), area * Mathf.Sign(transform.localScale.y), 1 ); //set how much piercing this object has piercing = stats.piercing; //destroy the projectile after its lifespan expires if (stats.lifespan > 0) Destroy(gameObject, stats.lifespan); //if the projectile is auto-aiming, automatically find a suitable enemy if (hasAutoAim) AcquireAutoAimFacing(); } //if the projectile is homing , it will automatically find a suitable target to move towards public virtual void AcquireAutoAimFacing() { float aimAngle; //find all enemis on screen EnemyStats[] targets = FindObjectsByType<EnemyStats>(FindObjectsInactive.Exclude, FindObjectsSortMode.None); //select a random enemy (if there is at least 1). otherwise, pick a random angle if (targets.Length > 0) { EnemyStats selectedTarget = targets[Random.Range(0, targets.Length)]; Vector2 difference = selectedTarget.transform.position - transform.position; aimAngle = Mathf.Atan2(difference.y, difference.x) * Mathf.Rad2Deg; } else { aimAngle = Random.Range(0f, 360f); } //point the projectile towards where we are aiming at transform.rotation = Quaternion.Euler(0, 0, aimAngle); } protected virtual void FixedUpdate() { //only drive movement ourselves if this is a kinematic if (rb.bodyType == RigidbodyType2D.Kinematic) { Weapon.Stats stats = weapon.GetStats(); transform.position += transform.right * stats.speed * Time.fixedDeltaTime; rb.MovePosition(transform.position); transform.Rotate(rotationSpeed * Time.fixedDeltaTime); } } protected virtual void OnTriggerEnter2D(Collider2D other) { EnemyStats es = other.GetComponent<EnemyStats>(); BreakableProps p = other.GetComponent<BreakableProps>(); //only collide with enemies or breakable stuff if (es) { //if there is an owner and the damage source is set to owner we calculate knockback using the owner instead of projectile Vector3 source = damageSource == DamageSource.owner && owner ? owner.transform.position : transform.position; //deals damage and destroys the projectile es.TakeDamage(GetDamage(), source); Weapon.Stats stats = weapon.GetStats(); piercing--; if (stats.hitEffect) { Destroy(Instantiate(stats.hitEffect, transform.position, Quaternion.identity), 5f); } } else if (p) { p.TakeDamage(GetDamage()); piercing--; Weapon.Stats stats = weapon.GetStats(); if (stats.hitEffect) { Destroy(Instantiate(stats.hitEffect, transform.position, Quaternion.identity), 5f); } } //destroy this object if it has run out of health from hitting other stuff if (piercing <= 0) Destroy(gameObject); } }
PlayerInventory
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class PlayerInventory : MonoBehaviour { [System.Serializable] public class Slot { public Item item; public Image image; public void Assign(Item assignedItem) { item = assignedItem; if (item is Weapon) { Weapon w = item as Weapon; image.enabled = true; image.sprite = w.data.icon; } else { Passive p = item as Passive; image.enabled = true; image.sprite = p.data.icon; } Debug.Log(string.Format("Assigned {0} to player.", item.name)); } public void Clear() { item = null; image.enabled = false; image.sprite = null; } public bool IsEmpty() { return item == null; } } public List<Slot> weaponSlots = new List<Slot>(6); public List<Slot> passiveSlots = new List<Slot>(6); [System.Serializable] public class UpgradeUI { public TMP_Text upgradeNameDisplay; public TMP_Text upgradeDescriptionDisplay; public Image upgradeIcon; public Button upgradeButton; } [Header("UI Elements")] public List<WeaponData> availableWeapons = new List<WeaponData>(); //list of upgrade options for weapons public List<PassiveData> availablePassives = new List<PassiveData>(); //list of upgrade options for passive items public List<UpgradeUI> upgradeUIOptions = new List<UpgradeUI>(); //list of ui for upgrade options present in the scene PlayerStats player; void Start() { player = GetComponent<PlayerStats>(); } //checks if the inventory has an item of a certain type public bool Has(ItemData type) { return Get(type); } public Item Get(ItemData type) { if (type is WeaponData) return Get(type as WeaponData); else if (type is PassiveData) return Get(type as PassiveData); return null; } //find a passive of certain type in the inventory public Passive Get(PassiveData type) { foreach (Slot s in passiveSlots) { Passive p = s.item as Passive; if (p.data == type) return p; } return null; } //find a weapon of certain type in the inventory public Weapon Get(WeaponData type) { foreach (Slot s in weaponSlots) { Weapon w = s.item as Weapon; if (w.data == type) return w; } return null; } //removes a weapon of a particular type as specified by <data> public bool Remove(WeaponData data, bool removeUpgradeAvailability = false) { //remove this weapon from the upgrade pool if (removeUpgradeAvailability) availableWeapons.Remove(data); for (int i = 0; i < weaponSlots.Count; i++) { Weapon w = weaponSlots[i].item as Weapon; if (w.data == data) { weaponSlots[i].Clear(); w.OnUnequip(); Destroy(w.gameObject); return true; } } return false; } //removes a passive of a particular type, as specified by <data> public bool Remove(PassiveData data, bool removeUpgradeAvailability = false) { //remove this passive from the upgrade pool if (removeUpgradeAvailability) availablePassives.Remove(data); for (int i = 0; i < passiveSlots.Count; i++) // //for (int i = 0; i < weaponSlots.Count; i++) { Passive p = passiveSlots[i].item as Passive; // //Passive p = weaponSlots[i].item as Passive; if (p.data == data) { passiveSlots[i].Clear(); // //weaponSlots[i].Clear(); p.OnUnequip(); Destroy(p.gameObject); return true; } } return false; } // if an ItemData is passed, determine what type it is and call the respective overload //we also have an optional boolean to remove this item from the upgrade list public bool Remove(ItemData data, bool removeUpgradeAvailability = false) { if (data is PassiveData) return Remove(data as PassiveData, removeUpgradeAvailability); else if (data is WeaponData) return Remove(data as WeaponData, removeUpgradeAvailability); return false; } //finds an empty slot and adds a weapon of a certain type,returns the slot number that the item was put in public int Add(WeaponData data) { int slotNum = -1; //try to find an empty slot for (int i = 0; i < weaponSlots.Capacity; i++) { if (weaponSlots[i].IsEmpty()) { slotNum = i; break; } } //if there is no empty slot, exit if (slotNum < 0) return slotNum; //otherwise create the weapon in the slot and get the type of the weapon we want to spawn Type weaponType = Type.GetType(data.behaviour); if (weaponType != null) { //spawn the weapon GameObject GameObject go = new GameObject(data.baseStats.name + " Controller"); Weapon spawnedWeapon = (Weapon)go.AddComponent(weaponType); spawnedWeapon.Initialise(data); spawnedWeapon.transform.SetParent(transform); //set the weapon to be a child of the player spawnedWeapon.transform.localPosition = Vector2.zero; spawnedWeapon.OnEquip(); //assign the weapon to the slot weaponSlots[slotNum].Assign(spawnedWeapon); //close the level up UI if it is on if (GameManager.instance != null && GameManager.instance.choosingUpgrade) GameManager.instance.EndLevelUp(); return slotNum; } else { Debug.LogWarning(string.Format( "Invalid weapon type specified for {0}.", data.name )); } return -1; } // finds an empty slot and adds a passive of a certain type, returns the slot number that the item was put in public int Add(PassiveData data) { int slotNum = -1; //try to find an empty slot for (int i = 0; i < passiveSlots.Capacity; i++) { if (passiveSlots[i].IsEmpty()) { slotNum = i; break; } } //if there is no empty slot, exit if (slotNum < 0) return slotNum; //otherwise create the passive in the slot and get the type of the passive we want to spawn GameObject go = new GameObject(data.baseStats.name + " Passive"); Passive p = go.AddComponent<Passive>(); p.Initialise(data); p.transform.SetParent(transform); //set the passive to be a child of the player p.transform.localPosition = Vector2.zero; //assign the passive to the slot passiveSlots[slotNum].Assign(p); if (GameManager.instance != null && GameManager.instance.choosingUpgrade) { GameManager.instance.EndLevelUp(); } player.RecalculateStats(); return slotNum; } // if we dont know what item is being added, this function will determine that public int Add(ItemData data) { if (data is WeaponData) return Add(data as WeaponData); else if (data is PassiveData) return Add(data as PassiveData); return -1; } public void LevelUpWeapon(int slotIndex, int upgradeIndex) { if (weaponSlots.Count > slotIndex) { Weapon weapon = weaponSlots[slotIndex].item as Weapon; //don't level up the weapon if it is already at max level if (!weapon.DoLevelUp()) { Debug.LogWarning(string.Format( "Failed to level up {0}.", weapon.name )); return; } } if (GameManager.instance != null && GameManager.instance.choosingUpgrade) { GameManager.instance.EndLevelUp(); } } public void LevelUpPassiveItem(int slotIndex, int upgradeIndex) { if (passiveSlots.Count > slotIndex) { Passive p = passiveSlots[slotIndex].item as Passive; if (!p.DoLevelUp()) { Debug.LogWarning(string.Format( "Failed to level up {0}.", p.name )); return; } } if (GameManager.instance != null && GameManager.instance.choosingUpgrade) { GameManager.instance.EndLevelUp(); } player.RecalculateStats(); } //determine what upgrade options should appear void ApplyUpgradeOptions() { //make a duplicate of the available weapon / passive upgrade lists so we can iterate through them in the function List<WeaponData> availableWeaponUpgrades = new List<WeaponData>(availableWeapons); List<PassiveData> availablePassiveItemUpgrades = new List<PassiveData>(availablePassives); //iterate through each slot in the upgrade UI foreach (UpgradeUI upgradeOption in upgradeUIOptions) { //if there are no more available upgrades, then we abort if (availableWeaponUpgrades.Count == 0 && availablePassiveItemUpgrades.Count == 0) return; //determine whether this upgrade should be for passive or active weapons int upgradeType; if (availableWeaponUpgrades.Count == 0) { upgradeType = 2; } else if (availablePassiveItemUpgrades.Count == 0) { upgradeType = 1; } else { //random generates a number between 1 and 2 upgradeType = UnityEngine.Random.Range(1, 3); } //generates an active weapon upgrade if (upgradeType == 1) { //pick a weapon upgrade, then remove it so that we don't get it twice WeaponData chosenWeaponUpgrade = availableWeaponUpgrades[UnityEngine.Random.Range(0, availableWeaponUpgrades.Count)]; availableWeaponUpgrades.Remove(chosenWeaponUpgrade); //ensure that the selected weapon data is valid if (chosenWeaponUpgrade != null) { //turns on the UI slot EnableUpgradeUI(upgradeOption); //loops through all our existing weapons. if we find a match, we will //hook an event listener to the button that will level up the weapon //when this upgrade option is clicked bool isLevelUp = false; for (int i = 0; i < weaponSlots.Count; i++) { Weapon w = weaponSlots[i].item as Weapon; if (w != null && w.data == chosenWeaponUpgrade) { //if the weapon is already at the max level, do not allow upgrade if (chosenWeaponUpgrade.maxLevel <= w.currentLevel) { //DisableUpgradeUI(upgradeOption); isLevelUp = false; break; } // set the event listener, item and level description to be that opf the next level upgradeOption.upgradeButton.onClick.AddListener(() => LevelUpWeapon(i, i)); //apply button functionality Weapon.Stats nextLevel = chosenWeaponUpgrade.GetLevelData(w.currentLevel + 1); upgradeOption.upgradeDescriptionDisplay.text = nextLevel.description; upgradeOption.upgradeNameDisplay.text = nextLevel.name; upgradeOption.upgradeIcon.sprite = chosenWeaponUpgrade.icon; isLevelUp = true; break; } } //if the code gets here, it means that we will be adding a new weapon , intead of upgrading an existing one if (!isLevelUp) { upgradeOption.upgradeButton.onClick.AddListener(() => Add(chosenWeaponUpgrade)); //apply button functionality upgradeOption.upgradeDescriptionDisplay.text = chosenWeaponUpgrade.baseStats.description; upgradeOption.upgradeNameDisplay.text = chosenWeaponUpgrade.baseStats.name; upgradeOption.upgradeIcon.sprite = chosenWeaponUpgrade.icon; } } } else if (upgradeType == 2) { //NOTE: we have to recode this system as right now it disables an upgrade slot if //we hit a weapon that has already reached its max level PassiveData chosenPassiveUpgrade = availablePassiveItemUpgrades[UnityEngine.Random.Range(0, availablePassiveItemUpgrades.Count)]; availablePassiveItemUpgrades.Remove(chosenPassiveUpgrade); if (chosenPassiveUpgrade != null) { //turns on the UI slot EnableUpgradeUI(upgradeOption); //loops through all our existing passives. if we find a match, we will //hook an event listener to the button that will level up the passive //when this upgrade option is clicked bool isLevelUp = false; for (int i = 0; i < passiveSlots.Count; i++) { Passive p = passiveSlots[i].item as Passive; if (p != null && p.data == chosenPassiveUpgrade) { //if the passive is already at the max level, do not allow upgrade if (chosenPassiveUpgrade.maxLevel <= p.currentLevel) { //DisableUpgradeUI(upgradeOption); isLevelUp = false; break; } // set the event listener, item and level description to be that of the next level upgradeOption.upgradeButton.onClick.AddListener(() => LevelUpPassiveItem(i, i)); //apply button functionality Passive.Modifier nextLevel = chosenPassiveUpgrade.GetLevelData(p.currentLevel + 1); upgradeOption.upgradeDescriptionDisplay.text = nextLevel.description; upgradeOption.upgradeNameDisplay.text = nextLevel.name; upgradeOption.upgradeIcon.sprite = chosenPassiveUpgrade.icon; isLevelUp = true; break; } } if (!isLevelUp) // spawn a new passive item { upgradeOption.upgradeButton.onClick.AddListener(() => Add(chosenPassiveUpgrade)); //apply button functionality Passive.Modifier nextLevel = chosenPassiveUpgrade.baseStats; upgradeOption.upgradeDescriptionDisplay.text = nextLevel.description; //apply initial description upgradeOption.upgradeNameDisplay.text = nextLevel.name; //apply initial name upgradeOption.upgradeIcon.sprite = chosenPassiveUpgrade.icon; } } } } } void RemoveUpgradeOptions() { foreach (UpgradeUI upgradeOption in upgradeUIOptions) { upgradeOption.upgradeButton.onClick.RemoveAllListeners(); DisableUpgradeUI(upgradeOption); //call the disableupgradeui method here to disable all UI options before applying upgrades to them } } public void RemoveAndApplyUpgrades() { RemoveUpgradeOptions(); ApplyUpgradeOptions(); } void DisableUpgradeUI(UpgradeUI ui) { ui.upgradeNameDisplay.transform.parent.gameObject.SetActive(false); } void EnableUpgradeUI(UpgradeUI ui) { ui.upgradeNameDisplay.transform.parent.gameObject.SetActive(true); } }
Item
using UnityEngine; using UnityEngine.UIElements; using System.Collections.Generic; /// <summary> /// base class for passive and weapon classes . it is intended to handle evolutions /// </summary> public abstract class Item : MonoBehaviour { public int currentLevel = 1, maxLevel = 1; protected ItemData.Evolution[] evolutionData; protected PlayerInventory inventory; protected PlayerStats owner; public virtual void Initialise(ItemData data) { maxLevel = data.maxLevel; //store the evolution data as we have to track whether all the catalysts are in the inventory so we can evolve evolutionData = data.evolutionData; //we have to find a better way to reference the player inventory in future, as this is inefficient inventory = FindFirstObjectByType<PlayerInventory>(); owner = FindFirstObjectByType<PlayerStats>(); } //call this function to get all the evolutions that the weapon can evolve to public virtual ItemData.Evolution[] CanEvolve() { List<ItemData.Evolution> possibleEvolutions = new List<ItemData.Evolution>(); //check each listed evolution and whether it is in the inventory foreach (ItemData.Evolution e in evolutionData) { if (CanEvolve(e)) possibleEvolutions.Add(e); } return possibleEvolutions.ToArray(); } // checks if a specific evolution is possible public virtual bool CanEvolve(ItemData.Evolution evolution, int levelUpAmount = 1) { //cannot evolve if the item hasn't reached the level to evolve if (evolution.evolutionLevel > currentLevel + levelUpAmount) { Debug.LogWarning(string.Format("Evolution failed. Current level {0}, evolution level {1}", currentLevel, evolution.evolutionLevel)); return false; } //checks to see if all the catalysts are in the inventory foreach (ItemData.Evolution.Config c in evolution.catalysts) { Item item = inventory.Get(c.itemType); if (!item || item.currentLevel < c.level) { Debug.LogWarning(string.Format("Evolution failed. Missing {0}", c.itemType.name)); return false; } } return true; } //attemptEvolution will spawn a new weapon for the character, and remove all the weapons that are supposed to be consumed public virtual bool AttemptEvolution(ItemData.Evolution evolutionData, int levelUpAmount = 1) { if (!CanEvolve(evolutionData, levelUpAmount)) return false; //should we consume passives / weapons? bool consumePassives = (evolutionData.consumes & ItemData.Evolution.Consumption.passives) > 0; bool consumeWeapons = (evolutionData.consumes & ItemData.Evolution.Consumption.weapons) > 0; //loop through all the catalysts and check if we should consume thme foreach (ItemData.Evolution.Config c in evolutionData.catalysts) { if (c.itemType is PassiveData && consumePassives) inventory.Remove(c.itemType, true); if (c.itemType is WeaponData && consumeWeapons) inventory.Remove(c.itemType, true); } //should we consume ourselves as well? if (this is Passive && consumePassives) inventory.Remove((this as Passive).data, true); else if (this is Weapon && consumeWeapons) inventory.Remove((this as Weapon).data, true); //add the new weapon onto our inventory inventory.Add(evolutionData.outcome.itemType); return true; } public virtual bool CanLevelUp() { return currentLevel <= maxLevel; } //whenever an item levels up, attempt to make it evolve public virtual bool DoLevelUp() { //tries to evolve into every listed evolution of this weapon, //if the weapon's evolution condition is levelling up foreach (ItemData.Evolution e in evolutionData) { if (e.condition == ItemData.Evolution.Condition.auto) AttemptEvolution(e); } return true; } //what effects you receive on equipping an item public virtual void OnEquip() { } //what effects are removed on unequipping an item public virtual void OnUnequip() { } }
XenoVault(TreasureChest)
using UnityEngine; public class XenoVault : MonoBehaviour { private void OnTriggerEnter2D(Collider2D col) { PlayerInventory p = col.GetComponent<PlayerInventory>(); if (p) { bool randomBool = Random.Range(0, 2) == 0; OpenXenoVault(p, randomBool); Destroy(gameObject); } } public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { //loop through every weapon to check whether it can evolve foreach (PlayerInventory.Slot s in inventory.weaponSlots) { Weapon w = s.item as Weapon; if (w.data.evolutionData == null) continue; //ignore weapon if it cannot evolve //loop through every possible evolution of the weapon foreach (ItemData.Evolution e in w.data.evolutionData) { //only attempt to evolve weapons via treasure chest evolution if (e.condition == ItemData.Evolution.Condition.treasureChest) { bool attempt = w.AttemptEvolution(e, 0); if (attempt) return; //if evolution succeds, stop } } } } }
August 19, 2025 at 1:50 am #18693::Based on the code and error messages, there are a few potential issues:
- In the XenoVault script, there’s a null check missing before accessing
w.data.evolutionData
. This could cause a NullReferenceException if the weapon slot is empty.
Modify the OpenXenoVault method to:
public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { //loop through every weapon to check whether it can evolve foreach (PlayerInventory.Slot s in inventory.weaponSlots) { Weapon w = s.item as Weapon; if (w == null || w.data == null || w.data.evolutionData == null) continue; //loop through every possible evolution of the weapon foreach (ItemData.Evolution e in w.data.evolutionData) { //only attempt to evolve weapons via treasure chest evolution if (e.condition == ItemData.Evolution.Condition.treasureChest) { bool attempt = w.AttemptEvolution(e, 0); if (attempt) return; //if evolution succeds, stop } } } }
- For the Projectile errors, ensure that the
weapon
variable in the WeaponEffect base class is properly initialized before the Projectile’s Start and FixedUpdate methods are called.
Would you like me to elaborate on any of these points or provide more specific debugging advice?
has upvoted this post. August 19, 2025 at 2:24 am #18695::There is a known issue with the Treasure Chest pickup for Part 15, as it fails to work if you do not have anything that can be evolved in your inventory.
@Alp’s suggestion above should help you remove the null reference.
August 19, 2025 at 3:44 am #18698::ive tried to change the method on the xenovault script but the error persists it’s worth mentioning that i had a test evolution of knife + aura since the beginning and im getting this error. do I need to add the evolution scriptable object to the Available Weapons List on the PlayerInventory as well? i’ve tried this and the evolution has appeared in the level up screen so i guess not. should i add it to any object?
also, can you elaborate on the projectiles part @alp? couldn’t figure what to change in my script
the weaponeffect script is like this:
using UnityEngine; public abstract class WeaponEffect : MonoBehaviour { [HideInInspector] public PlayerStats owner; [HideInInspector] public Weapon weapon; public float GetDamage() { return weapon.GetDamage(); } }
August 19, 2025 at 3:45 am #18699::For the XenoVault issue, the problem might be that the
weapon
orweapon.data
is null when trying to access the evolution data. Let’s modify the script to add more robust null checking:public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { //loop through every weapon to check whether it can evolve foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s == null || s.item == null) continue; Weapon w = s.item as Weapon; if (w == null || w.data == null || w.data.evolutionData == null) continue; //loop through every possible evolution of the weapon foreach (ItemData.Evolution e in w.data.evolutionData) { //only attempt to evolve weapons via treasure chest evolution if (e.condition == ItemData.Evolution.Condition.treasureChest) { bool attempt = w.AttemptEvolution(e, 0); if (attempt) return; //if evolution succeds, stop } } } }
For the Projectile errors, the issue is likely that the
weapon
andowner
variables in the WeaponEffect base class are not being set before the Projectile’s Start and FixedUpdate methods are called.You’ll need to modify the script that instantiates the Projectile to ensure these variables are set before the Projectile starts. For example, in the Weapon script where you create the Projectile:
GameObject projectileObject = Instantiate(projectilePrefab, spawnPosition, Quaternion.identity); Projectile projectile = projectileObject.GetComponent(); projectile.weapon = this; // Set the weapon reference projectile.owner = playerStats; // Set the owner reference
Could you confirm:
- Are you setting the
weapon
andowner
references when instantiating Projectiles? - Can you show me the code where Projectiles are created?
August 19, 2025 at 4:23 am #18702::ive tried the updates but the chest is still showing this error message if i try to pick it before i have the right requirements for an evolution and it does not destroy the chest. when i have the requirements it does work as intended
about projectile part, this is the script where the projectiles are instantiated (the weapon and owner are set there):
using UnityEngine; public class ProjectileWeapon : Weapon { protected float currentAttackInterval; protected int currentAttackCount; protected override void Update() { base.Update(); if (currentAttackInterval > 0) { currentAttackInterval -= Time.deltaTime; if (currentAttackInterval <= 0) Attack(currentAttackCount); } } public override bool CanAttack() { if (currentAttackCount > 0) return true; return base.CanAttack(); } protected override bool Attack(int attackCount = 1) { if (!currentStats.projectilePrefab) { Debug.LogWarning(string.Format("No projectile prefab assigned for {0}", name)); currentCooldown = data.baseStats.cooldown; return false; } //can we attack? if (!CanAttack()) return false; //otherwise calculate the angle and offset of our spammed projectile float spawnAngle = GetSpawnAngle(); //and spawn a copy of the projectile Projectile prefab = Instantiate( currentStats.projectilePrefab, owner.transform.position + (Vector3)GetSpawnOffset(spawnAngle), Quaternion.Euler(0, 0, spawnAngle) ); prefab.weapon = this; prefab.owner = owner; //reset the cooldown only if this attack was triggered by cooldown if (currentCooldown <= 0) currentCooldown += currentStats.cooldown; attackCount--; // do we perform another attack? if (attackCount > 0) { currentAttackCount = attackCount; currentAttackInterval = data.baseStats.projectileInterval; } return true; } //gets which direction the projectile should face when spawning protected virtual float GetSpawnAngle() { return Mathf.Atan2(movement.lastMovedVector.y, movement.lastMovedVector.x) * Mathf.Rad2Deg; } // generates a random point to spawn the projectile on and rotates the facing of the point by spawnAngle protected virtual Vector2 GetSpawnOffset(float spawnAngle = 0) { return Quaternion.Euler(0, 0, spawnAngle) * new Vector2( Random.Range(currentStats.spawnVariance.xMin, currentStats.spawnVariance.xMax), Random.Range(currentStats.spawnVariance.yMin, currentStats.spawnVariance.yMax) ); } }
August 19, 2025 at 4:25 am #18703::For the XenoVault issue, let’s modify the script to handle cases where no evolution is possible:
public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { bool evolutionAttempted = false; //loop through every weapon to check whether it can evolve foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s == null || s.item == null) continue; Weapon w = s.item as Weapon; if (w == null || w.data == null || w.data.evolutionData == null) continue; //loop through every possible evolution of the weapon foreach (ItemData.Evolution e in w.data.evolutionData) { //only attempt to evolve weapons via treasure chest evolution if (e.condition == ItemData.Evolution.Condition.treasureChest) { bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; //if evolution succeeds, stop } } } if (evolutionAttempted) break; } // Always destroy the chest, regardless of whether evolution was successful Destroy(gameObject); }
The key changes are:
- Added a flag to track if an evolution was attempted
- Always destroy the gameObject at the end of the method
- Break out of loops once an evolution is successful
This ensures that:
- The chest is always destroyed
- The first possible evolution is attempted
- No further attempts are made after a successful evolution
Regarding the Projectile script, the code you’ve shown looks correct for setting the
weapon
andowner
references.Could you provide more context about the specific Null Reference Exception you’re seeing in the Projectile script? Are you seeing any specific line numbers or additional error details?
August 19, 2025 at 2:00 pm #18711::To add on to the code that Alp provided, the highlighted portion below is what is important:
public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { //loop through every weapon to check whether it can evolve foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s == null || s.item == null) continue; Weapon w = s.item as Weapon; if (w == null || w.data == null || w.data.evolutionData == null) continue; //loop through every possible evolution of the weapon foreach (ItemData.Evolution e in w.data.evolutionData) { //only attempt to evolve weapons via treasure chest evolution if (e.condition == ItemData.Evolution.Condition.treasureChest) { bool attempt = w.AttemptEvolution(e, 0); if (attempt) return; //if evolution succeds, stop } } } }
The reason for that is that NullReferenceException stops execution of your code when they happen. This means that in your
XenoVault.OnTriggerEnter2D()
function:private void OnTriggerEnter2D(Collider2D col) { PlayerInventory p = col.GetComponent<PlayerInventory>(); if (p) { bool randomBool = Random.Range(0, 2) == 0; OpenXenoVault(p, randomBool); Destroy(gameObject); } }
If
OpenXenoVault()
causes a NullReferenceException, theDestroy()
function doesn’t run because the NullReferenceException blocks the rest of your function from occurring.As for the
NullReferenceException
with your projectiles, yourProjectileWeapon
is correct. The important part is here:// And spawn a copy of the projectile. Projectile prefab = Instantiate( currentStats.projectilePrefab, owner.transform.position + (Vector3)GetSpawnOffset(spawnAngle), Quaternion.Euler(0, 0, spawnAngle) ); prefab.weapon = this; prefab.owner = owner;
If the error only fires once and it doesn’t cause further problems, you can delay the function for 1 frame using
Invoke()
so it only fires when the variable is filled:protected virtual void Start() { if(!weapon) { Invoke("Start", 0); return; } rb = GetComponent<Rigidbody2D>(); Weapon.Stats stats = weapon.GetStats(); if (rb.bodyType == RigidbodyType2D.Dynamic) { rb.angularVelocity = rotationSpeed.z; rb.linearVelocity = transform.right * stats.speed; } //prevent the area from being 0, as it hides the projectile float area = stats.area == 0 ? 1 : stats.area; transform.localScale = new Vector3( area * Mathf.Sign(transform.localScale.x), area * Mathf.Sign(transform.localScale.y), 1 ); //set how much piercing this object has piercing = stats.piercing; //destroy the projectile after its lifespan expires if (stats.lifespan > 0) Destroy(gameObject, stats.lifespan); //if the projectile is auto-aiming, automatically find a suitable enemy if (hasAutoAim) AcquireAutoAimFacing(); }
For
FixedUpdate()
, prevent the function from firing if we are unable to retrieve the stats.protected virtual void FixedUpdate() { //only drive movement ourselves if this is a kinematic if (rb.bodyType == RigidbodyType2D.Kinematic) { if(!weapon) return; Weapon.Stats stats = weapon.GetStats(); transform.position += transform.right * stats.speed * Time.fixedDeltaTime; rb.MovePosition(transform.position); transform.Rotate(rotationSpeed * Time.fixedDeltaTime); } }
Let me know if the above fixes your issues. It is very good that you are striving to fix your
NulLReferenceExceptions
. People tend to ignore them and it causes massive problems (i.e. lots of bugs) in the long run.August 19, 2025 at 10:17 pm #18718::Hi Terence! thank you for the feedback, after checking everything in the answers, the projectile part is running smooth now but the chest still gives this error only if i can’t evolve a weapon at that point, so it wont get destroyed and give this null reference. as soon as i meet the requirements to evolve the weapon it works as intended.
NullReferenceException: Object reference not set to an instance of an object PlayerInventory.Get (WeaponData type) (at Assets/Scripts/Player/PlayerInventory.cs:97) PlayerInventory.Get (ItemData type) (at Assets/Scripts/Player/PlayerInventory.cs:74) Item.CanEvolve (ItemData+Evolution evolution, System.Int32 levelUpAmount) (at Assets/Scripts/Passive Items/Item.cs:55) Item.AttemptEvolution (ItemData+Evolution evolutionData, System.Int32 levelUpAmount) (at Assets/Scripts/Passive Items/Item.cs:70) XenoVault.OpenXenoVault (PlayerInventory inventory, System.Boolean isHigherTier) (at Assets/Scripts/Pickups/XenoVault.cs:35) XenoVault.OnTriggerEnter2D (UnityEngine.Collider2D col) (at Assets/Scripts/Pickups/XenoVault.cs:13)
here’s how my script looks like now:
using UnityEngine; public class XenoVault : MonoBehaviour { private void OnTriggerEnter2D(Collider2D col) { PlayerInventory p = col.GetComponent<PlayerInventory>(); if (p) { bool randomBool = Random.Range(0, 2) == 0; OpenXenoVault(p, randomBool); Destroy(gameObject); } } public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { //loop through every weapon to check whether it can evolve foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s == null || s.item == null) continue; Weapon w = s.item as Weapon; if (w == null || w.data == null || w.data.evolutionData == null) continue; //ignore weapon if it cannot evolve //loop through every possible evolution of the weapon foreach (ItemData.Evolution e in w.data.evolutionData) { //only attempt to evolve weapons via treasure chest evolution if (e.condition == ItemData.Evolution.Condition.treasureChest) { bool attempt = w.AttemptEvolution(e, 0); if (attempt) return; //if evolution succeds, stop } } } } }
now i have noticed one more bug that im not sure if its supposed to be there because you dont reach that part of playtesting in the video. when im playing the game, the items displays on the screen works fine, but when i finish that run it wont show the items ive used in that run. all slots are empty.
sorry for bothering with all this bugs and thank you so much for your patience! im trying to fix everything before i move to the next part
August 19, 2025 at 10:20 pm #18719::For the XenoVault chest issue, modify the script to handle potential null references more robustly:
public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { bool evolutionAttempted = false; // Null check for inventory and weaponSlots if (inventory?.weaponSlots == null) { Debug.LogWarning("Inventory or weapon slots are null"); return; } foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s?.item == null) continue; Weapon w = s.item as Weapon; if (w?.data?.evolutionData == null) continue; foreach (ItemData.Evolution e in w.data.evolutionData) { if (e.condition == ItemData.Evolution.Condition.treasureChest) { try { bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; } } catch (System.Exception ex) { Debug.LogError($"Evolution attempt failed: {ex.Message}"); } } } if (evolutionAttempted) break; } }
Regarding the second issue about items not showing after a run, this sounds like a potential problem with game state persistence or reset. Could you provide:
- The script responsible for ending/resetting the run
- How the inventory UI is being populated
- Any code related to run completion or scene transitions
Would you like me to elaborate on any of these points or provide more specific debugging advice?
August 19, 2025 at 10:45 pm #18720::thank you Alp! now i can collect the chest even if i can’t evolve the weapon. i just get this error now :
Evolution attempt failed: Object reference not set to an instance of an object UnityEngine.Debug:LogError (object) XenoVault:OpenXenoVault (PlayerInventory,bool) (at Assets/Scripts/Pickups/XenoVault.cs:52) XenoVault:OnTriggerEnter2D (UnityEngine.Collider2D) (at Assets/Scripts/Pickups/XenoVault.cs:13)
about the UI bug, this is the game manager script:
using UnityEngine; using UnityEngine.UI; using TMPro; using System.Collections; using System.Collections.Generic; public class GameManager : MonoBehaviour { public static GameManager instance; //define the different states of the game public enum GameState { Gameplay, Paused, GameOver, LevelUp } //store the current state of the game public GameState currentState; //store the previous state of the game before it was paused public GameState previousState; [Header("Damage Text Settings")] public Canvas damageTextCanvas; public float textFontSize = 20; public TMP_FontAsset textFont; public Camera referenceCamera; [Header("Screens")] public GameObject pauseScreen; public GameObject resultsScreen; public GameObject levelUpScreen; [Header("Current Stat Displays")] public TextMeshProUGUI currentHealthDisplay; public TextMeshProUGUI currentRecoveryDisplay; public TextMeshProUGUI currentMoveSpeedDisplay; public TextMeshProUGUI currentMightDisplay; public TextMeshProUGUI currentProjectileSpeedDisplay; public TextMeshProUGUI currentMagnetDisplay; [Header("Current Stat Displays")] public Image chosenCharacterImage; public TextMeshProUGUI chosenCharacterName; public TextMeshProUGUI levelReachedDisplay; public TextMeshProUGUI timeSurvivedDisplay; public List<Image> chosenWeaponsUI = new List<Image>(6); public List<Image> chosenPassiveItemsUI = new List<Image>(6); [Header("Stopwatch")] public float timeLimit; //time limit in seconds float stopwatchTime; //time elapsed since stopwatch started public TextMeshProUGUI stopwatchDisplay; public bool isGameOver = false; //flag to check if the player is chooseing their upgrades public bool choosingUpgrade; //reference to the player's game object public GameObject playerObject; void Awake() { if(instance == null) { instance = this; } else { Debug.LogWarning("Extra " + this + " deleted"); } DisableScreens(); } void Update() { //define the behaviour for each state switch (currentState) { case GameState.Gameplay: CheckForPauseAndResume(); UpdateStopwatch(); break; case GameState.Paused: CheckForPauseAndResume(); break; case GameState.GameOver: if(!isGameOver) { isGameOver = true; Time.timeScale = 0f; //stops the game entirely Debug.Log("Game Over"); DisplayResults(); } break; case GameState.LevelUp: if(!choosingUpgrade) { choosingUpgrade = true; Time.timeScale = 0f; // pause the game for now Debug.Log("Upgrades shown"); levelUpScreen.SetActive(true); } break; default: Debug.LogWarning("STATE DOES NOT EXIST"); break; } } IEnumerator GenerateFloatingTextCoroutine(string text, Transform target, float duration = 1f, float speed = 50f) { //start generating the floating text GameObject textObj = new GameObject("Damage Floating Text"); RectTransform rect = textObj.AddComponent<RectTransform>(); TextMeshProUGUI tmPro = textObj.AddComponent<TextMeshProUGUI>(); tmPro.text = text; tmPro.horizontalAlignment = HorizontalAlignmentOptions.Center; tmPro.verticalAlignment = VerticalAlignmentOptions.Middle; tmPro.fontSize = textFontSize; if (textFont) tmPro.font = textFont; rect.position = referenceCamera.WorldToScreenPoint(target.position); Destroy(textObj, duration); //parent generated text to the canvas textObj.transform.SetParent(instance.damageTextCanvas.transform); textObj.transform.SetAsFirstSibling(); //pan the text upwards and fade it over time WaitForEndOfFrame w = new WaitForEndOfFrame(); float t = 0; float yOffset = 0; while (t < duration) { tmPro.color = new Color(tmPro.color.r, tmPro.color.g, tmPro.color.b, 1 - t / duration); if (target) { yOffset += speed * Time.deltaTime; if (rect != null) // Add this null check { rect.position = referenceCamera.WorldToScreenPoint(target.position + new Vector3(0, yOffset)); } } else { // If target is dead, just pan up where the text is at. if (rect != null) // Add this null check { rect.position += new Vector3(0, speed * Time.deltaTime, 0); } } yield return w; t += Time.deltaTime; } } public static void GenerateFloatingText(string text, Transform target, float duration = 1f, float speed = 1f) { //if the canvas is not set, return the function so we dont generate any floating text if (!instance.damageTextCanvas) return; //find a relevant camera that we can use to convert the world position to a screen position if (!instance.referenceCamera) instance.referenceCamera = Camera.main; instance.StartCoroutine(instance.GenerateFloatingTextCoroutine(text, target, duration, speed)); } //define the method to change the state of the game public void ChangeState(GameState newState) { currentState = newState; } public void PauseGame() { if(currentState != GameState.Paused) { previousState = currentState; ChangeState(GameState.Paused); Time.timeScale = 0f; //stop the game pauseScreen.SetActive(true); Debug.Log("Game is paused"); } } public void ResumeGame() { if(currentState == GameState.Paused) { ChangeState(previousState); Time.timeScale = 1f; //resume the game pauseScreen.SetActive(false); Debug.Log("Game is resumed"); } } //define the method to check for pause and resume input void CheckForPauseAndResume() { if(Input.GetKeyDown(KeyCode.Escape)) { if(currentState == GameState.Paused) { ResumeGame(); } else { PauseGame(); } } } void DisableScreens() { pauseScreen.SetActive(false); resultsScreen.SetActive(false); levelUpScreen.SetActive(false); } public void GameOver() { timeSurvivedDisplay.text = stopwatchDisplay.text; ChangeState(GameState.GameOver); } void DisplayResults() { resultsScreen.SetActive(true); } public void AssignChosenCharacterUI(CharacterData chosenCharacterData) { chosenCharacterImage.sprite = chosenCharacterData.Icon; chosenCharacterName.text = chosenCharacterData.Name; } public void AssignLevelReachedUI(int levelReachedData) { levelReachedDisplay.text = levelReachedData.ToString(); } public void AssignChosenWeaponsAndPassiveItemsUI(List<Image> chosenWeaponsData, List<Image> chosenPassiveItemsData) { if (chosenWeaponsData.Count != chosenWeaponsUI.Count || chosenPassiveItemsData.Count != chosenPassiveItemsUI.Count) { Debug.Log("Chosen Weapons and Passive Items data lists have different lengths "); return; } //assign the chosen weapons data to choseWeaponsUI for (int i = 0; i < chosenWeaponsUI.Count; i++) { //check that the sprite of the corresponding element in chosenWeaponsData is not null if(chosenWeaponsData[i].sprite) { //enable the corersponding element in chosenWeaponUi and set its sprite to the corresponding sprite in choseWeaponsData chosenWeaponsUI[i].enabled = true; chosenWeaponsUI[i].sprite = chosenWeaponsData[i].sprite; } else { //if sprite is null, disable the corresponding element in choseWeaponsUI chosenWeaponsUI[i].enabled = false; } } //assign the chosen weapons data to chosenPassiveItemsUI for (int i = 0; i < chosenPassiveItemsUI.Count; i++) { //check that the sprite of the corresponding element in chosenPassiveItemsData is not null if (chosenPassiveItemsData[i].sprite) { //enable the corersponding element in chosenPassiveItemsUI and set its sprite to the corresponding sprite in chosenPassiveItemsData chosenPassiveItemsUI[i].enabled = true; chosenPassiveItemsUI[i].sprite = chosenPassiveItemsData[i].sprite; } else { //if sprite is null, disable the corresponding element in chosenPassiveItemsUI chosenPassiveItemsUI[i].enabled = false; } } } void UpdateStopwatch() { stopwatchTime += Time.deltaTime; UpdateStopwatchDisplay(); if(stopwatchTime >= timeLimit) { playerObject.SendMessage("Kill"); } } void UpdateStopwatchDisplay() { //calculate the number of minutes and seconds that have elapsed int minutes = Mathf.FloorToInt(stopwatchTime / 60); int seconds = Mathf.FloorToInt(stopwatchTime % 60); //update stopwatch text to display elapsed time stopwatchDisplay.text = string.Format("{0:00}:{1:00}", minutes, seconds); } public void StartLevelUp() { ChangeState(GameState.LevelUp); playerObject.SendMessage("RemoveAndApplyUpgrades"); } public void EndLevelUp() { choosingUpgrade = false; Time.timeScale = 1f; //resume the game levelUpScreen.SetActive(false); ChangeState(GameState.Gameplay); } }
and this is the inventory script:
using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; using TMPro; public class PlayerInventory : MonoBehaviour { [System.Serializable] public class Slot { public Item item; public Image image; public void Assign(Item assignedItem) { item = assignedItem; if (item is Weapon) { Weapon w = item as Weapon; image.enabled = true; image.sprite = w.data.icon; } else { Passive p = item as Passive; image.enabled = true; image.sprite = p.data.icon; } Debug.Log(string.Format("Assigned {0} to player.", item.name)); } public void Clear() { item = null; image.enabled = false; image.sprite = null; } public bool IsEmpty() { return item == null; } } public List<Slot> weaponSlots = new List<Slot>(6); public List<Slot> passiveSlots = new List<Slot>(6); [System.Serializable] public class UpgradeUI { public TMP_Text upgradeNameDisplay; public TMP_Text upgradeDescriptionDisplay; public Image upgradeIcon; public Button upgradeButton; } [Header("UI Elements")] public List<WeaponData> availableWeapons = new List<WeaponData>(); //list of upgrade options for weapons public List<PassiveData> availablePassives = new List<PassiveData>(); //list of upgrade options for passive items public List<UpgradeUI> upgradeUIOptions = new List<UpgradeUI>(); //list of ui for upgrade options present in the scene PlayerStats player; void Start() { player = GetComponent<PlayerStats>(); } //checks if the inventory has an item of a certain type public bool Has(ItemData type) { return Get(type); } public Item Get(ItemData type) { if (type is WeaponData) return Get(type as WeaponData); else if (type is PassiveData) return Get(type as PassiveData); return null; } //find a passive of certain type in the inventory public Passive Get(PassiveData type) { foreach (Slot s in passiveSlots) { Passive p = s.item as Passive; if (p.data == type) return p; } return null; } //find a weapon of certain type in the inventory public Weapon Get(WeaponData type) { foreach (Slot s in weaponSlots) { Weapon w = s.item as Weapon; if (w.data == type) return w; } return null; } //removes a weapon of a particular type as specified by <data> public bool Remove(WeaponData data, bool removeUpgradeAvailability = false) { //remove this weapon from the upgrade pool if (removeUpgradeAvailability) availableWeapons.Remove(data); for (int i = 0; i < weaponSlots.Count; i++) { Weapon w = weaponSlots[i].item as Weapon; if (w.data == data) { weaponSlots[i].Clear(); w.OnUnequip(); Destroy(w.gameObject); return true; } } return false; } //removes a passive of a particular type, as specified by <data> public bool Remove(PassiveData data, bool removeUpgradeAvailability = false) { //remove this passive from the upgrade pool if (removeUpgradeAvailability) availablePassives.Remove(data); for (int i = 0; i < passiveSlots.Count; i++) // //for (int i = 0; i < weaponSlots.Count; i++) { Passive p = passiveSlots[i].item as Passive; // //Passive p = weaponSlots[i].item as Passive; if (p.data == data) { passiveSlots[i].Clear(); // //weaponSlots[i].Clear(); p.OnUnequip(); Destroy(p.gameObject); return true; } } return false; } // if an ItemData is passed, determine what type it is and call the respective overload //we also have an optional boolean to remove this item from the upgrade list public bool Remove(ItemData data, bool removeUpgradeAvailability = false) { if (data is PassiveData) return Remove(data as PassiveData, removeUpgradeAvailability); else if (data is WeaponData) return Remove(data as WeaponData, removeUpgradeAvailability); return false; } //finds an empty slot and adds a weapon of a certain type,returns the slot number that the item was put in public int Add(WeaponData data) { int slotNum = -1; //try to find an empty slot for (int i = 0; i < weaponSlots.Capacity; i++) { if (weaponSlots[i].IsEmpty()) { slotNum = i; break; } } //if there is no empty slot, exit if (slotNum < 0) return slotNum; //otherwise create the weapon in the slot and get the type of the weapon we want to spawn Type weaponType = Type.GetType(data.behaviour); if (weaponType != null) { //spawn the weapon GameObject GameObject go = new GameObject(data.baseStats.name + " Controller"); Weapon spawnedWeapon = (Weapon)go.AddComponent(weaponType); spawnedWeapon.Initialise(data); spawnedWeapon.transform.SetParent(transform); //set the weapon to be a child of the player spawnedWeapon.transform.localPosition = Vector2.zero; spawnedWeapon.OnEquip(); //assign the weapon to the slot weaponSlots[slotNum].Assign(spawnedWeapon); //close the level up UI if it is on if (GameManager.instance != null && GameManager.instance.choosingUpgrade) GameManager.instance.EndLevelUp(); return slotNum; } else { Debug.LogWarning(string.Format( "Invalid weapon type specified for {0}.", data.name )); } return -1; } // finds an empty slot and adds a passive of a certain type, returns the slot number that the item was put in public int Add(PassiveData data) { int slotNum = -1; //try to find an empty slot for (int i = 0; i < passiveSlots.Capacity; i++) { if (passiveSlots[i].IsEmpty()) { slotNum = i; break; } } //if there is no empty slot, exit if (slotNum < 0) return slotNum; //otherwise create the passive in the slot and get the type of the passive we want to spawn GameObject go = new GameObject(data.baseStats.name + " Passive"); Passive p = go.AddComponent<Passive>(); p.Initialise(data); p.transform.SetParent(transform); //set the passive to be a child of the player p.transform.localPosition = Vector2.zero; //assign the passive to the slot passiveSlots[slotNum].Assign(p); if (GameManager.instance != null && GameManager.instance.choosingUpgrade) { GameManager.instance.EndLevelUp(); } player.RecalculateStats(); return slotNum; } // if we dont know what item is being added, this function will determine that public int Add(ItemData data) { if (data is WeaponData) return Add(data as WeaponData); else if (data is PassiveData) return Add(data as PassiveData); return -1; } public void LevelUpWeapon(int slotIndex, int upgradeIndex) { if (weaponSlots.Count > slotIndex) { Weapon weapon = weaponSlots[slotIndex].item as Weapon; //don't level up the weapon if it is already at max level if (!weapon.DoLevelUp()) { Debug.LogWarning(string.Format( "Failed to level up {0}.", weapon.name )); return; } } if (GameManager.instance != null && GameManager.instance.choosingUpgrade) { GameManager.instance.EndLevelUp(); } } public void LevelUpPassiveItem(int slotIndex, int upgradeIndex) { if (passiveSlots.Count > slotIndex) { Passive p = passiveSlots[slotIndex].item as Passive; if (!p.DoLevelUp()) { Debug.LogWarning(string.Format( "Failed to level up {0}.", p.name )); return; } } if (GameManager.instance != null && GameManager.instance.choosingUpgrade) { GameManager.instance.EndLevelUp(); } player.RecalculateStats(); } //determine what upgrade options should appear void ApplyUpgradeOptions() { //make a duplicate of the available weapon / passive upgrade lists so we can iterate through them in the function List<WeaponData> availableWeaponUpgrades = new List<WeaponData>(availableWeapons); List<PassiveData> availablePassiveItemUpgrades = new List<PassiveData>(availablePassives); //iterate through each slot in the upgrade UI foreach (UpgradeUI upgradeOption in upgradeUIOptions) { //if there are no more available upgrades, then we abort if (availableWeaponUpgrades.Count == 0 && availablePassiveItemUpgrades.Count == 0) return; //determine whether this upgrade should be for passive or active weapons int upgradeType; if (availableWeaponUpgrades.Count == 0) { upgradeType = 2; } else if (availablePassiveItemUpgrades.Count == 0) { upgradeType = 1; } else { //random generates a number between 1 and 2 upgradeType = UnityEngine.Random.Range(1, 3); } //generates an active weapon upgrade if (upgradeType == 1) { //pick a weapon upgrade, then remove it so that we don't get it twice WeaponData chosenWeaponUpgrade = availableWeaponUpgrades[UnityEngine.Random.Range(0, availableWeaponUpgrades.Count)]; availableWeaponUpgrades.Remove(chosenWeaponUpgrade); //ensure that the selected weapon data is valid if (chosenWeaponUpgrade != null) { //turns on the UI slot EnableUpgradeUI(upgradeOption); //loops through all our existing weapons. if we find a match, we will //hook an event listener to the button that will level up the weapon //when this upgrade option is clicked bool isLevelUp = false; for (int i = 0; i < weaponSlots.Count; i++) { Weapon w = weaponSlots[i].item as Weapon; if (w != null && w.data == chosenWeaponUpgrade) { //if the weapon is already at the max level, do not allow upgrade if (chosenWeaponUpgrade.maxLevel <= w.currentLevel) { //DisableUpgradeUI(upgradeOption); isLevelUp = false; break; } // set the event listener, item and level description to be that opf the next level upgradeOption.upgradeButton.onClick.AddListener(() => LevelUpWeapon(i, i)); //apply button functionality Weapon.Stats nextLevel = chosenWeaponUpgrade.GetLevelData(w.currentLevel + 1); upgradeOption.upgradeDescriptionDisplay.text = nextLevel.description; upgradeOption.upgradeNameDisplay.text = nextLevel.name; upgradeOption.upgradeIcon.sprite = chosenWeaponUpgrade.icon; isLevelUp = true; break; } } //if the code gets here, it means that we will be adding a new weapon , intead of upgrading an existing one if (!isLevelUp) { upgradeOption.upgradeButton.onClick.AddListener(() => Add(chosenWeaponUpgrade)); //apply button functionality upgradeOption.upgradeDescriptionDisplay.text = chosenWeaponUpgrade.baseStats.description; upgradeOption.upgradeNameDisplay.text = chosenWeaponUpgrade.baseStats.name; upgradeOption.upgradeIcon.sprite = chosenWeaponUpgrade.icon; } } } else if (upgradeType == 2) { //NOTE: we have to recode this system as right now it disables an upgrade slot if //we hit a weapon that has already reached its max level PassiveData chosenPassiveUpgrade = availablePassiveItemUpgrades[UnityEngine.Random.Range(0, availablePassiveItemUpgrades.Count)]; availablePassiveItemUpgrades.Remove(chosenPassiveUpgrade); if (chosenPassiveUpgrade != null) { //turns on the UI slot EnableUpgradeUI(upgradeOption); //loops through all our existing passives. if we find a match, we will //hook an event listener to the button that will level up the passive //when this upgrade option is clicked bool isLevelUp = false; for (int i = 0; i < passiveSlots.Count; i++) { Passive p = passiveSlots[i].item as Passive; if (p != null && p.data == chosenPassiveUpgrade) { //if the passive is already at the max level, do not allow upgrade if (chosenPassiveUpgrade.maxLevel <= p.currentLevel) { //DisableUpgradeUI(upgradeOption); isLevelUp = false; break; } // set the event listener, item and level description to be that of the next level upgradeOption.upgradeButton.onClick.AddListener(() => LevelUpPassiveItem(i, i)); //apply button functionality Passive.Modifier nextLevel = chosenPassiveUpgrade.GetLevelData(p.currentLevel + 1); upgradeOption.upgradeDescriptionDisplay.text = nextLevel.description; upgradeOption.upgradeNameDisplay.text = nextLevel.name; upgradeOption.upgradeIcon.sprite = chosenPassiveUpgrade.icon; isLevelUp = true; break; } } if (!isLevelUp) // spawn a new passive item { upgradeOption.upgradeButton.onClick.AddListener(() => Add(chosenPassiveUpgrade)); //apply button functionality Passive.Modifier nextLevel = chosenPassiveUpgrade.baseStats; upgradeOption.upgradeDescriptionDisplay.text = nextLevel.description; //apply initial description upgradeOption.upgradeNameDisplay.text = nextLevel.name; //apply initial name upgradeOption.upgradeIcon.sprite = chosenPassiveUpgrade.icon; } } } } } void RemoveUpgradeOptions() { foreach (UpgradeUI upgradeOption in upgradeUIOptions) { upgradeOption.upgradeButton.onClick.RemoveAllListeners(); DisableUpgradeUI(upgradeOption); //call the disableupgradeui method here to disable all UI options before applying upgrades to them } } public void RemoveAndApplyUpgrades() { RemoveUpgradeOptions(); ApplyUpgradeOptions(); } void DisableUpgradeUI(UpgradeUI ui) { ui.upgradeNameDisplay.transform.parent.gameObject.SetActive(false); } void EnableUpgradeUI(UpgradeUI ui) { ui.upgradeNameDisplay.transform.parent.gameObject.SetActive(true); } }
August 20, 2025 at 12:55 pm #18722::For the XenoVault error, modify the script to add more robust null checking:
public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { if (inventory == null) { Debug.LogWarning("Inventory is null"); return; } bool evolutionAttempted = false; foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s?.item == null) continue; Weapon w = s.item as Weapon; if (w?.data?.evolutionData == null) continue; foreach (ItemData.Evolution e in w.data.evolutionData) { if (e == null) continue; if (e.condition == ItemData.Evolution.Condition.treasureChest) { try { bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; } } catch (System.Exception ex) { Debug.LogError($"Evolution attempt failed: {ex.Message}"); } } } if (evolutionAttempted) break; } }
For the UI bug in the GameManager, add a method to reset the UI state when a new run starts:
public void ResetRunUI() { // Clear weapon and passive item UI foreach (Image weaponImage in chosenWeaponsUI) { weaponImage.enabled = false; weaponImage.sprite = null; } foreach (Image passiveImage in chosenPassiveItemsUI) { passiveImage.enabled = false; passiveImage.sprite = null; } // Reset other run-related UI elements stopwatchTime = 0; UpdateStopwatchDisplay(); }
Call this method when starting a new run or before loading the game scene.
Would you like me to elaborate on any of these points or provide more specific debugging advice?
August 20, 2025 at 7:21 pm #18728::Hi Alp! ive tried both solutions but it did not work. if i replace the method on the xenovault script , then unity cant compile the game and give me an error message on this line: ” if (e == null) continue; “
about the ResetRunUI method , i have added it to the gamemanager script and called it on the end of Awake function. is that right? it will just hide all the empty item icon borders in the end and wont show anything. before it was showing just the borders without the items ive collected through the game. Also the pause screen is not updating the stats when I pick an passive item. I have checked on debug mode and the values are updated on the player stats, but not showing on the stats display on pause screen.
August 20, 2025 at 7:25 pm #18729::For the XenoVault script, modify the code to remove the null check for the evolution:
public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { if (inventory == null) { Debug.LogWarning("Inventory is null"); return; } bool evolutionAttempted = false; foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s?.item == null) continue; Weapon w = s.item as Weapon; if (w?.data?.evolutionData == null) continue; foreach (ItemData.Evolution e in w.data.evolutionData) { if (e.condition == ItemData.Evolution.Condition.treasureChest) { try { bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; } } catch (System.Exception ex) { Debug.LogError($"Evolution attempt failed: {ex.Message}"); } } } if (evolutionAttempted) break; } }
For updating stats on the pause screen, add a method to the GameManager to update stats:
public void UpdatePauseScreenStats(PlayerStats playerStats) { currentHealthDisplay.text = "Health: " + playerStats.currentHealth.ToString("F0") + " / " + playerStats.maxHealth.ToString("F0"); currentRecoveryDisplay.text = "Recovery: " + playerStats.recovery.ToString("F1"); currentMoveSpeedDisplay.text = "Move Speed: " + playerStats.moveSpeed.ToString("F1"); currentMightDisplay.text = "Might: " + playerStats.might.ToString("F1"); currentProjectileSpeedDisplay.text = "Projectile Speed: " + playerStats.projectileSpeed.ToString("F1"); currentMagnetDisplay.text = "Magnet: " + playerStats.magnet.ToString("F1"); }
Call this method when pausing the game:
public void PauseGame() { if(currentState != GameState.Paused) { previousState = currentState; ChangeState(GameState.Paused); Time.timeScale = 0f; pauseScreen.SetActive(true); // Add this line to update stats UpdatePauseScreenStats(playerObject.GetComponent()); Debug.Log("Game is paused"); } }
Could you clarify:
- Where exactly are you calling ResetRunUI?
- Do you want to preserve the item icons between runs or reset them completely?
August 20, 2025 at 8:48 pm #18730::well i think ill wait for terence to sort this, because today im changing the original code too much without any results, it will make it difficult to follow the next steps if i keep on doing it like this
@terence are the pause screen’s stats and results screen’s item icons supposed to be working like before at the end of part 15 (if so ill review the video bit by bit to find the errors on my code) or should i move on until the updated UI videos to fix this?
August 20, 2025 at 11:21 pm #18736::No worries Bruno. For the UI issue, you can check out this code. It is addressed at the start of Part 16: https://blog.terresquall.com/community/topic/part-15-assignchosenweaponsandpassiveitemsui-not-working/
For the XenoVault, can you share your script with me again? The latest version.
The start of Part 16 addresses quite a few of these issues found in Part 15. Perhaps you can check out that part to see if it helps with your issues as well.
- 1 anonymous person
August 21, 2025 at 6:39 am #18738::ill check the topic you mentioned and follow the part 16 to see if all bugs get fixed then
my xenovault script is like this now: (now it gets destroyed even if I don’t have the evolution requirements, but will still give this error when i pick it in this situation)
using UnityEngine; public class XenoVault : MonoBehaviour { private void OnTriggerEnter2D(Collider2D col) { PlayerInventory p = col.GetComponent<PlayerInventory>(); if (p) { bool randomBool = Random.Range(0, 2) == 0; OpenXenoVault(p, randomBool); Destroy(gameObject); } } public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { if (inventory == null) { Debug.LogWarning("Inventory is null"); return; } bool evolutionAttempted = false; foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s?.item == null) continue; Weapon w = s.item as Weapon; if (w?.data?.evolutionData == null) continue; foreach (ItemData.Evolution e in w.data.evolutionData) { if (e.condition == ItemData.Evolution.Condition.treasureChest) { try { bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; } } catch (System.Exception ex) { Debug.LogError($"Evolution attempt failed: {ex.Message}"); } } } if (evolutionAttempted) break; } } }
the error is
Evolution attempt failed: Object reference not set to an instance of an object UnityEngine.Debug:LogError (object) XenoVault:OpenXenoVault (PlayerInventory,bool) (at Assets/Scripts/Pickups/XenoVault.cs:51) XenoVault:OnTriggerEnter2D (UnityEngine.Collider2D) (at Assets/Scripts/Pickups/XenoVault.cs:13)
August 21, 2025 at 6:40 am #18739::The error suggests there’s a null reference in the
AttemptEvolution
method. Let’s modify the XenoVault script to add more robust null checking:public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { if (inventory == null) { Debug.LogWarning("Inventory is null"); return; } bool evolutionAttempted = false; foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s?.item == null) continue; Weapon w = s.item as Weapon; if (w?.data?.evolutionData == null) continue; foreach (ItemData.Evolution e in w.data.evolutionData) { if (e == null) continue; if (e.condition == ItemData.Evolution.Condition.treasureChest) { try { // Add null checks before attempting evolution if (w == null || e == null) { Debug.LogWarning("Weapon or evolution data is null"); continue; } bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; } } catch (System.Exception ex) { Debug.LogError($"Evolution attempt failed: {ex.Message}"); } } } if (evolutionAttempted) break; } }
Could you provide more context about:
- The specific implementation of the
AttemptEvolution
method in theWeapon
class - The structure of the
ItemData.Evolution
class - The full error message and stack trace
This will help diagnose the exact cause of the null reference exception.
August 21, 2025 at 1:27 pm #18740::@brunoazalim The error was caused by your own message 😆 Specifically this line:
public void OpenXenoVault(PlayerInventory inventory, bool isHigherTier) { if (inventory == null) { Debug.LogWarning("Inventory is null"); return; } bool evolutionAttempted = false; foreach (PlayerInventory.Slot s in inventory.weaponSlots) { if (s?.item == null) continue; Weapon w = s.item as Weapon; if (w?.data?.evolutionData == null) continue; foreach (ItemData.Evolution e in w.data.evolutionData) { if (e.condition == ItemData.Evolution.Condition.treasureChest) { try { bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; } } catch (System.Exception ex) { Debug.LogError($"Evolution attempt failed: {ex.Message}"); } } } if (evolutionAttempted) break; } }
The error will be gone if you remove the highlighted line above. What the try and catch block is doing is if you do
w.AttemptEvolution()
whenw
is null, it will instead jump over to the catch block to prevent the error.You can instead replace the catch block with a
Destroy()
call if you want the treasure chest to be consumed without any effect after getting picked up.try { bool attempt = w.AttemptEvolution(e, 0); if (attempt) { evolutionAttempted = true; break; } } catch (System.Exception ex) {
Debug.LogError($"Evolution attempt failed: {ex.Message}");Destroy(gameObject); }August 21, 2025 at 8:43 pm #18755 - In the XenoVault script, there’s a null check missing before accessing
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: