Search Unity

Last used async task is called again when calling BuildPlayer

Discussion in 'Scripting' started by Decoy1207, Jan 14, 2020.

  1. Decoy1207

    Decoy1207

    Joined:
    Dec 20, 2017
    Posts:
    1
    Hi,

    I'm currently working on a custom build pipeline. For this i need to wait for some processes (light bakes/occlusion generation/anything else i might need further on) to complete before moving onto the actual building of the game.

    The code below is a greatly simplified version of the problem code but still creates the error.

    Code (CSharp):
    1.  public static async void TestAsyncCode() {
    2.     await TestAsyncTask();
    3.     TestVoidCode();
    4. }
    5.  
    6. private static async Task TestAsyncTask() {
    7.     await Task.Delay(1000);
    8.     UnityEngine.Debug.Log("Async Task Executed");
    9. }
    10.  
    11. private static void TestBuildCode() {
    12.     //When this is called the TestAsyncTask is started again.
    13.     BuildReport buildResult = BuildPipeline.BuildPlayer(new BuildPlayerOptions {
    14.         target = BuildTarget.StandaloneWindows64,
    15.         targetGroup = BuildTargetGroup.Standalone,
    16.         locationPathName = "I:/Builds/Game.exe".Replace(@"/", @"\"),
    17.         scenes = new string[] { "Assets/Scenes/NetworkModuleTest.unity".Replace(@"/", @"\")},
    18.     });
    19. }
    The code runs as expected but when i call the BuildPipeline.BuildPlayer() function the TestAsyncTask() is executed again. The Debug.Log is not called again, it instead throws an exception: "InvalidOperationException: An attempt was made to transition a task to a final state when it had already completed."

    As a workaround for the problem i have added a simple wait task so occlusion or light bakes are not started again, but i would like to know what i'm actually doing wrong here for the future.

    Unity version: 2019

    I'm aware of the preview package that allows for Coroutines in editor classes, but i prefer not to be depended on this package for the sake of compatibility with older versions of unity.

    Thanks,
    Thomas

    This is the call stack.
    bug.PNG
     
  2. UnLogick

    UnLogick

    Joined:
    Jun 11, 2011
    Posts:
    1,745
    A similar thing came up at work and I was asked to look into it. Knowing Unity I had a hunch already when I saw the code. So inside the BuildPlayer we have a Script Recompile and a script recompile builds new managed assemblies for everything including the assembly where your async method is running. What happens next is that Unity serializes everything, kills the entire managed app, starts a clean app, load the new assembly and finally load all the data back in. Certain things like the relative execution pointer inside the async method could obviously be affected by the recompile (even if the code doesn't change an #if ANDROID could have massive effects on the execution pointer).

    So I believe what you see is a symptom of this where essentially the outcome is best described as "undefined". What we're gonna do to circumvent this is to subscribe to EditorApplication.Update and then split our code into multiple stages, some worker stages and some stages waiting for an async task to complete (Task.GetAwaiter + TaskAwaiter.IsCompleted). This is tedius, the code isn't particularly pretty and overall very far from optimal, but it seems the only safe way to achieve the goal from C#.

    Be aware that EditorApplication.Update is also cleared on ScriptRecompile, so you need some way to re-register (InitializeOnLoad, OnEnable, etc).