Coroutines don't work in the unity build despite working in the editor

I’m making a VR game and I made a script that uses coroutines to determine when a sound has ended and will call an event when a sound ends. The project I made works very well when I play it in the unity editor. However, when I make a build of my project, the coroutine functions don’t seem to work. I noticed the game doesn’t recognize when sound ends.

I added some debug logs and created a development build to see what goes wrong in an actual build. The logs for dev builds are hard to read, but I noticed that I seems like more specifically, the WaitForSeconds and WaitUntil functions are not being called.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;

public class AnnunPanelQuestStep : QuestStep
{
    private bool isAnnunPanelTested = false;
    [SerializeField]
    private XRBaseInteractable annunPanelTestButton;

    [SerializeField]
    private AudioClip testButtonClip;

    private AudioSource annunPanelAudioSource;

    [SerializeField]
    private int annunPanelCheckedScore = 5;
    private void OnEnable()
    {
        annunPanelTestButton = interactables[0];

        GameEventsManager.instance.soundEvents.onSFXEnded += SetAnnunPanelBool;

        annunPanelTestButton.selectEntered.AddListener(AnnunPanelTestStart);
        annunPanelTestButton.selectExited.AddListener(AnnunPanelTestStop);
    }

    private void OnDisable()
    {
        GameEventsManager.instance.soundEvents.onSFXEnded -= SetAnnunPanelBool;

        annunPanelTestButton.selectEntered.AddListener(AnnunPanelTestStart);
        annunPanelTestButton.selectExited.RemoveListener(AnnunPanelTestStop);
    }

    private void AnnunPanelTestStart(SelectEnterEventArgs arg0)
    {
        SoundFXManager.Instance.PlaySFXClip(null, testButtonClip, 
            arg0.interactableObject.transform, ref annunPanelAudioSource);
    }

    private void AnnunPanelTestStop(SelectExitEventArgs arg0)
    {
        annunPanelAudioSource.Pause();
        if (isAnnunPanelTested)
        {
            Debug.Log("[Game] Annunciator panel released at the right time");
            DisableInteractableOutline(arg0);
            QuestStepTotalScore += annunPanelCheckedScore;
            FinishQuestStep();
        }
    }

    private void SetAnnunPanelBool(float timeToDestroyAudio)
    {
        isAnnunPanelTested = true;

        StartCoroutine(WaitAndRestartAnnunPanelTest(timeToDestroyAudio));
    }

    private IEnumerator WaitAndRestartAnnunPanelTest(float timeToDestroyAudio)
    {
        //yield return new WaitForSeconds(timeToDestroyAudio);

        while(timeToDestroyAudio > 0)
        {
            timeToDestroyAudio -= Time.deltaTime;
            yield return null;
        }

        if(annunPanelTestButton.isSelected)
        {
            isAnnunPanelTested = false;
            Debug.Log("[Game] Annunciator panel held past stopping time");
            SoundFXManager.Instance.PlaySFXClip(null, testButtonClip,
            annunPanelTestButton.transform, ref annunPanelAudioSource);
        }
    }
}

This script is used for determining a VR button’s behaviours. When you press the VR button, a sound is played. When you release the button the sound abruptly stops. You are supposed to press the button until the sound ends. Once you release the button after the sound ends, you can advance to the next quest. If you keep holding the button when the sound ends, the sound will restart after “timeToDestroyAudio” seconds, and the audio is played again. Then you have to hold the button again until it ends and release it to complete the quest.

One of the functions called SetAnnunPanelBool() is a subscriber to when the sound ends and will set the VR button to completed when the sound ends and start another coroutine that waits and checks if you are still holding the button to set the VR button to uncompleted.

public AudioSource PlaySFXClip(string questId, AudioClip audioClip, Transform spawnTransform, 
    ref AudioSource referencedAudioSource, float volume = 1f)
{
    if(questId != null)
    {
        GameEventsManager.instance.soundEvents.SFXQuestBegin(questId);
    }
    else
    {
        GameEventsManager.instance.soundEvents.SFXBegin();
    }
    // spawn the sound gameobject
    AudioSource audioSource = Instantiate(soundFXObject, spawnTransform.position, Quaternion.identity);

    referencedAudioSource = audioSource;
    // assign the audioClip
    audioSource.clip = audioClip;

    // assign volume
    audioSource.volume = volume;

    float clipLength = 0f;
    if (audioClip != null)
    {
        // play sound
        audioSource.Play();

        // get length of the sound FX clip
        clipLength = audioSource.clip.length;
    }

    StartCoroutine(WaitForSoundToEnd(questId, clipLength, audioSource));

    return audioSource;
}

This is how the soundFX clip works and

private IEnumerator WaitForSoundToEnd(string questId, float clipLength, AudioSource audioSource)
{
    yield return new WaitUntil(() => !audioSource.isPlaying);

    if(audioSource.clip != null)
    {
        //Debug.Log(audioSource.time + " " + audioSource.clip.name + " " + audioSource.clip.length);
    }

    float timeToDestroy = 1f;

    if (questId != null)
    {
        GameEventsManager.instance.soundEvents.SFXQuestEnded(questId);
        Destroy(audioSource.gameObject, timeToDestroy);
    }
    else if(questId == null && audioSource.time >= clipLength) // In this case, AudioSource is not paused (it is completed)
    {
        Debug.Log("[Game] non-quest step related SFX played successfully");
        GameEventsManager.instance.soundEvents.SFXEnded(timeToDestroy);
        Destroy(audioSource.gameObject, timeToDestroy);
    }
    else
    {
        Debug.Log("[Game] Annunciator panel stopped prematurely");
        Destroy(audioSource.gameObject, timeToDestroy);
    }
}

This is the coroutine that is called as soon as you play a sound that waits for the sound to end in order to invoke an event.
Does anyone know why coroutines are not being recognized in my unity build?

Most likely you have an error. Find out by checking the logs! You already have some debug output in there, what parts of it are running?

Time to start debugging!

By debugging you can find out exactly what your program is doing so you can fix it.

Use the above techniques to get the information you need in order to reason about what the problem is.

You can also use Debug.Log(...); statements to find out if any of your code is even running. Don’t assume it is.

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

1 Like

I created a new project and isolated the feature that isn’t working. This new project is a non-VR project. Here are the scripts I isolated

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SoundEvents
{
    public event Action<float> onSFXEnded;
    public void SFXEnded(float timeToDestroyAudio)
    {
        if (onSFXEnded != null)
        {
            onSFXEnded(timeToDestroyAudio);
        }
    }

    public event Action<string> onSFXQuestEnded;
    public void SFXQuestEnded(string id)
    {
        if (onSFXQuestEnded != null)
        {
            onSFXQuestEnded(id);
        }
    }
}
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;

public enum SoundType
{
    IGNITION,
    STALL_TEST
}

[RequireComponent(typeof(AudioSource))]
public class SoundFXManager : MonoBehaviour
{
    public static SoundFXManager Instance;

    [SerializeField] private AudioClip[] soundList;
    private AudioSource audioSource;

    [SerializeField] private AudioSource soundFXObject;

    //[SerializeField] private InputActionReference playSFXTest;
    [SerializeField] private Transform playSoundTransform;

    private bool isPlay = false;

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
        }
    }

    private void OnEnable()
    {
        //playSFXTest.action.performed += PlaySFXTestButton;
        GameEventsManager.instance.soundEvents.onSFXQuestEnded += CallAfterSound;
    }

    private void OnDisable()
    {
        //playSFXTest.action.performed -= PlaySFXTestButton;
        GameEventsManager.instance.soundEvents.onSFXQuestEnded -= CallAfterSound;
    }

    private void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    public void PlaySFXClip(string questId, AudioClip audioClip, Transform spawnTransform, 
        ref AudioSource referencedAudioSource, float volume = 1f)
    {
        // spawn the sound gameobject
        AudioSource audioSource = Instantiate(soundFXObject, spawnTransform.position, Quaternion.identity);

        referencedAudioSource = audioSource;
        // assign the audioClip
        audioSource.clip = audioClip;

        // assign volume
        audioSource.volume = volume;

        float clipLength = 0f;
        if (audioClip != null)
        {
            // play sound
            audioSource.Play();

            // get length of the sound FX clip
            clipLength = audioSource.clip.length;
        }

        StartCoroutine(WaitForSoundToEnd(questId, clipLength, audioSource));
    }

    private IEnumerator WaitForSoundToEnd(string questId, float clipLength, AudioSource audioSource)
    {
        yield return new WaitUntil(() => !audioSource.isPlaying);

        //Debug.Log(audioSource.time + " " + audioSource.clip.name + " " + audioSource.clip.length);

        float timeToDestroy = 1f;

        if (questId != null)
        {
            GameEventsManager.instance.soundEvents.SFXQuestEnded(questId);
            //Destroy(audioSource.gameObject, timeToDestroy);
        }
        else if(questId == null && audioSource.time >= clipLength) // In this case, AudioSource is not paused (it is completed)
        {
            Debug.Log("[Game] non-quest step related SFX played successfully");
            GameEventsManager.instance.soundEvents.SFXEnded(timeToDestroy);
            //Destroy(audioSource.gameObject, timeToDestroy);
        }
        else
        {
            Debug.Log("[Game] Annunciator panel stopped prematurely");
        }

        Destroy(audioSource.gameObject, timeToDestroy);
    }

    private void CallAfterSound(string id)
    {
    }
}

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerInput : MonoBehaviour
{
    [SerializeField]
    private bool isButtonCubeComplete = false;
    
    [SerializeField]
    private GameObject buttonCube;

    [SerializeField]
    private AudioClip testSound;

    private AudioSource testAudioSource;

    InputAction.CallbackContext callbackContext;

    private void OnEnable()
    {
        GameEventsManager.instance.soundEvents.onSFXEnded += SetButtonCubeCompletedBool;
    }

    private void OnDisable()
    {
        GameEventsManager.instance.soundEvents.onSFXEnded -= SetButtonCubeCompletedBool;
    }

    public void OnActivate(InputAction.CallbackContext context)
    {
        callbackContext = context;
        if(context.performed)
        {
            SoundFXManager.Instance.PlaySFXClip(null, testSound, transform, ref testAudioSource);

            buttonCube.GetComponent<Renderer>().material.color = Color.red;
            Debug.Log("Activated");
        }
        
        if(context.canceled)
        {
            if(testAudioSource != null) { testAudioSource.Pause(); }
            Debug.Log("[Game] Annunciator panel released at the right time");

            buttonCube.GetComponent<Renderer>().material.color = Color.green;

            FinishStep();
            Debug.Log("Deactivated");
        }
    }

    private void FinishStep()
    {
        if (isButtonCubeComplete)
        {
            if (buttonCube.activeSelf)
            {
                buttonCube.SetActive(false);
            }
            else
            {
                buttonCube.SetActive(true);
            }
        }
    }

    private void SetButtonCubeCompletedBool(float timeToDestroyAudio)
    {
        isButtonCubeComplete = true;

        StartCoroutine(WaitAndRestartAudio(timeToDestroyAudio));
    }

    private IEnumerator WaitAndRestartAudio(float timeToDestroyAudio)
    {
        yield return new WaitForSeconds(timeToDestroyAudio);

        if(callbackContext.performed)
        {
            isButtonCubeComplete = false;
            Debug.Log("[Game] Annunciator panel held past stopping time");
            SoundFXManager.Instance.PlaySFXClip(null, testSound, transform, ref testAudioSource);
        }
    }
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameEventsManager : MonoBehaviour
{
    public static GameEventsManager instance { get; private set; }

    public QuestEvents questEvents;
    public SoundEvents soundEvents;

    private void Awake()
    {
        if (instance != null)
        {
            Debug.LogError("Found more than one Game Events Manager in the scene.");
        }
        instance = this;

        // initialize all events
        questEvents = new QuestEvents();
        soundEvents = new SoundEvents();
    }
}

It turns out it is the coroutines that aren’t being called in the build. The new project does the same thing as the VR project but instead you press spacebar to call OnActivate() function to change the color of a box and play a sound fx for as long as you hold spacebar.

Cool, keep debugging, find out why they aren’t. Often there are subtle timing differences between editor and build, and even from run to run, and perhaps your code has introduced a timing dependency you’re not aware of.

See notes in my first post for how to be successful in your debugging adventures today!!

Okay I figured it out. It turns out the coroutines are working but the necessary conditions are not being met by the coroutines because they are not triggering the Wait() functions at the exact frame I assumed would be triggered.

    private IEnumerator WaitForSoundToEnd(string questId, float clipLength, AudioSource audioSource)
    {
        float clipFinishTolerance = 1f;
        yield return new WaitUntil(() => !audioSource.isPlaying || Approximately(audioSource.time, clipLength, clipFinishTolerance));

        if(audioSource.clip != null)
        {
            //Debug.Log(audioSource.time + " " + audioSource.clip.name + " " + audioSource.clip.length);
        }

        float timeToDestroy = clipFinishTolerance + 1f;

        if (questId != null)
        {
            GameEventsManager.instance.soundEvents.SFXQuestEnded(questId);
            Destroy(audioSource.gameObject, timeToDestroy);
        }
        else if(questId == null && Approximately(audioSource.time, clipLength, clipFinishTolerance)) // In this case, AudioSource is not paused (it is completed)
        {
            Debug.Log("[Game] non-quest step related SFX played successfully");
            GameEventsManager.instance.soundEvents.SFXEnded(timeToDestroy);
            Destroy(audioSource.gameObject, timeToDestroy);
        }
        else
        {
            Debug.Log("[Game] Annunciator panel stopped prematurely");
            Destroy(audioSource.gameObject, timeToDestroy);
        }
    }

    private bool Approximately(float a, float b, float tolerance)
    {
        return Mathf.Abs(a - b) <= tolerance;
    }

In the WaitUntil, I initially set the condition to wait until audiosource is not playing, then in the series of if statements after I compared to see if audiosource.time (elapsed time) is greater than or equal to the audiosource clip length (clipLength). This is essentially just like comparing audiosource.time == audiosource.clip.length with both values being floats. WaitUntil is not called until after the audio completes in the build, which resets the elapsed time to 0, so it never breaks the WaitUntil. WaitUntil is also for some reason frame accurate in the editor, but not frame accurate in the build, and that was what caused the inconsistency between running in the editor and running in the build.

In the code snippet, I changed waituntil to check if the audiosource is not playing or the time elapsed is approximately close to the cliplength by a large margin of variance and not it works.

Thanks Kurt! To be honest, I never knew you could put breakpoints on your code and debug your code through the builds of your project. This is going to be a gamechanger!

And for a moment, I was so worried I might have to stop using coroutines in my projects lol.

1 Like

Welcome to it! So glad you recognize the incredible power of debugging… no matter how weird or inexplicable a bug you ever encounter, debugging is your one go-to tool to track it down. The more you debug, the better you get, the more ways you intuitively think “Hm, how can this fail?”