- Home /
DontDestory gameobject doesn't work on button click after a new scene loads
Hi guys,
I have Scene 1 where I load a GameManager script into a GameObject. It's a singleton and set to DontDestory when it loads.
If I go to Scene 2 and pull in the GameObject, I set the On Click to a function for that button. This works if I load directly into Scene 2.
The problem is when I go back to Scene 1 and load and click a button that loads Scene 2, the button on Scene has lost it's reference to the GameObject even though it's under Don't Destroy.
Any thoughts?
Elaborating:
In the Editor: I open Scene 1 and I have a button that references a gameObject "Game Manager", which itself has a script called GameManager.cs. In the script I have a method to play audio and load scene 2. In the script I also have a method to load Scene 1.
Still in the Editor: I open Scene 2 and I have another gameObject "Game Manager" with has the same script. I have a button in the scene that calls the other method in the Game Manager. Works fine if I just load each scene independently.
When I play the game: Scene 1 loads, the button click works fine and loads Scene 2, but Scene 2 the button has lost it's link to the Game Manager object.
GameManager inherits a Singleton class that make sure there's only one and marks it DontDestory
public class GameManager : Singleton<GameManager>
{
public AudioClip ButtonClickAudio;
public bool NewGame;
public AudioSource GameSFXSource;
// Start is called before the first frame update
void Start()
{
GameSFXSource.clip = null;
}
// Update is called once per frame
void Update()
{
}
public void LoadPoliceDesk()
{
SceneManager.LoadScene("PoliceDesk");
}
public void LoadMainMenu()
{
SceneManager.LoadScene("MainMenu");
}
public void NewOrContinue()
{
if (NewGame)
{
NewGame = false;
PlayButtonClick();
SceneManager.LoadScene("StoryIntro");
}
else
{
LoadPoliceDesk();
}
}
public void PlayButtonClick()
{
GameSFXSource.clip = ButtonClickAudio;
GameSFXSource.Play();
}
Singleton.cs using UnityEngine;
//<T> can be any Type
public class Singleton<T> : MonoBehaviour where T : Component
{
//The instance is accessible only by the getter
private static T _instance;
public static T Instance
{
get
{
if(_instance == null)
{
Debug.Log("Looking for existing instnace of " + typeof(T).Name + " in memory to return");
//Making sure there not other instances of same type in Memory
_instance = FindObjectOfType<T>();
if(_instance == null)
{
Debug.Log("Instance not found.. creating instnace of " + typeof(T).Name + " in memory");
GameObject obj = new GameObject();
obj.name = typeof(T).Name;
_instance = obj.AddComponent<T>();
}
}
return _instance;
}
}
//Virtual Awake() can be overridden
public virtual void Awake()
{
if(_instance == null)
{
//If null, then this instance is now the singleton of the assigned type
_instance = this as T;
//making sure this instance is kept persisted across screens
DontDestroyOnLoad(this.gameObject);
}
else
{
Debug.Log("Imposter of " + typeof(T).Name + " found. Destroying! pew pew pew.. boom!");
//Destroy the imposter!
Destroy(gameObject);
}
}
}
I've had some more luck if I make the GameObject with the GameManager script a pre-fab. I can get it to work that way to go back to Scene 1, but then the buttons don't work anymore. It's very odd.
Answer by GrayLightGames · Oct 13, 2019 at 04:15 AM
Hi @FredMastro, I believe the following is happening: your scene 2 button is linked to your scene 2 GameManager, which because of the singleton implementation, is destroyed. So it's expected that the linkage on the button would be lost. Unity does not relink everything to your singleton instance. You'd like the button to reference the scene 1 GameManager which is being carried into scene 2, yes? You'd need to do something like repoint the button to the GameManager instance. One easy-to-implement option would be to have the button reference a controller or other GameObject that is not singleton, but have that object refresh its link to the GameManager instance when the scene loads. So your button would call IntermediateController.GameManagerFunction(), which would just in turn call the intended function on the GameManager instance. Since the controller would refresh its linkage to the scene 1 GameManager, you'd be good to go. If that doesn't make sense, reply and I'll try to clarify.
Ahh, that makes sense @GrayLightGames . I would have the intermediateController call the Game$$anonymous$$anager instance function directly.
Great, glad I could help. Good luck with your project!
Thanks GrayLightGames, this was a simple solution to the problem I was having too.
Answer by ivaylo5ev · Oct 13, 2019 at 06:59 AM
Allright, Here is the cause. Your singleton has the following logic working:
When the singleton object is loaded on a scene, it checks if it is the only instance in the awake method.
If it is so, then the current object is set as the singleton instance and that instance will be used in your code.
If there is already an instance, then the singleton code will destroy what points to "this" in the Awake method, in order to leave the other instance alive.
Now, back to your setup. Lets look at Scene 1. It has some game object with your singleton component in place, we will refer it to as "Scene 1' Singleton". You have a field on a button pointing to the game object that owns "Scene 1 Singleton", and that link is established in the editor.
On Scene 2 you have another game object with the singleton component. You have a button in Scene 2 which has a click handler, linking to the owner game object of the "Scene 2' Singleton".
Both scenes will work perfectly when launched independently.
Now what happens when you hit play: you load Scene 1 (for example). "Scene 1 Singleton" checks if it is the only instance, and since it indeed is, remains alive. Your button will work as expected. You press the button and it loads Scene 2. By that time, "Scene 1 Singleton" is already set as "Don't destroy on load" and will be preserved. As scene 2 loads, "Scene 1 Singleton" will be alive and well. But, Scene 2 has another game object -- remember "Scene 2' Singleton". Right now, as the scene loads, its Awake method is called and it checks if it is the only instance. It will say it is not, since it will see "Scene 1' Singleton" is there. So it will be destroyed instead (see step 3 from the beginning of my post). This will leave the Scene 2's button to be associated with null
instead of what Scene 2' Singleton
once was, and clicking the button will fail.
I hope this explains the behavior you observe. But I believe you also need a solution.
I assume that your singleton game object is different game object than the one having the button. In that case, you need to do the following:
Create a custom component which has the following code (Replace
Click1
andClick2
with your click handler methods):public class GameManagerProxy: MonoBehaviour { public void Click1() { GameManager.Instance.Click1(); } public void Click2() { GameManager.Instance.Click2(); } }
Then, in both scene 1 and scene 2, you add GameManagerProxy
to the component which has the button (any component which is different than the singleton game object will be OK). In your button point to the proxy component instead of the singleton and select the appropriate click handler.
Do the same for Scene 2
This will make sure both scenes are calling the existing singleton object.
Thanks, yes @ivaylo5ev , this backs up what @GrayLightGames, that intermediate object to call the original's instance. Thank you both.
I couldn't reward you both, I split the reward though.
No worries, I hope you get the clear picture of the situation. Singletons in unity are hard to manage and not very pretty to use in general, they stray a lot from the original Singleton design pattern. The good of situations like this is is that you'll hopefully get better understanding for the lifecycle of the unity game objects and scene transitions. I'd be glad if I helped.