学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
如下皮皮:“上面的代码有点看不懂,
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协程(九)
作者:shirlnGame
地址:https://mmzsblog.cn/articles/2021/01/11/1610371333716.html
-----------------------------
如未加特殊说明,此网站文章均为原创。
网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
公众号转载请联系网站首页的微信号申请白名单!
