《学Unity的猫》——第九章:状态机与Unity协程,好奇猫与铁皮怪水管
文章目錄
- 9.1 會吐水的鐵皮怪
- 9.2 狀態機是什么
- 9.3 使用協程實現狀態機
- 9.4 進程與線程
- 9.4.1 什么是進程
- 9.4.2 什么是線程
- 9.5 Unity的協程
- 9.5.1 Unity的協程是什么
- 9.5.2 Unity生命周期對協程的影響
- 9.5.3 協程的啟動
- 9.5.4 協程的退出
- 9.5.5 協程的主要應用
簡介:我是一名Unity游戲開發工程師,皮皮是我養的貓,會講人話,它接到了喵星的特殊任務:學習編程,學習Unity游戲開發。
于是,發生了一系列有趣的故事。
9.1 會吐水的鐵皮怪
我把衣服丟進洗衣機里,倒入洗衣粉,調節水量,按了速洗,啟動。
皮皮豎著尾巴跟過來,我伸了個懶腰回到電腦前繼續寫文章。
不久,聽到水聲嘩嘩嘩地流,不祥的預感。我趕緊起身去看,水漫金山了。
“手欠貓!又把洗衣機的水管掏出來了!”
看了眼皮皮幼稚的圓臉,算了算了。
皮皮:“這個鐵皮怪為什么可以一次性吐那么多水出來?”
我一臉黑線:“這個叫洗衣機,它的功能就是洗衣服,水是從上面進水口進來的。”
皮皮舔舔自己的腳毛,仿佛在質疑洗衣機。
9.2 狀態機是什么
我拿出紙和筆,畫了洗衣機的狀態圖。
我:“你可以把洗衣機看成是一個有限狀態機。”
皮皮:“什么是有限狀態機?”
我:“有限狀態機是一種數學模型,英文全稱是Finite State Machine,縮寫FSM,簡稱狀態機,它是現實事物運行規則抽象而成的一個數學模型。”
我繼續講:“看這里,洗衣機有幾個狀態:開始、進水、漂洗、排水、脫水、結束。這些狀態由一系列事件來驅動,比如按啟動按鈕,開始進水,水位達到目標水位,進入漂洗狀態,正轉5秒,停2秒,反轉5秒,停2秒,循環執行10次,然后進入排水狀態,達到最低水位,進入脫水狀態,脫水30秒,接著又回到進水狀態,重復上述流程3次,最終結束。”
皮皮:“哇,好復雜,它也是程序控制的嗎?”
我:“是的呀,我們可以用代碼寫一個簡單的狀態機。”
9.3 使用協程實現狀態機
我打開Unity,創建了一個腳本CoroutineTest.cs。
CoroutineTest.cs代碼如下
將腳本掛到Main Camera上,點擊運行。
輸出了
如下
按一下空白鍵,輸出了
如下
再按一下空白鍵,輸出了
如下
皮皮:“上面的代碼有點看不懂,StartCoroutine、IEnumerator、yield return null是什么?”
我:“上面用到了Unity的協程。”
皮皮:“你之前都沒教我協程,直接一上來就寫我看不懂的代碼,不厚道。”
我:“程序員是一個不斷學習和成長的職業,實際項目中遇到一些沒學過的東西很正常,特別是現在這個知識爆炸的時代。不懂就查,自學能力是程序員最重要的能力之一,不要總是依賴別人教你。”
我心想會不會有點過分,皮皮只是拔了洗衣機的水管。
沒想到皮皮很認真地點了點頭,然后望著我呆呆地問:“怎么查?”
我的錯,我之前沒教過皮皮如何使用搜索引擎。
我打開CSDN,說:“以后你有問題可以在CSDN搜索,我給你注冊個賬號,實在不懂,你就訪問這個人的博客 https://blog.csdn.net/linxinfa,給他留言或者私信,他看到了會耐心回答你的問題的。”
剛好,這個時候衣服洗好了,我去把衣服拿出來晾好。
我回到屋內時,皮皮轉過頭說:“查了很多文章,還是沒明白協程的準確定義。”
我:“看在你這么認真的態度,我來講給你聽吧。要搞明白協程,需要先理解進程與線程。”
9.4 進程與線程
9.4.1 什么是進程
進程是一個具有一定獨立功能的程序在一個數據集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程序運行的載體。
簡單來說,進程就是應用程序的啟動實例,比如我們打開Unity編輯器,其實就是啟動了一個Unity編輯器進程。我們可以在任務管理器中看到操作系統中運行的進程。推薦使用ProcessExplorer來查看進程。
ProcessExplorer下載地址:https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer
如下,在ProcessExplorer中看到了Unity.exe進程,一個進程可以啟動另一個進程,比如Unity.exe進程又啟動了UnityCrashHandle64.exe這個進程來監聽Unity.exe的崩潰。
9.4.2 什么是線程
線程是程序執行中一個單一的順序控制流程,是程序執行流的最小單元,是處理器調度和分派的基本單位。
一個進程可以有一個或多個線程,各個線程之間共享程序的內存空間,也就是所在進程的內存空間。
同樣使用ProcessExplorer,可以查看某個進程中的線程。
右鍵Unity.exe進程,點擊菜單Properties。
點擊Threads標簽頁,可以看到它創建的線程,可以看到Unity.exe進程創建了97個線程。
9.5 Unity的協程
9.5.1 Unity的協程是什么
簡單來說,協程是一個有多個返回點的函數。
協程不是多線程,協程還是在主線程里面。進程和線程由操作系統調度,協程由程序員在協程的代碼里面顯示調度。
在Unity運行時,調用協程就是開啟了一個IEnumerator(迭代器),協程開始執行,在執行到yield return之前和其他的正常的程序沒有差別,但是當遇到yield return之后會立刻返回,并將該函數暫時掛起。在下一幀遇到FixedUpdate或者Update之后判斷yield return后邊的條件是否滿足,如果滿足則向下執行。
9.5.2 Unity生命周期對協程的影響
我拿出紙和筆,畫了MonoBehvaviour生命周期的一部分。
皮皮:“我記得FixedUpdate、Update和LateUpdate這三個函數,上次你講MonoBehvaviour生命周期的時候有講到。”
我:“記性不錯,本質上,Unity的協程是一個迭代器,遇到yield return的時候就掛起來,然后在MonoBehvaviour的生命周期中判斷條件是否滿足,滿足地話則迭代器執行下一步。”
9.5.3 協程的啟動
使用StartCoroutine啟動協程,例:
IEnumerator TestCoroutine() {yield return null; }啟動協程
// 得到迭代器 IEnumerator itor = TestCoroutine(); // 啟動協程 StartCoroutine(itor);// 也可以直接這樣寫 // StartCoroutine(TestCoroutine());皮皮:“這個IEnumerator是什么?”
我:“IEnumerator是一個迭代器接口,它有一個重要的方法MoveNext。”
Unity的協程遇到yield return的時候就掛起來,迭代器游標記錄了當前運行的位置,即Current,調用MoveNext()的時候,迭代器游標就下移一步,協程就從上一次的位置繼續運行。
皮皮:“沒有看到哪里去調用了這個MoveNext()呀。”
我:“Unity底層幫我們調用的,就像MonoBehvaviour的Update函數一樣。”
皮皮:“那如果我把MonoBehvaviour腳本禁用,協程還會繼續執行嗎?”
我:“協程的運行是和MonoBehvaviour平行的,執行了StartCoroutine之后,禁用MonoBehvaviour腳本,不會影響協程的運行,不過如果禁用了gameObject,則協程會立即退出,即使重新激活gameObject,協程也不會繼續運行。”
9.5.4 協程的退出
做個簡單的測試,CoroutineTest.cs腳本代碼如下:
using System.Collections; using UnityEngine;public class CoroutineTest : MonoBehaviour {void Start(){// 啟動協程StartCoroutine(TestCoroutine());}IEnumerator TestCoroutine(){while(true){Debug.Log("Coroutine is running");yield return null;}} }將CoroutineTest.cs腳本掛到一個空物體上
可以看到Console窗口輸出了日志,輸出了Coroutine is running。
我們可以從調用堆棧中看到,第一條日志是我們通過StartCoroutine啟動協程,內部其實是執行了一次迭代器的MoveNext方法。
而后面的日志,是通過UnityEngine.SetupCoroutine對象調用InvokeMoveNext方法,再執行了迭代器的MoveNext方法。
此時,我們把CoroutineTest腳本禁用,并不會影響協程的運行,日志會繼續輸出。
但如果把gameObject禁用,則協程立即停止了,即使重新激活gameObject,協程也不會繼續運行了。
皮皮:“上面是我們通過禁用gameObject讓協程退出,如果使用代碼的方式,如何強制退出協程呢?”
我:“有兩種方式。”
方式一,啟動協程是,把迭代器對象緩存起來,
然后我們就可以使用StopCoroutine方法來強制退出協程了。
// 退出協程 StopCoroutine(itor);方式二,是在協程內部執行yeild break。
IEnumerator TestCoroutine() {while(true){Debug.Log("Coroutine is running");// yield break會直接退出協程yield break;}Debug.Log("這里永遠不會被執行到"); }9.5.5 協程的主要應用
我:“協程的方便之處就是可以使用看似同步的寫法來寫異步的邏輯,這樣可以避免大量的委托回調函數。”
皮皮:“什么是回調函數?”
我:“舉個例子,剛剛洗衣機的狀態圖還記得嗎,進水是一個過程,需要等,站在程序的角度說,它是一個耗時的操作,當達到設定水位的時候,才進入漂洗狀態。如果不用協程,我們可能就需要申明一個委托函數,把進入漂洗狀態的函數設置給這個委托,當達到設定水位的時候,調用這個委托函數,即可進入漂洗狀態,這個委托函數就是回調函數。”
類似下面這樣
如果使用協程,則代碼可以簡潔。
using System.Collections; using UnityEngine;// 洗衣機 public class Washer : MonoBehaviour {/// <summary>/// 水位/// </summary>int m_waterLevel;private void Start(){StartCoroutine(StartWasher());}// 啟動洗衣機IEnumerator StartWasher(){// 加水while (true){m_waterLevel += 1;if(m_waterLevel >= 60){break;}yield return null;}// TODO 漂洗} }皮皮:“太酷了,看出來狀態機很適合使用協程來實現。”
我:“是的呀,現在看明白了吧。”
皮皮:“那個yield return null是不是可以看做是等一幀的意思?”
我:“是的,執行yield return null,協程就掛起了,在下一幀Update之后會執行yield null,就會執行協程迭代器的MoveNext,從而繼續執行協程。”
皮皮:“生命周期中有個yield WaitForSeconds,這個WaitForSeconds是等n秒的意思嗎?”
我:“是的,我可以使用它實現一個簡單的延時調用。”
示例:
皮皮:“可以了,我現在需要停下去休息一下,yield return new WaitForSeconds(9999);”
我:“我也要去休息一下了,yield break。”
《學Unity的貓》——第十章:Unity的物理碰撞,流浪喵星計劃
總結
以上是生活随笔為你收集整理的《学Unity的猫》——第九章:状态机与Unity协程,好奇猫与铁皮怪水管的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 交叉编译pcre
- 下一篇: 疫苗接种预约挂号排队通知助手系统开发