0
\$\begingroup\$

one of my players always, consistently get a NullReferenceException in a "will be destroyed" behaviour. No other report were sent (they are automatic). Only from that device and I couldn't reproduce it.

My theory is that it's regarding some misterious behaviour of the garbage gollector and Unity's Destroy mechanism. (by the way I use IL2CPP)

Here's the setup:

  • All UI_TownLabels (MonoBehaviour desc.) reference a TownHallBehaviour. (MonoBehaviour desc.)

  • All TownHallBehaviour reference Tile (pure C#) object.

  • A Tile object holds references with anything regarding that Tile, including MonoBehaviour descendants, like TileBehaviour.

Here's the case:

  • When the user clicks on a button to end the game, a tween effect is started.

  • When it's finished, I destroy all TileBehaviours' gameObject, and all UI_TownLabels' gameObject.

  • Before being destroyed, UI_TownLabel's LateUpdate is still called as it should be, because Destroy is effective after the end of the current frame.

  • However here when it tries to get its TownHallBehaviour's Tile's TileBehaviour's Transform's Position, there is a NullReferenceException.

Strangely it seems that the TownHallBehaviour, the Tile, and the TileBehaviour are all NOT null. Only the transform of the TileBehaviour. At least this is what suspect because the exception comes from Tile.get_Position()

Here's what's in the Label's LateUpdate:

var targetPosition = TownHallBehaviour.Tile.Position;

Here's a snippet from Tile:

public class Tile : TileBase
{
        private readonly TileBehaviour tileBehaviour;

        public Tile(TileBehaviour tileBehaviour)
        {
            this.tileBehaviour = tileBehaviour;
        }

        public Vector3 Position
        {
            get { return Transform.position; }
        }

        public Transform Transform
        {
            get { return tileBehaviour.transform; }
        }
}

(I thought for a time that maybe this LateUpdate is in the next frame... and there is a Label in the scene which isn't in the Label collection thus doesn't get destroyed...

... but the code for that is simple, it looks good. And even if it would be the case, there must would be other players affected, or at least it shouldn't occur always for this user. Right?)

Thanks in advance for all the help!

EDIT:

I added some logging to the LateUpdate method:

LogSystem.Log("TownHallBehaviour is " + TownHallBehaviour.TryToString());
LogSystem.Log("TownHallBehaviour.Tile is " + TownHallBehaviour.Tile.TryToString());
LogSystem.Log("TownHallBehaviour.Tile.TileBehaviour is " + TownHallBehaviour.Tile.TileBehaviour.TryToString());

Here is the TryToString() method:

public static string TryToString(this object obj)
{
    return obj == null ? GetNullInfo(obj) : obj.ToString();
}

private static string GetNullInfo(object obj)
{
    var literallyNull = System.Object.ReferenceEquals(obj, null);
    var equalsNull = obj == null;
    return $"<literally null: {literallyNull}, equals null: {equalsNull}>";
}

And suprisingly here is the log I received, also, I have now received this issue from additional devices...

TownHallBehaviour is (Town Hall on (-1, -1))

TownHallBehaviour.Tile is ((-1, -1), Plains, HasUnit)

TownHallBehaviour.Tile.TileBehaviour is null

TileBehaviour is null even though previously the Position getter was the source of the exception and not the Transform getter which is the one accessing TileBehaviour. And it doesn't even print the NullInfo.

When I tried using TryToString() on an object deliberately set to null, it displays the NullInfo as it should.

However when I destroy only the Tiles, and not the labels, the TryToString in LateUpdate returns "null" for TileBehaviour. However the exception printed is

The object of type 'TileBehaviour' has been destroyed but you are still trying to access it.

Did I implement this NullInfo method badly?

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Are you able to check whether it's literally null with Object.ReferenceEquals(tileBehaviour, null), or just Unity's pseudo-null? That may help us narrow down the issue. Ordinarily, it should be impossible for a reference to be nulled by the garbage collector, but it can be put into Unity's pseudo-null state after a Destroy or DestroyImmediate while references are still pointing at it. Any logging you can add to confirm which object this is happening to and when would help thin the search space. \$\endgroup\$
    – DMGregory
    Commented Dec 20, 2023 at 11:54
  • \$\begingroup\$ @DMGregory I'll add some logging and release a new build, hopefully the guy that experiences this issue will give it a second try. Thanks for the suggestion! \$\endgroup\$
    – Tudvari
    Commented Dec 20, 2023 at 12:10
  • \$\begingroup\$ @DMGregory Hi, I've updated the post with some new informations. Thanks in advance! :) \$\endgroup\$
    – Tudvari
    Commented Dec 21, 2023 at 6:46
  • \$\begingroup\$ @DMGregory I think I found the issue with my code, though I have to check it once I get home. TryToString(this object obj) casts all objects into objects, thus it will use the basic C# null check. And C# wise TileBehaviour is still not null. It is only null by Unity terminology because it is destroyed. But by C# it is not null yet. Thus C# will convert it into a string, which is overriden by Unity so when it's destroyed it will return a "null" string. \$\endgroup\$
    – Tudvari
    Commented Dec 22, 2023 at 9:55
  • 1
    \$\begingroup\$ That makes sense. We've had some similar situations come up in past Q&A. \$\endgroup\$
    – DMGregory
    Commented Dec 22, 2023 at 12:06

0

You must log in to answer this question.

Browse other questions tagged .