How to unsubscribe from an event inside of the gamemanager? - c#

In my GameManager script I have a delegate. In my Door script, I subscribe to this delegate. I've tried unsubscribing from it in both the OnDisable and OnDestroy methods in the Door script. In both cases, I get an error when I stop running the game in the editor:
Some objects were not cleaned up when closing the scene. (Did you spawn new GameObjects from OnDestroy?)
Using Debug.Log, I found out that this is because the GameManager will always be destroyed before the Door script. Even if I do a null check inside either the OnDisable or OnDestroy of the Door script to see if the GameManager is null, I get the same error
if (GameManager.Instance)
{
GameManager.Instance.OnAllEnemiesKilled -= OpenDoor;
}
Somebody told me that I don't need to unsubscribe from it, as the delegate will automatically become null when the Door object is destroyed, but that's not true. During runtime, after the Door is destroyed, my update loop inside of the GameManager is still printing that the delegate has one subscriber: the Door.

I suspect you're spawning a new singleton when you call GameManager.Instance after it's been destroyed? If so, do something like this instead:
public class GameManager : MonoBehaviour
{
public static GameManager Instance
{
get
{
if (isDestroyed) return null;
// Your spawn logic...
}
}
static bool isDestroyed;
void OnDestroy()
{
isDestroyed = true;
}
}

As Lece pointed out, I'm spawning a new singleton when I call GameManager.Instance. Rather than creating a static bool to remedy the problem however, I replaced if (_instance == null) with if ((object)_instance == null) inside my Instance getter and it solved the problem. This is because, while the object is destroyed in the native code, it still exists in the managed code. So I'm now referencing it in the managed world. Moreover, Unity makes it so that when an object is destroyed in native code, its value in managed code will still return null.

Related

How to handle scene loading event in Unity

I know I can inherit from MonoBehaviour and add some handler functions to SceneManager.sceneLoaded, as described in official document https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager-sceneLoaded.html.
My question is, do I need to create an empty GameObject in the scene, and attach a script to that GameObject to do the stuff? Or there is some other better practice?
This is good practice. Create a SceneManager empty GameObject. Attach a script inheriting from MonoBehaviour and handle all scene related actions in there.
Very lucid, straight to the point.
It is one way to go, yes.
You do not necessarily require a GameObject for this.
You can also e.g. use [RuntimeInitializeOnLoadMethod] and attach the listener within there and then further process it.
public static class SomeClass
{
[RuntimeInitializeOnLoadMethod]
private static void Init()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
private static void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
Debug.Log("OnSceneLoaded: " + scene.name);
Debug.Log(mode);
}
}
What is "better" depends a lot on your use case and requirements of course

MissingReferenceException when trying to capture a variable in a lambda

I'm trying to access a value of a local variable inside a lambda according to this post, but I get this excepton:
MissingReferenceException: The object of type 'Transform' has been destroyed but you are still trying to access it.
Here is my code:
private static void ValidateComponent<T>(Transform transform, Delegate validationMethod, object[] args) where T : Component
{
#if UNITY_EDITOR // EditorApplication is only available when running in the editor
if (EditorApplication.isPlaying) return;
if (transform == null) return; // in case inspector field is not assigned
var t = transform;
EditorApplication.delayCall += () =>
{
var component = t.GetComponent<T>();
if (component == null)
{
LogAddingComponent<T>(t);
var addedComponent = t.gameObject.AddComponent<T>();
validationMethod.DynamicInvoke(args.Prepend(addedComponent));
}
else
{
validationMethod.DynamicInvoke(args.Prepend(component));
}
};
#endif
}
What am I doing wrong?
You are attempting to pin transform via the variable t only to use it later during your delayCall callback. That's dangerous considering many types in Unity contain unmanaged resources hence the error. The effect is the same if you created a Font in WinForms, Disposed it, then attempted to use the font.
OP:
Is there a better way to approach this then?
From what I understand, Transforms are always attached to the GameObject and it is the GameObject that is being destroyed taking the Transform along with it.
The only way I know to prevent game object destruction is viaDontDestroyOnLoad but I'm not sure if its applicable here since the latter is really designed for protection whilst transitioning to a new scene, but you never know perhaps it prevents things from being destroyed in other scenarios.
Try calling DontDestroyOnLoad at the top of ValidateComponent like so:
private static void ValidateComponent<T>(Transform transform,
Delegate validationMethod,
object[] args)
where T : Component
{
#if UNITY_EDITOR // EditorApplication is only available when running in the editor
if (EditorApplication.isPlaying) return;
if (transform == null) return; // in case inspector field is not assigned
DontDestroyOnLoad (transform.gameObject); // <---- new
var t = transform;
.
.
.
Otherwise, there is this Unity Answer, that suggests simply disabling and re-enabling the object rather than whatever is destroying the object in the first place. Following that approach you shouldn't require any changes to your code above.
Editor-time-only scripts
BTW, regarding the #if UNITY_EDITOR bit, if you want to avoid having to decorate your code with #ifs everywhere (as an ex c/c++ programmer I certainly don't miss having to do that all the time), consider simply moving your script into a child folder called Editor. Unity automatically treats anything placed in such folders as Editor-time-only and won't be incorporated into your final game build. Better yet, no need for the #if stuff.
The best reason for it though is to prevent build errors when Unity is producing your final game due to forgetting to either use #if or Editor folders. I typically run into this one with 3rd party scripts obtained from the Unity store. Instead of having to fix the code I simply move it to a child Editor folder.
See also
Special folder names, Unity

Unity MLAPI ServerRPC not getting called

I'm implementing a system in which a player can pickup an item from the game world. After picking the item up, I want to destroy the game object. To do this, I figured I'd make a ServerRPC that deletes the object, but the ServerRPC is not getting called. I tried making a ClientRPC as well, but that one isn't being called either. I noticed that it only gets called when the host of the game tries to pick up the item -- any ideas on how to fix this? The documentation is pretty bare but please let me know if I missed anything. Here is my code:
public override void Interact(GameObject player)
{
player.GetComponent<PlayerInventory>().AddItem(this.item);
Debug.Log("before");
TestClientRpc();
TestServerRpc();
}
[ServerRpc]
public void TestServerRpc()
{
Debug.Log("server");
// Destroy(gameObject);
}
[ClientRpc]
public void TestClientRpc()
{
Debug.Log("client");
// Destroy(gameObject);
}
EDIT: image of the components on the weapon
EDIT: This class extends a class that extends the networkbehaviour class. The objects are just sitting in the scene at start -- they are not spawned/instantiated (not sure if that matters or not).
The most plausible solution to this problem , either it is running into a error in some previous lines or as some other person pointed out , requireOwnership is not set to false .
For in depth analysis of MLAPI , https://youtube.com/channel/UCBStHvqSDEF751f0CWd3-Pg
I found the tutorial series on this channel best
I needed to add the following attribute to my ServerRpc:
[ServerRpc(RequireOwnership = false)]
since the player was not the owner of the item.
I have the same issue. Been looking at it for 2 days but I am inclined to think it is a bug to be honest.
I just spent 4+ hours banging my head against the wall because they didn't write this important info in the documentation. When calling ServerRpc from a client that doesn't own the Network Object, RPC attribute should be set to RequireOwnership = false.
For me the issue was that the GameObject(with NetwrokObject component) was sitting in the scene before starting the game, I couldn't use NetworkVariables nor ServerRpc. I think those are only usable on spawned object, but not sure. So I would say : either instantiate the object and add it to the NetworkManager or use normal variable on those objects.
Had the same issue but later realized I just forgot to inherit NetworkBehaviour
For Example:
using Unity.Netcode;
using UnityEngine;
class myObject : NetworkBehaviour {
public override void Interact(GameObject player)
{
player.GetComponent<PlayerInventory>().AddItem(this.item);
Debug.Log("before");
TestClientRpc();
TestServerRpc();
}
[ServerRpc]
public void TestServerRpc()
{
Debug.Log("server");
//Destroy(gameObject);
}
[ClientRpc]
public void TestClientRpc()
{
Debug.Log("client");
// Destroy(gameObject);
}
}
I had the same problem, where the ServerRpc wouldn't even be called. For me it worked after I made the GameObject that my script was attached to a prefab and added it to the NetworkManager's list of NetworkPrefabs.
Note: (I am using Netcode for GameObjects, I don't think it will be the same for other Multiplayer solutions)

Is OnDestroy reliable in Unity?

In Unity I have a script in which I connect to a socket via TCP and want to use this connection every frame. I need to dispose and clean up after that. My idea was to use Start() to start a connection and OnDestroy()
public class Foo : MonoBehaviour
{
void Start()
{
// start TCP connection
}
void Update()
{
// use connection
}
void OnDestroy()
{
// cleanup
}
}
I need the cleanup to execute whatever happens. Does the OnDestroy() method guarantee to be called before the application stops (in standalone mode and in editor) no matter what happens to the object? If not, how can I guarantee the cleanup?
No it is not!
Even OnApplicationQuit might not get called when your app e.g. crashes for some reason.
And there are other specific cases where neither is called. I know that from my own experience that e.g. on the HoloLens2 apps are not closed but only hibernated. If you then close them via the HoloLens home "menu" then you actually kill them via task manager.
This is pretty dirty and causes neither OnDestroy nor OnApplicationQuit or any other Unity specific messages to be called and we ended up with zomby threads and still occupied TCP ports.
If you really want to go sure (e.g. for giving free the connection, killing threads etc) what I finally did was creating a dedicated class with deconstructor (Finalizer)
The deconstructor is pure c# and does not rely on Unity being shutdown correctly so it is guaranteed to be called even if the app was terminated due to a crash as soon as the Garbage Collector automatically does its job.
A MonoBehaviour itself shouldn't implement any constructor nor destructor but a private "normal" class can:
public class Foo : MonoBehaviour
{
private class FooInternal
{
private bool disposed;
public FooInternal()
{
// create TCP connection
// start thread etc
}
public void Update ()
{
// e.g. forward the Update call in order to handle received messages
// in the Unity main thread
}
public ~FooInternal()
{
Dispose();
}
public void Dispose()
{
if(disposed) return;
disposed = true;
// terminate thread, connection etc
}
}
private FooInternal _internal;
void Start()
{
_internal = new FooInternal ();
}
void Update()
{
_internal.Update();
}
void OnDestroy ()
{
_internal.Dispose();
}
}
if you never pass on the reference to _internal to anything else, the GC should automatically kill it after this instance has been destroyed as well.
You're looking for OnEnable for establishing your connection, and OnDisable for cleaning it up.
The issue with OnDestroy (and OnApplicationQuit fwiw) is it won't be called if the script is disabled, which can happen if:
You call SetActive(false) on the GameObject or any of its parents.
You set enabled = false on the script component.
You disable the GameObject or any of its parents in play mode by unchecking the box in the inspector.
You disable the script component in play mode by unchecking the box in the inspector.
The editor is about to recompile the scripts while play mode is running (an especially irritating event to miss for certain types of cleanup code [cough lookin' at you, Vuforia]).
The issue with Start is it won't be called if the script is re-enabled after having been disabled (it'll be called the first time, but no more), which happens in all of the cases opposite of the above list (as well as when the editor finishes recompilation of scripts in play mode).
On the other hand:
OnDisable will be called when the script transitions to disabled for any of the above reasons, plus all the stuff that OnDestroy and OnApplicationQuit cover.
OnEnable will be called when the script transitions to enabled for any of the above reasons, plus all the stuff that Start covers.
Additionally, you will have a 1:1 correspondence of OnEnable and OnDisable calls by Unity (of course you can call them yourself whenever you want).
You will only almost have a 1:1 correspondence of Start and OnDestroy: The notable exception is if a script component is initially disabled (i.e. you disabled it in edit mode) but its GameObject is enabled, you'll still get OnDestroy called on the script even if Start wasn't called (which may be a bug now that I think about it). Plus, of course, you will miss all the above cases.
OnEnable + OnDisable, however, handle all that cleanly, and generally do exactly what you hope that they do, in a straightforward manner with (theoretically) no weird quirks or gotchas.
For the question itself, yes, OnDestroy is reliable: It is called as documented when the script is destroyed.
Of course it wouldn't be called if the application crashes, but in general you'd have bigger problems and that's not the kind of thing you normally code around. It also probably wouldn't be called if you exploded a grenade next to the machine running the game but, again, that's all in "undefined behavior" territory.

Problem with event unsubscribing OnDisable

I register events in OnEnable and unregister them in OnDisable. In most cases it works but I have one simple script which is causing the MissingReferenceException: The object of type 'GraphicsSwitch' has been destroyed but you are still trying to access it. when I load a new scene and invoke the event by pressing a button.
I know that the OnDisable is called as I checked with Debug.Log. The event is not invoked in the new scene before I press a button in the new scene.
After adding if (this != null) the error is gone. Checking if the gameObject is null is not working. It seems that the event is not unsubscribed and the method is called on destroyed object from the previous scene.
The code is very simple:
private void OnEnable()
{
AdjustGraphics();
GameSettings.GraphicsChanged += AdjustGraphics;
}
private void OnDisable()
{
GameSettings.GraphicsChanged -= AdjustGraphics;
}
private void AdjustGraphics()
{
//without this line I get the error
if (this != null)
gameObject.SetActive(GameSettings.Graphics >= requiredQuality);
}
Method AdjustGraphics should not be called in the new loaded scene. I thought the objects should be unsubscribed without any delay. Is it something I am missing when it comes to unsubscribing events?
I know objects in Unity are not destroyed right away but the OnDisable is called on time so my script/object should not listen to GameSettings.GraphicsChanged event anymore when new scene is loaded.
This link helped me:
https://forum.unity.com/threads/how-to-remove-and-inputaction-phase-callback.894601/
private void doSomething(CallbackContext ctx)
{
// do the thing
}
//register
action.started += doSomething;
//unregsiter
action.started -= doSomething;
Correct me if I'm wrong but when you loading a new scene, your objects are destroyed hence the MissingReferenceException unless you use DontDestroyOnLoad.
If you don't want to use it, then make sure to initialize the object at the start of the scene load i.e. make sure your object exists in the scene before calling any of its functions.

Categories

Resources