《学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
代码如下
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,给他留言或者私信,他看到了会耐心回答你的问题的。”
刚好,这个时候衣服洗好了,我去把衣服拿出来晾好。
我回到屋内时,皮皮转过头说:“查了很多文章,还是没明白协程的准确定义。”
我:“看在你这么认真的态度,我来讲给你听吧。要搞明白协程,需要先理解进程与线程。”
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
让协程退出,如果使用代码的方式,如何强制退出协程呢?”
我:“有两种方式。”
方式一,启动协程是,把迭代器对象缓存起来,
// 启动协程
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:{// TODObreak;}}}// 启动洗衣机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的物理碰撞,流浪喵星计划
《学Unity的猫》——第九章:状态机与Unity协程,好奇猫与铁皮怪水管相关推荐
- unity学习记录第九章-过关和UI
unity学习记录第九章 要点 胜利过关 UI文本 笔记 1.创建一个脚本控制开门动画 2.在GameMannager里设置一个类类型的变量,用来传递方法 3.当列表中的收集物为0时开门通关 4.使用 ...
- Kotlin极简教程:第9章 轻量级线程:协程
原文链接:https://github.com/EasyKotlin 在常用的并发模型中,多进程.多线程.分布式是最普遍的,不过近些年来逐渐有一些语言以first-class或者library的形式提 ...
- 《Kotin 极简教程》第9章 轻量级线程:协程(2)
<Kotlin极简教程>正式上架: 点击这里 > 去京东商城购买阅读 点击这里 > 去天猫商城购买阅读 非常感谢您亲爱的读者,大家请多支持!!!有任何问题,欢迎随时与我交流~ ...
- unity 下一帧执行_理解Unity中的优化(三):协程(Coroutines)
Coroutines: Coroutines与其他脚本代码的执行方式不同.在性能分析中,大多数的脚本代码只会在Unity的生命周期方法下出现一次.但是协程总是会在两个地方出现. 在性能分析中,Coro ...
- 《快学 Go 语言》第 11 课 —— 千军万马跑协程
协程和通道是 Go 语言作为并发编程语言最为重要的特色之一,初学者可以完全将协程理解为线程,但是用起来比线程更加简单,占用的资源也更少.通常在一个进程里启动上万个线程就已经不堪重负,但是 Go 语言允 ...
- 爬虫第四章 单线程+多任务异步协程
单线程+多任务异步协程: asyncio 事件循环 loop: 无限循环的对象,事件循环中最终需要将一些特殊的函数注册到该事件循环中特殊的函数: 被ansyc关键字修饰的函数协程: 本质上是一个对象, ...
- 《学Unity的猫》——第八章:Unity预设文件,无限纸团喷射机
文章目录 8.1 无限纸团喷射机 8.2 预设是什么 8.3 预设的制作 8.4 预设的修改 8.4.1 Open Prefab--进入神秘世界 8.4.2 退出神秘世界 8.4.3 Override ...
- Unity线程与协程
文章目录 前言 一.unity真的不支持多线程吗? 1.unity中使用多线程 2.unity中多线程的停止 3.unity中使用多线程的问题 二.协同程序 1.协程的使用 2.协程的原理 总结 前言 ...
- Unity 面试题汇总(三)Unity 基础相关
Unity 面试题汇总(三)Unity 基础相关 目录 Unity 面试题汇总(三)Unity 基础相关 0.FSM(状态机).HFSM(分层状态机).BT(行为树)的区别 1.什么是协同程序? 2. ...
最新文章
- windows下查看当前进程,杀掉进程等
- 使用Charles在iOS6上进行抓包
- $git学习总结系列(4)——gitignore文件
- netty系列之:netty中的Channel详解
- python中with open写csv文件_Python中的CSV文件使用with语句的方式详解
- python内置函数map_Python内置函数(34)——map
- 双十一重磅福利来袭,拯救 “四大皆空” 的你!
- ELKStack之极速入门(上)
- Spring Boot设置指定包的日志级别
- python DataFrame数据分组统计groupby()函数
- 腾达无线usb wifi 网卡u1/u3 RTL8192EU在centos7.6/7.7的编译适配
- 字体,字号与尺寸对应表
- 2022你不容错过的软件测试项目实战(APP项目实战)免费版
- 图书馆信息管理系统文档
- Easy EDA #学习笔记06# | L9110S H桥2路直流电机驱动板设计(附.4056 充电、过充过放保护电路设计)
- “数据结构”视频资料
- 2024中国科学技术大学计算机考研信息汇总
- Android闹钟APP
- 他人炒股心得,值得借鉴
- Leetcode_584. 寻找用户推荐人