微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

《学Unity的猫》——第九章:状态机与Unity协程,好奇猫与铁皮怪水管

简介:我是一名Unity游戏开发工程师,皮皮是我养的猫,会讲人话,它接到了喵星的特殊任务:学习编程,学习Unity游戏开发。
于是,发生了一系列有趣的故事。

在这里插入图片描述

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

如下

在这里插入图片描述


皮皮:“上面的代码有点看不懂,StartCoroutineIEnumeratoryield 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生命周期的一部分。

在这里插入图片描述


皮皮:“我记得FixedUpdateUpdateLateUpdate这三个函数,上次你讲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底层帮我们调用的,就像MonoBehvavIoUrUpdate函数一样。”
皮皮:“那如果我把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让协程退出,如果使用代码的方式,如何强制退出协程呢?”
我:“有两种方式。”
方式一,启动协程是,把迭代器对象缓存起来,

// 启动协程
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的物理碰撞,流浪喵星计划

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐