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
.
Это выглядит значительно лучше, чем мешанина из колбэков. а работает так же хорошо.