0

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.

AlfieM
  • 1

1 Answers1

0

Yes, this is a common issue with Unity and leads to a lot confusion.

In short:
Unity implemented a custom "==" operator for the UnityEngine.Object base class which does some extra checks when compared to null. This was a neat way to solve the inherit managed / native object differences.

You get much more information from this blog post about the == operator.

So why did they actually do that? The first reason on the blog post is actually the poor one. The second reason is the important one. All objects that are derived from UnityEngine.Object (which includes classes like GameObject, MonoBehaviour, Material, Mesh, ...) have a native code counter part on the C++ side of the engine core.

The managed classes are merely wrapper objects around some native resource. The issue arises from the fact that the native object can be destroyed from outside the managed object. Either by using Destroy on that object, or implicitly by loading a new scene. Since managed objects can not be destroyed manually, they will stick around until you get rid of all references to them so they eventually get garbage collected. Whenever such an object looses its native part, the object simply "fakes" that it is null since you can't use anything from that object that relies on the native part.

In the blog they mentioned that in the long run it wasn't really a good decision as it breaks in several cases. However since it was like that from the very beginning, they won't change it any time soon.

The real issue with a custom == operator is that operators are not virtual methods. So which operator will be used is determined by the compiler at compile time. That means you can have a reference to a dead gameobject which behaves differently based on the variable type it's stored in.

GameObject go = new GameObject("Temp");
DestroyImmediate(go);

if (go == null)  // this is true

object obj = go;
if (obj == null)  // this is false

So watch out when using interfaces or any of the neat C# operators like the null-coalescing operator ?? / ??=, the null conditional operator ?. / ?[] or any other means that directly checks if a reference is null.

So to address your issue with the MissingReferenceException, there are possible several causes why your object might have been destroyed / is not alive anymore. First of all it could have been destroyed, somewhere. Second reason could be that it never was "alive". This can happen when you create a MonoBehaviour with "new". Components can only live on gameobjects and they can only be created by using AddComponent or implicitly when a whole gameobject is cloned (with Instantiate). Common mistakes are that you may reference an outdated or simply the wrong object. Another reason could be that you're using a Find method or GetComponent call and it actually can't find what you're looking for. In this case (only when testing in the editor) a fake null object is returned which should be treated like "null".

Bunny83
  • 620
  • 8
  • 23
  • Thanks for your in-depth response, that all makes sense as to how self can be null, so thank you very much for that. With regards to the destruction of the GameObject. I don't call Destroy on the gameobject at all. The script is attached to a UI gameobject in the scene, so it should be present from the launch of the game. The script actually does all of the same stuff as in the CollectionChanged in Start, so it has no issue with GameObject/Component assignments there. There is also the strange behaviour of the error disappearing on a save/load. – AlfieM Sep 15 '20 at 09:12
  • Well it's hard to tell from the information provided. Since this is a callback you might have subscribed an object to this callback that (at some point) has been destroyed. Keep in mind that right in this line: `foreach (Transform child in transform)` accessing the transform property will fail if the object has been destroyed. First you should identify the exact object that is fake-null. Maybe monitor OnDestroy. Keep in mind that you can pass a context object to Debug.Log. Though when you're destroying an object that doesn't really help. – Bunny83 Sep 15 '20 at 10:50
  • Understandable! I hadn't thought of using OnDestroy, I'll have a dig around and try to see what is being destroyed. Thanks again. – AlfieM Sep 15 '20 at 11:20