I. Coroutines
What is a coroutine?
A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.
Coroutines are not new threads; they are an iterator management mechanism provided by U3D. For example, a common case:
void Start () {
print (0);
StartCoroutine (wait (3));
print (1);
}
IEnumerator wait(float s) {
print (2);
yield return new WaitForSeconds (s);
print (3);
}
// => 0 2 1 [3s later] 3
Pass the iterator returned by wait to StartCoroutine, and the coroutine can control the execution inside the iterator, similar to:
IEnumerator iter;
void Start () {
print (0);
iter = wait ();
print (1);
}
IEnumerator wait() {
print (2);
yield return 1;
print (3);
}
// => 0 1
Why are 2 and 3 not output?
Because wait only creates an iterator. We know that if you don't call the next() method (MoveNext in C#, next in JS) on an iterator, it won't execute. So, let's try:
IEnumerator iter;
void Start () {
print (0);
iter = wait ();
print (1);
iter.MoveNext ();
iter.MoveNext ();
}
IEnumerator wait() {
print (2);
yield return 1;
print (3);
}
// => 0 1 2 3
Manually calling the iterator's MoveNext() method gives us the expected result. This is the parameter of StartCoroutine. Internally, StartCoroutine gets a reference to the iterator and can control when to call MoveNext and when to pause. So what we see is: the code is executing in segments (separated by yield return statements)
Still not quite clear? No problem, let's continue:
IEnumerator iter;
void Start () {
print (0);
iter = wait (3);
print (1);
iter.MoveNext ();
iter.MoveNext ();
}
IEnumerator wait(float s) {
print (2);
yield return new WaitForSeconds (s);
print (3);
}
// => 0 1 2 3
Wait, why is there no 3-second wait after outputting 2?
Because we're manually managing the iterator now. After the first MoveNext(), we didn't do any corresponding processing on the WaitForSeconds type object returned by yield return, so there's no 10-second wait.
StartCoroutine might do this kind of processing internally:
-
After getting the parameter (iterator), call the iterator's
MoveNext()at a certain timing (not immediately; as for when, we'll discuss that later) -
If the return value of
MoveNext()(that is, the thing afteryield return) is aWaitForSecondsobject, executeMoveNext()again after s seconds
The direct result we see is finally a delay of s seconds before executing the code below yield return. Alright, we've almost figured it out. One last example:
IEnumerator iter;
void Start () {
print (0);
StartCoroutine (iter = wait (3));
print (1);
iter.MoveNext ();
}
IEnumerator wait(float s) {
print (2);
yield return new WaitForSeconds (s);
print (3);
}
// => 0 2 1 3
Guess whether there's a 3-second wait after outputting 2?
No, because we manually called MoveNext(). When the coroutine calls MoveNext() for the first time, the iterator has already moved one step. The coroutine never saw the WaitForSeconds, so after the coroutine gets the iterator, it doesn't execute immediately but waits for a certain time period belonging to the coroutine (it's said to be after Update; if you're interested, you can look for a U3D function execution order diagram. Of course, this isn't important)
What is a coroutine? The answer is what was mentioned at the beginning:
A coroutine is an iterator management mechanism provided by U3D
P.S. A senior's blog post says, "A coroutine is actually an IEnumerator (iterator)." Which one is more correct, no need to argue, right?
II. Delayed Calls Implemented with Coroutines
Here's a very flexible delayed call implementation:
using UnityEngine;
using System.Collections;
using System;
public class Delay {
public static IEnumerator run(Action action, float delaySeconds) {
yield return new WaitForSeconds(delaySeconds);
action();
}
}
Code modified from A Better Way to Implement Delayed Code Execution in Unity
Static method, convenient to call, can be used as a utility function, doesn't need to be bound to any object. The calling method is a bit ugly, as follows:
void Start() {
print (1);
StartCoroutine(Delay.run (() => {
print (2);
}, 3));
}
// => 1 [3s later] 3
Special note: There are 2 situations that will cause delayed call failure:
- Scene switch after
StartCoroutine(Application.LoadLevel)
Because the coroutine stops executing after switching scenes, the delayed call fails
Destroythe current object or ancestor objects of the current object afterStartCoroutine
Because after the object is destroyed, all coroutine tasks added by StartCoroutine on all scripts of that object will stop executing, so the delayed call fails
III. Delayed Calls with Invoke
Invoke is similar to JS's setTimeout, and there's also InvokeRepeating similar to setInterval, but it's far less powerful than JS. The Invoke series can only accept method names in string form, and calls by name with delay. For example:
int arg;
void Start() {
arg = 1;
Invoke ("doSth", 3);
}
void doSth() {
print (arg);
}
// => [3s later] 1
Because string form cannot pass parameters, a global variable is used to pass them. The syntax is very concise, with nothing confusing
Special note: The 2 situations of delayed call failure mentioned above still exist
IV. Delayed Calls Implemented with Time.time
A rather笨 method, but it can avoid the problem of scene switching and object destruction causing delayed call failure. Code as follows:
using UnityEngine;
using System.Collections;
using System;
public class Wait : MonoBehaviour {
static Action _action;
static float time;
static float delayTime;
// Use this for initialization
void Start () {
// Don't destroy this object when switching scenes
DontDestroyOnLoad (gameObject);
reset ();
}
// Update is called once per frame
void Update () {
if (Time.time > time + delayTime) {
_action();
reset();
}
}
void reset() {
time = 0;
delayTime = int.MaxValue;
}
public static void runAfterSec(Action action, float s) {
_action = action;
time = Time.time;
delayTime = s;
}
}
Usage requires attention:
-
Create the above script
-
Create an empty object and bind the above script to it
Then you can call it from anywhere, for example:
void Start() {
int arg = 1;
Wait.runAfterSec (() => {
print (arg);
}, 3);
Application.LoadLevel("scene1");
}
// => Scene transition [3s later] 1
If you think the functionality is weak, you can change it to use a queue to store action, etc. Extend it yourself
V. Summary
The first method is recommended, that is, using coroutines to implement delayed calls. If higher controllability is required, the third method can be adopted. If the scenario is relatively simple, you can use the second method. After all, passing parameters and defining functions is not very convenient
Reference Materials
-
Unity Coroutine (Coroutine) Principle In-Depth Analysis: Not entirely correct, but quite helpful
No comments yet. Be the first to share your thoughts.