May 18, 2018

Unity 2018: async/await

Некоторое время назад возникла необходимость подружить Юнити и aync/await, для того, чтобы избавиться от мешанины колбэков в загрузчике сцен (исходники которого есть на Гитхабе, а ролик о нем можно найти на Ютубе). К счастью, Net 4.6 в Юнити в версии 2018.1 вышел из экспериментально-нестабильного состояния, а все наши проекты крутятся на нем с самого первого дня, когда появилась такая возможность.

Естественно, API Юнити ничего не слышал о "новых" возможностях C# и предлагает немного навелосипедить и чуть-чуть костыльнуть. Что ж, не впервой.

Быстрое общение с Гуглом выдало статью от сентября 2017 где автор очень складно все рассказывает, но предлагает закинуть в свой проект собственноручно написанную библиотеку классов эдак на 30. Я много раз становился на такие грабли, когда баги движка начинают интерферировать с багами сторонней библиотеки и удваивают количество времени, необходимое на их решение или избавление от оной. Поэтому немного порыв стековерфлоу и покопавшись в исходниках этой либы пришел к более простому и специфичному для моих нужд решению.

Все, что нам нужно сделать, это обернуть SceneManager.LoadSceneAsync в метод, который возвращал бы Task. При этом Task.Run для этого не годится — мало того, что это будет спавнить дополнительный поток, который не нужен, так как Юнити и так проводит загрузку сцены в отдельном потоке, так еще в этом случае нам не будет доступен сам API.

Однако вызов LoadSceneAsync возвращает AsyncOperation, статус которого можно проверять в коротине, а это то, что нам нужно. Сложив эти два элемента вместе, получаем:

Task<string> LoadAsync(string scene) {
  var t = new TaskCompletionSource<string>();
  // Запускаем коротину загрузки и передаем в нее колбэк 
  // при вызове которого сетим в t имя загруженной сцены, 
  // что вызовет завершение таски и вызов продолжения
  StartCoroutine(LoadingScene(scene, () => t.TrySetResult(scene)));
  
  return t.Task;
}

IEnumerator LoadingScene(string scene, System.Action callback){
  // Запускаем операцию
  var operation = SceneManager.LoadSceneAsync(scene,
                                              LoadSceneMode.Additive);
  // Ждем завершения загрузки
  while (!operation.isDone) {
    yield return null;
    // Кидаем опциональный event о состоянии прогресса
    // который можно использовать для отображения прогресс бара
    OnProgressTick(operation.progress);
  }
  // по завершению вызываем колбэк
  callback.Invoke();
}

Теперь мы можем использовать метод LoadAsync следующим образом:

public async void Load(){
  await LoadAsync("someUIScene");
  // do some other stuff
  await LoadAsync("someLevelScene");
  // do some stuff
  // do more stuff
  await Task.WhenAll(arrayOfSceneNames.Select(s => LoadAsync(s)));
  // do more stuff when all scenes from arrayOfSceneNames were loaded
}

Аналогичным образом можно организовать и обертку для UnloadSceneAsync.

Это выглядит значительно лучше, чем мешанина из колбэков. а работает так же хорошо.