协程遇到的坑与解决办法

论坛 期权论坛 编程之家     
选择匿名的用户   2021-6-1 08:13   1064   0

相信在unity中用过协程的人肯定都遇到过下面的这个报错


哈哈哈是不是很眼熟,但是知道了问题的原因后就能很好的避免掉了,出现这个原因就是协程开启的时候开启协程的MonoBehavior挂载的物体是未激活状态,知道了问题原因所在就很容易解决了.

    if (gameObject.activeInHierarchy)
        {            
            StartCoroutine(Render());
        }

在开启的地方加一个判断就行了,但是这只是权宜之策,理论上来讲是不能用未激活的物体开启协程的.

在开启的时候判断是否能开启很重要,同样的关闭协程也很重要,要是关闭的不好就会出现下面这个报错
/*emmm关键时候不报错了...大概意思就是这个物体已经销毁了但是你还继续调用它*/
出现这个报错的主要原因就是协程没有适当的关闭引起的,因为协程的yield return机制是把当前任务挂起,当return的内容完成后再执行下面的操作,假设你加载一个物体,当界面关闭的时候再卸载掉,但是在界面关闭的时候你的协程并没有关闭,协程会继续执行,如果再物体已经卸载掉的情况下再调用了物体的组件就会报上面那个错,所以协程的关闭也相当重要.
一般来说当MonoBehavior隐藏的时候应该把协程也相应的停了,在OnDisable的时候调用一下StopAllCoroutines();就可以了

说到关闭还要注意一点,就是协程中开启协程这种情况,一个MonoBehavior只能关闭它自己开启的协程

private class TestA : MonoBehavior
{
    private TestB testB;
    private void OnEnable()
    {
        StartCoroutine(Render());
    }
    private void OnDisable()
    {
        StopCoroutine(Render());
    }
    private IEnumerator Render()
    {
        yield return testB.StartCoroutine(RenderB());
    }
}

public class TestB : MonoBehavior
{
    public IEnumerator RenderB()
    {
        yield return 0;
    }
}

上面这个例子中,在TestA脚本OnDisable中关闭了TestA中开启的协程但是RenderB会继续执行,因为RenderB是TestB开启的,也就是说只有用testB.StopCoroutine(RenderB)才能停止.

再说个怎么在不继承MonoBehavior的脚本中开启和关闭协程,举例子
我们的工程中的奖励机制是通用的,所以我就封装了一个加载奖励的协程,一个通用的加载类,但是没有继承MonoBehavior

public class ShowReward
{
    public ShowReward(Transform transform)
    {
        // 传入物体来查找组件
    }

    protected virtual IEnumerator OnShow(Data data)
    {
        // 渲染
        switch (data.type)
        {
            case 1:
                //展示物品
                break;
            case 2:
                //展示人物
                break;
            default:
                break;
        }
        yield return 0;
    }

    protected virtual IEnumerator OnShowRole()
    {
    }
}

这样是没问题,但是有一天我们的策划说,人物的展示不这么展示了要换一种别的展示方法,我的第一想法是重写OnShow方法,但是只改人物加载的部分其余的都不变重复的代码太多了,所以我决定在提炼出一个方法来


    protected virtual IEnumerator OnShow(Data data)
    {
        // 渲染
        switch (data.type)
        {
            case 1:
                //展示物品
                break;
            case 2:
                //展示人物
                break;
            default:
                break;
        }
        yield return 0;
    }

     protected virtual IEnumerator OnShowRole()
    {
        // 渲染人物
        yield return 0;
    }

那么问题来了,怎么才能开启协程呢,这样的话确实只用重写OnShowRole方法就可以,扩展性强并且还坚持了开放封闭原则,于是我灵机一动,传了一个MonoBehavior进来


    protected virtual IEnumerator OnShow(Data data, MonoBehaviour mono)
    {
        // 渲染
        switch (data.type)
        {
            case 1:
                //展示物品
                break;
            case 2:
                //展示人物
                mono.StartCoroutine(OnShowRole());
                break;
            default:
                break;
        }
        yield return 0;
    }

    protected virtual IEnumerator OnShowRole()
    {
        // 渲染人物
        yield return 0;
    }

完美解决,MonoBehavior最好传入开启OnShow的脚本,这样关闭的时候再MonoBehavior中调用StopAllCoroutines();方法就可以把所有这个脚本开启的协程全都关闭掉了.鸡贼哈哈哈哈.

不时更新,要是大家有啥报错也可以评论区写一下,一起研究下,协程这东西是真有意思,嘿嘿.

关于协程开启关闭不了问题
是这样的就是我在使用StopCoroutine()时遇到的

using UnityEngine;

public class TestCoroutine : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(TestOne());
        print("Started " + Time.time);
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown("space"))
        {
            StopCoroutine(TestOne());
            print("Stopped " + Time.time);
        }
    }

    private IEnumerator TestOne()
    {
        while (true)
        {
            Debug.LogError("running");
            yield return new WaitForSeconds(1.0f);
        }
    }
}

代码大致是这样的,打印的结果是这样的

也就是说在我调用StopCoroutine()方法并没有使协程停下
后来去看了下官方文档,给出的官方例子是用把开启的协程存储到一个IEnumerator或者Coroutine

 private IEnumerator coroutine;
    void Start()
    {
        print("Started " + Time.time);
        coroutine = TestOne();
        StartCoroutine(coroutine);
    }

    void Update()
    {
        if (Input.GetKeyDown("space"))
        {
            StopCoroutine(coroutine);
            print("Stopped " + Time.time);
        }
    }

    private IEnumerator TestOne()
    {
        while (true)
        {
            Debug.LogError("running");
            yield return new WaitForSeconds(1.0f);
        }
    }

Coroutine同理

private Coroutine coroutine;
    void Start()
    {
        print("Started " + Time.time);
        coroutine = StartCoroutine(TestOne());
    }

    void Update()
    {
        if (Input.GetKeyDown("space"))
        {
            StopCoroutine(coroutine);
            print("Stopped " + Time.time);
        }
    }

    private IEnumerator TestOne()
    {
        while (true)
        {
            Debug.LogError("running");
            yield return new WaitForSeconds(1.0f);
        }
    }

然后我又试了一下用方法名的方法,发现也是可以的,唯独是第一种方法不行

void Start()
    {
        print("Started " + Time.time);
        StartCoroutine("TestOne");
    }

    void Update()
    {
        if (Input.GetKeyDown("space"))
        {
            StopCoroutine("TestOne");
            print("Stopped " + Time.time);
        }
    }

    private IEnumerator TestOne()
    {
        while (true)
        {
            Debug.LogError("running");
            yield return new WaitForSeconds(1.0f);
        }
    }


可以看到在调用停止后就结束运行了,至于为什么直接调用方法不行,我个人理解是那肯定是两个IEnumerator的引用不一样
举个例子我可以开启多个TestOne()协程,我每个开启的时调用TestOne()时都会返回一个新的IEnumerator,所以当我在调用StopCoroutine()时TestOne()也是返回一个新的IEnumerator,所以没有关闭我想关闭的那个协程

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:3875789
帖子:775174
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP