Работа с Корутинами в Unity

от автора


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

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

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

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

yield return new WaitForFixedUpdate(); 

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

yield return 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 ParralelCoroutinesGroup(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); 	} } 

Теперь достаточно вызывать на корутинах метод ParralelCoroutinesGroup и ждать, пока метод 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).ParralelCoroutinesGroup(this, "test");  		while (CoroutineExtension.GroupProcessing("test")) 			yield return null;  		Debug.Log("Group 1 finished");  		for (int i = 10; i < 15; i++) 			RegularCoroutine(i).ParralelCoroutinesGroup(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)); 	} }  

Готово!

ссылка на оригинал статьи http://habrahabr.ru/post/216185/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *