...

вторник, 18 марта 2014 г.

[Из песочницы] Работа с Корутинами в Unity



Корутины (Coroutines, сопрограммы) в Unity — простой и удобный способ запускать функции, которые должны работать параллельно в течение некоторого времени. В работе с корутинами ничего принципиально сложного нет и интернет полон статей с поверхностным описанием их работы. Тем не менее, мне так и не удалось найти ни одной статьи, где описывалась бы возможность запуска группы корутинов с продолжением работы после их завершения.

Хочу предложить вам небольшой паттерн, реализующий такую возможность, а также подбор информации о корутинах.

Корутины представляют собой простые C# итераторы, возвращающие IEnumerator и использующие ключевое слово yield. В Unity корутины регистрируются и выполняются до первого yield с помощью метода StartCoroutine. Дальше Unity опрашивает зарегистрированные корутины после каждого вызова Update и перед вызовом LateUpdate, определяя по возвращаемому в yield значению, когда нужно переходить к следующему блоку кода.


Существует несколько вариантов для возвращаемых в yield значений:


Продолжить после следующего FixedUpdate:



yield return new WaitForFixedUpdate();


Продолжить после следующего LateUpdate и рендеринга сцены:



yield return new WaitForEndOfFrame();


Продолжить через некоторое время:



yield return new WaitForSeconds(0.1f); // продолжить примерно через 100ms


Продолжить по завершению другого корутина:



yield return StartCoroutine(AnotherCoroutine());


Продолжить после загрузки удаленного ресурса:



yield return new WWW(someLink);


Все прочие возвращаемые значения указывают, что нужно продолжить после прохода текущей итерации цикла Update:



yield return null;


Выйти из корутина можно так:



yield return break;


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


Я уже написал, что корутины работают параллельно, следует уточнить, что они работают не асинхронно, то есть выполняются в том же потоке.


Простой пример корутина:



void Start()
{
StartCoroutine(TestCoroutine());
}

IEnumerator TestCoroutine()
{
while(true)
{
yield return null;
Debug.Log(Time.deltaTime);
}
}


Этот код запускает корутин с циклом, который будет писать в консоль время, прошедшее с последнего фрейма.

Следует обратить внимание на то, что в корутине сначала вызывается yield return null, и только потом идет запись в лог. В нашем случае это имеет значение, потому что выполнение корутина начинается в момент вызова StartCoroutine(TestCoroutine()), а переход к следующему блоку кода после yield return null будет осуществлён после метода Update, так что и до и после первого yield return null Time.deltaTime будет указывать на одно и то же значение.


Также нужно заметить, что корутин с бесконечным циклом всё еще можно прервать, вызвав StopAllCoroutines(), StopCoroutine(«TestCoroutine»), или уничтожив родительский GameObject.


Хорошо. Значит с помощью корутинов мы можем создавать триггеры, проверяющие определенные значения каждый фрейм, можем создать последовательность запускаемых друг за другом корутинов, к примеру, проигрывание серии анимаций, с различными вычислениями на разных этапах. Или просто запускать внутри корутина другие корутины без yield return и продолжать выполнение. Но как запустить группу корутинов, работающих параллельно, и продолжить только по их завершению?


Конечно, вы можете добавить классу, в котором определен корутин, переменную, указывающую на текущее состояние:


Класс, который нужно двигать:



public bool IsMoving = false;

IEnumerator MoveCoroutine(Vector3 moveTo)
{
IsMoving = true;

// делаем переход от текущей позиции к новой
var iniPosition = transform.position;
while (transform.position != moveTo)
{
// тут меняем текущую позицию с учетом скорости и прошедшего с последнего фрейма времени
// и ждем следующего фрейма
yield return null;
}

IsMoving = false;
}


Класс, работающий с группой классов, которые нужно двигать:



IEnumetaror PerformMovingCoroutine()
{
// делаем дела

foreach(MovableObjectScript s in objectsToMove)
{
// определяем позицию
StartCoroutine(s.MoveCoroutine(moveTo));
}

bool isMoving = true;
while (isMoving)
{
isMoving = false;
Array.ForEach(objectsToMove, s => { if (s.IsMoving) isMoving = true; }
if (isMoving) yield return null;
}

// делаем еще дела
}


Блок «делаем еще дела» начнет выполнятся после завершения корутина MoveCoroutine у каждого объекта в массиве objectsToMove.


Что ж, уже интересней.

А что, если мы хотим создать группу корутинов, с возможностью в любом месте и в любое время проверить, завершила ли группа работу?

Сделаем!


Для удобства сделаем всё в виде методов расширения:



public static class CoroutineExtension
{
// для отслеживания используем словарь <название группы, количество работающих корутинов>
static private readonly Dictionary<string, int> Runners = new Dictionary<string, int>();

// MonoBehaviour нам нужен для запуска корутина в контексте вызывающего класса
public static void ParallelCoroutinesGroup(this IEnumerator coroutine, MonoBehaviour parent, string groupName)
{
if (!Runners.ContainsKey(groupName))
Runners.Add(groupName, 0);

Runners[groupName]++;
parent.StartCoroutine(DoParallel(coroutine, parent, groupName));
}


static IEnumerator DoParallel(IEnumerator coroutine, MonoBehaviour parent, string groupName)
{
yield return parent.StartCoroutine(coroutine);
Runners[groupName]--;
}

// эту функцию используем, что бы узнать, есть ли в группе незавершенные корутины
public static bool GroupProcessing(string groupName)
{
return (Runners.ContainsKey(groupName) && Runners[groupName] > 0);
}
}


Теперь достаточно вызывать на корутинах метод ParallelCoroutinesGroup и ждать, пока метод CoroutineExtension.GroupProcessing возвращает true:



public class CoroutinesTest : MonoBehaviour
{

// Use this for initialization
void Start()
{
StartCoroutine(GlobalCoroutine());
}

IEnumerator GlobalCoroutine()
{
for (int i = 0; i < 5; i++)
RegularCoroutine(i).ParallelCoroutinesGroup(this, "test");

while (CoroutineExtension.GroupProcessing("test"))
yield return null;

Debug.Log("Group 1 finished");

for (int i = 10; i < 15; i++)
RegularCoroutine(i).ParallelCoroutinesGroup(this, "anotherTest");

while (CoroutineExtension.GroupProcessing("anotherTest"))
yield return null;

Debug.Log("Group 2 finished");
}

IEnumerator RegularCoroutine(int id)
{
int iterationsCount = Random.Range(1, 5);

for (int i = 1; i <= iterationsCount; i++)
{
yield return new WaitForSeconds(1);
}

Debug.Log(string.Format("{0}: Coroutine {1} finished", Time.realtimeSinceStartup, id));
}
}



Готово!



This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий