I am trying to understand why I am getting a missing reference exception after a scene change when using singletons.
So, I have two GameObjects in the first scene. A main camera with a GameManager script attached and another shop object with a Purchaser script attached. Both scripts are also singletons created like this for example:
public static Purchaser Instance
void Awake(){
Instance = this
}
They then both reference each other in the Start() function, again like this for example:
void Start(){
game = GameManager.Instance
}
Before a scene change, both scripts use each others singleton references to call methods from one another and everything seems to be working fine. Once I change scenes neither of those objects is in the next scene so they both get destroyed. However, once I go back to the main scene I receive a missing reference exception when the purchaser script attempts to call a method from GameManager using the singleton reference it gets from Start(), that changes the text of a text object attached to the main camera. This function is called after a button is pressed that is attached to the shop object that calls this function in the Purchaser script:
UpdateMoney(){
game.UpdateMoney(100);
}
I read around and was seeing that this may be because Start() will only being called once throughout the whole game run. Which meant the GameManger singleton instance of the Purchaser script was still the old one from before the scenes changed and that instance was destroyed. But, I just tested to see if this was true by putting a debug log in each scripts Start() function, and saw that after each scene change back to the main scene the debug log would go through from both scripts Start(). So, would it be right to say Start() is only ever called once for the life of script but not for the whole game run right? Shouldn't this also mean that once the game changes back to the main scene and both GameObjects are created again, the Purchaser script should now have an updated reference to the newly created GameManager script since Start() was called again?
What I also found was that this worked instead of using the game reference in UpdateMoney():
UpdateMoney(){
GameManager.Instance.UpdateMoney(100);
}
So, why does this work instead of using the game reference retrieved in Start()? Does this mean when Start() is called GameManager.Instance is still the old GameManager.Instance which is why game = GameManager.Instance does not work? Sorry this is very wordy. Any help is much appreciated.
Based on what I understand, you have a singleton made in the main scene. The game object where the singleton is attached to is destroyed after switching scenes.
First of all, is there a reason why you need the singleton to be a MonoBehaviour and attached to a game object? Because you can just make a class with
private static MyClass instance = null
public static MyClass Instance
{
get {
if(instance == null)
instance = new MyClass();
return instance;
}
}
This way, your singleton will always have a value and can be passed thru scenes unless instance is set to null.
Second, Start() is called once only. However, I encountered issues before that if the MonoBehaviour is attached to a disabled game object, the Start() will not be called. You can check if this is what's happening to you when you switch back to the main scene.
Third, if you really need the singleton to be a MonoBehaviour, you can use DontDestroyOnLoad(instance.gameObject) so the game object of the singleton will not be destroyed even after a scene switch. However. I assume the game objects are set on the scene. If it is not from a prefab, you can just do something like this
private static MyClass instance = null;
public static MyClass Instance
{
get {
if(instance == null){
GameObject inst = new GameObject("MyClass Singleton");
instance = inst.AddComponent<MyClass>();
DontDestroyOnLoad(inst);
}
return instance;
}
}
If you do this, then you can remove the preset game objects from the main scene and let the first call to MyClass.Instance make the game object for you.
Lastly, if you do not want to do that, you should set instance = null; on the game object's OnDestroy() so that when you enter the main scene, the new instances will be set to the singleton. This would mean the singleton will not have a value after you switch scenes outside of the main scene.
Related
This question already has answers here:
How to pass data (and references) between scenes in Unity
(6 answers)
Closed 1 year ago.
I am new to Unity, and starting on creating my first simple projects. For some days already I've been facing a problem and doing a ton of research but I still cant get it to work, here is it:
As far as I know, it is a smart idea to have a "GameManager" object with a "GameManager" script that holds the main functions of the game (please correct me if it isn't best practice). So lets put as an example I have Scene 1 and Scene 2:
Scene1:
-GameManager
-Label
in GameManager there is a function called ChangeLabelText()
Scene2:
-Button
-Scene2Script
in Scene2script there is a function called ButtonOnClick()
and here is my problem: how do I get ButtonOnClick() to call GameManager.ChangeLabelText()?
I always get a reference error.
I've tried to do it within the same Scene and it works perfectly, but not between different scenes.
Any ideas?
Thank you !
Changing scenes in Unity results in Unity destroying each instance. If you want to keep a specific instance/GameObject across several scenes, you may use the DontDestroyOnLoad method. You can pass the GameObject, this specific GameManager instance is attached to, as the parameter. You'd probably want to use a singleton pattern as well for two reasons:
You may not want to have two or more instances of the GameManager class at once. A singleton will prevent that.
You have to find a way to reference this instance nonetheless, since the components in your other class are totally new instances and they have no idea where this GameManager component is.
Example for a singleton pattern:
GameManager.cs
public static GameManager Instance; // A static reference to the GameManager instance
void Awake()
{
if(Instance == null) // If there is no instance already
{
DontDestroyOnLoad(gameObject); // Keep the GameObject, this component is attached to, across different scenes
Instance = this;
} else if(Instance != this) // If there is already an instance and it's not `this` instance
{
Destroy(gameObject); // Destroy the GameObject, this component is attached to
}
}
Example.cs
private GameManager gameManager;
void Start() // Do it in Start(), so Awake() has already been called on all components
{
gameManager = GameManager.Instance; // Assign the `gameManager` variable by using the static reference
}
This is of course only the basic principle. You may apply that in various slightly different variations if you need to.
I created a football score calculator. I have a button that changes the scene, but when I change the scene and reopen it the score value is reset to 0. Here is the code:
public class Main : MonoBehaviour {
public Text plusUp;
public int value = 0;
public void button(int sc)
{
SceneManager.LoadScene(sc);
}
public void plus()
{
value++;
plusUp.text = value.ToString();
}
}
1. You can use the method Object.DontDestroyOnLoad on the object that hold the variable you want to save. It will keep the GameObject this script is attached too alive when another scene is loaded:
void Awake()
{
DontDestroyOnLoad(this.gameObject);
}
See the documentation for more informations
You can also make a Singleton but this design pattern is a little more complex as Unity will destroy it when you load another scene. You still have to use DontDestroyOnLoad see how to implement this pattern on their GitHub page
2. Or you can save the value on the disk before loading another scene and then load the value with PlayerPrefs helpers methods:
public int value = 0;
void Awake()
{
//Load the saved score (this value will be saved even if you restart the app)
value = PlayerPrefs.GetInt("Score");
}
public void button1(int sc)
{
//Save your value before you load the scene
PlayerPrefs.SetInt("Score", value);
SceneManager.LoadScene(sc);
}
See the documentation for more informations on the types.
There's a few core concepts that would prove useful to understand this issue.
Scope: Each scene operates in it's own scope. Any variables, objects, or changes that occur in one scene do not automatically transfer to another scene. When you start a scene, all objects in the scene are instantiated and initalized, and their Awake()/Start() methods are called if they are Monobehaviours.
Initialization - When an object is instantiated, it is initialized with constructors or default values. Monobehaviours do not have constructors, so any variables will defer back to default values.
Data persistence - When you change scenes, all game objects in the previous scene are destroyed, while all objects in the new scene are instantiated and initialized. Because all objects in the previous scene are destroyed, any values set on those objects disappear. You can prevent a GameObject from being destroyed with DoNotDestroyOnLoad(), but that does not overwrite the objects defined in the new scene. It is usually not advised to use DoNotDestroyOnLoad() as a core part of your game logic, as it often results in scenes being dependent on one another ("scene 1 has to define the values of a GameObject and pass it to scene 2 to be usable" = bad practice).
Solving your problem
It looks like you want score to persist as a value regardless of scene. Since all GameObjects and Monobehaviours are scoped within the scene, you can:
Force the object to be scene-analagous using the Singleton pattern.
Store score data to a file whenever it changes, and read from that file in the Start() method.
My recommended approach: Use a ScriptableObject to hold the score, and refrence that object when changing the score and updating your gameObjects. ScriptableObjects are scoped at the project level, so they automatically persist between scenes.
How do I set a gameObject true/false GameObject.SetActive(true); that is present in another scene while I do the above through a script from my current scene?
As #Eddge said, you can't access objects from scenes that aren't loaded.
Scene objects are actually created when the scene is loaded, so it's not possible to access them before.
The problem being, when the new scene is loaded, the old scene (with the script that wanted to call SetActive in your case) has been unloaded.
DontDestroyOnLoad
Do not destroy the target Object when loading a new Scene.
The load of a new Scene destroys all current Scene objects. Call Object.DontDestroyOnLoad to preserve an Object during level loading.
-- https://docs.unity3d.com/ScriptReference/Object.DontDestroyOnLoad.html
This is suitable if having an object from your current scene persist in the loaded scene helps solving your issue.
LoadScene additively
For cases when DontDestroyOnLoad doesn't cut it, it's possible to load your new scene without unloading the old one, giving you a chance to pass information between the two.
(this is actually what DontDestroyOnLoad does, as it puts the target object into a "scene" called DontDestroyOnLoad, that never gets unloaded)
You can use SceneManagement.LoadScene with the mode LoadSceneMode.Additive to achieve that. Be aware that both scenes will be active at the same time until you unload the first one!
See LoadScene, LoadSceneAsync and UnloadSceneAsync.
Statics
You can also simply store data in static fields, which are kept throughout your app's lifetime, to retain data and references across scenes.
That might be a simpler solution to pass a few primitive types, but remember that they are globals, and thus suffer the same drawbacks.
See Singleton pattern
There is a hacky way I just found out recently, scriptable objects will also work fine if you want to send data from scene to scene because scriptable is saved as an asset in the project so it is independent of the scene. Just create a scriptable object, then you can update your data from one scene and access them from another scene.
Example Code:
Simply create a scriptable object code:
[CreateAssetMenu(fileName = "StoreData")]
public class StoreData : ScriptableObject {
public bool objectActive;
}
Then access your scriptable object in the first scene and set your boolean in the scriptable object to true:
public class FirstSceneClass : MonoBehaviour {
public StoreData storeData;
public SetObjectActive() {
storeData.objectActive = true;
}
}
Then when you change your scene you can access your scriptable object again and set your gameObject to be active or not based on the boolean:
public class SecondSceneClass : MonoBehaviour {
public StoreData storeData;
void Start() {
if (storeData.objectActive) {
// Set your game object active here
}
}
}
in many of my scripts I have to deal with the playerObject. I don't want to search for it in each script.
I thought about creating an static class to have permanent access to the player.
This is how I do it
public class Globals
{
public static GameObject playerObject = GameObject.FindGameObjectWithTag(StringCollection.PLAYER); // The playerObject
public static Rigidbody playerRigid = GameObject.FindGameObjectWithTag(StringCollection.PLAYER).GetComponent<Rigidbody>();
} // The rigidbody of the player
So I want to store all the variables that I need many times. When loading a new scene, it says the rigidbody tries to access an object, that got destroyed previously.
How can I fix it?
I thought about creating an object inheriting from Monobehaviour that gets never destroyed but then I would have to create a reference to it in all my scripts..
Anything that inherits from Unity's UnityEngine.Object are destroyed even when made static.
If you want it to stay, you have to call DontDestroyOnLoad on it. In this case call:
DontDestroyOnLoad(Globals.playerObject);
This should make the playerObject and every component that is attached to it to stay through the next scene.
I have a strange problem with DontDestroyOnLoad. I have a map as a starting scene. From there, the user can click on certain map-objects and a new level is loaded with Application.LoadLevel() When the level is finished the map is loaded again. But, the objects with DontDestroyOnLoad attached are duplicated on the second load.
Current script:
void Awake()
{
DontDestroyOnLoad(transform.gameObject);
}
I searched online and found this possible solution to remove the duplicates:
public class NoDestroy : MonoBehaviour {
public static NoDestroy instance;
void Awake()
{
if (instance == null)
{
instance = this;
}
else
{
Destroy(this.gameObject);
return;
}
DontDestroyOnLoad(this.gameObject);
}
}
The above script simply does not work. How can I fix the problem?
Because the above script uses a static instance, it'll only work if a `single GameObject has it attached - worse yet, it'll delete other objects which try to use the same behavior.
The biggest problem is that whenever you load a scene, all objects in that scene get loaded. If they weren't unloaded (thanks to DontDestroyOnLoad) they'll be duplicated, since the scene doesn't know that they "already exist"
The above script might work better if you try to box your persistant objects under an umbrella, and only the umbrella object (usually called Toolbox) isn't destroyed. This is mostly appropriate for manager scripts, however.
If you know the objects that are meant to not be destroyed, consider loading them via a "Loading" scene. Since this moves the persistent objects out of your map scene, they won't get duplicated when reloading the map scene. Bonus to this pattern since it makes it easier to implement curtain drop.
If you want to implement this as a simple behavioural script, consider adding an ID like so
public class NoDestory : MonoBehaviour
{
private static Dictionary<string, GameObject> _instances = new Dictionary<string, GameObject>();
public string ID; // HACK: This ID can be pretty much anything, as long as you can set it from the inspector
void Awake()
{
if(_instances.ContainsKey(ID))
{
var existing = _instances[ID];
// A null result indicates the other object was destoryed for some reason
if(existing != null)
{
if(ReferenceEquals(gameObject, existing)
return;
Destroy(gameObject);
// Return to skip the following registration code
return;
}
}
// The following code registers this GameObject regardless of whether it's new or replacing
_instances[ID] = gameObject;
DontDestroyOnLoad(gameObject);
}
}
This will prevent the duplication of an object with the same ID value as one that already exists, as well as allowing recreation if said object has been Destory-ed elsewhere. This can be further refined by adding a special Editor script to hand over a new ID each time the script is implemented.
You could possibly make an "Initializer" scene that's the starting scene of the project, and all your "Don't Destroy On Load" objects get initialized in there. Then you immediately change to the real starting scene (your map screen) and all your objects exist and aren't duplicated. If you have a starting screen already, you might be able to use that instead of creating a whole new scene.
In case if anyone still needs the answer:
Answer which is available everywhere (WRONG ONE):
private static Sample sampleInstance;
void Awake()
{
DontDestroyOnLoad(this);
if (sampleInstance == null)
{
sampleInstance = this;
}
else
{
DestroyObject(gameObject);
}
}
Tl;dr Jump to solution.
What happens here is, when new scene is loaded, everything's fine. But as soon as you go back to previous scene, your original "Sample" game object is NOT destroyed and the new "Sample" game object which get created, is destroyed. So, what happens is, wherever you have referenced your Sample script (like in button onclick etc.) start referencing the duplicate script (which was destroyed of course), due to which they don't reference any script. Hence button.onClick no longer works.
So, the correct solution is to destroy the original Sample game object and not the duplicate, hence making the duplicate as the new original.
Here it is (THE CORRECT SOLUTION):
private static GameObject sampleInstance;
private void Awake()
{
if (sampleInstance != null)
Destroy(sampleInstance);
sampleInstance = gameObject;
DontDestroyOnLoad(this);
}
I have tested it. This works for me!!
if (Instance==null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
For use cases where you have some startup logic that only needs to be initialized once, consider creating a Startup scene that only loads once at the beginning of your game. That way, whatever scene switching you do from that point on won't create duplicates of the game objects created with the Startup scene.
In relation to networking, that's what Unity did in their Boss Room example:
https://github.com/Unity-Technologies/com.unity.multiplayer.samples.coop/blob/main/Assets/Scripts/ApplicationLifecycle/ApplicationController.cs#L94