Notifications
Article
Let's Talk About Coroutines
Updated a month ago
497
0
Understanding Unity's Misunderstood Timing Systems
Unity's coroutines and Time systems while crucial to understanding how a game executes, in what order and how quickly it executes, are often misunderstood or completely avoided by new and even many experienced users. Unity's example documentation focuses on the Update() and FixedUpdate() functions without really going into the alternatives. If you don't need something running synchronously or at a constant rate, you can save some time for other tasks, by using a coroutine, or careful threading. Lets take a look at Unity's timing system, and then understand the ways in which we can work around it.
It is important to understand that when a computer program needs to run on something, so lets observe 3 key concepts about how Unity will run. First understand that Unity is software and software must have hardware devices on which to run. Second understand that hardware resources are allocated either as space for storage, or time for computing; computing costs can be divided over cores, and memory allocated to different devices. Different strategies for allocating resources to devices can speed up computation, but also raise the complexity of writing the program. Third, graphics computing and behaviour computing are different, but tied to each-other; Unity handles the graphics, you program the behaviours, but one can bottle-neck the other.
A bottleneck is a scenario in which one process is waiting for another to complete, the term comes from the shape of a bottle which limits the flow at the top, whereas if the shape of the bottle was uniform, the flow would not be limited by a part of the whole. If different devices in your computer are waiting on each-other to complete a task, the slowest task will become the bottleneck.
Anything that a user interacts with in Unity must be synchronous, this means any input reading or physics simulation should be performed every "update frame", as this will prevent the user's input from diverging from the in game reaction the user expects. Unlike simulated physics interactions however, non-interactive visuals and background-tasks can happen outside Unity's main thread of execution.

Understanding Time

Unity works by scheduling three separate tasks in cooperation:
  1. Behaviour: Any gameplay logic through a MonoBehaviour
  2. Physics: Manipulation of locations and interactions of objects in the scene.
  3. Rendering: Painting the visual representation of the scene onto the screen
It doesn't necessarily schedule all of these all of the time, for example if the current render is lagging behind and will render the scene state from 3 seconds ago, it would be pointless to render, so that render will be dropped, in a practice called frame dropping. Unity actually schedules physics updates first, and suspends the physics simulation if the rendering step needs to catch up(drift exceeds the Time Manager's Maximum Allowed Timestep) , this is what causes the simulation to slow down with the frame rate.

FixedUpdate() and Update()

As the names suggest FixedUpdate() is called at a fixed interval, and update is called regularly, but not necessarily at a fixed interval. Using our understanding of Unity's 3 tasks, we now understand that these events represent our hooks into, and therefore also our responsibility for insuring that physics and rendering are happening smoothly and in the way we want. If we don't divide up our behaviours properly we might slow the whole process, and thus our game's framerate to a halt.

Defer your Enthusiasm

A common mistake with new Unity developers is that they do not separate physics and rendering calculation, this goes unnoticed for some time until the game becomes more complicated. Strange edge cases start to pop up as the game is more thoroughly tested, or has simply been running for a while as the physics simulation starts to diverge. The solution is simple, design your classes so that the physics calculations occur in FixedUpdate, and any rendering/input specific data happens in Update.
If you need to share information as is commonly the case in player control logic, store future data in the object's instance variables, and then read them back in the corresponding update function. The multi-platform nature of modern games often makes this division into two scripts, a player motor, and several player inputs; where the correct input is decided based on whether input is touch based, has an accelerometer, has gamepads or is a keyboard and mouse. With this architecture multiple inputs can be used even though the physics logic is shared, making the code more manageable and consistent.

Delta Time

Because the physics-rendering duality changes the distance between calls to Update and FixedUpdate, it is important that you include this in your calculations, for example, if you have an effect which should rotate an object at 1Hz, then you should rotate it by Time.deltaTime*360.0 every frame. If you need it to rotate by rotationSpeed Hz, then it should rotate by Time.deltaTime*360.0*this.rotationSpeed. Remember that you can make this calculation every render frame or every physics frame, so if you perform your update in FixedUpdate() rather than in Update() you should multiply by Time.fixedDeltaTime instead.

TimeScale

Time comes in two flavours, unscaled and scaled. Unity provides Time.timeScale (default: 1 (unscaled) )to allow you to force a slow down or speed up the simulation(if processing allows). This is most commonly used to pause the game(setting this to or very close to zero), but can also be used for slow/fast motion effects.

Target Framerate

In addition to the time-scale, Unity provides QualitySettings.targetFrameRate, this makes Unity intentionally lower the rendering rate, which conserves cycles for other processes.

Manual Physics Control

Recent versions of Unity have added manual physics control, simply set Physics.autoSimulation = false, and you become responsible for calling FixedUpdate using Physics.simulate(). This is not very useful for single player games, but is incredibly useful for multiplayer games which make use of the deterministic lockstep model.

The Concurrency Problem

So what if we want Unity to schedule something outside the normal Update()/FixedUpdate() loop? We have an effect or we need to talk to a web server, or we want something to run after the rendering of the current frame. In this case we need to tell Unity when we want it scheduled.

Scheduling

Your game and the Unity editor are running in one big loop, which appears asynchronous, but is not (see figure at the top of the page). This much will become immediately obvious if you ever accidentally put an infinite loop into your logic. Since the editor runs as part of your game to allow inspector properties to be edited at runtime, having an infinite loop will cause everything to lock up, requiring you to restart everything including the editor. One day hopefully the editor will be able to recover without restart, but for now always remember to save your changes before running or they will be lost if it hangs.
So why use concurrent programming? Two reasons:
  1. Asynchronous Tasks: Some tasks can be sliced into smaller bits, and scheduled at arbitrary times. If a task doesn't need to be run every frame it can be scheduled asynchronously.
  2. Parallelism: Modern computing architecture has many cores, CPUs have a few cores but are general purpose, GPUs have many cores but are not general purpose. Using 2 cores makes a task take 1/2 the time (minus overhead) and so on for n cores. A single synchronous program cannot exploit threaded parallelism. Special APIs are needed for a program to exploit GPU computation(like the Physics(Physx) engine)

Thread Safety

In a computer, tasks can run asynchronously on different threads(at an arbitrary time) and in parallel(possibly at the same time) on different cores. This means that a task may be scheduled at any time, so what happens if two tasks need to touch the same piece of memory which they expect to be in a particular state? Critical Sections are the name given to areas in a program's execution where this is the case. Critical sections are protected by locks specified by the programmer, which prevent them from accessing the same memory at the same time. The condition where two different threads of execution want the same memory is called a race condition.
Even though Mono behaviours usually execute synchronously(one after the other), Unity may be doing things behind the scenes asynchronously. How can we be sure not to disrupt(clobber) memory that the UnityEngine is using, if we don't know anything about it? The answer is we can't, this is what is meant by the statement "Unity objects are not thread safe." It is only safe to operate on a Unity object when Unity has scheduled your MonoBehaviour, at all other times no object that interact with Unity can be accessed safely. This is because when Unity calls your MonoBehaviour, it is in a critical section that has insured the engine is thread-safe for the duration of the section. You can still perform non Unity async tasks in the Mono/.Net runtime as long as they don't affect Unity. It is always safe to run background tasks on threads, as long as you only read the result within a MonoBehaviour event, and do not try to modify a Unity object from the thread. Alternatively you can tell Unity you would like to perform a task at a time when you would otherwise like to be scheduled. There are two ways to do this the classic way is a coroutine, the newer way is a Unity Job.

Unity Jobs

The Unity jobs system is new and complicated, but it allows you to use more of the underlying hardware, in theory without having concurrency issues, it does however mean that you will have to think a lot about how to redesign some systems. So Unity Jobs give you parallelism similar to threads (actually backed by threads with some bells and whistles), but they are big and complicated and outside the scope of this document. If you just need something to run asynchronously, but not in parallel, sticking with a coroutine will save you a lot of headache. A Unity job provides an interface to the engine's internal threaded scheduling system, allowing you to schedule a parallel job. The system protects you from race conditions the caveat being that you must use the provided interface to access any underlying object storage.

Enter The Coroutine

Don't Call Us We'll Call You

Recall that with Update() and FixedUpdate() the UnityEngine called your function at a time that was convenient to it, this is the same principle with a coroutine. With a coroutine, you provide a thing that needs to be executed and can be "sliced", rather than specifying the precise way for it to be executed as you would with thread management. Note that a coroutine cannot provided parallelism only asynchronous execution, so your behaviour will still run on a single core, if you need parallelism, run computations on threads, but only write to Unity objects within the main loop, not the threads. Think of this like the observer pattern in an MVC model, you can read from the model when it's time for the controller to run, but the model should not directly write to the view. If you find yourself making a lot of divide and conquer calculations the unity jobs system will allow you you to use the MapReduce programming pattern with Unity objects.

The Execution Stack and Stack Frames

Whenever you call a function it needs memory for its arguments and local variables, the runtime holds these values in what is called a stack frame, on the execution stack. Under normal circumstances when a function returns, the stack frame containing its local variables is removed from the stack, and control is returned to the calling function. With a generator this is not the case, rather the callee's stack frame becomes a new separate execution stack which can persists while control is returned to the caller using the yield keyword. So what exactly is a generator?

Generators (IEnumerable)

A generator is a type of Iterator which does not remember previous items. An iterator is the object which represents the traversal of a collection when you use the foreach semantics of a language. There is technically nothing that prevents you from using them manually. The foreach keyword is just syntactics sugar for the statement:
while(o.HasNext()) { return o.Next() }
So what if we could use these semantics to make a task which can be broken up into smaller pieces? Imagine each fragment of the task is actually an element in a todo list, now let this list be a task queue which can be dynamically modified. If we consider it this way, the problem reduces to the above where we just keep asking for the next task fragment and running them until we run out. We'll specify our task fragments using generator semantics and ask Unity to schedule our task fragments at a safe time.

Generators as Coroutines

Since we need Unity's scheduler to schedule task for us, the UnityEngine provides us with an interface for doing this, namely MonoBehaviour.StartCoroutine() and the yield return new <YieldInstruction> semantics. Let's start by building a simple example and just calling it. In our example we'll produce a generator and then just call it without providing it to the scheduler. In each call execution will return to the yield point, the for loop increments and the next value is "yielded".
void Start() { IEnumerator enumerator = self.f() while (enumerator.MoveNext()) { object item = enumerator.Current; Debug.Log(item); } } IEnumerator f() { Debug.log("Entering Generator"); for(int i=0;i<5;i++) { Debug.log("Leaving Generator"); yield return i; Debug.log("Re-entering Generator"); } }
This example will produce 1 to 5. These integers are the objects that will be "yielded" in Start(). We can see that the generator was re-entered multiple times at the point where it left off. A common yield instruction in Unity would be WaitForSeconds(<seconds>) . So how does that work with the Unity scheduler? As we saw with out example in Start() the scheduler would schedule our coroutine immediately in the next coroutine step, then the wait for seconds instruction would yield and the Unity scheduler would use this information to decide when the coroutine should next be executed.
//... StartCoroutine(f) //... IEnumerable f(){ /*Schedule a task over and over with at least 1 second in between*/ while(true){ /*Put omething here to make it run immediately and then wait*/ yield return new WaitForSeconds(1.0f) /*Put something here if you want to do the wait the first time through, and then run repeatedly*/ }}
And that's all there is too it, the yield return new WaitForSeconds() semantics look intimidating, but it's doing exactly that, suspending execution and returning an object that tells the scheduler when it would next like to run. Note that coroutines cannot return values as they have to yield, so store the results of any computation you want to keep at the end of the routine in an instance or class variable.

Chaining Coroutines

You might have also noticed that StartCoroutine() returns a coroutine, this means you can use the yield StartCoroutine(<IEnumerator>) call as YieldInstruction, causing your coroutine to suspend execution for another routine, which will be scheduled as it is yielded to the scheduler. This may be done repeatedly forming a yield chain.

Special Checks for Coroutines

Note that during normal execution, if a MonoBehaviour is not enabled, it will not receive Unity events such as OnCollisionEnter(), or Update(), however a Coroutine will continue running whenever it was scheduled, so you must check Monobehaviour.enabled yourself to decide if action should be taken. If you have a coroutine in a loop which you want to prevent from executing while disabled or the gameobject to which it is attached is inactive use
yield return new WaitUntil(()=>this.isActiveAndEnabled );
at the top of your loop, or at any point when the execution should check if it should continue.

Cheers, Abram Wiebe

Abram Wiebe
Freelance Software Developer - Programmer
4
Comments