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