Careless use of native events in Unity can lead to unpredictable circumstances

Hello, I wanted to tell you very briefly about the bitter experience of using events in Unity, which led to incomprehensible errors and a ton of wasted time (good thing I guessed before solving the problem).

If you don’t want to read, then the conclusion: Unity saves event subscriptions even when restarting the scene with its objects deleted.

My application tracked the user’s intent to output using an event subscription Application.wantsToQuit +=. After calling the handler, my Save Game surface event was called, under certain conditions (it doesn’t matter which). My OnSave event was subscribed to by many of the game’s key classes, which called their function handlers that implemented the actual saving of the class’s critical data. In the wait loop that ran before letting the program close, I checked if my event OnSave != null, (wait for all signed handlers to finish) and only then proceeded to end the program or restart the scene. The code is as simple as possible:

            Scene currentScene = SceneManager.GetActiveScene();
            SceneManager.LoadScene(currentScene.name);
else
            Application.wantsToQuit -= OnApplicationWantsToQuit;
            Application.Quit();

And everything was going perfectly, when I needed certain conditions were called that led to the reaction of saving days both to closing the program and to restarting the scene. However, I ran into a big error when trying to restart the scene again (a second time) in the same game session. And of course, during the repeated restart of the scene, it seemed to me that the script in which this saving was implemented became null.
The Unity console said that I was trying to access a script that was destroyed, but the funniest thing is that physically the script was not null and the reference to it was valid, moreover, when the application closing event was triggered, the reference to the script was also not null. And here strange miracles begin, because after the reaction to the event, in the processing script itself, this same scrip became zero, although there was no reason for this and I did not change the link on it anywhere, what a delusion I thought. I completely rewrote the entire system and tried to understand why it happened exactly at the second call and why the script exists in the debugger before the reaction to the closing, and then it becomes null. The answer came to me by surprise, I thought that such a thing could not happen, but I still assumed that the event that received a subscription in the previous scene causes handlers in the new one, despite the fact that the handlers and the event in terms of game objects were completely overwritten.

And here, the most attentive will see that in the script above, there is no unsubscribe from the event when we restart the scene instead of closing the game. In short, what happens during such careless handling of events: we add a handler function to the application close event, a handler function whose object will be destroyed after the scene is reloaded, but the event will somehow keep calling the handler method, and in the subsequent firing of the same event, it will contain a reference to that handler from the previous scene, even though another one has already been created. I don’t really understand why this is happening and why resources such as events are not overwritten when the scene is overloaded, they actually belong to instances of the class that then do not exist, but it is worth noting that the class that implemented this processing did not have instances, it was executed by itself by itself, without instances, maybe because such data as references to in-memory event variables remain unchanged. Still, you need to remember that the class itself has a different allocation in the program memory than its instances, because it represents the prescribed behavior for these instances, but when I use it as if it were already an object, then they can such cases happen. I just didn’t think that unity. does not change the actual location of these things in the memory of the running game session process, so when you reload scenes you end up in the same place only with different data, and things like event Action remain with the already saved list of subscriptions. But there is another opinion, more logical, to which I am inclined, that in fact, the Applicattion events themselves are immutable during one game session, so they will keep the reference even after another scene is loaded. Haha, now I think this is not a very interesting discussion, since there is a logical explanation like what I just wrote. But in any case, maybe it will be useful to someone. Sorry for the incompetence if anything.

Therefore, to solve this problem, it was enough to simply add an unsubscribe to the event before restarting the scene.:grin::+1:

This is a combination of factors mostly due to how C# works and how Unity handles native objects versus managed ones.

Long-story-short: You are solely responsible for unsubscribing to a native C# event. The only way you could expect an object that is subscribed to automatically be released is if the object that is being subscribed to is being released. The Application object is a static object and will persist for the lifetime of the application. So it falls upon you to manually unsub from any of its events at the appropriate time. As a general rule of thumb, if I sub to something in OnEnable then I unsub in OnDisable. Likewise with Awake/Start and OnDestroy. Failure to unsub manually means that the object that is subscribed to the event will persist in memory for as long as the object that it is subscribed to is present.

As for the seemingly odd null behavior - well. That’s a bit less obvious for people that are new to Unity. Basically, all classes that derive from Unity.Object have their equality operators overridden so that if the native-side object has been destroyed the the managed-side (i.e. your scripts) will return null even though the actual managed object has yet to be released. It’s a bit of a controversial topic but honestly I get it. It makes it easy to call Destroy on something and pretend it’s gone while still allowing for the scripts to handle any last-minute things they need to. Simple to use in simple cases, but it can cause headaches if you’re not aware of the subtle ways in which it can work.

3 Likes

Thanks for your reply <3