Search Unity

Question Missing references on scene load

Discussion in 'Editor & General Support' started by yomvol, May 31, 2023.

  1. yomvol

    yomvol

    Joined:
    Jun 1, 2022
    Posts:
    3
    Hey, guys. I'm stuck with one issue. I have two scenes: menu and the actual game. The problem occurs when I return to main menu from game. Persistent singleton Level Manager loads scenes. On Awake it tries to bind to some UI elements. I know that Awake is called even for persistent objects upon level load (proven by logs). The strange thing is that after return to main menu in debugger I can see that all references are bound successfully and valid, but right after I continue the program flow I see in Inspector that these references have become missing(!).

    The only possible hypothesis I have is that chronology: all objects in menu scene are loaded -> Awake is called for LevelManager -> Everything is fine -> All non-persistent objects get destroyed ??? -> All scene objects are instantiated once again. I'm pretty sure that nothing interferes with those references after Awake and the singleton class is really basic.

    upload_2023-5-31_3-36-27.jpeg

    upload_2023-5-31_3-36-43.jpeg

    Code:
    Code (CSharp):
    1. using System;
    2. using System.Threading.Tasks;
    3. using UnityEngine;
    4. using UnityEngine.InputSystem;
    5. using UnityEngine.InputSystem.Utilities;
    6. using UnityEngine.Rendering.Universal;
    7. using UnityEngine.SceneManagement;
    8. using UnityEngine.UI;
    9. using TMPro;
    10. public class LevelManager : PersistentSingleton<LevelManager>
    11. {
    12.    [SerializeField] private Camera _mainCam;
    13.    [SerializeField] private Canvas _menuCanvas;
    14.    [SerializeField] private Canvas _loadingCanvas;
    15.    [SerializeField] private Image _rotatingIndicator;
    16.    [SerializeField] private TextMeshProUGUI _prompt;
    17.    [SerializeField] private float _rotationSpeed;
    18.    private float _target;
    19.    private float _prevRotationTime = 0f;
    20.    protected override void Awake()
    21.    {
    22.        base.Awake();
    23.        Debug.Log("Level Manager has AWAKENED!");
    24.        Debug.Log(SceneManager.GetActiveScene().name);
    25.        if (SceneManager.GetActiveScene().name == "Menu")
    26.        {
    27.            _mainCam = Camera.main;
    28.            _menuCanvas = GameObject.Find("Canvas").GetComponent<Canvas>();
    29.            _loadingCanvas = GameObject.Find("LoadingCanvas").GetComponent<Canvas>();
    30.            _rotatingIndicator = _loadingCanvas.transform.GetChild(1).gameObject.GetComponent<Image>();
    31.            _prompt = _loadingCanvas.transform.GetChild(2).gameObject.GetComponent<TextMeshProUGUI>();
    32.            _loadingCanvas.gameObject.SetActive(false);
    33.            Debug.Log("Tied everything: " + _prompt.text);
    34.        }
    35.    }
    36.    public async void LoadScene(string sceneName)
    37.    {
    38.        Cursor.visible = false;
    39.        var scene = SceneManager.LoadSceneAsync(sceneName);
    40.        scene.allowSceneActivation = false;
    41.        if (_menuCanvas != null)
    42.        {
    43.            _menuCanvas.gameObject.SetActive(false);
    44.            _mainCam.GetUniversalAdditionalCameraData().renderPostProcessing = false;
    45.            _loadingCanvas.gameObject.SetActive(true);
    46.        }
    47.        do
    48.        {
    49.            //await Task.Delay(100);
    50.            _target = scene.progress;
    51.        } while (scene.progress < 0.9f);
    52.        await Task.Delay(5000);
    53.        _prompt.text = "Press any key";
    54.        _target = 1.0f;
    55.        var tcs = new TaskCompletionSource<bool>();
    56.        Action<InputControl> onAnyButtonPressed = delegate (InputControl ctrl)
    57.        {
    58.            //Debug.Log($"{ctrl} pressed");
    59.            scene.allowSceneActivation = true;
    60.            tcs.TrySetResult(true);
    61.        };
    62.        InputSystem.onAnyButtonPress.CallOnce(onAnyButtonPressed);
    63.        await tcs.Task;
    64.    }
    65.    private void Update()
    66.    {
    67.        if (_rotatingIndicator != null && _rotatingIndicator.isActiveAndEnabled && Time.time - _prevRotationTime > _rotationSpeed)
    68.        {
    69.            _prevRotationTime = Time.time;
    70.            if (_target < 1.0f)
    71.            {
    72.                _rotatingIndicator.transform.RotateAround(_rotatingIndicator.transform.position, Vector3.back, _target * 360f);
    73.            }
    74.            else
    75.            {
    76.                _rotatingIndicator.transform.rotation = Quaternion.identity;
    77.            }
    78.        }
    79.    }
    80. }
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,752
    Great hypotheses. What did you find when you actually debugged it?

    My guess is one part of your scene is DontDestroyOnLoad and it references another part that isn't.

    But feel free to experiment, just remember, your nullref is NOT special.

    It will be fixed by the same three steps ALWAYS!

    How to fix a NullReferenceException error

    https://forum.unity.com/threads/how-to-fix-a-nullreferenceexception-error.1230297/

    Three steps to success:
    - Identify what is null <-- any other action taken before this step is WASTED TIME
    - Identify why it is null
    - Fix that

    There has NEVER been another way (except blind unintentional luck) to fix a Nullref.

    To further the investigation of your "time of order" hypothesis as well as to do steps #2 above, try this:

    Time to start debugging! Here is how you can begin your exciting new debugging adventures:

    You must find a way to get the information you need in order to reason about what the problem is.

    Once you understand what the problem is, you may begin to reason about a solution to the problem.

    What is often happening in these cases is one of the following:

    - the code you think is executing is not actually executing at all
    - the code is executing far EARLIER or LATER than you think
    - the code is executing far LESS OFTEN than you think
    - the code is executing far MORE OFTEN than you think
    - the code is executing on another GameObject than you think it is
    - you're getting an error or warning and you haven't noticed it in the console window

    To help gain more insight into your problem, I recommend liberally sprinkling
    Debug.Log()
    statements through your code to display information in realtime.

    Doing this should help you answer these types of questions:

    - is this code even running? which parts are running? how often does it run? what order does it run in?
    - what are the names of the GameObjects or Components involved?
    - what are the values of the variables involved? Are they initialized? Are the values reasonable?
    - are you meeting ALL the requirements to receive callbacks such as triggers / colliders (review the documentation)

    Knowing this information will help you reason about the behavior you are seeing.

    You can also supply a second argument to Debug.Log() and when you click the message, it will highlight the object in scene, such as
    Debug.Log("Problem!",this);


    If your problem would benefit from in-scene or in-game visualization, Debug.DrawRay() or Debug.DrawLine() can help you visualize things like rays (used in raycasting) or distances.

    You can also call Debug.Break() to pause the Editor when certain interesting pieces of code run, and then study the scene manually, looking for all the parts, where they are, what scripts are on them, etc.

    You can also call GameObject.CreatePrimitive() to emplace debug-marker-ish objects in the scene at runtime.

    You could also just display various important quantities in UI Text elements to watch them change as you play the game.

    Visit Google for how to see console output from builds. If you are running a mobile device you can also view the console output. Google for how on your particular mobile target, such as this answer or iOS: https://forum.unity.com/threads/how-to-capturing-device-logs-on-ios.529920/ or this answer for Android: https://forum.unity.com/threads/how-to-capturing-device-logs-on-android.528680/

    If you are working in VR, it might be useful to make your on onscreen log output, or integrate one from the asset store, so you can see what is happening as you operate your software.

    Another useful approach is to temporarily strip out everything besides what is necessary to prove your issue. This can simplify and isolate compounding effects of other items in your scene or prefab.

    Here's an example of putting in a laser-focused Debug.Log() and how that can save you a TON of time wallowing around speculating what might be going wrong:

    https://forum.unity.com/threads/coroutine-missing-hint-and-error.1103197/#post-7100494

    "When in doubt, print it out!(tm)" - Kurt Dekker (and many others)

    Note: the
    print()
    function is an alias for Debug.Log() provided by the MonoBehaviour class.
     
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,939
    The entries showing as 'missing' mean they have been destroyed at some point. So likely they have been destroyed between scene loads.

    Also be aware that Awake is called during a scene load, so there is the potential of race conditions or objects not even existing yet. Start can be used to initiate stuff after the scene has loaded, before Update is called (as it states in the docs).

    Mind you, I'm seeing lots of mixed up responsibility here. A level manager should have nothing to do with the main menu UI. Nor should it contain the code for a loading screen. I think a lot of responsibility needs to be separated here.
     
    Kurt-Dekker likes this.
  4. yomvol

    yomvol

    Joined:
    Jun 1, 2022
    Posts:
    3
    I opted out usage of Start since it won't be called for my persistent singleton after scene is loaded, judging by the docs. Start is called only once in a lifetime.

    Strangely, references in Awake always work reliably, with no race condition on the first load of Menu (start of the game).
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,939
    Awake is only called once in a game objects lifetime too, so it doesn't have that particular advantage over Start. The main difference is Awake is called as an object is loaded, Start is called before Update is called on the game object.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,752
    Oh dear, just saw this:

    There were like 12 different variations of that on the Unity wiki before that site went down.

    I tested many and every single one of them failed in some condition of uses: leave the thing in scene, load a new scene, access it before the first scene, etc.

    You don't need that level of "clever complexity" of a typed base. It gives you nothing and does you no favors, as you can see by the nullrefs you're getting and how difficult it is to reason why.

    Instead, consider these patterns:

    ULTRA-simple static solution to a GameManager:

    https://forum.unity.com/threads/i-need-to-save-the-score-when-the-scene-resets.1168766/#post-7488068

    https://gist.github.com/kurtdekker/50faa0d78cd978375b2fe465d55b282b

    OR for a more-complex "lives as a MonoBehaviour or ScriptableObject" solution...

    Simple Singleton (UnitySingleton):

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://gist.github.com/kurtdekker/775bb97614047072f7004d6fb9ccce30

    Unity3D Singleton with a Prefab (or a ScriptableObject) used for predefined data:

    https://gist.github.com/kurtdekker/2f07be6f6a844cf82110fc42a774a625

    These are pure-code solutions, DO NOT put anything into any scene, just access it via .Instance!

    The above solutions can be modified to additively load a scene instead, BUT scenes do not load until end of frame, which means your static factory cannot return the instance that will be in the to-be-loaded scene. This is a minor limitation that is simple to work around.

    If it is a GameManager, when the game is over, make a function in that singleton that Destroys itself so the next time you access it you get a fresh one, something like:

    Code (csharp):
    1. public void DestroyThyself()
    2. {
    3.    Destroy(gameObject);
    4.    Instance = null;    // because destroy doesn't happen until end of frame
    5. }
    There are also lots of Youtube tutorials on the concepts involved in making a suitable GameManager, which obviously depends a lot on what your game might need.

    OR just make a custom ScriptableObject that has the shared fields you want for the duration of many scenes, and drag references to that one ScriptableObject instance into everything that needs it. It scales up to a certain point.

    And finally there's always just a simple "static locator" pattern you can use on MonoBehaviour-derived classes, just to give global access to them during their lifecycle.

    WARNING: this does NOT control their uniqueness.

    WARNING: this does NOT control their lifecycle.

    Code (csharp):
    1. public static MyClass Instance { get; private set; }
    2.  
    3. void OnEnable()
    4. {
    5.   Instance = this;
    6. }
    7. void OnDisable()
    8. {
    9.   Instance = null;     // keep everybody honest when we're not around
    10. }
    Anyone can get at it via
    MyClass.Instance.
    , but only while it exists.

    If you really insist on a barebones C# singleton, here's a highlander (there can only be one):

    https://gist.github.com/kurtdekker/b860fe6734583f8dc70eec475b1e7163