#游戏unity#协程
因为对战系统主要实现的是回合制的战斗,是卡牌中常用的方式;而博主从没有制作过回合制的游戏,所以去网上查了回合制游戏的设计思路与整体框架,博主认为回合制游戏最重要的点就是交替攻击,宏观来看,也就是将双方的攻击作为一个循环,当其中一方血量为0时,循环终止,对战结束。在其中,博主偶然发现了一个新鲜的玩意,可以较为直观简洁的实现交替攻击,那就是——协程!这篇博客,博主先详细介绍协程(也都是自己的见解,可能会有些不准确,欢迎交流哦!)
一开始当我看到协程的时候,我首先认为它是跟线程一样的存在,是在主线程执行时进行的一条程序执行的支线,与主线程执行的没有太大关系,就像操作系统实验中做过的线程通信一样的感觉。然而,在经过自己的学习与尝试后,发现,我初次的理解是完全错误的。(好吧,朋友们可是忽略我上面的话了其实…)
协程并不是线程,协程是运行在主线程中的,是和主线程同步执行的代码,不同的地方是运行的方法可以被yield return在当前帧进行打断,到下一帧后可以继续从被打断的地方继续运行。注意它是在每一帧都会被调用的。现在给出一个官网上的解释:
A coroutine is a function that is executed partially and, presuming suitable conditions are met, will be resumed at some point in the future until its work is done.
即协程是一个分部执行,遇到条件(yield return 语句)会挂起,直到条件满足才会被唤醒继续执行后面的代码。Unity在每一帧(Frame)都会去处理对象上的协程。
Unity主要是在Update后去处理协程(检查协程的条件是否满足)。
接下来给大家上一个小小的实例,便于理解,场景中有一个空的GameObject对象,其绑定了下面的脚本——
using UnityEngine;
using System.Collections;
public class Test : MonoBehaviour
{
int frame = 0;
void Start ()
{
this.StartCoroutine(CountDown());
}
void Update ()
{
Debug.Log("Now is frame: " + (++frame));
}
IEnumerator CountDown()
{
Debug.Log("step - 1");
yield return null;
Debug.Log("step - 2");
yield return null;
Debug.Log("step - 3");
yield return null;
Debug.Log("step - 4");
}
}
可以在控台看到他的输出,如下图
当进入Start方法时开始启动协程,这时候协程开始运行,输出“step1”后遇到第一个yield return后暂停本帧的运行,接下来进入Update方法输出“frame1”,由于协程调用是在Update之后,所以第二帧开始后,先执行了第二个Update输出“frame2”,然后从协程的上次暂停处继续执行,输出“step2”后遇到第二个yield return后暂停本帧的运行,如此反复,当输出“step4”后发现方法已经执行完毕,协程结束。
协程不是线程,也不是异步执行的,是Unity每帧LateUpdate()之后都会去处理的函数。
在网上找到的运行图如下
而yield break的效果会立即中断协程的运行。从示例代码也可以看出,协程重要的几个语法就是yield return, IEnumerator 和 Unity StartCoroutine。
好了,博主觉得大家应该对协程有了初步的了解,接下来应该梳理下协程控制的语法结构啦
1.IEnumerator
IEnumerator是枚举类的一个接口,相信接触C#的都知道,问题是很多人分不清 IEnumerator 和 IEnumerable,如下是官网上他们的定义:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
public interface IEnumerator
{
bool MoveNext();
void Reset();
Object Current { get; }
}
我觉得从代码定义上大家也能看出它们一个明显的区别,我的理解是——
1.IEnumerable是一个更“上层”的接口,而它的“下级”是IEnumerator接口,也就是说,离底层实现更接近的是IEnumerator。
2.IEnumerator object具体实现了遍历器iterator(通过MoveNext(),Reset(),Current)。
3.从这两个接口的用词选择上,也可以看出其不同:IEnumerable是一个声明式的接口,声明实现该接口的class是“可枚举(enumerable)”的,但并没有说明如何实现枚举器(iterator);IEnumerator是一个实现式的接口,IEnumerator object就是一个iterator。
需要注意的一点就是只要集合保持不变,枚举数就将保持有效。如果对集合进行了更改(例如添加、修改或删除元素),则该枚举数将失效且不可恢复,并且下一次对 MoveNext 或 Reset 的调用将引发 InvalidOperationException。如果在 MoveNext 和 Current 之间修改集合,那么即使枚举数已经无效,Current 也将返回它所设置成的元素。
因为枚举集合并没有保护好自己的数据集,也无法阻止其他线程对枚举集合数据的修改,所以在一定程度上是不安全的,若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。
2.yield关键字
在迭代器块中用于向枚举数对象提供值或发出迭代结束信号。它的形式为下列之一:
yield return <expression_r>;
yield break;
备注 :
计算表达式并以枚举数对象值的形式返回;expression_r 必须可以隐式转换为迭代器的 yield 类型。
yield 语句只能出现在 iterator 块中,该块可用作方法、运算符或访问器的体。这类方法、运算符或访问器的体受以下约束的控制:
不允许不安全块。
方法、运算符或访问器的参数不能是 ref 或 out。
yield 语句不能出现在匿名方法中。
当和 expression_r 一起使用时,yield return 语句不能出现在 catch 块中或含有一个或多个 catch 子句的 try 块中。
yield return 提供了迭代器一个比较重要的功能,即取到一个数据后马上返回该数据,不需要全部数据装入数列完毕,这样有效提高了遍历效率。
yield return new WaitForFixedUpdate();
等待直到下一个固定帧速率更新函数。
yield return new WaitForEndOfFrame();
等待直到所有的摄像机和GUI被渲染完成后,在该帧显示在屏幕之前。
yield return new WaitForSeconds(1);
在给定的秒数内,暂停协同程序的执行。
3.Unity StartCoroutine
Unity使用 StartCoroutine(routine: IEnumerator): Coroutine 启动协程,参数必须是 IEnumerator 对象。
其实,StartCoroutine(string methodName)和StartCoroutine(IEnumeratorroutine)都可以开启一个协程,
区别:使用字符串作为参数时,开启协程时最多只能传递一个参数,并且性能消耗会更大一点; 而使用IEnumerator 作为参数则没有这个限制。
具体比较深入的理解,推荐一篇博客StartCoroutine
运行时想要终止协程——
1).在Unity3D中,使用StopCoroutine(stringmethodName)来终止该MonoBehaviour指定方法名的一个协同程序,使用StopAllCoroutines()来终止所有该MonoBehaviour可以终止的协同程序。
包括StartCoroutine(IEnumerator routine)的。
2).还有一种方法可以终止协同程序,即将协同程序所在gameobject的active属性设置为false,当再次设置active为ture时,协同程序并不会再开启;
如是将协同程序所在脚本的enabled设置为false则不会生效。
需要注意的一个小发现:
using UnityEngine;
using System.Collections;
public class TestCoroutine : MonoBehaviour {
private bool isStartCall = false; //Makesure Update() and LateUpdate() Log only once
private bool isUpdateCall = false;
private bool isLateUpdateCall = false;
// Use this for initialization
void Start () {
if (!isStartCall)
{
Debug.Log("Start Call Begin");
StartCoroutine(StartCoutine());
Debug.Log("Start Call End");
isStartCall = true;
}
}
IEnumerator StartCoutine()
{
Debug.Log("This is Start Coroutine Call Before");
yield return new WaitForSeconds(1f);
Debug.Log("This is Start Coroutine Call After");
}
// Update is called once per frame
void Update () {
if (!isUpdateCall)
{
Debug.Log("Update Call Begin");
StartCoroutine(UpdateCoutine());
Debug.Log("Update Call End");
isUpdateCall = true;
this.enabled = false;
//this.gameObject.SetActive(false);
}
}
IEnumerator UpdateCoutine()
{
Debug.Log("This is Update Coroutine Call Before");
yield return new WaitForSeconds(1f);
Debug.Log("This is Update Coroutine Call After");
yield return new WaitForSeconds(1f);
Debug.Log("This is Update Coroutine Call Second");
}
void LateUpdate()
{
if (!isLateUpdateCall)
{
Debug.Log("LateUpdate Call Begin");
StartCoroutine(LateCoutine());
Debug.Log("LateUpdate Call End");
isLateUpdateCall = true;
}
}
IEnumerator LateCoutine()
{
Debug.Log("This is Late Coroutine Call Before");
yield return null;
Debug.Log("This is Late Coroutine Call After");
}
}
先在Update中调用 this.enabled = false; 得到的结果:
然后把 this.enabled = false; 注释掉,换成 this.gameObject.SetActive(false); 得到的结果如下:
通过设置MonoBehaviour脚本的enabled对协程是没有影响的,但如果 gameObject.SetActive(false) 则已经启动的协程则完全停止了,即使在Inspector把gameObject 激活还是没有继续执行。
也就是说,我们可以理解为协程函数的地位完全是跟MonoBehaviour是一个层次的,不受MonoBehaviour的状态影响,但跟MonoBehaviour脚本一样受gameObject 控制。
这样解释是不是清楚多了?推荐一篇很棒的学习博客协程理解
期待下次,开始写回合制脚本的博客哟。