#游戏unity#协程

26 篇文章 1 订阅

#游戏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 控制。
这样解释是不是清楚多了?推荐一篇很棒的学习博客协程理解
期待下次,开始写回合制脚本的博客哟。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值