Search code examples
c#unity-game-enginevuforia

Why does Unity C# Coroutine execute upon click of an unrelated button


The script is part of a Unity/Vuforia AR app in which the user is able to place various PreFab models via the Vuforia ground detection system. These Prefabs are loaded into AssetBundles in order to manage memory overhead.

The script to accomplish this is executing prematurely.

This Coroutine script is attached to each button. Upon "onClick" the script is intended to load an AssetBundle containing a Prefab, and then instantiate the loaded Prefab object into the AR world. The problem currently is that the script is executing upon the click on a UI panel opening button, which enables access to the actual placement UI buttons to which the script has been attached.

I arrived at this script the excellent via input from @derHugo. That thread can be found here: Can a Prefab loaded from asssetBundle be passed to Vuforia AnchorBehavior declaration

Here is the script that is attached to the placement buttons

using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using Vuforia;

public class anchorManagerBundles : MonoBehaviour
{
    public PlaneFinderBehaviour plane;
    public ContentPositioningBehaviour planeFinder;
    private AnchorBehaviour model;
    public string nameOfAssetBundle;
    public string nameOfObjectToLoad;
    private static bool alreadyLoading;
    private static AssetBundle assetBundle;

    void Start()
    {
        // only load the bundle once
        if (!alreadyLoading)
        {
            // set the flag to make sure this is never done again
            alreadyLoading = true;
            StartCoroutine(LoadAsset(nameOfAssetBundle, nameOfObjectToLoad));
        }
        else
        {
            LoadObjectFromBundle(nameOfObjectToLoad);
        }
    }

    private IEnumerator LoadAsset(string assetBundleName, string objectNameToLoad)
    {
        string filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "AssetBundles");
        filePath = System.IO.Path.Combine(filePath, assetBundleName);

        if (assetBundle == null)
        {
            Debug.Log("Failed to Load assetBundle!!");
            yield break;
        }

        { 
            var assetBundleCreateRequest = AssetBundle.LoadFromFileAsync(filePath); 
            yield return assetBundleCreateRequest; assetBundle = assetBundleCreateRequest.assetBundle; 
        }

    private IEnumerator LoadObjectFromBundle(string objectNameToLoad)
    {
        AssetBundleRequest assetRequest = assetBundle.LoadAssetAsync<GameObject>(objectNameToLoad);
        yield return assetRequest;

        GameObject loadedAsset = (GameObject)assetRequest.asset;

        model = loadedAsset.GetComponent<AnchorBehaviour>();
    }

    public void create()
    {
        planeFinder.AnchorStage = model;
    }
}

The desired/expected result was that upon clicking the UI button, the selected AssetBundle is loaded and the named PreFab is loaded for placement into the AR world upon tapping the screen.

I added a debug break in order to identify if the Asset Bundle is successfully loaded. The error below is then registered when the unrelated UI panel opening button is pressed, which does not have the script attached, indicating that the script is running prematurely.

Failed to Load assetBundle!!
UnityEngine.Debug:Log(Object)
<LoadAsset>d__8:MoveNext() (at Assets/Scripts/anchorManagerBundles.cs:38)
UnityEngine.MonoBehaviour:StartCoroutine(IEnumerator)
anchorManagerBundles:Start() (at Assets/Scripts/anchorManagerBundles.cs:23)

Then i proceed to the actual placement button, of course the there is no prefab placed because of the premature execution.

There is no content to place at the anchor. Set the "Anchor Stage" field to the content you wish to place.
UnityEngine.Debug:LogError(Object)
Vuforia.ContentPositioningBehaviour:CreateAnchorAndPlaceContent(Func`2, Vector3, Quaternion)
Vuforia.ContentPositioningBehaviour:PositionContentAtPlaneAnchor(HitTestResult)
UnityEngine.Events.UnityEvent`1:Invoke(HitTestResult)
Vuforia.PlaneFinderBehaviour:PerformHitTest(Vector2)
UnityEngine.Events.UnityEvent`1:Invoke(Vector2)
Vuforia.AnchorInputListenerBehaviour:Update()

The question is why does this script execute prematurely, and thus thwarting the loading of the asset bundle and its selected PreFab


Solution

  • Despite the fact you have a { too much there


    your check for

    if (assetBundle == null)
    

    makes no sense at this point .. it will always be null so you tell your routine "If the assetBundle was not loaded yet .. then please do not load it".


    You should move the check to the end. I personally would then invert the check you had in order to skip the loading if it was loaded already.

    Note: You also missed the last line

    LoadObjectFromBundle(objectNameToLoad);
    

    at the end of LoadAsset so the object is never loaded for the first instance calling LoadAsset. For the others it actually makes no sense to call LoadObjectFromBundle in Start (sorry my bad for not noticing it the first time). The coroutine is not done yet but they will try to load an object from an assetBundle which wasn't set yet.

    So I would change it to

    private IEnumerator LoadAsset(string assetBundleName, string objectNameToLoad)
    {
        // can be done in one single call
        var filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "AssetBundles", assetBundleName);
    
        // if at this point the assetBundle is already set we can skip the loading
        // and directly continue to load the specific object
        if (!assetBundle)
        {
            var assetBundleCreateRequest = AssetBundle.LoadFromFileAsync(filePath);
            yield return assetBundleCreateRequest;
    
            assetBundle = assetBundleCreateRequest.assetBundle;
    
            if (!assetBundle)
            {
                Debug.LogError("Failed! assetBundle could not be loaded!", this);
            }
        }
        else
        {
            Debug.Log("assetBundle is already loaded! Skipping", this);
        }
    
        // YOU ALSO MISSED THIS!!
        yield return LoadObjectFromBundle(objectNameToLoad);
    }
    
    private IEnumerator LoadObjectFromBundle(string objectNameToLoad)
    {
        // This time we wait until the assetBundle is actually set to a valid value
        // you could simply use
        //yield return new WaitUntil(() => assetBundle != null);
        // but just to see what happens I would let them report in certain intervals like
        var timer = 0f;
        while (!assetBundle)
        {
            timer += Time.deltaTime;
            if (timer > 3)
            {
                timer = 0;
                Debug.Log("Still waiting for assetBundle ...", this);
            }
    
            yield return null;
        }
    
        var assetRequest = assetBundle.LoadAssetAsync<GameObject>(objectNameToLoad);
        yield return assetRequest;
    
        var loadedAsset = (GameObject)assetRequest.asset;
    
        model = loadedAsset.GetComponent<AnchorBehaviour>();
    
        // add a final check
        if(!model)
        {
            Debug.LogError("Failed to load object from assetBundle!", this);
        }
        else
        {
            Debug.Log("Successfully loaded model!", this);
        }
    }
    
    // and now make sure you can't click before model is actually set
    public void create()
    {
        if(!model)
        {
            Debug.LogWarning("model is not loaded yet ...", this);
            return;
        }
    
        planeFinder.AnchorStage = model;
    }