因?yàn)楣ぷ鞯脑?,第一次接觸到Unity3D游戲引擎,開拓了眼界,學(xué)到不少新知識。
今天與大家聊一聊我覺得在Unity3D中最基礎(chǔ)也是最重要的概念:Unity3D的單線程與多線程。
最近項(xiàng)目中,有一個(gè)網(wǎng)絡(luò)操作的小需求,我按照其他移動端的研發(fā)經(jīng)驗(yàn),使用Unity的網(wǎng)絡(luò)庫UnityEngine.WWW或者UnityEngine.Networking,然后new Thread放入子線程運(yùn)行網(wǎng)絡(luò),結(jié)果非常尷尬的遇到了can only be called from the main thread.的異常,百思不得其解。
為什么不能在子線程中操作UnityEngine SDK?
我?guī)е@個(gè)疑問,開始了探索。發(fā)現(xiàn)如下規(guī)律:
1. UnityEngine的API與對象都不能在子線程中運(yùn)行
2. UnityEngine定義的基本結(jié)構(gòu)(int,float,Struct定義的數(shù)據(jù)類型)可以在子線程中運(yùn)行。例如Vector3(Struct)可以,但Texture2d對象則不可以,因?yàn)槠涓割悶閁nityEngine.Object)。
概括起來,Unity3D中的子線程無法運(yùn)行Unity SDK API。
其實(shí),當(dāng)Unity對其SDK做了這個(gè)限制之后,我們可以非??隙ǖ恼fUnity是單線程的游戲引擎。
為什么要做這個(gè)限制?因?yàn)?/span>游戲中邏輯更新和畫面更新的時(shí)間點(diǎn)要求有確定性,必須按照幀序列嚴(yán)格保持同步,否則就會出現(xiàn)游戲中的對象不同步的現(xiàn)象。多線程也能保證這個(gè)效果,但如果引入多線程,會加大同步處理的難度與游戲的不穩(wěn)定性。腦補(bǔ)一下這張圖中的場景,如果卡頓了是什么感受:-)
明白了Unity3D是單線程設(shè)計(jì)之后,我繼續(xù)帶著疑問探索。
Unity3D的線程運(yùn)行機(jī)制是怎樣的呢?
最近寫了一點(diǎn)游戲腳本,一般都要求繼承MonoBehavior對象,會在OnStart()中做地圖初始化,在OnUpdate()中進(jìn)行幀刷新,然后MonoBehavior腳本可以作為一個(gè)Component掛載到某個(gè)GameObject中。
這一點(diǎn)點(diǎn)入門級的理解大致能在游戲中把地圖引擎跑起來,但是并沒有深入的去理解這些框架函數(shù)的執(zhí)行原理。
先上一張Unity3D運(yùn)行機(jī)制的圖:
初看起來可能不太理解,但是隨著項(xiàng)目深入,相信會逐步理解其中的含義。
第一次學(xué)**Unity3D,我認(rèn)為可以簡單一點(diǎn)理解:這是一個(gè)單線程的幀循環(huán),每一次繪制都會重新走一遍生命周期。
展開來說,主要包含以下部分:
1. 初始化。對于常用的OnStart()框架方法只會被調(diào)用一次。
2. 物理。比如游戲中的碰撞處理。
3. 事件輸入。例如手勢
4. Update(), 協(xié)程(Coroutine),LastUpdate()
5. 渲染Rendering。包含常用的OnPreRender(), onPostRender()
6. 銷毀。
詳細(xì)的理解可以查看官方文檔:http://docs.unity3d.com/Manual/ExecutionOrder.html (點(diǎn)擊『閱讀原文』)
掌握這張圖的精髓或許是理解Unity3D的關(guān)鍵,需要時(shí)間沉淀。目前我所掌握的有:
OnPreRender: 在相機(jī)開始渲染場景之前調(diào)用此函數(shù)。
OnPostRender: 在相機(jī)完成場景渲染后調(diào)用此函數(shù)。
Update: 在每幀上調(diào)用一次 Update() 函數(shù)。
Unity3D中協(xié)程(Coroutine)究竟是什么?
介紹完了Unity3D的生命周期,來說說協(xié)程(Coroutine)。
協(xié)程是什么呢?總體來說,對與Unity,它是單線程的設(shè)計(jì),它更傾向使用time slicing(時(shí)間分片)的協(xié)程(coroutine)去完成異步任務(wù),融合到了剛剛提到的生命周期中。
要理解協(xié)程,先回顧下線程:線程是操作系統(tǒng)級別的概念,現(xiàn)代操作系統(tǒng)都實(shí)現(xiàn)并且支持線程,線程的調(diào)度對應(yīng)用開發(fā)者是透明的,開發(fā)者無法預(yù)期某線程在何時(shí)被調(diào)度執(zhí)行。基于此,一般那種隨機(jī)出現(xiàn)的BUG,多與線程調(diào)度相關(guān)。
而協(xié)程Coroutine是編譯器級的,本質(zhì)還是一個(gè)線程時(shí)間分片去執(zhí)行代碼段。它通過**相關(guān)的代碼使得代碼段能夠?qū)崿F(xiàn)分段式的執(zhí)行,顯式調(diào)用yield函數(shù)后才被掛起,重新開始的地方是yield關(guān)鍵字指定的,一次一定會跑到一個(gè)yield對應(yīng)的地方。因?yàn)閰f(xié)程本質(zhì)上還是在主線程里執(zhí)行的,需要內(nèi)部有一個(gè)類似棧的數(shù)據(jù)結(jié)構(gòu),當(dāng)該coroutine被掛起時(shí)要保存該coroutine的數(shù)據(jù)現(xiàn)場以便恢復(fù)執(zhí)行。
在Unity3D中,協(xié)程是可自行停止運(yùn)行 (yield),直到給定的 YieldInstruction 結(jié)束再繼續(xù)運(yùn)行的函數(shù)。 協(xié)程 (Coroutines) 的不同用途:
· yield; 在下一幀上調(diào)用所有 Update 函數(shù)后,協(xié)同程序?qū)⒗^續(xù)運(yùn)行。
· yield WaitForSeconds(2); 在指定的時(shí)間延遲之后,為此幀調(diào)用所有 Update 函數(shù)之后繼續(xù)運(yùn)行
· yield WaitForFixedUpdate(); 在所有腳本上調(diào)用所有 FixedUpdate 后繼續(xù)運(yùn)行
· yield WWW 完成 WWW 下載后繼續(xù)運(yùn)行。
· yield StartCoroutine(MyFunc); 連接協(xié)同程序,并等待 MyFunc coroutine 首先結(jié)束。
也就是說,將代碼段分散在不同的幀中,每次執(zhí)行一段,下一幀再執(zhí)行yield掛起的地方。
舉個(gè)例子: 在OnStart()框架函數(shù)中調(diào)用startCoroutine(GetHttpData)執(zhí)行以下代碼端,其實(shí)是第一次發(fā)起網(wǎng)絡(luò)請求,下一次執(zhí)行時(shí)則走入yield之后的代碼段繼續(xù)執(zhí)行,從而實(shí)現(xiàn)了一個(gè)時(shí)間分片的”異步”效果,而不是像線程那樣在操作系統(tǒng)層面分CPU時(shí)間片去執(zhí)行。
IEnumerator GetHttpData(string str) {
UnityWebRequest www = UnityWebRequest.Get(str);
yield return www.Send();
if(www.isError) {
Debug.Log(www.error);
}
else {
// Show results as text
Debug.Log("Downloaded " + str);
// Or retrieve results as binary data
byte[] results = www.downloadHandler.data;
}
}
Unity中無法使用子線程了嗎?
先回顧下前面提到的內(nèi)容:
(1) Unity是單線程設(shè)計(jì)的游戲引擎,子線程中無法運(yùn)行Unity SDK
(2) Unity主循環(huán)是單線程,游戲腳本MonoBehavior有著嚴(yán)格的生命周期
(3) 傾向使用time slicing(時(shí)間分片)的協(xié)程(coroutine)去完成異步任務(wù)
這三點(diǎn)是Unity3D最為基礎(chǔ)也是最為重要的概念,熟練掌握才算入門了Unity3D開發(fā)。
但這些并不意味著無法在Unity中使用多線程,只是需要注意使用的場景。
試想一下,如果在幀序列的主循環(huán)單線程中處理大量耗時(shí)操作,勢必會帶來游戲畫面的卡頓,幀率的下降。
因此,對于不是畫面更新,也不是常規(guī)的邏輯更新(指包括AI、物理碰撞、角色控制這些),而是一些其他后臺任務(wù),則可以將這個(gè)獨(dú)立出來開辟一個(gè)子線程。
所以,在不使用Unity SDK的前提下,確保做好主子線程的同步(采用C#中的delegate等機(jī)制),那么是可以合理使用子線程的。
概括起來,結(jié)合過往移動端的研發(fā)經(jīng)驗(yàn),我認(rèn)為有以下幾點(diǎn)可以在子線程中處理:
(1) 大量耗時(shí)的數(shù)據(jù)計(jì)算
(2) 網(wǎng)絡(luò)請求
(3) 復(fù)雜密集的I/O操作
(4) Unity3D的NativePlugin中可以新建子線程。通過NativePlugin可以接入移動端iOS與Android中的成熟庫,可以是Objective C, Java, C++三種語言交叉混合的方式組成NativePlugin,然后使用Android或者iOS的SDK開辟子線程。
(這也是Unity3D中比較關(guān)鍵的技術(shù),后續(xù)有空單獨(dú)開篇談?wù)勎业睦斫狻#?nbsp;
關(guān)于這個(gè)話題,先聊到這里,或許遇到新問題又會有更為深入的發(fā)現(xiàn)。