一捆愁、【Unity3D】協(xié)程Coroutine的運(yùn)用
首先穆趴,在Unity3D的Update()的函數(shù)是不能被打斷的脸爱,也就是說如下代碼,如果綁定在任何一個(gè)對象上面未妹,你的游戲?qū)豢ㄋ啦痉希荒蹸trl+Alt+Delete立即結(jié)束了:
using UnityEngine;
using System.Collections;
public class MyCoroutine : MonoBehaviour
{
private int a;
void Start()
{
a = 0;
}
void Update()
{
Debug.Log("0");
while (a == 0)
{
//去做些事情空入,然后做完讓a!=0。
}
Debug.Log("1");
}
}
本來我是打斷族檬,Update()函數(shù)你等我一下歪赢,然后處理一些事情,你讀下面的代碼导梆,而我做完這些事情的標(biāo)志就是讓a不等于0轨淌。
可惜事與愿違,且不說Update()函數(shù)看尼,每幀都被讀取递鹉,也就說時(shí)刻在執(zhí)行里面的代碼,這個(gè)機(jī)制媳拴。單單是Unity3d是要讀完Update()函數(shù)的代碼屈溉,才會給你刷新一幀這個(gè)機(jī)制偿荷,已經(jīng)足以讓這游戲瞬間崩潰小压。因此怠益,也啟發(fā)了我蜻牢,Update()盡可能地不要扔些循環(huán)給它做抢呆,里面頂多就放些條件判斷好了,這樣你的游戲才會流暢梯码,才是所謂的“優(yōu)化好”轩娶。
那么闯捎,爺確實(shí)有些比較耗時(shí)的任務(wù)瓤鼻,這怎辦茬祷?那就通通開個(gè)子線程——協(xié)程Coroutine,別都寫在主線程沃粗,Update()函數(shù)最盅。
1.延遲執(zhí)行某段代碼
using UnityEngine;
using System.Collections;
public class MyCoroutine : MonoBehaviour
{
void Start()
{
Debug.Log("0");
Debug.Log("1");
StartCoroutine(Thread1());
}
void Update()
{
}
IEnumerator Thread1()
{
yield return new WaitForSeconds(3.0f);
Debug.Log("2");
}
}
yield return new WaitForSeconds(3.0f);
這一句就是中斷這線程3秒的意思,也就是在這行停3秒盼产。并且戏售,中斷線程的語句,只能寫在IEnumerator Thread1(){}這些協(xié)程里面锋喜,而不能寫在Update()里面段标,因?yàn)閁pdate()這個(gè)主線程根本不能被中斷逼庞。
而開子線程Thread1,或者按照Unity3d的術(shù)語砸逊,應(yīng)該說是 開協(xié)程Thread1的語句StartCoroutine(Thread1());應(yīng)該放在只在開始執(zhí)行一次的Start()里面穆咐,不然在Update()每幀都執(zhí)行一次对湃,子線程Thread1里面的程度,得開多少次安鹧丁种呐?
另外,IEnumerator Thread1(){}在讀完所有代碼阔墩,自動(dòng)死亡,會被系統(tǒng)的線程回收機(jī)制自動(dòng)回收忘苛,我們自管開線程就行蜀肘,其余的不用管西乖!
2.每隔幾秒執(zhí)行某段代碼
如果我不想每幀都執(zhí)行某些代碼薄腻,而是比如想每1秒i+1庵楷,初始=0的i,i++到10即停止童漩,這又怎么辦呢差凹?你可以這樣寫:
using UnityEngine;
using System.Collections;
public class MyCoroutine : MonoBehaviour
{
private int i;
void Start()
{
i = 0;
StartCoroutine(Thread1());
}
void Update()
{
}
IEnumerator Thread1()
{
while (true)
{
Debug.Log("i=" + i);
i++;
if (i > 10)
{
break;
}
yield return new WaitForSeconds(1.0f);
}
}
}
這一段也很好理解危尿,就是在Thread1中上個(gè)死循環(huán),但死循環(huán)里面的代碼并不是這么好讀弥搞,讀到 yield return new WaitForSeconds(1.0f);就要停頓1秒邮绿。讀其余代碼的時(shí)間可以忽略不計(jì),因此攀例,協(xié)程Coroutine配合一個(gè)有條件break的死循環(huán)船逮,可以做到每隔幾秒執(zhí)行某段代碼的效果。
但還是那句話粤铭,這一切通通都只能寫到協(xié)程IEnumerator Thread1()里面挖胃,因?yàn)閁pdate()不能停頓,游戲和動(dòng)畫一樣,都是每一幀不停被刷新的頁面赌躺。
3.同步
比如我想執(zhí)行完某段代碼悄泥,立即執(zhí)行另一段代碼,做到回調(diào)的效果瓢姻,那該怎么辦呢儡嘶?
當(dāng)然最簡單就是在寫完一段代碼,在下一行寫另一段代碼「幌遥可是,如果這些代碼不是立即完成的律杠,需要等待股耽,就要用到協(xié)程的同步盖矫。
比如证芭,協(xié)程1需要耗時(shí)X秒,我并不知道,而協(xié)程2則需要在協(xié)程1之后馬上執(zhí)行孙援,這又該怎么辦呢?你可以這樣寫:
using UnityEngine;
using System.Collections;
public class MyCoroutine : MonoBehaviour
{
private int i;
void Start()
{
i = 0;
StartCoroutine(Thread1());
}
void Update()
{
}
IEnumerator Thread1()
{
while (true)
{
Debug.Log("i=" + i);
i++;
if (i > 3)
{
break;
}
yield return new WaitForSeconds(1.0f);
}
Debug.Log("線程1已經(jīng)完成了");
StartCoroutine(Thread2());
}
IEnumerator Thread2()
{
Debug.Log("線程2開始");
yield return null;//這句必須有吱瘩,C#要求每個(gè)協(xié)程都要有yield return
//,雖然這句話看起來并沒有什么卵用盆色,但你就是要寫-_-强霎!
}
}
二、對yield return的理解
下面來看看兩段顯示人物對話的代碼(對話隨便復(fù)制了一段內(nèi)容)惨好,功能是一樣的煌茴,但是方法不一樣:
1 using UnityEngine;
2 using System.Collections;
3
4 public class dialog_easy : MonoBehaviour {
5 public string dialogStr = "yield return的作用是在執(zhí)行到這行代碼之后,
將控制權(quán)立即交還給外部日川。yield return之后的代碼會在外部代碼再次
調(diào)用MoveNext時(shí)才會執(zhí)行蔓腐,直到下一個(gè)yield return——或是迭代結(jié)束。
雖然上面的代碼看似有個(gè)死循環(huán)龄句,但事實(shí)上在循環(huán)內(nèi)部我們始終會把控制權(quán)
交還給外部回论,這就由外部來決定何時(shí)中止這次迭代散罕。有了yield之后,我們
便可以利用“死循環(huán)”傀蓉,我們可以寫出含義明確的“無限的”斐波那契數(shù)列欧漱。";
6 public float speed = 5.0f;
7
8 private float timeSum = 0.0f;
9 private bool isShowing = false;
10 // Use this for initialization
11 void Start () {
12 ShowDialog();
13 }
14
15 // Update is called once per frame
16 void Update () {
17 if(isShowing){
18 timeSum += speed * Time.deltaTime;
19 guiText.text = dialogStr.Substring(0, System.Convert.ToInt32(timeSum));
20
21 if(guiText.text.Length == dialogStr.Length)
22 isShowing = false;
23 }
24 }
25
26 void ShowDialog(){
27 isShowing = true;
28 timeSum = 0.0f;
29 }
30 }
這段代碼實(shí)現(xiàn)了在GUIText中逐漸顯示一個(gè)字符串的功能,速度為每秒5個(gè)字僚害,這也是新手常用的方式硫椰。如果只是簡單的在GUIText中顯示一段文字,ShowDialog()函數(shù)可以做的很好萨蚕;但是如果要讓字一個(gè)一個(gè)蹦出來靶草,就需要借助游戲的循環(huán)了,最簡單的方式就是在Update()中更新GUIText岳遥。
從功能角度看奕翔,這段代碼完全沒有問題;但是從代碼封裝性的角度來看浩蓉,這是一段很惡心的代碼派继,因?yàn)楸緫?yīng)由ShowDialog()完成的功能放到了Update()中,并且在類中還有兩個(gè)private變量為這個(gè)功能服務(wù)捻艳。如果將來要修改或者刪除這個(gè)功能驾窟,需要在ShowDialog()和Update()中修改,并且還可能修改那兩個(gè)private變量∪瞎欤現(xiàn)在代碼比較簡單绅络,感覺還不算太壞,一旦Update()中再來兩個(gè)類似的的功能嘁字,估計(jì)寫完代碼一段時(shí)間之后自己修改都費(fèi)勁恩急。
如果通過yield return null實(shí)現(xiàn)幀與幀之間的同步,則代碼優(yōu)雅了很多:
1 using UnityEngine;
2 using System.Collections;
3
4 public class dialog_easy : MonoBehaviour {
5 public string dialogStr = "yield return的作用是在執(zhí)行到這行代碼之后纪蜒,
將控制權(quán)立即交還給外部衷恭。yield return之后的代碼會在外部代碼再次
調(diào)用MoveNext時(shí)才會執(zhí)行,直到下一個(gè)yield return——或是迭代結(jié)束纯续。
雖然上面的代碼看似有個(gè)死循環(huán)随珠,但事實(shí)上在循環(huán)內(nèi)部我們始終會把控制權(quán)
交還給外部,這就由外部來決定何時(shí)中止這次迭代杆烁。有了yield之后牙丽,我們
便可以利用“死循環(huán)”,我們可以寫出含義明確的“無限的”斐波那契數(shù)列兔魂。";
6 public float speed = 5.0f;
7
8 // Use this for initialization
9 void Start () {
10 StartCoroutine(ShowDialog());
11 }
12
13 // Update is called once per frame
14 void Update () {
15 }
16
17 IEnumerator ShowDialog(){
18 float timeSum = 0.0f;
19 while(guiText.text.Length < dialogStr.Length){
20 timeSum += speed * Time.deltaTime;
21 guiText.text = dialogStr.Substring(0, System.Convert.ToInt32(timeSum));
22 yield return null;
23 }
24 }
25 }
相關(guān)代碼都被封裝到了ShowDialog()中烤芦,這么一來,不論是要增加析校、修改或刪除功能构罗,都變得容易了很多铜涉。根據(jù)官網(wǎng)手冊的描述,yield return null可以讓這段代碼在下一幀繼續(xù)執(zhí)行遂唧。在ShowDialog()中芙代,每次更新文字以后yield return null,直到這段文字被完整顯示盖彭∥婆耄看到這里,可能有童鞋不解:
- 為什么在協(xié)程中也可以用Time.deltaTime召边?
- 協(xié)程中的Time.deltaTime和Update()中的一樣嗎铺呵?
- 這樣使用協(xié)程,會不會出現(xiàn)與主線程訪問共享資源沖突的問題隧熙?(線程的同步與互斥問題)
- yield return null太神奇了片挂,為什么會在下一幀繼續(xù)執(zhí)行這個(gè)函數(shù)?
- 這段代碼是不是相當(dāng)于為ShowDialog()構(gòu)造了一個(gè)自己的Update()贞盯?
參考Unity協(xié)程(Coroutine)原理深入剖析
協(xié)程不是線程音念,也不是異步執(zhí)行的。協(xié)程和 MonoBehaviour 的 Update函數(shù)一樣也是在MainThread中執(zhí)行的躏敢。使用協(xié)程你不用考慮同步和鎖的問題闷愤。
協(xié)程其實(shí)就是一個(gè)IEnumerator(迭代器),IEnumerator 接口有兩個(gè)方法 Current 和 MoveNext() 件余,迭代器方法運(yùn)行到 yield return 語句時(shí)肝谭,會返回一個(gè)expression表達(dá)式并保留當(dāng)前在代碼中的位置。 當(dāng)下次調(diào)用迭代器函數(shù)時(shí)執(zhí)行從該位置重新啟動(dòng)蛾扇。unity3d在每幀做的工作就是:調(diào)用協(xié)程(迭代器)MoveNext() 方法,如果返回 true 魏滚,就從當(dāng)前位置繼續(xù)往下執(zhí)行镀首。
- 協(xié)程和Update()一樣更新,自然可以使用Time.deltaTime了鼠次,而且這個(gè)Time.deltaTime和在Update()當(dāng)中使用是一樣的效果(使用yield return null的情況下)
- 協(xié)程并不是多線程更哄,它和Update()一樣是在主線程中執(zhí)行的,所以不需要處理線程的同步與互斥問題
- yield return null其實(shí)沒什么神奇的腥寇,只是unity3d封裝以后成翩,這個(gè)協(xié)程在下一幀就被自動(dòng)調(diào)用了
- 可以理解為ShowDialog()構(gòu)造了一個(gè)自己的Update(),因?yàn)閥ield return null讓這個(gè)函數(shù)每幀都被調(diào)用了
三赦役、Unity StartCoroutine 和 yield return 深入研究
public class MainTest : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Debug.Log("start1");
StartCoroutine(Test());
Debug.Log("start2");
}
IEnumerator Test()
{
Debug.Log("test1");
yield return null;
Debug.Log("test2");
}
運(yùn)行結(jié)果是:
start1
test1
start2
test2
當(dāng)StartCoroutine剛調(diào)用的時(shí)候麻敌,可以理解為正常的函數(shù)調(diào)用,然后接著看調(diào)用的函數(shù)里面掂摔。當(dāng)被調(diào)用函數(shù)執(zhí)行到y(tǒng)ield return null术羔;(暫停協(xié)程赢赊,等待下一幀繼續(xù)執(zhí)行)時(shí),根據(jù)Unity解釋協(xié)同程序就會被暫停级历,其實(shí)我個(gè)人認(rèn)為他這個(gè)解釋不夠精確释移,先返回開始協(xié)程的地方,然后再暫停協(xié)程寥殖。也就是先通知調(diào)用處玩讳,“你先走吧,不用管我”嚼贡,然后再暫停協(xié)程熏纯。。這里如果把yeild return null改為yield return new WaitForSeconds(3);就可以看到test2是3秒之后才打印出來的编曼。
四豆巨、Unity協(xié)程(一):徹底了解yield return null 和 yield return new WaitForSeconds
WaitForEndOfFrame,顧名思義是在等到本幀的幀末進(jìn)行在進(jìn)行處理
yield return null表示暫緩一幀掐场,在下一幀接著往下處理往扔,也有人習(xí)慣寫成yield return 0或者yield return 1,于是誤區(qū)就隨之而來了熊户,很多同學(xué)誤認(rèn)為yield return后面的數(shù)字表示的是幀率萍膛,比如yield return 10,表示的是延緩10幀再處理嚷堡,實(shí)則不然蝗罗,yield return num;的寫法其實(shí)后面的數(shù)字是不起作用的,不管為多少蝌戒,表示都是在下一幀接著處理串塑。
yield return new WaitForSeconds,這個(gè)要注意的是1·實(shí)際時(shí)間等于給定的時(shí)間乘以Time.timeScale的值北苟。2·觸發(fā)間隔一定大等于1中計(jì)算出的實(shí)際時(shí)間桩匪,而且誤差的大小取決于幀率,因?yàn)樗窃诿繋幚韰f(xié)程的時(shí)候去計(jì)算時(shí)間間隔是否滿足條件友鼻,如果滿足則繼續(xù)執(zhí)行傻昙。例如,當(dāng)幀率為5的情況下彩扔,一幀的時(shí)間為200ms妆档,這時(shí)即使時(shí)間參數(shù)再小,最快也要200ms之后才能繼續(xù)執(zhí)行剩余部分虫碉。
這是一張關(guān)于MonoBehaviour的執(zhí)行順序圖關(guān)于協(xié)程的部分贾惦,由圖可見,yield 是在yield WaitForSeconds之前處理的,再結(jié)合上段的分析可以得出一個(gè)結(jié)論:在同一幀里執(zhí)行的兩個(gè)協(xié)程纤虽,不論先后關(guān)系如何乳绕,不論WaitForSeconds給定的值為多少,yield return null所在的協(xié)程都要比yield return new WaitForSeconds的協(xié)程更先執(zhí)行逼纸。同類型的協(xié)程則跟其開啟的先后順序相關(guān)
最后再提個(gè)點(diǎn)洋措,yield return null和yield return new WaitForSeconds協(xié)程最好別一起混著用,特別是同時(shí)開啟的這兩個(gè)協(xié)程還有相互依賴的關(guān)系杰刽,因?yàn)閹适遣环€(wěn)定的菠发,所以有可能引起某些非必現(xiàn)的bug。
五贺嫂、Unity 協(xié)程原理探究與實(shí)現(xiàn)
IEnumerator TestCoroutine()
{
yield return null; //返回內(nèi)容為null
yield return 1; //返回內(nèi)容為1
yield return "sss"; //返回內(nèi)容為"sss"
yield break; //跳出滓鸠,類似普通函數(shù)中的return語句
yield return 999; //由于break語句,該內(nèi)容無法返回
}
void Start()
{
IEnumerator e = TestCoroutine();
while (e.MoveNext())
{
Debug.Log(e.Current); //依次輸出枚舉接口返回的值
}
}
/* 枚舉接口的定義
public interface IEnumerator
{
object Current
{
get;
}
bool MoveNext();
void Reset();
}*/
/*運(yùn)行結(jié)果:
Null
1
sss
*/
首先注意注釋部分枚舉接口的定義
Current屬性為只讀屬性第喳,返回枚舉序列中的當(dāng)前位的內(nèi)容
MoveNext()把枚舉器的位置前進(jìn)到下一項(xiàng)糜俗,返回布爾值,新的位置若是有效的曲饱,返回true悠抹;否則返回false
Reset()將位置重置為原始狀態(tài)
再看下Start函數(shù)中的代碼,就是將yield return 語句中返回的值依次輸出扩淀。
第一次MoveNext()后楔敌,Current位置指向了yield return 返回的null,該位置是有效的(這里注意區(qū)分位置有效和結(jié)果有效驻谆,位置有效是指當(dāng)前位置是否有返回值卵凑,即使返回值是null;而結(jié)果有效是指返回值的結(jié)果是否為null胜臊,顯然此處返回結(jié)果是無意義的)所以MoveNext()返回值是true勺卢;
第二次MoveNext()后,Current新位置指向了yield return 返回的1象对,該位置是有效的值漫,MoveNext()返回true
第三次MoveNext()后,Current新位置指向了yield return 返回的"sss"织盼,該位置也是有效的,MoveNext()返回true
第四次MoveNext()后酱塔,Current新位置指向了yield break沥邻,無返回值,即位置無效羊娃,MoveNext()返回false唐全,至此循環(huán)結(jié)束
先來回顧下Unity的協(xié)程具體有些功能:
- 將協(xié)程代碼中由yield return語句分割的部分分配到每一幀去執(zhí)行。
- yield return 后的值是等待類(WaitForSeconds、WaitForFixedUpdate)時(shí)需要等待相應(yīng)時(shí)間邮利。
- yield return 后的值還是協(xié)程(Coroutine)時(shí)需要等待嵌套部分協(xié)程執(zhí)行完畢才能執(zhí)行接下來內(nèi)容弥雹。
1.分幀
實(shí)現(xiàn)分幀執(zhí)行之前,先將上述迭代器的代碼簡單修改下延届,看下輸出結(jié)果
IEnumerator TestCoroutine()
{
Debug.Log("TestCoroutine 1");
yield return null;
Debug.Log("TestCoroutine 2");
yield return 1;
}
void Start()
{
IEnumerator e = TestCoroutine();
while (e.MoveNext())
{
Debug.Log(e.Current); //依次輸出枚舉接口返回的值
}
}
/*運(yùn)行結(jié)果
TestCoroutine 1
Null
TestCoroutine 2
1
*/
前面有說過剪勿,每次MoveNext()后會返回yield return后的內(nèi)容,那yield return之前的語句怎么辦呢方庭?
當(dāng)然也執(zhí)行啊厕吉,遇到y(tǒng)ield return語句之前的內(nèi)容都會在MoveNext()時(shí)執(zhí)行的。
到這里應(yīng)該很清楚了械念,只要把MoveNext()移到每一幀去執(zhí)行头朱,不就實(shí)現(xiàn)分幀執(zhí)行幾段代碼了么!
既然要分配在每一幀去執(zhí)行龄减,那當(dāng)然就是Update和LateUpdate了项钮。這里我個(gè)人喜歡將實(shí)現(xiàn)代碼放在LateUpdate之中,為什么呢希停?因?yàn)閁nity中協(xié)程的調(diào)用順序是在Update之后烁巫,LateUpdate之前,所以這兩個(gè)接口都不夠準(zhǔn)確脖苏;但在LateUpdate中處理程拭,至少能保證協(xié)程是在所有腳本的Update執(zhí)行完畢之后再去執(zhí)行。
IEnumerator e = null;
void Start()
{
e = TestCoroutine();
}
void LateUpdate()
{
if (e != null)
{
if (!e.MoveNext())
{
e = null;
}
}
}
IEnumerator TestCoroutine()
{
Log("Test 1");
yield return null; //返回內(nèi)容為null
Log("Test 2");
yield return 1; //返回內(nèi)容為1
Log("Test 3");
yield return "sss"; //返回內(nèi)容為"sss"
Log("Test 4");
yield break; //跳出棍潘,類似普通函數(shù)中的return語句
Log("Test 5");
yield return 999; //由于break語句恃鞋,該內(nèi)容無法返回
}
void Log(object msg)
{
Debug.LogFormat("<color=yellow>[{0}]</color>{1}", Time.frameCount, msg.ToString());
}
再來看看運(yùn)行結(jié)果,黃色中括號括起來的數(shù)字表示當(dāng)前在第幾幀亦歉,很明顯我們的協(xié)程完成了每一幀執(zhí)行一段代碼的功能恤浪。
2.延時(shí)等待
要是完全理解了case1的內(nèi)容,相信你自己就能完成“延時(shí)等待”這一功能肴楷,其實(shí)就是加了個(gè)計(jì)時(shí)器的判斷嘛水由!
既然要識別自己的等待類,那當(dāng)然要獲取Current值根據(jù)其類型去判定是否需要等待赛蔫。假如Current值是需要等待類型砂客,那就延時(shí)到倒計(jì)時(shí)結(jié)束;而Current值是非等待類型呵恢,那就不需要等待鞠值,直接MoveNext()執(zhí)行后續(xù)的代碼即可。
這里著重說下“延時(shí)到倒計(jì)時(shí)結(jié)束”渗钉。既然知道Current值是需要等待的類型彤恶,那此時(shí)肯定不能在執(zhí)行MoveNext()了钞钙,否則等待就沒用了;接下來當(dāng)?shù)却龝r(shí)間到了声离,就可以繼續(xù)MoveNext()了芒炼。可以簡單的加個(gè)標(biāo)志位去做這一判斷术徊,同時(shí)驅(qū)動(dòng)MoveNext()的執(zhí)行本刽。
private void OnGUI()
{
if (GUILayout.Button("Test")) //注意:這里是點(diǎn)擊觸發(fā),沒有放在start里弧关,為什么盅安?
{
enumerator = TestCoroutine();
}
}
void LateUpdate()
{
if (enumerator != null)
{
bool isNoNeedWait = true, isMoveOver = true;
var current = enumerator.Current;
if (current is MyWaitForSeconds)
{
MyWaitForSeconds waitable = current as MyWaitForSeconds;
isNoNeedWait = waitable.IsOver(Time.deltaTime);
}
if (isNoNeedWait)
{
isMoveOver = enumerator.MoveNext();
}
if (!isMoveOver)
{
enumerator = null;
}
}
}
IEnumerator TestCoroutine()
{
Log("Test 1");
yield return null; //返回內(nèi)容為null
Log("Test 2");
yield return 1; //返回內(nèi)容為1
Log("Test 3");
yield return new MyWaitForSeconds(2f); //等待兩秒
Log("Test 4");
}
運(yùn)行結(jié)果里黃色表示當(dāng)前幀,青色是當(dāng)前時(shí)間世囊,很明顯等待了2秒(雖然有少許誤差但總體不影響)别瞭。
上述代碼中,把函數(shù)觸發(fā)放在了Button點(diǎn)擊中而不是Start函數(shù)中株憾?
這是因?yàn)槲沂怯肨ime.deltaTime去做計(jì)時(shí)蝙寨,假如放在了Start函數(shù)中,Time.deltaTime會受Awake這一幀執(zhí)行時(shí)間影響嗤瞎,時(shí)間還不短(我測試時(shí)有0.1s左右)墙歪,導(dǎo)致運(yùn)行結(jié)果有很大誤差,不到2秒就結(jié)束了贝奇,有興趣的可以自己試一下~
六虹菲、從 各種點(diǎn) 理解Unity中的協(xié)程
什么是協(xié)同程序?什么是協(xié)程掉瞳?
unity協(xié)程是一個(gè)能夠暫停協(xié)程執(zhí)行毕源,暫停后立即返回主函數(shù),執(zhí)行主函數(shù)剩余的部分陕习,直到中斷指令完成后霎褐,從中斷指令的下一行繼續(xù)執(zhí)行協(xié)程剩余的函數(shù)。函數(shù)體全部執(zhí)行完成该镣,協(xié)程結(jié)束冻璃。
由于中斷指令的出現(xiàn),使得可以將一個(gè)函數(shù)分割到多個(gè)幀里去執(zhí)行损合。
性能:
在性能上相比于一般函數(shù)沒有更多的開銷
協(xié)程的好處:
讓原來要使用異步 + 回調(diào)方式寫的非人類代碼, 可以用看似同步的方式寫出來省艳。
能夠分步做一個(gè)比較耗時(shí)的事情,如果需要大量的計(jì)算嫁审,將計(jì)算放到一個(gè)隨時(shí)間進(jìn)行的協(xié)程來處理跋炕,能分散計(jì)算壓力
協(xié)程的壞處:
協(xié)程本質(zhì)是迭代器,且是基于unity生命周期的土居,大量開啟協(xié)程會引起gc
如果同時(shí)激活的協(xié)程較多,就可能會出現(xiàn)多個(gè)高開銷的協(xié)程擠在同一幀執(zhí)行導(dǎo)致的卡幀
協(xié)程書寫時(shí)的性能優(yōu)化:
常見的問題是直接new 一個(gè)中斷指令,帶來不必要的 GC 負(fù)擔(dān)擦耀,可以復(fù)用一個(gè)全局的中斷指令對象棉圈,優(yōu)化掉開銷;在 Yielders.cs 這個(gè)文件里眷蜓,已經(jīng)集中地創(chuàng)建了上面這些類型的靜態(tài)對象
這個(gè)鏈接分析了一下https://blog.csdn.net/liujunjie612/article/details/70623943
協(xié)程是在什么地方執(zhí)行分瘾?
協(xié)程不是線程,不是異步執(zhí)行吁系;協(xié)程和monobehaviour的update函數(shù)一樣也是在主線程中執(zhí)行
unity在每一幀都會處理對象上的協(xié)程德召,也就是說,協(xié)程跟update一樣都是unity每幀會去處理的函數(shù)
經(jīng)過測試汽纤,協(xié)程至少是每幀的lateUpdate后運(yùn)行的上岗。
協(xié)程怎么結(jié)束?
方法一:StopCoroutine(string methodName);
方法二:stopAllCoroutines暫停的是當(dāng)前腳本下的所有協(xié)程
方法三:gameObject.active = false 可以停止該對象上全部協(xié)程的執(zhí)行蕴坪,即使再次激活肴掷,也不能繼續(xù)執(zhí)行。但注意MonoBehaviour enabled = false 不能停止協(xié)程背传;對比 update卻是可以在MonoBehaviour enabled = false 就中止
原因:由于協(xié)程在StartCoroutine時(shí)被注冊到的GameObject上呆瞻,他的生命期受限于GameObject的生命期,因此受GameObject是否active的影響径玖。
結(jié)論:協(xié)程雖然是在MonoBehvaviour啟動(dòng)的(StartCoroutine)但是協(xié)程函數(shù)的地位完全是跟MonoBehaviour是一個(gè)層次的痴脾,不受MonoBehaviour的狀態(tài)影響。
協(xié)程結(jié)束的標(biāo)志是什么梳星?
如果最后一個(gè) yield return 的 IEnumerator 已經(jīng)迭代到最后一個(gè)是赞赖,MoveNext 就會 返回 false 。這時(shí)丰泊,Unity就會將這個(gè) IEnumerator 從 cortoutines list 中移除薯定。
只有當(dāng)這個(gè)對象的 MoveNext() 返回 false 時(shí),即該 IEnumertator 的 Current 已經(jīng)迭代到最后一個(gè)元素了瞳购,才會執(zhí)行 yield return 后面的語句话侄。
中斷函數(shù)類型:
null 在下一幀所有的Update()函數(shù)調(diào)用過之后執(zhí)行
WaitForSeconds() 等待指定秒數(shù),在該幀(延遲過后的那一幀)所有update()函數(shù)調(diào)用完后執(zhí)行学赛。即等待給定時(shí)間周期年堆, 受Time.timeScale影響,當(dāng)Time.timeScale = 0f 時(shí)盏浇,yield return new WaitForSecond(x) 將不會滿足变丧。
WaitForFixedUpdate 等待一個(gè)固定幀,即等待物理周期循環(huán)結(jié)束后執(zhí)行
WaitForEndOfFrame 等待幀結(jié)束绢掰,即等待渲染周期循環(huán)結(jié)束后執(zhí)行
StartCoroutine 等待一個(gè)新協(xié)程暫停
WWW 等待一個(gè)加載完成痒蓬,等待www的網(wǎng)絡(luò)請求完成后童擎,isDone=true后執(zhí)行
協(xié)程的執(zhí)行順序:
開始協(xié)程->執(zhí)行協(xié)程->遇到中斷指令中斷協(xié)程->返回上層函數(shù)繼續(xù)執(zhí)行上層函數(shù)的下一行代碼->中斷指令結(jié)束后,繼續(xù)執(zhí)行中斷指令之后的代碼->協(xié)程結(jié)束
協(xié)程可以嵌套協(xié)程嗎攻晒?
可以顾复,yield return StartCoroutine就是,執(zhí)行順序是:
子協(xié)程中斷后鲁捏,會返回父協(xié)程芯砸,父協(xié)程暫停,返回父協(xié)程的上級函數(shù)给梅。
決定父協(xié)程結(jié)束的標(biāo)志是子協(xié)程是否結(jié)束褥影,當(dāng)子協(xié)程結(jié)束后返回父協(xié)程執(zhí)行其后的代碼才算結(jié)束岩喷。
同一時(shí)刻同一腳本實(shí)例中能有多少個(gè)運(yùn)行的協(xié)程吓揪?
在一個(gè)MonoBehaviour提供的主線程里只能有一個(gè)處于運(yùn)行狀態(tài)的協(xié)程镣典。因?yàn)閰f(xié)程不是線程,不是并行的曹质。同一時(shí)刻婴噩、一個(gè)腳本實(shí)例中可以有多個(gè)暫停的協(xié)程,但只有一個(gè)運(yùn)行著的協(xié)程
協(xié)程和線程的區(qū)別羽德?
線程是利用多核達(dá)到真正的并行計(jì)算几莽,缺點(diǎn)是會有大量的鎖、切換宅静、等待的問題章蚣,而協(xié)程是非搶占式,需要用戶自己釋放使用權(quán)來切換到其他協(xié)程, 因此同一時(shí)間其實(shí)只有一個(gè)協(xié)程擁有運(yùn)行權(quán), 相當(dāng)于單線程的能力姨夹。
協(xié)程是 C# 線程的替代品, 是 Unity 不使用線程的解決方案纤垂。
使用協(xié)程不用考慮同步和鎖的問題
多個(gè)協(xié)程可以同時(shí)運(yùn)行,它們會根據(jù)各自的啟動(dòng)順序來更新
其他注意點(diǎn):
1磷账、IEnumerator 類型的方法不能帶 ref 或者 out 型的參數(shù)峭沦,但可以帶被傳遞的引用
2、在函數(shù) Update 和 FixedUpdate 中不能使用 yield 語句逃糟,否則會報(bào)錯(cuò)吼鱼, 但是可以啟動(dòng)協(xié)程
3、在一個(gè)協(xié)程中绰咽,StartCoroutine()和 yield return StartCoroutine()是不一樣的菇肃。
前者僅僅是開始一個(gè)新的Coroutine,這個(gè)新的Coroutine和現(xiàn)有Coroutine并行執(zhí)行取募。
后者是返回一個(gè)新的Coroutine琐谤,是一個(gè)中斷指令,當(dāng)這個(gè)新的Coroutine執(zhí)行完畢后玩敏,才繼承執(zhí)行現(xiàn)有Coroutine斗忌。
七质礼、實(shí)現(xiàn)自己的WaitForSeconds
在Unity中StartCoroutine/yield return這個(gè)模式到底是怎么應(yīng)用的?其中的原理是什么织阳?
Coroutine几苍,你究竟干了什么?
Coroutine陈哑,你究竟干了什么?(小續(xù))
WaitForSeconds本身是一個(gè)普通的類型伸眶,但是在StartCoroutine中惊窖,其被特殊對待了,一般而言厘贼,StartCoroutine就是簡單的對某個(gè)IEnumerator 進(jìn)行MoveNext()操作界酒,但如果他發(fā)現(xiàn)IEnumerator其實(shí)是一個(gè)WaitForSeconds類型的話,那么他就會進(jìn)行特殊等待嘴秸,一直等到WaitForSeconds延時(shí)結(jié)束了毁欣,才進(jìn)行正常的MoveNext調(diào)用,而至于WWW或者WaitForFixedUpdate等類型岳掐,StartCoroutine也是同樣的特殊處理凭疮,如果用代碼表示一下的話,大概是這個(gè)樣子:
foreach(IEnumerator coroutine in coroutines)
{
if(!coroutine.MoveNext())
// This coroutine has finished
continue;
if(!coroutine.Current is YieldInstruction)
{
// This coroutine yielded null, or some other value we don't understand; run it next frame.
continue;
}
if(coroutine.Current is WaitForSeconds)
{
// update WaitForSeconds time value
}
else if(coroutine.Current is WaitForEndOfFrame)
{
// this iterator will MoveNext() at the end of the frame
}
else /* similar stuff for other YieldInstruction subtypes or WWW etc. */
}
2.嵌套
IEnumerator UnityCoroutine()
{
Debug.Log("Unity coroutine begin at time : " + Time.time);
yield return new WaitForSeconds(2);
yield return StartCoroutine(InnerUnityCoroutine());
Debug.Log("Unity coroutine end at time : " + Time.time);
}
IEnumerator InnerUnityCoroutine()
{
Debug.Log("Inner Unity coroutine begin at time : " + Time.time);
yield return new WaitForSeconds(2);
Debug.Log("Inner Unity coroutine end at time : " + Time.time);
}
void Start()
{
StartCoroutine(UnityCoroutine());
}
“外層”的UnityCoroutine只有在“內(nèi)層”的InnerUnityCoroutine“執(zhí)行”完畢之后才會繼續(xù)“執(zhí)行”