有时在 Unity 中等待 Coroutine 完成

Sometimes wait for Coroutine to complete in Unity

我正在开发一个在 Unity 中保存游戏状态的系统。当前结构如下所示:

public void SaveGame() {
    StopAllCoroutines();
    if (source != null)
    {
        source.Cancel();
    }
    StartCoroutine(DoSave());
}

protected IEnumerator DoSave(){
    /// Code that prepares a bunch of stuff necessary to save the game goes here
    foreach (item in itemsToSave)
    {
        // Code that adds 'item' to a data structure in memory that is accessible by later functions goes here //
        yield return new WaitForSeconds(0.05f);
    }
    
    source = new CancellationTokenSource();   
    WriteFiles(source.Token);
}

private async Task WriteFiles(CancellationToken token)
{
    Task task = Task.Run(() => GenerateSaveFiles(), token);
    // (A canceled task will raise an exception when awaited).
    await task;
}

private void GenerateSaveFiles()
{
    /// Functions that actually write to disk go here 
    /// E.g. File.WriteAllText();
    /// This function has access to the place in memory where 'items' were being added to in the DoSave() foreach loop, so it just dumps that memory storage to disk
}

我这样设置是为了可以在单独的线程中异步保存并跟踪它的状态(例如,是否失败,以及错误消息)。

SaveGame() 然后在不同的事件(如拾取物品、杀死敌人等)时围绕我的代码库调用以实时保存游戏。这对我 99% 的用例都适用,但有些情况下我想调用 SavGame() 并等待它完成,然后再做任何其他事情。例如,如果玩家死亡,我想将状态写入磁盘而不给其他任何事情(例如在不同场景中重生玩家)发生的机会。

我研究了回调之类的一些东西(例如 https://codesaying.com/action-callback-in-unity/),但我不确定如何针对我的主要 SaveGame() 任务的用例正确设置它 运行 异步,但其他时候可以等待它完成,或者至少让调用 SaveGame() 的方法知道它何时完成。

只需做一个任何人都可以访问的简单布尔值 属性:

public bool IsSavingGame { 
    get; private set; 
}

// ...

private IEnumerator SaveGameCoroutine(){
    IsSavingGame = true;

    // Your save functionality.

    IsSavingGame = false;
}

例如,如果你想在场景保存时拒绝加载任何场景,你可以编写这个脚本:

// or you can singleton it
[SerializeField]
private SaveManager yourSaveManger

public void LoadScene(){
    StartCoroutine(LoadSceneCoroutine());
}

private IEnumerator LoadSceneCoroutine(){
    while (yourSaveManger.IsSavingGame){
        // Show a loading or saving screen or something
        yield return null;
    }

    SceneManager.LoadScene("Your Scene Name");
}

好的,经过大量的阅读和努力,我能够让它发挥作用。由于我使用 Coroutine 的设置以及嵌套在其中的 Task 和其他 Coroutines,因此有很多事情会妨碍。

我的解决方案是 two-fold

  1. 对于协程 (DoSave()) 和嵌套在其中的任何其他协程,我传递了一个参数 waitForSaveComplete 如果为真,我不会 运行 [=] 中的任何 yield 语句14=] 或其嵌套协程。因此,在我的问题示例中,我将所有 yield 语句包装成这样:
if (!waitForSaveComplete) 
{    
    yield return new WaitForSeconds(0.05f);
}

我还有一些同步嵌套协程,我用同样的方式包装了它们的 yield 语句。

接下来,我把嵌套协程的调用改成了:

yield return StartCoroutine(MySynchronizedCoroutine());

至:

if(!waitForSaveComplete) {
    yield return StartCoroutine(MySynchronizedCoroutine(waitForSavComplete));
} else 
{ 
    // IMPORTANT:  Make sure MySynchronizedCoroutine also uses 
    // waitForSaveComplete to wrap its own yield statements 
    // or else this won't work!!!
    StartCoroutine(MySynchronizedCoroutine(waitForSavComplete));
}    

像这样包装 yield 语句是可行的,因为这是协程暂停并放弃控制的地方,直到满足相应的 Wait 条件。到那时它是 'blocking' 鉴于我不确定为什么我花了这么长时间才想到这个。

  1. 对于任务,我在异步情况下使用了 WaitUntil 以确保它在协程本身发生任何其他事情之前完成。在 waitForSaveComplete 的情况下,我什至没有启动任务,而是 运行ning GenerateSaveFiles() 直接像这样
if (waitForSaveComplete) 
{
    GenerateSaveFiles();
} else
{
    Task writeTask = WriteFiles(source.Token);
    yield return new WaitUntil(() => writeTask.IsCompleted);
}

再一次,另一个 'duh' 时刻,只是不喜欢.. 在不需要的情况下开始任务。

这似乎可行,但也有点复杂,因为嵌套协同程序似乎是滋生可怕、难以跟踪错误的沃土。唉,我的异步编程经验非常有限,所以我没有更好的设计或实现想法。