Unity development inevitably needs to use Coroutine. The feature of Coroutine to do asynchronous tasks with step code makes programmers get rid of the coding mode of asynchronous operation and callback, making the code logic more coherent and easy to read. However, while being surprised at the utility and magic of coroutines, I always feel unable to fully grasp them because I don’t know the implementation principle behind them. Such as:
MonoBehaviour.StartCoroutine
Why is the received parameterIEnumerator
.IEnumerator
What does it have to do with coroutines?- Since the coroutine function return value declaration is
IEnumerator
Why is it inside the functionyield return
Is it a different type of return value? yield
What is it, commonyield return
.yield break
What does it mean? What’s the difference?- Why was it used?
yield return
You can make the code “stop” there, and then you can pick up where you left off, right? - Specific,
yield return new WaitForSeconds(3)
.yield return webRequest.SendWebRequest()
Why is it possible to wait for a specified time or for the request to complete and then execute the following code?
If you have the same question as ME, might as well read this article, I believe it will answer your doubts.
What is IEnumerator
According to the Microsoft documentation, IEnumerator is the base interface for all non-generic enumerators. In other words, IEnumerator defines an iterative method for any set. As long as any set implements its own IEnumerator, its users can iterate over the elements in the set through the IEnumerator instead of adopting different iteration methods for different sets.
IEnumerator is defined as follows
public interface IEnumerator
{
object Current { get; }
bool MoveNext();
void Reset();
}
Copy the code
The IEnumerator interface consists of a property and two methods
- The Current property gets the element in the collection at the Current iteration position
- The MoveNext method advances the current iteration position to the next position, returning true if it successfully advances to the next position, or false if it has advanced to the end of the collection
- The Reset method sets the current iteration position to the original position (this position is before the first element in the collection, so when the Reset method is called and then the MoveNext method is called, the Curren value is the first element in the collection).
For example, we often use the foreach keyword to traverse collections, but foreach is just syntactic sugar provided by C#
foreach (var item in collection)
{
Console.WriteLine(item.ToString());
}
Copy the code
Essentially, foreach loops also use IEnumerator to traverse collections. The compiler converts the foreach loop above to code similar to the following at compile time
{
var enumerator = collection.GetEnumerator();
try
{
while (enumerator.MoveNext()) // Determine whether the set has successfully advanced to the next element.
{
varitem = enumerator.Current; Console.WriteLine(item.ToString()); }}finally
{
// dispose of enumerator.}}Copy the code
How is yield related to IEnumerator
Yield is a C# keyword that is essentially a syntactic sugar for quickly defining iterators. Methods that yield to them are automatically compiled by the compiler into an iterator, which is called an iterator function. The return value of the iterator function is an object of the automatically generated iterator class
Imagine if we didn’t have the yield keyword. For every iterator we defined, we had to create a class that implemented the IEnumerator interface with the correct implementation of the properties and methods. With the yield keyword, you can quickly define an iterator with just a few lines of code. The compiler does everything from creating the iterator class to implementing the IEnumerator interface
// The iterator defined by the iterator function
IEnumerator Test()
{
yield return 1;
Debug.Log("Surprise");
yield return 3;
yield break;
yield return 4;
}
Copy the code
yield return
The statement can return a value representing the current element iterated overyield break
Statement can be used to terminate an iteration, indicating that there are currently no elements to be iterated over
As shown below, elements can be iterated through the iterator defined in the code above
IEnumerator enumerator = Test(); // Calling the iterator function directly does not execute the body of the method, but returns the iterator object
bool ret = enumerator.MoveNext();
Debug.Log(ret + "" + enumerator.Current); // (1) Prints: True 1
ret = enumerator.MoveNext();
// (2) Print: Surprise
Debug.Log(ret + "" + enumerator.Current); // (3) Print: True 3
ret = enumerator.MoveNext();
Debug.Log(ret + "" + enumerator.Current); // (4) Print: False 3
Copy the code
(1)(3)(4) printed correctly, (1)(3) printed correctly, (4) because the iteration was yield break, so MoveNext returned false
Note that the (2) print location is triggered after the second MoveNext call, meaning that if the second MoveNext call is not made, the (2) print will not be triggered, meaning that the debug.log (“Surprise”) line will not be executed. Yield Return 1 appears to “stop” the code, and when the MoveNext method is called again, the code continues execution from where it “stopped”
Why does yield Return “stop” code
To understand how code “stops” and then resumes, go to IL. However, the compiled IL is an intermediate language similar to assembly language, which is low-level and difficult to understand. So I used Unity’s IL2CPP, which converts IL generated by C# compilation into C++. Yield return can be studied by curving C++ code implementations
For example, in the following C# class, variable names are complicated to make it easier to locate variables inside a function
public class Test
{
public IEnumerator GetSingleDigitNumbers()
{
int m_tag_index = 0;
int m_tag_value = 0;
while (m_tag_index < 10)
{
m_tag_value += 456;
yield returnm_tag_index++; }}}Copy the code
The generated class is in the test. CPP file. Because the file is quite long, only some important fragments are taken (the full file can be seen here).
// Test/<GetSingleDigitNumbers>d__0
struct U3CGetSingleDigitNumbersU3Ed__0_t9371C0E193B6B7701AD95F88620C6D6C93705F1A : public RuntimeObject
{
public:
// System.Int32 Test/<GetSingleDigitNumbers>d__0::<>1__state
int32_t ___U3CU3E1__state_0;
// System.Object Test/<GetSingleDigitNumbers>d__0::<>2__current
RuntimeObject * ___U3CU3E2__current_1;
// Test Test/<GetSingleDigitNumbers>d__0::<>4__this
Test_tD0155F04059CC04891C1AAC25562964CCC2712E3 * ___U3CU3E4__this_2;
// System.Int32 Test/<GetSingleDigitNumbers>d__0::<m_tag_index>5__1
int32_t ___U3Cm_tag_indexU3E5__1_3;
// System.Int32 Test/<GetSingleDigitNumbers>d__0::<m_tag_value>5__2
int32_t ___U3Cm_tag_valueU3E5__2_4;
public:
inline int32_t get_U3CU3E1__state_0() const { return ___U3CU3E1__state_0; }
inline void set_U3CU3E1__state_0(int32_t value)
{
___U3CU3E1__state_0 = value;
}
inline RuntimeObject * get_U3CU3E2__current_1() const { return ___U3CU3E2__current_1; }
inline void set_U3CU3E2__current_1(RuntimeObject * value)
{
___U3CU3E2__current_1 = value;
Il2CppCodeGenWriteBarrier((void**)(&___U3CU3E2__current_1), (void*)value);
}
inline int32_t get_U3Cm_tag_indexU3E5__1_3() const { return ___U3Cm_tag_indexU3E5__1_3; }
inline void set_U3Cm_tag_indexU3E5__1_3(int32_t value)
{
___U3Cm_tag_indexU3E5__1_3 = value;
}
inline int32_t get_U3Cm_tag_valueU3E5__2_4() const { return ___U3Cm_tag_valueU3E5__2_4; }
inline void set_U3Cm_tag_valueU3E5__2_4(int32_t value)
{
___U3Cm_tag_valueU3E5__2_4 = value;
}
};
Copy the code
You can see GetSingleDigitNumbers function really is a kind of U3CGetSingleDigitNumbersU3Ed__0_t9371C0E193B6B7701AD95F88620C6D6C93705F1A by definition, The local variables m_tag_index and m_tag_value are defined as the member variables ___U3Cm_tag_indexU3E5__1_3 and ___U3Cm_tag_valueU3E5__2_4, respectively, and corresponding GET and set methods are generated for them. The ___U3CU3E2__current_1 member variable corresponds to the Current property of IEnumerator. Focus here on the additional generated ___U3CU3E1__state_0 member variable, which can be thought of as a state machine. The different state values it represents determine how the entire function logic should be executed, and we’ll see how this works later.
// System.Boolean Test/<GetSingleDigitNumbers>d__0::MoveNext()
IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR bool U3CGetSingleDigitNumbersU3Ed__0_MoveNext_mED8994A78E174FF0A8BE28DF873D247A3F648CFB (U3CGetSingleDigitNumbersU3Ed__0_t9371C0E193B6B7701AD95F88620C6D6C93705F1A * __this, const RuntimeMethod* method)
{
static bool s_Il2CppMethodInitialized;
if(! s_Il2CppMethodInitialized) {il2cpp_codegen_initialize_method (U3CGetSingleDigitNumbersU3Ed__0_MoveNext_mED8994A78E174FF0A8BE28DF873D247A3F648CFB_MetadataUsageId);
s_Il2CppMethodInitialized = true;
}
int32_t V_0 = 0;
int32_t V_1 = 0;
bool V_2 = false;
{
int32_t L_0 = __this->get_U3CU3E1__state_0(a); V_0 = L_0;int32_t L_1 = V_0;
if(! L_1) {gotoIL_0012; }} {goto IL_000c;
}
IL_000c:
{
int32_t L_2 = V_0;
if ((((int32_t)L_2) == ((int32_t)1)))
{
gotoIL_0014; }} {goto IL_0016;
}
IL_0012:
{
goto IL_0018;
}
IL_0014:
{
goto IL_0068;
}
IL_0016:
{
return (bool)0;
}
IL_0018:
{
__this->set_U3CU3E1__state_0((- 1));
// int m_tag_index = 0;
__this->set_U3Cm_tag_indexU3E5__1_3(0);
// int m_tag_value = 0;
__this->set_U3Cm_tag_valueU3E5__2_4(0);
goto IL_0070;
}
IL_0030:
{
// m_tag_value += 456;
int32_t L_3 = __this->get_U3Cm_tag_valueU3E5__2_4(a); __this->set_U3Cm_tag_valueU3E5__2_4(((int32_t)il2cpp_codegen_add((int32_t)L_3, (int32_t) ((int32_t)456))));
// yield return m_tag_index++;
int32_t L_4 = __this->get_U3Cm_tag_indexU3E5__1_3(a); V_1 = L_4;int32_t L_5 = V_1;
__this->set_U3Cm_tag_indexU3E5__1_3(((int32_t)il2cpp_codegen_add((int32_t)L_5, (int32_t)1)));
int32_t L_6 = V_1;
int32_t L_7 = L_6;
RuntimeObject * L_8 = Box(Int32_t585191389E07734F19F3156FF88FB3EF4800D102_il2cpp_TypeInfo_var, &L_7);
__this->set_U3CU3E2__current_1(L_8);
__this->set_U3CU3E1__state_0(1);
return (bool)1;
}
IL_0068:
{
__this->set_U3CU3E1__state_0((- 1));
}
IL_0070:
{
// while (m_tag_index < 10)
int32_t L_9 = __this->get_U3Cm_tag_indexU3E5__1_3(a); V_2 = (bool) ((((int32_t)L_9) < ((int32_t) ((int32_t)10)))? 1 : 0);
bool L_10 = V_2;
if (L_10)
{
gotoIL_0030; }} {// }
return (bool)0; }}Copy the code
The method corresponding to the IEnumerator MoveText U3CGetSingleDigitNumbersU3Ed__0_MoveNext_mED8994A78E174FF0A8BE28DF873D247A3F648CFB members. Its implementation makes use of the GOto statement, which is the key to the code “stopping” and resuming
As a step by step, the c# code logic dictates that the first call to the moveNext function should execute the following code
int m_tag_index = 0;
int m_tag_value = 0;
if (m_tag_index < 10)
{
m_tag_value += 456;
return m_tag_index++;
}
Copy the code
The corresponding c++ code is shown below. After IL_0030 completes, it returns true, indicating that there are still elements. The state is 1
// Initially, the ___U3CU3E1__state_0 value is 0
goto IL_0012;
goto IL_0018; // IL_0018 internally initializes m_tag_index and m_tag_value to 0. Also set ___U3CU3E1__state_0 to -1
goto IL_0070; // check whether m_tag_index is less than 10
goto IL_0030; // IL_0030 internally increses m_tag_index to current and sets ___U3CU3E1__state_0 to 1
Copy the code
The second call to the moveNext function corresponds to C#
if (m_tag_index < 10)
{
m_tag_value += 456;
return m_tag_index++;
}
Copy the code
The corresponding c++ code is
// the value of ___U3CU3E1__state_0 is 1
goto IL_000c;
goto IL_0014;
goto IL_0068; // Set ___U3CU3E1__state_0 to -1
IL_0070 // check whether m_tag_index is less than 10
goto IL_0030; // Returns 1, true, and iterable elements
Copy the code
When the moveNext function is called for the 11th time, the value of m_tag_index is 10 and the function should end. The return value should be false, indicating that there are no more elements to return. So the corresponding C++ code is
// the ___U3CU3E1__state_0 value is 1
goto IL_000c;
goto IL_0014;
goto IL_0068
IL_0070 // if m_tag_index is not less than 10, IL_0030 is not entered
{
// }
return (bool)0;
}
Copy the code
At this point, I think the mystery of the code “stop” and restore has finally been solved. To sum up, the compiler generates blocks of code for each partitioned statement according to its functional logic, depending on where it can “stop”. The yield statement is the dividing line. If you want code to “stop,” you do not execute the corresponding block, and if you want code to resume, you execute the corresponding block. The scheduling context is saved by defining all the variables to be saved as member variables.
Unity coroutine mechanism implementation principle
Now we can talk about yield return versus coroutine, or IEnumerator versus coroutine
Coroutines are lighter than threads and can be scheduled entirely under the control of user programs. The coroutine can be used to yield the execution of the schedule, to save the context during the schedule, and to restore it when the schedule comes back. Isn’t this similar to “stopping” the code and then resuming it? That’s right, Unity implements coroutines by scheduling execution rights using the yield return generated IEnumerator in conjunction with controlling when MoveNext is triggered
. To be specific, Unity through every MonoBehaviour StartCoroutine start from a collaborators, you will get an IEnumerator (StartCoroutine parameter is IEnumerator, The argument is an overloaded version of the method name and reflection will fetch the corresponding IEnumerator. In its game loop, it decides whether to implement the MoveNext method based on the condition. This condition is obtained from the Current property of IEnumerator, the yield return value.
To start a coroutine, Unity calls the resulting IEnumerator’s MoveNext once to get its Current value. So every time a coroutine is started, the coroutine function immediately executes to the first yield return and “stops”.
For the different Current types (which are typically subclasses of YieldInstruction), Unity has some default processing in place, such as:
-
If Current is null, it does nothing. In the next game loop, MoveNext is called. So the yield return NULL is used to wait for a frame
-
If Current is of type WaitForSeconds, Unity will get its wait time, and each game loop will determine if the time is up, and MoveNext will only be called when it is. So the yield return WaitForSeconds is used to wait for the specified time
-
If the Current is UnityWebRequestAsyncOperation type, it is a subclass of AsyncOperation, and AsyncOperation has isDone attribute, said the operation is completed, only isDone is true, Unity calls MoveNext. For UnityWebRequestAsyncOperation, only request is completed, will isDone attribute set to true.
This is why we can use the following synchronous code to complete an asynchronous network request operation.
using(UnityWebRequest webRequest = UnityWebRequest.Get("https://www.cnblogs.com/iwiniwin/p/13705456.html")) { yield return webRequest.SendWebRequest(); if(webRequest.isNetworkError) { Debug.Log("Error " + webRequest.error); } else { Debug.Log("Received "+ webRequest.downloadHandler.text); }}Copy the code
Implement your own Coroutine
Unity is bound and MonoBehavior coroutines, only through MonoBehavior. StartCoroutine open coroutines, while in development, some not inherit MonoBehavior class cannot use coroutines, in this case, we can encapsulate a set of coroutines. After understanding the Unity coroutine implementation principle, I think it is not difficult to implement their own coroutine, interested students to act quickly.
Here is an implementation already packaged in the Remote File Explorer for those situations where you want to use a coroutine but cannot use MonoBehavior when creating the Editor tool. Remote File Explorer is a cross-platform Remote File browser that allows users to manipulate directory files on the platform on which the application is running using the Unity Editor. The internal messaging part of the Remote File Explorer makes extensive use of coroutines. It is a good example of how the synchronous code of the coroutine can implement asynchronous tasks
Of course, you can use Coroutines in the Unity Editor. Unity also provides a package for Coroutines