Working with Coroutines in Unity


Cority (Coroutines, coroutines) in Unity is a simple and convenient way to run functions that must work in parallel for some time. There is nothing fundamentally complicated in working with coroutines and the Internet is full of articles with a superficial description of their work. However, I still could not find a single article describing the possibility of launching a group of coroutines with continued work after their completion.
I want to offer you a small pattern that implements this opportunity, as well as the selection of information about coroutines.



Coroutines are simple C # iterators that return an IEnumerator and use the yield keyword . In Unity, coroutines are recorded and executed before the first yield using the methodStartCoroutine . Next, Unity polls the registered coroutines after each call to Update and before calling LateUpdate, determining by the value returned in yield when to proceed to the next block of code.

There are several options for returning values to yield :

Continue after the following FixedUpdate:
yield return new WaitForFixedUpdate();


Continue after the following LateUpdate and scene rendering:
yield return new WaitForEndOfFrame();


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


Continue to complete another corutin:
yield return StartCoroutine(AnotherCoroutine());


Continue after loading the remote resource:
yield return new WWW(someLink);


All other return values ​​indicate what needs to be continued after passing the current iteration of the Update loop:
yield return null;


You can exit corutin like this:
yield return break;


Using WaitForSeconds creates a long-lived object in memory (managed by the heap), so using it in fast loops can be a bad idea.

I already wrote that coroutines work in parallel, it should be clarified that they do not work asynchronously, that is, they are executed in the same thread.

A simple example of corutin:

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


This code starts coroutine with a loop that will write to the console the time elapsed since the last frame.
It should be noted that in coroutine, yield return null is first called , and only then is a log entry. In our case, this matters because coroutine execution starts at the moment of calling StartCoroutine (TestCoroutine ()) , and the transition to the next block of code after yield return null will be performed after the Update method , so that before and after the first yield return null Time. deltaTime will point to the same value.

It should also be noted that coroutine with an infinite loop can still be interrupted by calling StopAllCoroutines (), StopCoroutine ("TestCoroutine") , or by destroying the parent GameObject.

Good. So with the help of coroutines we can create triggers that check certain values ​​of each frame, we can create a sequence of coroutines launched one after another, for example, playing a series of animations with different calculations at different stages. Or just run other coroutines inside coroutine without yield return and continue execution. But how to launch a group of coroutines working in parallel and continue only at their completion?

Of course, you can add to the class in which coroutine is defined, a variable indicating the current state:

The class to be moved:

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


A class that works with a group of classes to be moved:

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;
	}
	// делаем еще дела
}


The “do more things” block will begin to be executed after the MoveCoroutine coroutine is completed for each object in the objectsToMove array .

Well, already more interesting.
But what if we want to create a group of coroutines, with the ability to check anywhere, at any time, whether the group has completed its work?
Let's do it!

For convenience, we will do everything in the form of extension methods:

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


Now, enough to cause korutinah method ParallelCoroutinesGroup and wait method CoroutineExtension.GroupProcessing returns 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));
	}
}


Done!


Also popular now: