Search Unity

How to wait on EditorCoroutine in IPreprocessBuildWithReport script

Discussion in 'Scripting' started by Rabadash8820, Jan 14, 2021.

  1. Rabadash8820

    Rabadash8820

    Joined:
    Aug 20, 2015
    Posts:
    94
    I'm having some issues using the new `EditorCoroutines` (or any `yield` instructions, really) in Unity build scripts. Specifically, I have a pre-build script that derives from `IPreprocessBuildWithReport`, and I would like this script to wait for some Resources to load. I have tried the two approaches shown in the following code, but neither is working. Does anyone know of a way to wait on `yield` statements in a build script? Any help would be much appreciated!

    Code (CSharp):
    1.  
    2. public class PreprocessBuildEntrypoint : IPreprocessBuildWithReport {
    3.     public void OnPreprocessBuild(BuildReport report) {
    4.         IEnumerator longOp = longRunningOperation();
    5.  
    6.         // Option 1: Start an EditorCoroutine
    7.         // Doesn't work because the operation won't be done before we reach the next statements
    8.         EditorCoroutineUtility.StartCoroutine(longOp, owner: this);
    9.  
    10.         // Option 2: Loop over the operation's IEnumerator
    11.         // Doesn't work because the loop hogs Unity's main thread and prevents the actual operation from running
    12.         while (longOp.MoveNext()) ;
    13.  
    14.         // Do some other stuff that depends on the long-running operation being complete
    15.     }
    16.     private IEnumerator longRunningOperation() {
    17.         ResourceRequest req = Resources.LoadAsync<MyObject>("resource-name");
    18.         while (!req.isDone)
    19.             yield return null;
    20.         // Do stuff with the result of `req`
    21.     }
    22. }
    23.  
     
    will_unity731 likes this.
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Generally you would move the stuff on line 14 into around line 20.

    You could also use events... for example:
    Code (CSharp):
    1. public class PreprocessBuildEntrypoint : IPreprocessBuildWithReport {
    2.     event Action OnLongRunningOperationCompleted;
    3.  
    4.     public void OnPreprocessBuild(BuildReport report) {
    5.         IEnumerator longOp = longRunningOperation();
    6.         OnLongRunningOperationCompleted += DoLater;
    7.  
    8.         // Start an EditorCoroutine
    9.         EditorCoroutineUtility.StartCoroutine(longOp, owner: this);
    10.     }
    11.  
    12.     void DoLater() {
    13.         // Do some other stuff that depends on the long-running operation
    14.     }  
    15.  
    16.     private IEnumerator longRunningOperation() {
    17.         ResourceRequest req = Resources.LoadAsync<MyObject>("resource-name");
    18.         while (!req.isDone)
    19.             yield return null;
    20.         // Do stuff with the result of `req`
    21.         OnLongRunningOperationCompleted?.Invoke();
    22.     }
    23. }
     
  3. Rabadash8820

    Rabadash8820

    Joined:
    Aug 20, 2015
    Posts:
    94
    @PraetorBlue Thanks for the reply! I should have clarified that this long-running operation, and the statements that follow, need to be completed before the build continues. If I only call `EditorCoroutineUtility.StartCoroutine` and let `OnPreprocessBuild` return, then the resources most likely won't be done loading yet.
     
  4. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Why use a coroutine at all then? Just do the whole operation directly inside OnPreprocessBuild.
     
  5. Rabadash8820

    Rabadash8820

    Joined:
    Aug 20, 2015
    Posts:
    94
    Because I need to be able to wait on `yield` statements, like the `Resources.LoadAsync` call in the example above, or web requests. For the particular case of Resources, I could just call the blocking `Resources.Load` method, but there is no such equivalent for some of the other methods that I'm calling.
     
  6. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    As far as I know there's no way to delay the build process without hogging the main thread inside your callback, e.g.
    while (!myTask.isDone)
    or something. So if the async task also needs to use the main thread, I'm not sure how you'd do it. Are you sure the operation needs the main thread? I thought the point of the async APIs was so they could be done on other threads. What operation specifically are you trying to do?
     
  7. Rabadash8820

    Rabadash8820

    Joined:
    Aug 20, 2015
    Posts:
    94
    Yeah that was my thinking as well, but when I made test builds, they just ran in an infinite loop with `ResourceRequest.progress` never advancing above 0 and `isDone` staying false. I'm still trying some different setups to see if I can get `Resources.LoadAsync` to work though.

    Well, it's kinda complicated... I have a custom configuration system in place so that I can load "secrets" at build or run time without having to store them in version control (think API keys, passwords, and the like). I have an `IConfigurationSource` interface with a single `Load` method, and implementations for reading from (gitignored) Resources, from CSV or JSON files on disk, Remote Config, etc. To account for as many config sources as possible, I had to make `IConfigurayionSource.Load` "awaitable" by returning `IEnumerator`, since some APIs (particularly Remote Config) have no blocking alternatives, plus then sources can be loaded in parallel. This system works great at runtime, but I also need it to run in this pre-build script so that some of the loaded secrets can be written to files within the build, like AndroidManifest.xml and Info.plist. And so here we are, I need to call and wait on `IEnumerator`-returning methods within a pre-build script. So to answer ur question, I guess the API that started all this mess was Remote Config's `ConfigManager.FetchConfigs` method, which runs asynchronously.
     
  8. Rabadash8820

    Rabadash8820

    Joined:
    Aug 20, 2015
    Posts:
    94
    Well, in the end I just had to modify my custom configuration system. Those `IConfigurationSources` now have a `Load` and a `LoadAsync` method. In builds and in Play Mode, where Coroutines are supported, I can call the `LoadAsync` method and get some performance benefits. In build scripts, where (Editor)Coroutines are NOT supported, I can call `Load`, and things will work, albeit a teeny bit slower. This also means that certain config sources cannot be loaded in build scripts if they only have a `LoadAsync` implementation (as is the case for Remote Config), but those sources were probably never meant to be called at build time anyway.