I've been trying to resolve the following error that is thrown by the BoostInventory script, whenever I try to add a boost to the inactiveBoosts ObservableCollection, in my unity project:
MissingReferenceException: The object of type 'BoostInventory' has been destroyed but you are still trying to access it. Your script should either check if it is null or you should not destroy the object. BoostInventory.InactiveBoosts_CollectionChanged (System.Object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) (at Assets/Scripts/BoostInventory.cs:38) System.Collections.ObjectModel.ObservableCollection`1[T].OnCollectionChanged (System.Collections.Specialized.NotifyCollectionChangedEventArgs e) (at <525dc68fbe6640f483d9939a51075a29>:0) [...cont.]
The boostInventory script manages the display of an inventory of inactive boosts. When the inactiveBoosts list is changed, all of the currently displayed boosts should be removed and the icons for the inactiveBoosts should be redrawn in the correct positions. However, when this error is raised by purchasing boosts that should get added to inactiveBoosts, the boosts do not get displayed and do not get added.
Following the error brings me to the foreach
loop in the CollectionChanged function for the inactiveBoosts ObservableCollection:
private void InactiveBoosts_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//clear currently displayed boosts
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
//calculate the number of rows requred to display all the inactive boosts
float nrows = Convert.ToSingle(Math.Ceiling(gameManager.inactiveBoosts.Count / Convert.ToSingle(ncols)));
//set the size of the collecting to the right number of rows
rectTransform.sizeDelta = new Vector2(colWidth * ncols, nrows * colHeight);
if (gameManager.inactiveBoosts.Count > 0)
{
for (int i = 0; i < gameManager.inactiveBoosts.Count; i++)
{
Boost currentBoost = gameManager.inactiveBoosts[i];
int xi = i % ncols;
int yi = Convert.ToInt32(Math.Floor(i / Convert.ToSingle(ncols)));
float xpos = (xi * colWidth) + (colWidth / 2) - (colWidth * ncols / 2);
float ypos = -(yi * colHeight) - (colHeight / 2) + (colHeight * nrows / 2);
GameObject newChild = Instantiate(boostPanelPrefab, transform, false);
newChild.transform.localPosition = new Vector2(xpos, ypos);
//set the boost of the icon to be the current inactive boost
newChild.GetComponent<InacitveBoost>().boost = currentBoost;
//set the image of the boost to match the affected building
newChild.transform.Find("BuildingIcon").GetComponent<UnityEngine.UI.Image>().sprite = boostDisplay.buildingIconDict[currentBoost.affectedBuildings];
//set the time written on the boost
newChild.transform.Find("TimeText").GetComponent<TextMeshProUGUI>().text = NumberStringFormatters.SecondsToMinutesAndSeconds(currentBoost.boostDuration);
//set the multiplier written on the boost
newChild.transform.Find("MultiplierText").GetComponent<TextMeshProUGUI>().text = "×" + currentBoost.boostMultiplier.ToString("F1");
}
noBoostIcon.SetActive(false);
}
else
{
noBoostIcon.SetActive(true);
}
}
The code for adding the boosts when purchased are in a Purchaser script, as below:
public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
{
// A consumable product has been purchased by this user.
if (String.Equals(args.purchasedProduct.definition.id, globalPackage, StringComparison.Ordinal) || String.Equals(args.purchasedProduct.definition.id, factoryFarmingPackage, StringComparison.Ordinal) || String.Equals(args.purchasedProduct.definition.id, varietyPackage, StringComparison.Ordinal))
{
Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
// The consumable item has been successfully purchased, add package to player's inventory.
foreach (Boost boostPurchase in packageContents[args.purchasedProduct.definition.id])
{
gameManager.inactiveBoosts.Add(boostPurchase);
}
}
// Or ... an unknown product has been purchased by this user.
else
{
Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
}
// Return a flag indicating whether this product has completely been received, or if the application needs
// to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still
// saving purchased products to the cloud, and when that save is delayed.
return PurchaseProcessingResult.Complete;
}
Due to the error, I decided to try putting all the InactiveBoosts_CollectionChanged
code inside an if (self == null)
. This removes the error, but of course now the appropriate code is skipped over. I tried investigating how self can equal null (surely if self == null, then the script won't be able to throw the error as it doesn't exist?) and found this stackoverflow question. I don't understand a lot of what is going on on this page, but I did notice the section that said
It's almost certainly the case that you have a synchronization bug, where two threads are trying to add to the queue simultaneously. Perhaps both threads are incrementing an index into the array and then putting their value into the same location.
I thought this might mean that I have issues because I'm trying to add multiple items to the list in one go, but the error is also thrown when adding only one boost to the list.
To further add to my confusion, this error only appears on the first load of the game. If I save the game and reload then everything works as expected. When saving, the inactiveBoosts ObservableCollection is converted to a list and serialised by unity, then on loading a new observable collection is created from the saved list. I don't see how this could prevent the error from occurring, but it does.
This is my first question here, so apologies if I haven't provided enough detail (or have gone on for too long!). Just ask if you need more information.