Салют, хабровчане. Совсем немного времени остается до старта курса «Разработчик игр на Unity», в связи с этим мы подготовили для вас еще один интересный перевод.

(Примечание: пока еще я не обладаю идеальным пониманием всех подкапотных тонкостей

Допустим, мне нужна кнопка, при нажатии на которую воспроизводилась бы классическая анимация коробки. Но есть одна загвоздка: кнопка должна переходить в неактивное состояние (затемняться посредством
Из соображений чистоты кода, я хочу использовать
Следующий простой компактный код реализует требуемую задачу. (

Вот несколько вопросов, которыми вы должны были задаться:
Ознакомьтесь с этой статьей, о том, как оставшаяся часть программы может быть захвачена и продолжена так, как мы захотим (например, в каком потоке? В вызывающем потоке или в потоке, который запускает задачу?)
Тогда вы заметите, что в Unity тоже есть такая концепция, но она не видна нам на первый взгляд.
Все не совсем так, как в обычных программах на C#. Взглянем на официальную документацию
Теперь вернемся к нашей
Теперь
Это псевдокод, который мы хотим получить в кадре, отличном от того, в котором мы нажали кнопку и тем самым отключили ее:
Включите в код побольше логов, и мы попробуем пошаговую отладку.
Порядок обновления выглядит следующим образом:


Осталось демистифицировать «ожидающий объект», который я не разгадал полностью, но, по крайней мере, вижу, как он работает, потому что он был намеренно закодирован.
В первом кадре, где я нажал на кнопку, нет ничего странного.

В следующем кадре, где происходит магия, стек вызова может точно показать нам, кто обрабатывает для нас проверку через каждые несколько кадров. Я не совсем понимаю, что здесь происходит. (хотя это работает)

Когда я проверял

Но там есть что-то под названием
Этот метод имеет очень запутанное описание:
docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.yield?view=netframework-4.8
Английский не мой родной язык, и я думаю, что ни C#

Предполагаемый вариант использования в примечании, кажется, говорит о том, что метод на самом деле не
Но в нашем случае получатель контекста — это не обычный контекст, а
Он возвращает
Существует популярный пакет UniTask, который полностью исключает
Это хороший разговор, который показывает, что

Создаем сетевой шутер в космосе за полтора часа.

async
в Unity уже и так работает без каких-либо плагинов или оборачивающих Task
корутин, имитирующих асинхронное поведение проверяя завершение на каждом кадре. Но это все-равно своего рода магия. Давайте же немного углубимся в эту тему.(Примечание: пока еще я не обладаю идеальным пониманием всех подкапотных тонкостей
async/await
в C#, по этому я буду стараться дополнять изложенное, по мере углубления моего понимания.)Пример

Допустим, мне нужна кнопка, при нажатии на которую воспроизводилась бы классическая анимация коробки. Но есть одна загвоздка: кнопка должна переходить в неактивное состояние (затемняться посредством
.interactable
) до тех пор, пока коробка не закончит вращение.Из соображений чистоты кода, я хочу использовать
await
, ожидающий завершение вращения коробки, и сразу добавить строку, восстанавливающую interactable
, вместо чего-то вроде создания объекта, содержащего корутину, проверяющую состояние на каждом кадре для запуска выполнения этой задачи. Наличие очевидного линейного кода в одном месте является большим плюсом с точки зрения читаемости. Такой код и писать приятно.Следующий простой компактный код реализует требуемую задачу. (
async
методы также отображаются в списке делегатов Unity, не волнуйтесь.)using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
public class SpinBox : MonoBehaviour
{
public Button button;
public Animation ani;
public async void SpinAndDisableButton()
{
ani.Play();
button.interactable = false;
while (ani.isPlaying == true)
{
await Task.Yield();
}
button.interactable = true;
}
}

Вот несколько вопросов, которыми вы должны были задаться:
- «Кто» «исполняет»
while
и оставшуюся часть метода? Каким магическим образом он проверяется в следующем кадре, тогда как вызовSpinAndDisableButton
выполняется только один раз, и нет никакихUpdate
или корутин для повторного запуска. - Каков тайминг каждого запуска?
- Что такое
Task.Yield()
? Кажется, это ключ ко всему, что здесь происходит. Я полагаю, вы привыкли кyield return null
в корутинах. Хорошей догадкой было бы сказать — попробуйте снова в следующем кадре, но вы говорите это через энумератор C#. Формулировка «yield» схожа, и даже поведение аналогично.
Контекст синхронизации
Ознакомьтесь с этой статьей, о том, как оставшаяся часть программы может быть захвачена и продолжена так, как мы захотим (например, в каком потоке? В вызывающем потоке или в потоке, который запускает задачу?)
Тогда вы заметите, что в Unity тоже есть такая концепция, но она не видна нам на первый взгляд.
В сравнении с обычной программой на C#
Все не совсем так, как в обычных программах на C#. Взглянем на официальную документацию
await. await
подразумевает возврат вызывающей стороне объекта Task
. Вызывающий объект может продолжать до тех пор, пока ему не понадобится результат выполнения, и он больше не может позволить себе делать в это время что-то еще (не имеет значения, является ли задача многопоточной или нет), кроме как await
. Эта цепочка может продолжаться до тех пор, пока вы, наконец, не доберетесь до Main
. Где, если мы также встречаем async
, есть еще один await генерируемый компилятором, который немедленно запрашивает результат.Теперь вернемся к нашей
SpinAndDisableButton
. Куда идет возврат await
? В Unity у нас нет Main
, так как он глубоко скрыт в коде движка. Вопрос, по сути, такой же, как и кто исполняет Update
, LateUpdate
и так далее. Это PlayerLoop
API, которое поддерживает код движка в рамках игрового цикла, чтобы обеспечить упорядоченный рендеринг объектов и всего, что исполняется в кадре. Но раньше нас это не волновало, поскольку до сих пор эти точки входа возвращали void
.Теперь
await
возвращает точку выполнения кому-то в надежде, что он сможет продолжить с этой же точки позже, в определенный момент, автоматически. Затем игровой цикл продолжается, пока await
находится в ожидании. В противном случае мы бы вообще не увидели вращающуюся коробку, если бы мы действительно ожидали, пока коробка не закончит анимацию, потому что анимация не может закончиться, если смена кадров не продолжается. Так что же произойдет в следующем кадре?Это псевдокод, который мы хотим получить в кадре, отличном от того, в котором мы нажали кнопку и тем самым отключили ее:
for (игровой цикл)
{
if(анимация коробки закончена)
{
Включаем кнопку.
}
Анимация продолжается.
Отправить матрицу преобразования коробки на рендеринг.
Мы видим обновленный рендеринг коробки.
}
Отладка
Включите в код побольше логов, и мы попробуем пошаговую отладку.
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
public class SpinBox : MonoBehaviour
{
public Button button;
public Animation ani;
public async void SpinAndDisableButton()
{
Debug.Log($"Started async {Time.frameCount}");
ani.Play();
button.interactable = false;
while (ani.isPlaying == true)
{
await Task.Yield();
}
button.interactable = true;
Debug.Log($"Finished async {Time.frameCount}");
}
public void Update()
{
Debug.Log($"Update {Time.frameCount}");
}
public void LateUpdate()
{
Debug.Log($"Late Update {Time.frameCount}");
}
}
Порядок обновления выглядит следующим образом:


- Кадр, в котором мы запускаем анимацию, предшествует
Update
иLateUpdate
, потому что клик мыши происходит благодаряEventSystem
иGraphicRaycaster
из UGUI Unity, который, как оказалось, имеет порядок выполнения вUpdate
выше любого из ваших скриптов. while
ожидание возникает магическим образом в каждом кадре с этого места, по таймингу послеUpdate
, но доLateUpdate
. Как будто мы породили корутину, но мы этого не сделали!- Еще один интересный момент заключается в том, что в первом кадре, где мы только начали воспроизводить анимацию, присутствуют два
Awaiting
, поскольку система событий UGUI появилась еще раньше, намекая на то, чтоawait
подписка, немедленно вступает в силу без необходимости резюмировать что-либо в следующем кадре - В старые времена мы должны были поставить проверку в
Update
или что-то в этом роде.await
подписка устраняет необходимость в этом.
Осталось демистифицировать «ожидающий объект», который я не разгадал полностью, но, по крайней мере, вижу, как он работает, потому что он был намеренно закодирован.
В первом кадре, где я нажал на кнопку, нет ничего странного.
Update
(тот же магический MonoBehaviour Update
) EventSystem
использует Input
API и видит мой клик, после чего сканирует мою кнопку на канвасе и видит, что он может что-то сделать. Затем он вызывается в public async void
и достигает этой строки. (Вы также можете использовать эти ExecuteEvents
вручную! Это вспомогательный static
класс.)
В следующем кадре, где происходит магия, стек вызова может точно показать нам, кто обрабатывает для нас проверку через каждые несколько кадров. Я не совсем понимаю, что здесь происходит. (хотя это работает)

Когда я проверял
UnitySynchronizationContext
, многое, кажется, вызывается из кода движка, поэтому я не могу сделать ничего, кроме как угадать.
Но там есть что-то под названием
WorkRequest
, что представляет каждое из ваших ожидаемых незаконченных дел. Я предполагаю, что возврат await
должным образом зарегистрировано «игровым» способом, который совместим с кадровой парадигмой и гарантирует, что весь безопасный код замкнут в основном потоке, чтобы вы могли полноценно делать что-нибудь после любого await
, потому что это будет совершено в подходящий момент кадра.Task.Yield()
Этот метод имеет очень запутанное описание:
docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.yield?view=netframework-4.8
Возвращает
YieldAwaitable
Контекст который, будучи ожидаемым, будет асинхронно переходить обратно в текущий контекст во время ожидания. Если текущий SynchronizationContext не равен NULL, он рассматривается как текущий контекст. В противном случае планировщик задач, связанный с текущей выполняемой задачей, обрабатывается как текущий контекст.
Замечания
Вы можете использоватьawait Task.Yield()
; в асинхронном методе, чтобы заставить метод завершиться асинхронно. Если существует текущий контекст синхронизации (объект SynchronizationContext), он отравит остаток выполнения метода обратно в этот контекст. Тем не менее, контекст решит, как расставить приоритеты для этой работы относительно другой работы, которая может быть ожидающей. Контекст синхронизации, который присутствует в UI потоке в большинстве сред UI, часто отдает приоритет работе, размещенной в контексте выше, чем работа ввода и рендеринга. По этой причине не полагайтесь, чтоawait Task.Yield()
; будет сохранять UI отзывчивым. Для получения дополнительной информации смотрите «Полезные абстракции, реализованные с помощью ContinueWith» в блоге «Параллельное программирование с .NET».
Английский не мой родной язык, и я думаю, что ни C#
yield return
, ни Task.Yield()
не отражают функцию, которую они выполняют. Но давайте перейдем к определению метода.
Предполагаемый вариант использования в примечании, кажется, говорит о том, что метод на самом деле не
async
, но вы хотели бы превратить его в async
, поэтому вам нужно await
(ожидать) где-то в нем, поэтому Task.Yield()
— идеальный козел отпущения. Вместо того, чтобы сразу завершать метод (помните, что async не будет волшебным образом превращать метод в асинхронный, это зависит от содержимого метода), теперь вы заставляете его быть асинхронным.Но в нашем случае получатель контекста — это не обычный контекст, а
UnitySynchronizationContext
. Теперь Task.Yield()
имеет более полезную функцию, которая эффективно продолжает работу в следующем кадре. Если бы это был обычный SynchronizationContext
, я предполагаю это могло бы произвести к бесконечному циклу в нашем примере, так как он будет выполнять continue прямиком в while
снова и снова.Он возвращает
YieldAwaiter
, который вызывающий объект может использовать для продолжения. Как свидетельствует наш лог, «Awaited» регистрировался в начале каждого кадра, потому что контекст (код рядом с await
) был сохранен в этом авейтере. UnitySynchronizationContext
делает какую-то магию, ждет кадр и использует этот авейтер для продолжения, затем он достигает while
и снова возвращает новый YieldAwaiter
. Это, вероятно, продолжит добавление нового WorkRequest
для кода ранее в каждом кадре в качестве ожидающей задачи, пока while
не станет false
.UniTask
Существует популярный пакет UniTask, который полностью исключает
SynchronizationContext
и вводит новый тип более легкой задачи UniTask
, которая напрямую привязывается к PlayerLoop
API. Затем вы можете выбрать время, когда должна произойти проверка в следующем кадре. (Инициализация? Late update?) Но, по сути, вы знаете, что await
работает без какого-либо плагина. Это будет полезно с Addressables, где AsyncOperationHandle
может быть .Task
, который вы можете использовать с await
.Для просмотра
Это хороший разговор, который показывает, что
Await
уже отлично работает, но есть плюсы и у обоснованно используемых корутин. Вы можете уделить больше внимания этому слайду, чтобы немного разобраться, если вы все еще в замешательстве.
Создаем сетевой шутер в космосе за полтора часа.
lair
Вы думаете неправильно. yield — это уступить, в том числе и "уступить поток выполнения" (по аналогии с "уступить дорогу").