More Effective Coroutines
01 What is MEC
More Effective Coroutines (MEC) is a free asset on the Unity Asset Store. It is an improved implementation of coroutines that runs about twice as fast as Unity's coroutines do and has zero per-frame memory allocations. It has been tested and refined extensively to maximize performance and create a rock solid platform for coroutines in your app.
MEC keeps Unity's familiar yield return structure, so switching over is mostly a matter of find and replace. The sections below walk through the changes, then cover the extra capabilities you get along the way.
02 Feature Comparison
| Feature | Unity | MEC Free | MEC Pro |
|---|---|---|---|
| Uses the yield return structure | ✓ | ✓ | ✓ |
| Time to run 100,000 empty coroutines | ~110.53 ms | ~9.62 ms | ~9.64 ms |
| Singleton instances of coroutines | ✗ | ✗ | ✓ |
| Switch the timing of coroutines mid-process | ✓ | ✓ | ✓ |
| CallDelayed, CallPeriodically, CallContinuously | ✗ | ✓ | ✓ |
| Choose whether to link a coroutine to a GameObject | ✗ | ✓ | ✓ |
Timing segments available in each version:
| Segment | Unity | MEC Free | MEC Pro |
|---|---|---|---|
| Update | ✓ | ✓ | ✓ |
| FixedUpdate | ✓ | ✓ | ✓ |
| LateUpdate | ✗ | ✓ | ✓ |
| SlowUpdate | ✗ | ✓ | ✓ |
| RealtimeUpdate | ✓ | ✗ | ✓ |
| EndOfFrame | ✓ | ✗ | ✓ |
| LateFixedUpdate | ✗ | ✗ | ✓ |
| ManualTimeframe | ✗ | ✗ | ✓ |
03 Adding the Namespaces
MEC coroutines are defined in the MEC namespace, and they rely on System.Collections.Generic rather than the System.Collections functionality that Unity coroutines use. System.Collections is hardly ever used for anything except Unity's coroutines, so an easy way to switch is a find and replace, then fix the lines that show errors. Make sure these two using statements are at the top of every script that uses MEC:
using System.Collections.Generic;
using MEC;
04 Replacing StartCoroutine
Replace every StartCoroutine call with Timing.RunCoroutine. You pick the execution loop when you define the process; it defaults to Segment.Update.
// Unity
StartCoroutine(_CheckForWin());
// MEC, in the Update segment
Timing.RunCoroutine(_CheckForWin());
// In a specific segment
Timing.RunCoroutine(_CheckForWin(), Segment.FixedUpdate);
Timing.RunCoroutine(_CheckForWin(), Segment.LateUpdate);
Timing.RunCoroutine(_CheckForWin(), Segment.SlowUpdate);
RunCoroutine returns a CoroutineHandle. That handle is your reference to the running coroutine: pass it to KillCoroutines, PauseCoroutines, ResumeCoroutines, WaitUntilDone, IsRunning, and SetSegment to control the coroutine after it starts. You can also supply an optional string tag to group coroutines for batch operations.
CancelWith (see section 07) on any coroutine that moves or changes GameObjects, or you will start to see null reference exceptions when you do things like switch screens in the middle of a transition.05 Function Headers
The process header changes from IEnumerator to IEnumerator<float>:
// from
IEnumerator _CheckForWin() { ... }
// to
IEnumerator<float> _CheckForWin() { ... }
It is a good habit to put an underscore before every coroutine function. Coroutines have a tendency to look like they run correctly but actually do nothing if you call them without RunCoroutine. The leading underscore reminds you to always write Timing.RunCoroutine(_CheckForWin()) rather than calling _CheckForWin() like a normal function.
06 Yield Statements
To wait one frame, use yield return Timing.WaitForOneFrame; (or yield return 0;, which is equivalent). WaitForOneFrame reads more clearly to anyone unfamiliar with coroutines.
IEnumerator<float> _CheckForWin()
{
while (_cubesHit < TotalCubes)
{
WinText.text = "Have not won yet.";
yield return Timing.WaitForOneFrame;
}
WinText.text = "You win!";
}
To pause for a number of seconds, use Timing.WaitForSeconds in place of Unity's new WaitForSeconds:
yield return Timing.WaitForSeconds(0.1f);
Unity lets you wait for another coroutine to finish with a shorthand yield return. In MEC Free, use the longhand form with WaitUntilDone:
yield return Timing.WaitUntilDone(Timing.RunCoroutine(_CheckForWin()));
(MEC Pro adds a shorthand that calls RunCoroutine for you.)
07 CancelWith
MEC coroutines do not automatically stop when the GameObject they were created on is destroyed or disabled, the way Unity's do. That is intentional, since it does not always make sense, and for coroutines that never touch the scene the check would be wasted work. When a coroutine does affect UI or scene objects, turn the check on with the CancelWith extension so you do not get errors when changing screens:
Timing.RunCoroutine(_moveMyButton().CancelWith(gameObject));
.CancelWith on all coroutines that affect UI elements.CancelWith adds roughly 20 bytes to the unavoidable GC allocation that all coroutines generate. If you want to avoid even that, you can do the same thing by hand: after every yield return inside the coroutine, check
if (gameObject != null && gameObject.activeInHierarchy)
08 WaitUntilDone
Unity lets you yield on various objects. MEC does the same through WaitUntilDone:
yield return Timing.WaitUntilDone(wwwObject);
yield return Timing.WaitUntilDone(asyncOperation);
// MEC Pro additions
yield return Timing.WaitUntilDone(newCoroutine);
yield return Timing.WaitUntilTrue(functionDelegateThatReturnsBool);
yield return Timing.WaitUntilFalse(functionDelegateThatReturnsBool);
09 SlowUpdate
Unity's coroutines have no concept of a slow update loop. MEC's SlowUpdate runs (by default) seven times a second and uses absolute timescale, so slowing Unity's timescale does not slow it down. It is ideal for tasks like displaying text, where updating faster would not be visible anyway.
SlowUpdate differs from simply yielding Timing.WaitForSeconds(1f/7f) in two ways: it uses absolute timescale, and all SlowUpdate ticks happen on the same frame, which matters when several text boxes should change together.
Timing.RunCoroutine(_UpdateTime(), Segment.SlowUpdate);
private IEnumerator<float> _UpdateTime()
{
while (true)
{
clock = Timing.LocalTime;
yield return 0f;
}
}
Time.deltaTime will not return the correct value during SlowUpdate, because Unity's Time class knows nothing about this segment. Use Timing.DeltaTime instead.You can change how often SlowUpdate runs:
Timing.Instance.TimeBetweenSlowUpdateCalls = 3f; // once every 3 seconds
10 Tags
When you start a coroutine you can supply a tag: a string that identifies it. Tag a coroutine or a group of them and you can later kill them all with KillCoroutines(tag).
Timing.RunCoroutine(_shout(1, "Hello"), "shout");
Timing.RunCoroutine(_shout(2, "World!"), "shout");
Timing.RunCoroutine(_shout(3, "I"), "shout2");
Timing.RunCoroutine(_shout(4, "Like"), "shout2");
Timing.RunCoroutine(_shout(5, "Cake!"), "shout2");
Debug.Log("Killed " + Timing.KillCoroutines("shout2"));
11 LocalTime and DeltaTime
MEC keeps track of the local time inside each segment and keeps Timing.LocalTime and Timing.DeltaTime updated. Unity's default Time class works fine most of the time, but not in the SlowUpdate segment, where you should use the MEC values instead.
12 Helper Functions
Three helpers on the Timing object cover patterns you reach for constantly:
- CallDelayed calls an action once after a number of seconds.
- CallContinuously calls an action every frame for a number of seconds.
- CallPeriodically calls an action every "x" seconds for a number of seconds.
// Start _RunFor5Seconds two seconds from now
Timing.CallDelayed(2f, delegate {
Timing.RunCoroutine(_RunFor5Seconds(handle)); });
// Push this object forward one unit per second for 4 seconds
Timing.CallContinuously(4f, delegate {
PushOnGameObject(Vector3.forward); }, Segment.FixedUpdate);
13 Multiple Instances
If you do nothing special, the Timing object adds itself to a new object named "Timing Controller" and hands out all coroutine processes from there. If you want more control, attach the Timing object to a GameObject yourself, or create more than one. The Timing.RunCoroutine functions are static, but with a handle to a specific instance you can call yourInstance.RunCoroutineOnInstance() to run on that instance. Creating multiple instances effectively groups processes that can be paused or destroyed together. All static methods have instance variants (RunCoroutineOnInstance, KillCoroutinesOnInstance, and so on).
14 Quick Reference
Running and stopping
| Method | Description |
|---|---|
Timing.RunCoroutine(coroutine) | Runs a coroutine in the Update segment. Returns a CoroutineHandle. |
Timing.RunCoroutine(coroutine, segment) | Runs a coroutine in the specified segment. |
Timing.RunCoroutine(coroutine, tag) | Runs a coroutine with a tag for batch operations. |
Timing.KillCoroutines() | Kills all coroutines on the main instance. Returns the count killed. |
Timing.KillCoroutines(handle) | Kills a single coroutine by handle. |
Timing.KillCoroutines(tag) | Kills all coroutines with the given tag. |
Timing.IsRunning(handle) | True if the handle points to a coroutine that is still running (paused counts as running). |
Pausing and resuming
| Method | Description |
|---|---|
Timing.PauseCoroutines() / (handle) / (tag) | Pauses all, one, or a tagged group of coroutines. |
Timing.ResumeCoroutines() / (handle) / (tag) | Resumes all, one, or a tagged group of coroutines. |
Waiting and timing
| Method | Description |
|---|---|
Timing.WaitForOneFrame | Constant. Use with yield return to wait one frame. |
Timing.WaitForSeconds(seconds) | Pauses for the given duration. |
Timing.WaitUntilDone(handle) | Pauses the current coroutine until the target finishes. |
Timing.LocalTime | Time in seconds that the current segment has been running. |
Timing.DeltaTime | Time since the last tick in the current segment. Use this in SlowUpdate. |
Timing.SetSegment(handle, segment) | Moves a running coroutine to a different segment. |
Helpers and extensions
| Method | Description |
|---|---|
Timing.CallDelayed(delay, action) | Calls the action once after a delay. |
Timing.CallContinuously(time, action) | Calls the action every frame for the given duration. |
Timing.CallPeriodically(time, period, action) | Calls the action every period seconds for the given duration. |
coroutine.CancelWith(gameObject) | Stops the coroutine when the GameObject is destroyed or disabled. |
15 FAQ
Does MEC have a function for StopCoroutine?
Yes, it is Timing.KillCoroutines(). It takes a handle or a tag. To end a coroutine from inside its own function, use yield break; instead, which is the equivalent of return; in a normal function.
Does MEC completely remove GC allocations?
No. MEC removes all per-frame GC allocations. When a coroutine is first created, the function pointer and any variables you pass in are placed on the heap and eventually collected. This first allocation happens in both Unity and MEC. MEC coroutines allocate less on average, and produce less GC allocation than Unity's in all cases, except if you allocate large strings and use them as a coroutine tag.
Any advantages besides speed and lower GC?
Yes. Unity's coroutines are attached to the object they were started on; MEC uses a central object to run all processes. So: Unity coroutines will not start if the object is disabled, while MEC does not care. Disabling a GameObject stops its Unity coroutines (and they do not resume on re-enable), while MEC coroutines keep going unless you tell them otherwise. Destroying the object kills its Unity coroutines, but not MEC's. MEC also lets you create coroutine groups to pause, resume, or destroy together, and lets you run in LateUpdate or SlowUpdate. MEC Pro adds even more segments.
