Why does gameobject.Find/GetComponent not work when a new scene is loaded? (638172)

I’m transitioning from from LandingPage scene to ItemFocusTest scene with a singleton script that uses Scenemanager.Loadscene called from a button.

When Unity loads up the ItemFocusTest scene, certain scripts/gameobjects seem to fail to get references to other objects and components in the scene, preventing the scene from working properly.

It’s important to note that ItemFocusTest works correctly when loaded manually in the Unity Editor - meshRenderers are found, their material.colors set to color.clear and a reference is cached to the TileClicker script on the Main Camera. (Look for all-caps below)

void Start () {
         tapTiles                            = tileParent.GetComponentsInChildren<TapTile>();            // Get an array of all the tiles
         oneOfTheTileRenderers               = tapTiles[0].GetComponent<MeshRenderer>();                 // Get the meshrenderer of one of the tiles
         tileMat                             = oneOfTheTileRenderers.sharedMaterial;                     // Find the shared material of all the tiles - so when we fade the material, it fades on all the tiles
         tileMat.color                       = opague;                                                   // Make sure the material is fully visible when the scene loads
         touchIcon                           = GameObject.Find("TouchIcon");

         imageFaderMat                       = GameObject.Find("ImageFader").GetComponent<MeshRenderer>().sharedMaterial; // **THIS REFERENCE ISN'T BEING CACHED!**
         imageFaderMat.color                 = Color.clear;

         tileClicker                         = GameObject.Find("Main Camera").GetComponent<TileClicker>(); // **NEITHER IS THIS ONE!**
     }

On loading ItemFocusTest from LandingPage, I get the following error message:

At a certain point in itemFocusTest, using enough items SHOULD trigger a set of fades and turn off clicking interaction. Instead, I get the following error…

Again, all this works when the scene is loaded in isolation in the Unity Editor.

What’s going on? What’s stopping the script from finding the correct gameObjects and/or components? Is there a later event than Start that I can use for initialization? I’m baffled.

–Rev

Please use Code tags when posting code in the forum, as per the sticky at the top: Using code tags properly - Unity Engine - Unity Discussions

It might be a good idea to break up your referencing into chunks to make debugging easier and increase readability. For instance:

imageFaderMat = GameObject.Find("ImageFader").GetComponent<MeshRenderer>().sharedMaterial;

should really be something like:

GameObject imageFader = GameObject.Find("ImageFader");
MeshRenderer imageFaderRenderer = imageFader.GetComponent<MeshRenderer>();
imageFaderMat = imageFaderRenderer.sharedMaterial;

You don’t even need to throw in null-checks at each step, but just separating them out this way means that the warning and errors thrown will have better context.

I’m a bit confused about where this script is that you’ve copied here. If it’s on an object that’s being loaded in the new scene (created when you change scenes), and the object it’s trying to reference (ImageFader) is also being created in the new scene, then you should just make a public reference and drag it in rather than using GameObject.Find at all in my opinion.

If, as seems to be the case, the ImageFader object is being carried over from the old scene, make sure that the object is set to DontDestroyOnLoad or that the scene is Additive, or else it’ll just get wiped out with everything else when the scene changes. What’s the scene hierarchy look like in the editor when the error happens? Is there only one copy of the ImageFader object, and if there is, does it have the correct components attached or is it just an empty GameObject?

2 Likes

Goddamnit, apologies Lysander, I knew something was wrong but I couldn’t find the menu option and I got lazy. Thanks for the heads up.

Good point on breaking up the reference line, I hadn’t thought about it in terms of getting error reports. Appreciate the advice.

–Rev

I also wonder if GameObject.Find isn’t working because it’s trying to locate a gameobject that hasn’t had it’s Start routine called yet. Maybe when you’re loading the scene, that other object hasn’t yet been loaded which is why it can’t be located. Things may work a bit differently in the editor.

2 Likes

As a quick note, because it just occurred to me, but you should do all of your initialization in Awake, or at least the stuff that you CAN properly initialize with only the data within this script. Start should, as a good practice, be reserved for things that require other scripts in order to initialize properly. This way, if you’re referencing something in ScriptB from ScriptA in Start, you can be absolutely sure that the Awake function has run in ScriptB- if you initialize everything for both scripts in Start, then you can’t be sure that the Start function in ScriptB has run before ScriptA tries to reference it. Does that make sense?

Every Awake function is called at the scene start, then when those are all done, every Start function is called, but one component’s Awake function is not normally guaranteed to be called before any other other component’s Awake function (the order is not quite random, but it’s not reliable either), and the same for the Start function.

In situations where you need more precise control and ScriptB’s Start function really does have to run before ScriptA’s Start function does (or something in ScriptA’s Update function is relying on ScriptA’s Update function already having been run this frame, etc…) then look into Script Execution Order.

I’m only bringing this up because it has a small chance of being relevant here, if ImageFader’s Start function is placing the MeshRenderer on the GameObject, and this script is trying to grab a reference to the MeshRenderer before ImageFader has had a chance to place it, this kind of problem can happen. It’s also just good stuff to know :).

Edit: Jinx :wink:

2 Likes

@Reverend-Speed I’ve always had issues with GameObject.Find in Start().

They most likely haven’t been initialised yet, and thus cannot be found. Any time I’m looking for GameObjects in a scene startup, I use a co-routine.

So move your code into a co-routine, and use WaitForEndOfFrame().

Something like this…

void Start()
{
  StartCoroutine(InitCoroutine());
}

IEnumerator InitCoroutine()
{
   yield return new WaitForEndOfFrame();

   // Do your code here to assign game objects
}
2 Likes

Excellent solution!

1 Like

@Dustin-Horne - Yeah, that was sorta my initial thought, but as you’ll see further down this post, the GetComponent is attempted again well into the runtime of the scene. Many thanks for your input, though!

@DonLoquacious - Thank you so much for that thoughtful response! This might as well be printed on the inside of my eyelids (I just about have it memorised nowadays), but it’s so damned easy to miss one thing and slip up. Appreciate your eyes and input on the problem.

The Meshrenderer in question is set as a component on the ImageFader object in-editor, and so doesn’t rely on a script to add the component on the gameobject. Maybe I should look into somehow getting a reference to the tilecounter from the ImageFader and… then… use that to set a reference from the tileCounter to the ImageFader…? Nah, surely that’s crazy…!

@Meltdown - Just. Golf. Clap. That is an EXCELLENT solution and I’ll be sure to use it in future. Unfortunately, it doesn’t appear to be helping with this issue (which is baffling). Currently, this is what I get shortly after switching to the second scene…

The TileCounter.cs code currently is as follows…

using UnityEngine;
using System.Collections;

public class TileCounter : MonoBehaviour {

    private int         tileClickOnCount    = 0;                // The number of tiles clicked on thus far
    public Transform    tileParent;                             // The game object which contains the (currently hex-shaped) tiles
    private TapTile[]   tapTiles;                               // An array of TapTile scripts constructed from the children of tileParent
    public int          tileClickOnMax      = 0;                // The number of tiles to click before all visible tiles begin to fade out

    private Renderer    oneOfTheTileRenderers;                  // The first tile renderer of the tileParent children, used to get the material shared between tiles.
    private Material    tileMat;                                // The material used on all the tiles - located through oneOfTheTileRenderers.
    private bool        startFading         = false;            // Flag/bool which is prevent the Update method from fading the tiles until tileCount exceeds tileClickOnMax
    private Color       opague              = Color.white;      // Making a single name for a fully white, fully visible color
    public  float       tileFadeSpeed       = 1.0f;             // The speed at which the tile material fades to transparent
    // public  float       tileFadeDuration    = 1.0f;             //
    // private float       tileFadeTimer       = 0.0f;
    private GameObject  touchIcon;

    private AudioSource audMovieTrack;
    public  float       fadeSpeed = 4.0f;

    private Material        imageFaderMat;

    public TileClicker     tileClicker;

    void Awake ()
    {
        audMovieTrack = GetComponent<AudioSource>();
        audMovieTrack.volume = 1.0f;
    }

    // Use this for initialization
    void Start () {
        StartCoroutine(InitCoroutine());
    }
 
    // Update is called once per frame
    void Update () {

        if (startFading)
        {
            imageFaderMat.color = Color.Lerp(imageFaderMat.color, Color.white, fadeSpeed * Time.deltaTime);
        }

        //if (startFading)//&& tileFadeTimer < tileFadeDuration) // If startFading is true...
        //{
        //    // tileMat.SetFloat("_Mode", 3.0f);
        //    FadeTileMaterial();
        //    // tileFadeTimer += Time.deltaTime;
        //}
    }

    IEnumerator InitCoroutine()
    {
        yield return new WaitForEndOfFrame();

        tapTiles = tileParent.GetComponentsInChildren<TapTile>();               // Get an array of all the tiles
        oneOfTheTileRenderers = tapTiles[0].GetComponent<MeshRenderer>();       // Get the meshrenderer of one of the tiles
        tileMat = oneOfTheTileRenderers.sharedMaterial;                         // Find the shared material of all the tiles - so when we fade the material, it fades on all the tiles
        tileMat.color = opague;                                                 // Make sure the material is fully visible when the scene loads
        touchIcon = GameObject.Find("TouchIcon");

        GameObject imgFader = GameObject.Find("ImageFader");
        MeshRenderer imgRend = imgFader.GetComponent<MeshRenderer>();
        imageFaderMat = imgRend.sharedMaterial;
        imageFaderMat.color = Color.clear;

        tileClicker = GameObject.Find("Main Camera").GetComponent<TileClicker>();
    }

    public void AddOneToCount()                 // Called from a tile when it's clicked on, increments the tileClickOnCount and checks to see if it's more than tileClickOnMax
    {
        if(tileClickOnCount < 1)
        {
            Debug.Log("Turning off icon");
            touchIcon.SetActive(false);
        }
        tileClickOnCount++;
        if(tileClickOnCount >= tileClickOnMax)
        {
            tileClicker = GameObject.Find("Main Camera").GetComponent<TileClicker>();
            tileClicker.TurnOffTapOnTile();
            StopMovieTrack();
        }
    }

    //void AllFallDown()
    //{
    //    startFading = true;                     // Starts the fade in the main Update loop
    //    // tileMat.SetFloat("_Mode", 3.0f);
    //    //for (int i = 0; i < tapTiles.Length; i++)
    //    //{
    //    //    if (tapTiles[i] != null)
    //    //    {
    //    //        tapTiles[i].DropTile();
    //    //    }
    //    //}
    //}

    public void PlayMovieTrack()
    {
        audMovieTrack.Play();
    }

    void StopMovieTrack()
    {
        StartCoroutine(FadeVolumeDown());
        imageFaderMat = GameObject.Find("ImageFader").GetComponent<MeshRenderer>().sharedMaterial;
        startFading = true;
    }

    IEnumerator FadeVolumeDown()
    {
        while (audMovieTrack.volume > 0.0f)
        {
            audMovieTrack.volume = Mathf.MoveTowards(audMovieTrack.volume, 0.0f, fadeSpeed * Time.deltaTime);
         
            yield return null;
        }
    }
}

Note the additional attempt to get the reference in StopMovieTrack() - This is called wayyy after the scene initialises, but it just gives me the same error…

I’m sure this has to be a small thing that I’m just not seeing - I appreciate the time you folks are giving to this… Hope you’re more observant than I am!

–Rev

Replace

yield return new WaitForEndOfFrame();

with

yield return new WaitForSeconds(5);

If everything works, then we know its an issue with stuff not being initialised on time.

Is it possible instead of using GetComponent to rather assign the component to your script in the editor? I try to assign as many references as I can in the editor, instead of using GetComponent or GameObject.Find etc.

@Meltdown : Great minds think alike, it seems. Quite late on Weds night, I remembered that I could just assign the MeshRenderer as a public variable and presto, everything worked perfectly for the next morning.

The issue is still annoying, though. Unlike yourself, I’ve been lucky enough to have been treated fairly well by Awake/Start/GetComponent/GameObject.Find in the past, so this seems like an sudden and emotionally scarring betrayal. I’ve always thought that getting references to private vars was the proper way of doing things - will have to think about this, now.

For yucks, I ran WaitForSeconds (5) on the script and it still crashed and burned (Meshrenderer not found). I’m starting to suspect my menu singleton is interrupting stuff, somehow… but for the moment, the scripting problem is fixed*!

Thanks so much for helping out, everybody!

–Rev

  • by not using scripting at all.

Are you using a GameObject singleton pattern but not using DontDestroyOnLoad? It’s possible that destroy is getting called but you have a game object that isn’t really going away because you have a reference to it elsewhere.

Hey, just saw your response. I have to admit ignorance here, as I don’t know what a ‘cameo next’ singleton pattern is - however, I am using DontDestroyOnLoad. Here’s the code…

using UnityEngine;
using UnityEngine.SceneManagement;
using System.Collections;

public class DocManager : MonoBehaviour {

    private Animator anim;
    public  float   fadeTime = 0.33f;

    private AudioSource aud;
    private float       startingVolume;
    public  float       audFadeSpeed = 1.0f;
    private bool        audIsPlaying = true;

    private const string TWITTER_ADDRESS = "http://twitter.com/intent/tweet";
    private const string TWEET_LANGUAGE = "en";

    private static DocManager _instance;
    public static DocManager Instance { get { return _instance; } }
 
    void Awake () {
        if(_instance != null && _instance != this)
        {
            Destroy(gameObject);
        }
        else
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }

        SceneManager.sceneLoaded += OnLevelChange;

        aud = GetComponent<AudioSource>();
        startingVolume = aud.volume;
    }

    void Start ()
    {
        anim = GetComponentInChildren<Animator>();
    }
 
    public void LoadScene (string sceneName)
    {
        anim.SetTrigger("ClearToBlack");
        Debug.Log("Loading scene...");
        StartCoroutine(FadeAndLoad(sceneName));
    }

    IEnumerator FadeAndLoad(string sceneName)
    {

        yield return new WaitForSeconds(fadeTime);
        SceneManager.LoadScene(sceneName);
    }

    public void OnLevelChange(Scene level, LoadSceneMode mode)
    {
        Debug.Log("Calling BlackToClear");
        anim.SetTrigger("BlackToClear");
    }

    public void ToggleSound ()
    {
        StopCoroutine("ChangeVolume");
        if (audIsPlaying)
        {
            StartCoroutine(ChangeVolume(0.0f));
            audIsPlaying = false;
        } else
        {
            StartCoroutine(ChangeVolume(startingVolume));
            audIsPlaying = true;
        }
    }

    IEnumerator ChangeVolume (float target)
    {
        while(aud.volume != target)
        {
            aud.volume = Mathf.MoveTowards(aud.volume, target, audFadeSpeed * Time.deltaTime);
            yield return null;
        }
        yield return null;
    }

    public void ShareToTwitter (string textToDisplay)
    {
        Application.OpenURL(TWITTER_ADDRESS + "?text=" + WWW.EscapeURL(textToDisplay) + "&lang=" + WWW.EscapeURL(TWEET_LANGUAGE));
    }
}

Gah. I’m a combination of baffled but also relieved that I’ve got a workaround for this. Might just drop the issue, tbh. But thank you so much for your time on this!

All the best,

–Rev

I’m sorry, I corrected it. I meant GameObject singleton pattern… autocorrect got me as I was replying from my phone. In your Awake you’re checking _instance but calling Destroy(gameObject). What is the value of gameObject? Shouldn’t you be destroying _instance and then resetting it’s value?

Hah! Autocorrect, amirite? =)

Uh… hang on. Let me walk through the logic of this…

if(_instance != null && _instance != this) // IF the private static variable of the class is NOT empty AND it ISN'T this game object...
        {
            Destroy(gameObject); // ...THEN this must be a new instance of the class (w/gameobject) (probably a new version or prefab in the new scene that's just loaded), which we DON'T want - get rid of it. (There should only ever be one version of a Singleton, the first instance of it to be loaded when the project runs)
        }
        else // OTHERWISE - if there's no gameObject running DocManager currently occupying the _instance variable...
        {
            _instance = this; // Make the static variable refer to 'this' (ie. the DocManager running on THIS gameObject)
            DontDestroyOnLoad(gameObject); // ...and mark the gameObject 'DontDestroyOnLoad' so when the next scene is loaded, this gameObject and the data on it is retained, thus preserving some runtime data throughout the course of the project.
        }

That still seems to make sense, I think? I’m about ready to throw my hands up at this thing and mark it down as the opposite of a learning experience. =) At least the project works!

–Rev