Когда вы вызываете функцию, она должна полностью выполниться, прежде чем вернуть какое-то значение. Фактически, это означает, что любые действия, происходящие в функции, должны выполниться в течение одного кадра; вызовы функций не пригодны для, скажем, процедурной анимации или любой другой временной последовательности. В качестве примера мы рассмотрим задачу по уменьшению прозрачности объекта до его полного исчезновения.
void Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
}
}
Как можно заметить, функция Fade не имеет визуального эффекта, который мы хотели получить. Для скрытия объекта нам нужно было постепенно уменьшить прозрачность, а значит иметь некоторую задержку для того, чтобы были отображены промежуточные значения параметра. Однако функция выполняется в полном объеме, за одно обновление кадра. Промежуточные значения, увы, не будут видны и объект исчезнет мгновенно.
С подобными ситуациями можно справиться, добавив в функцию Update код, изменяющий прозрачность кадр за кадром. Однако для подобных задач зачастую удобнее использовать корутины.
Корутин похож на функцию, которая может приостановить выполнение и вернуть управление Unity, а затем продолжить работу с того места, на котором остановилась, в следующем кадре. В C# сопрограмма объявляется так:
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return null;
}
}
По сути, это функция, объявленная с возвращаемым типом IEnumerator и с оператором yield return, включенным где-то в теле. Нулевая строка yield return — это точка, в которой выполнение будет приостановлено и возобновлено в следующем кадре. Чтобы запустить сопрограмму, вам нужно использовать функцию StartCoroutine:
void Update()
{
if (Input.GetKeyDown("f"))
{
StartCoroutine("Fade");
}
}
Можно заметить, что счетчик цикла в функции Fade сохраняет правильное значение во время работы корутины. Фактически, любая переменная или параметр будут корректно сохранены между вызовами оператора yield.
По умолчанию сопрограмма возобновляется в кадре после того, как она уступает, но также можно ввести временную задержку, используя WaitForSeconds:
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return new WaitForSeconds(.1f);
}
}
Это можно использовать как способ распространения эффекта на определенный период времени, но это также полезная оптимизация. Многие задания в игре нужно выполнять периодически, и самый очевидный способ сделать это — включить их в функцию «Обновление». Однако эта функция обычно вызывается много раз в секунду. Когда задачу не нужно повторять так часто, вы можете поместить ее в сопрограмму, чтобы получать обновления регулярно, но не каждый кадр. Примером этого может быть сигнал тревоги, который предупреждает игрока о приближении врага. Код может выглядеть примерно так:
function ProximityCheck()
{
for (int i = 0; i < enemies.Length; i++)
{
if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
return true;
}
}
return false;
}
Если врагов много, то вызов этой функции в каждом кадре может привести к значительным накладным расходам. Однако вы можете использовать сопрограмму, чтобы вызывать ее каждую десятую долю секунды:
IEnumerator DoCheck()
{
for(;;)
{
ProximityCheck();
yield return new WaitForSeconds(.1f);
}
}
Это значительно уменьшит число проверок, но не окажет заметного влияния на игровой процесс.
Примечание. Вы можете остановить сопрограмму с помощью StopCoroutine и StopAllCoroutines. Корутины также останавливаются, когда GameObject, к которому они присоединены, отключается с помощью SetActive(false). Вызов Destroy(example) (где example является экземпляром MonoBehaviour) немедленно запускает OnDisable, и сопрограмма обрабатывается, фактически останавливая ее. Наконец, OnDestroy вызывается в конце кадра.
Корутины не останавливаются при отключении MonoBehaviour путем установки значения false для экземпляра MonoBehaviour.
Coroutine — Это такие же функции, которые вы уже наверняка используете, но они выполняются параллельно основному потоку.
Тут стоит уточнить, параллельно не значит асинхронно. То есть, если вы запихнете свой «тяжелый» код в кучу корутин которые будут работать параллельно, программа работать быстрее не будет.
Зачем же тогда нужны корутины?
Они помогут нам упростить свой код. К примеру, необходимо плавно менять цвет спрайта.
Можно, конечно, делать это в Update, но корутину можно написать так что-бы она, что-то делала раз в пол секунды или в секунду. И для этого не нужно будет проверять каждый раз сколько времени прошло с последнего вызова Update.
Вот пример, который выведет сообщение спустя пол секунды после его запуска.
IEnumerator YourCoroutine() {
yield return new WaitForSeconds(0.5f);
Debug.Log("Прошло пол секунды");
}
А если обернуть в цикл? Он будет выполняться каждые пол секунды.
IEnumerator YourCoroutine() {
while(true){
yield return new WaitForSeconds(0.5f);
Debug.Log("Прошло пол секунды");
}
}
Замените 0.5f на нужное значение для получения нужной задержки.
Кстати для запуска такой функции-корутины в нужном месте необходимо написать:
StartCoroutine("YourCoroutine");
или
StartCoroutine(YourCoroutine());
Yield, что это?
Эта команда, отдает процессорное время основному потоку и продолжает выполнение корутины с этого места когда произойдет необходимое событие, а событий есть достаточное количество.
1.Продолжить после следующего FixedUpdate
yield return new WaitForFixedUpdate();
2. Продолжить через некоторое время
yield return new WaitForSeconds(0.1f);
3. Продолжить после следующего LateUpdate и рендеринга сцены
yield return new WaitForEndOfFrame();
4. Продолжить после другой корутины:
yield return StartCoroutine(AnotherCoroutine());
5. После текущего Update
yield return null;
6. По завершении скачивания
yield return new WWW(someLink);
7. Завершить корутину
yield return break;
Как завершить корутину?
Иногда корутину нужно завершить преждевременно. Для это есть несколько путей.
- StopAllCoroutines()
- StopCoroutine(«YourCoroutine»)
- Уничтожить родительский GameObject
Делаем выводы
Корутины удобны в некоторых случаях и иногда без них совсем туго, поэтому советую вникнуть в их суть. Часто они помогают облегчить код и сэкономить время. И помните, корутины это не потоки!)