學Unity的貓之狀態機與Unity協程(九)

2021-02-19 愛上遊戲開發
9.1 會吐水的鐵皮怪

我把衣服丟進洗衣機裡,倒入洗衣粉,調節水量,按了速洗,啟動。皮皮豎著尾巴跟過來,我伸了個懶腰回到電腦前繼續寫文章。不久,聽到水聲譁譁譁地流,不祥的預感。我趕緊起身去看,水漫金山了。 

「手欠貓!又把洗衣機的水管掏出來了!」 

看了眼皮皮幼稚的圓臉,算了算了。 

皮皮:「這個鐵皮怪為什麼可以一次性吐那麼多水出來?」 

我一臉黑線:「這個叫洗衣機,它的功能就是洗衣服,水是從上面進水口進來的。」 

皮皮舔舔自己的腳毛,仿佛在質疑洗衣機。

9.2 狀態機是什麼

我拿出紙和筆,畫了洗衣機的狀態圖。我:「你可以把洗衣機看成是一個有限狀態機。」 

皮皮:「什麼是有限狀態機?」 

我:「有限狀態機是一種數學模型,英文全稱是Finite State Machine,縮寫FSM,簡稱狀態機,它是現實事物運行規則抽象而成的一個數學模型。」我繼續講:「看這裡,洗衣機有幾個狀態:開始、進水、漂洗、排水、脫水、結束。這些狀態由一系列事件來驅動,比如按啟動按鈕,開始進水,水位達到目標水位,進入漂洗狀態,正轉5秒,停2秒,反轉5秒,停2秒,循環執行10次,然後進入排水狀態,達到最低水位,進入脫水狀態,脫水30秒,接著又回到進水狀態,重複上述流程3次,最終結束。」 

皮皮:「哇,好複雜,它也是程序控制的嗎?」

我:「是的呀,我們可以用代碼寫一個簡單的狀態機。」

9.3 使用協程實現狀態機

我打開Unity,創建了一個腳本CoroutineTest.cs。CoroutineTest.cs代碼如下

using System.Collections;
using UnityEngine;

public class CoroutineTest : MonoBehaviour
{
    /// <summary>
    /// 當前狀態
    /// </summary>
    private int m_state;

    void Start()
    {
     // 設置初始狀態
        m_state = 0;
        // 使用協程啟動狀態機
        StartCoroutine(TestFSM());
    }

    /// <summary>
    /// 使用協程實現一個簡單的狀態機
    /// </summary>
    /// <returns></returns>
    private IEnumerator TestFSM()
    {
        Debug.Log("初始狀態:" + m_state);
        while (true)
        {
            switch (m_state)
            {
                case 0:
                    {
                     // 檢測空白鍵是否按下
                        if (Input.GetKeyDown(KeyCode.Space))
                        {
                            Debug.Log("按下了空白鍵,狀態切換: 0->1");
                            m_state = 1;
                        }
                    }
                    break;
                case 1:
                    {
                     // 檢測空白鍵是否按下
                        if (Input.GetKeyDown(KeyCode.Space))
                        {
                            Debug.Log("按下了空白鍵,狀態切換: 1->0");
                            m_state = 0;
                        }
                    }
                    break;
            }
            yield return null;
        }
    }
}

將腳本掛到Main Camera上,點擊運行。輸出了

初始狀態:0

如下按一下空白鍵,輸出了

按下了空白鍵,狀態切換: 0->1

如下再按一下空白鍵,輸出了

按下了空白鍵,狀態切換: 1->0

如下皮皮:「上面的代碼有點看不懂,StartCoroutine、IEnumerator、yield return null是什麼?」 

我:「上面用到了Unity的協程。」 

皮皮:「你之前都沒教我協程,直接一上來就寫我看不懂的代碼,不厚道。」 

我:「程式設計師是一個不斷學習和成長的職業,實際項目中遇到一些沒學過的東西很正常,特別是現在這個知識爆炸的時代。不懂就查,自學能力是程式設計師最重要的能力之一,不要總是依賴別人教你。」 

我心想會不會有點過分,皮皮只是拔了洗衣機的水管。沒想到皮皮很認真地點了點頭,然後望著我呆呆地問:「怎麼查?」我的錯,我之前沒教過皮皮如何使用搜尋引擎。我打開CSDN,說:「以後你有問題可以在CSDN搜索,我給你註冊個帳號,實在不懂,你就訪問這個人的博客 https://blog.csdn.net/linxinfa,給他留言或者私信,他看到了會耐心回答你的問題的。」剛好,這個時候衣服洗好了,我去把衣服拿出來晾好。

我回到屋內時,皮皮轉過頭說:「查了很多文章,還是沒明白協程的準確定義。」 

我:「看在你這麼認真的態度,我來講給你聽吧。要搞明白協程,需要先理解進程與線程。」

皮皮:「微信搜索公眾號 [愛上遊戲開發],回復 「資料」,免費領取 200G 學習資料!」

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。」

public interface IEnumerator
{
    object Current { get; }

    bool MoveNext();
    void Reset();
}

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讓協程退出,如果使用代碼的方式,如何強制退出協程呢?」 

我:「有兩種方式。」方式一,啟動協程是,把迭代器對象緩存起來,

皮皮:「微信搜索公眾號 [愛上遊戲開發],回復 「資料」,免費領取 200G 學習資料!」

// 啟動協程
var itor = TestCoroutine();
StartCoroutine(itor);

然後我們就可以使用StopCoroutine方法來強制退出協程了。

// 退出協程
StopCoroutine(itor);

方式二,是在協程內部執行yeild break。

IEnumerator TestCoroutine()
{
    while(true)
    {
        Debug.Log("Coroutine is running");
        // yield break會直接退出協程
        yield break;
    }
 Debug.Log("這裡永遠不會被執行到");
}

9.5.5 協程的主要應用

我:「協程的方便之處就是可以使用看似同步的寫法來寫異步的邏輯,這樣可以避免大量的委託回調函數。」 

皮皮:「什麼是回調函數?」 

我:「舉個例子,剛剛洗衣機的狀態圖還記得嗎,進水是一個過程,需要等,站在程序的角度說,它是一個耗時的操作,當達到設定水位的時候,才進入漂洗狀態。如果不用協程,我們可能就需要申明一個委託函數,把進入漂洗狀態的函數設置給這個委託,當達到設定水位的時候,調用這個委託函數,即可進入漂洗狀態,這個委託函數就是回調函數。」類似下面這樣

using UnityEngine;

// 洗衣機
public class Washer : MonoBehaviour
{
    public enum WASHER_STATE
    {
        /// <summary>
        /// 準備
        /// </summary>
        INIT,
        /// <summary>
        /// 加水
        /// </summary>
        ADD_WATER,
        /// <summary>
        /// 漂洗
        /// </summary>
        POTCH
    }

    /// <summary>
    /// 狀態
    /// </summary>
    private WASHER_STATE m_state;

    /// <summary>
    /// 飄洗的委託
    /// </summary>
    System.Action m_potchDelegate;
    /// <summary>
    /// 水位
    /// </summary>
    int m_waterLevel;

    private void Start()
    {
        StartWasher();
    }

    void Update()
    {
        switch (m_state)
        {
            case WASHER_STATE.ADD_WATER:
                {
                    m_waterLevel += 1;
                    // 判斷是否達到水位
                    if (m_waterLevel >= 60)
                    {
                        // 調用漂洗委託
                        if(null != m_potchDelegate)
                        {
                            m_potchDelegate();
                        }
                    }
                }
                break;
            case WASHER_STATE.POTCH:
                {
     // TODO
                    break;
                }
        }
    }

    // 啟動洗衣機
    void StartWasher()
    {
        // 把漂洗函數賦值給委託
        m_potchDelegate = Potch;

        m_state = WASHER_STATE.INIT;

        // 加水
        AddWater();
    }

    // 進水
    void AddWater()
    {
        // 進入進水狀態
        m_state = WASHER_STATE.ADD_WATER;
    }

    // 漂洗
    void Potch()
    {
        // 進入漂洗狀態
        m_state = WASHER_STATE.POTCH;
    }
}

如果使用協程,則代碼可以簡潔。

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秒的意思嗎?」 

我:「是的,我可以使用它實現一個簡單的延時調用。」示例:

IEnumerator DelayCallTest()
{
    Debug.Log("測試 WaitForSeconds");
    yield return new WaitForSeconds(3);
    Debug.Log("這裡會在3秒後被執行");
}

皮皮:「可以了,我現在需要停下去休息一下,yield return new WaitForSeconds(9999);」 

我:「我也要去休息一下了,yield break。」

相關焦點

  • 學Unity的貓之Unity預設(八)
    8.1 無限紙團噴射機皮皮特別喜歡玩紙團,我也特意詢問了幾個養貓的朋友,發現貌似所有的貓都天生對紙團沒有抵抗力。無聊時抽出一張紙,揉成一團,咻一聲扔出去,皮皮就會變成一道閃電跟著飛出去,撿到紙團又叼回來。時間久了,即使我手上沒有紙團,只是用嘴發出咻咻咻的聲音,皮皮也會兩眼冒金光四處張望尋找紙團的蹤跡,屢試不爽。
  • unity什麼意思
    unity什麼意思uni前綴,只包含一個的,更多例子還有:uniform, unique, unilateral, etc. 發音類似於有你,整個世界中有你就夠了,不需要別人,也就是只包含一個的。ity,常見的名詞後綴,表示某種性質或狀態,更多例子還有:quantity, elasticity, productivity, etc.unity,聯合、統一、團結、和睦。學單詞,只記住意思可不行,會用才行,小夥伴們可以在評論區造句,我們一起學習哦!我先來:造句:Unity is strength. (團結就是力量)
  • Unity基礎之物理引擎
    我們在unity裡面建了一個正方體cube , 要如何使這個cube可以跟現實中的物體一樣受重力呢 ? 這就需要用到Rigidbody(剛體)組件了 .單擊cube , 在右邊的Inspector面板添加Rigidbody組件添加完Rigidbody組件後,cube1就可以受重力影響了,運行unity時,cube1會因為受到重力往下落.下面為大家介紹 Rigidbody 組件常用參數 : 1. Mass : 物體的質量 .
  • Unity 實用技巧 - 物理系統初識
    Sleeping:當剛體的速度遠低於一個設定的最小線性速度時,物理引擎就會認為它陷入睡眠狀態。此時,GO不會再次移動,直到它收到一個訊號。這種模式意味著處理器不會花費時間更新剛體狀態。可以使用WakeUp函數來喚醒GameObject。
  • Unity 2018.3 Beta版發布
    直播課程:Facial AR Remote面部捕捉解決方案課程(第一期)直播地址:https://connect.unity.com/events/unitychina-facialarUnity官方教師培訓報名火熱進行中Unity將在10月22-26日,舉辦為期5天的專業的Unity官方教師培訓課程,誠邀廣大教師與
  • 在unity中用C#連接資料庫步驟
    所以本文就介紹一下unity連MySQL資料庫所遇到的一些坑。unity連接資料庫,首先你需要導入如圖所示的五個數據連結庫。在本圖中,歐陽講他們放到了Mysql文件夾下,在這裡歐陽強調一下――最好將他們放到Plugins文件夾下,不然會出現莫名其妙的問題哦。
  • Unity編碼篇 Rigidbody類
    >AddForce 方法 : 給物體加一個瞬時的力 , 物體受這個力運動 (扔手雷的時候就是用這個方法) ;AddTorque 方法 : 給物體添加一個扭矩 (這個方法用的較少) ;Sleep 方法 : 使物體進入休眠狀態
  • unity實戰之大主宰
    然後回到unity,點擊windows ->package manager,就能找到自己的資源,全部import這裡是獲取場景後的結果:通關視頻展示請移步:通關視頻https://www.bilibili.com/video/BV1hf4y1i7N8/項目代碼和資源連結:項目傳送門 (資源太多了,直接發的壓縮包)https://gitee.com/lhyyes/unity-game-homework/tree/master
  • 使用unity製作RPG遊戲3——2D精靈
    context=%7B%22nid%22%3A%22news_9564882242542237691%22%2C%22sourceFrom%22%3A%22bjh%22%2C%22url_data%22%3A%22bjhauthor%22%7D下面需要利用Tiled2Unity把01地圖導入unity下載Tiled2Unity,在根目錄下打開可執行文件進入unity。
  • 2021新年匯總:Unity項目原型快速開發資源,看這一篇就夠
    Example Game : https://assetstore.unity.com/packages/templates/flappy-bird-style-example-game-80330 Tower Defense Template : https://assetstore.unity.com/packages/essentials
  • Unity項目開發過程中常見的問題,你遇到過嗎?
    最近看到有朋友問一個unity遊戲開發團隊,需要掌握哪些知識之類的問題。
  • 博主營地 | Unity3D 實用技巧 - 理論知識庫(一)
    發稿入口:unity.cn/articles  更多Unity博主專屬權益見文末  其實學習Unity3D引擎,我們除了懂得實操,同時兼顧理解一些理論知識,也是挺不錯的積累。今天小編會把常用的一些理論問題匯總起來,後面可以做一個快速的記憶和學習。  請簡述值類型與引用類型的區別。
  • 王者榮耀是怎樣煉成的(三)unity組件與腳本
    上回書王者榮耀是怎樣煉成的(二)《王者榮耀》unity安裝及使用的小白零基礎入門 說到了unity的基本操作。
  • Unity3D 尋路系統
    https://docs.unity3d.com/Manual/class-NavMeshAgent.html四、組件Nav Mesh Obstacle的使用http://docs.unity3d.com/Manual
  • 【大咖專欄】Unity AR Foundation 平面檢測實戰
    ARSession提供了一個靜態的協程方法CheckingAvailability,可以查詢設備是否支持AR。下面的代碼自定義一個腳本組件用來查詢設備是否支持AR,注意Start函數的返回值修改為IEnumerator,以實現異步查詢。
  • unity遊戲製作初始人物控制代碼
    大家好,今天小編帶大家學習一哈unity遊戲製作中初始人物控制代碼。1.我們知道遊戲中,選中人物,在人物未開始運動前,往往會有一個初始的動作,好的,我們這節課通過unity中相關代碼和基礎設置來實現這一效果。
  • unity業餘愛好者說一下
    unity業餘愛好者說一下,這幾天傳的關於《太吾繪卷》代碼的事幾乎都是無中生有的事...一群用.net和vs做工程的人談論第三方引擎做的遊戲...真是雞同鴨講。太吾繪卷現在針對幾個常見誤會說一下1.只有一個main (x)unity的腳本都是依附於各個精靈的,沒有main,只有update2.沒有注釋(x)你反編譯出來的代碼有注釋
  • Unity編碼篇 Mathf類和Random類
    Time.fixedTime) * 0.06f, 0, 0);// 小球變色transform.GetComponent<MeshRenderer>().material.color = new Color(0, Mathf.Sin(Time.fixedTime) * 0.5f, 0, 1);}}然後我們在unity
  • 使用Unity 粒子系統實現 2D 人物足跡效果
    之後深入一想腳本大概要怎麼寫,感覺頭大的不行,又想到了 Hierarchy 裡面被 FootStep12345678 塞得滿滿當當的壯麗場面..還是換個方法吧 轉念一想:unity 的粒子系統似乎可以滿足所有特性:生成淡出銷毀全自動,跟隨人物也是基本操作,比較存疑的就是用腳本控制粒子的鏡像與旋轉。
  • 【官方說明】Unity 5 引擎專業版和個人版解析
    下載:Unity 5 Personal Edition(個人版)和Professional Edition(專業版)今天可在 http://unity3d.com/cn/get-unity 立即下載。提問:如您有任何問題請在中文官方論壇中參與討論,或者聯繫:chinasales@unity3d.com