草庐IT

Unity的协程详解

vinkey_st 2023-05-22 原文

一、协程的定义

协程,即为协同程序. Unity中的协程由协程函数和协程调度器两部分构成.协程函数使用的是C#的迭代器, 协程调度器则利用了MonoBehaviour中的生命周期函数来实现. 协程函数实现了分步, 协程调度器实现了分时. 

注:因为协程分时分步执行的特性,当多个协程的耗时操作挤在同一时间执行也会造成卡顿。

二、协程的用法

using System.Collection;
using UnityEngine;

// 定义一个协程函数,返回一个迭代器接口
IEnumerator CoroutineFunc()
{
    Debug.Log("第一次进入");
    yield return null;
    Debug.Log("第二次进入");
    yield return null;
}

// 在继承自MonoBehaviour的类中调用此协程函数
void Start()
{
    // 获取迭代器接口
    IEnumerator enumerator = CoroutineFunc();
    // 返回的Coroutine对象保存起来可用于停止协程
    Coroutine coroutine = StartCoroutine(enumerator);
    // 相当于在外部 yield break;
    StopCoroutine(coroutine);
}

三、Unity规定的协程返回值的含义

含义代码
下一帧再执行后续代码yield return null;  yield retun x(x代表任意数字)
结束该协程yield break;
等待固定时间执行后续代码

yield return new WaitForSeconds(0.3f);

yield return new WaitForSecondsRealtime(0.3f); //不受timescale影响

函数执行完毕后执行后续代码yield return FunctionName();
异步执行完毕后执行后续代码yield return AsyncOperation;
协程执行完毕后执行后续代码yield return Coroutine;
帧渲染完成后执行后续代码yield return new WaitForEndOfFrame();
物理帧更新后执行后续代码yield return new WaitForFixedUpdate();
参数为true时执行后续代码yield return new WaitUntil(arg);
参数为false时执行后续代码yield return new WaitWhile(arg);

注: 为了优化性能,yield return 后面需要new的返回值应该预先创建, 而不是在协程函数中反复创建.

各个 yield return 在生命周期的位置

四、协程函数与普通函数的区别

操作协程函数普通函数
返回值可分步返回多次只能返回一次
获取返回值的方式调用后执行MoveNext(),通过Current属性获取当前返回值;调用函数;
返回顺序  根据实际情况交错返回根据调用顺序返回

注:执行协程函数返回的是一个迭代器接口而并非得到结果

五、协程与多线程的联系与区别

区别:

协程多线程
切换时机自定CPU时间片为单位的系统调度
CPU核心与主线程在同一核心根据操作系统调度不同
对主线程的影响卡顿会影响主线程卡死都不会影响主线程
线程同步问题不存在线程同步问题需要注意线程同步问题
线程开销不存在线程开销存在线程创建、销毁、切换的开销
书写方式与普通函数一致回调函数

联系:

协程与多线程都是异步操作,都是为了提高CPU的利用率存在的。

六、Unity协程的原理

using System;
using System.Collection;
using System.Collection.Gernic;
using UnityEngine;

// 感谢唐老师的指导

public class YieldInstruction
{
    public IEnumerator ie;
    public float executeTime;
}



public class CoroutineMgr : MonoBehaviour
{
    private List<YieldInstruction> list = new List<YieldInstruction>();

    public void StartCoroutine(IEnumerator ie)
    {
        ie.MoveNext();
        if((ie.Current is null) || (ie.Current is int))
        {
            list.Add(new YieldInstruction{ ie=ie,executeTime=0; });
        }
        else if(ie.Current is WaitForSeconds)
        {
            list.Add(new YieldInstruction{ 
                ie=ie,
                executeTime=Time.time+(ie.Currentas WaitForSeconds).second });
        }
        else if (...)
        {...}
    }



    void Update()
    {
        // 倒序遍历方便移除
        for(int i=list.Count-1; i>=0; i--)
        {
            if(list[i].executeTime<=Time.time)
            {
                if(list[i].ie.MoveNext())
                {
                    // 如果是已定义的类型
                    if((ie.Current is null) 
                    || (ie.Current is int)) 
                    || (ie.Current is WaitForSeconds))
                    {
                        // 继续指定执行时机
                    }
                    else
                    {
                        list.RemoveAt(i);
                    }
                }
                else
                {
                    list.RemoveAt(i);
                }
            }
        }
    }
}

七、Unity协程的垃圾的来自何处

在我尚不了解协程的时候, 用协程制作了真炎幸魂的终极技能:八重火垣. 因在协程中循环使用协程, 在我顾忌协程是否有坏处的时候, 发现了这么一句话:

协程的坏处:
协程本质是迭代器,且是基于unity生命周期的,大量开启协程会引起gc

这句话对我小小的心灵造成了巨大的伤害:

"我的杰作刚出生就要宣判死刑了吗!!!!!  不要!!!!!!!   嘤嘤嘤~~~"

即使是现在的我, 依然不太能理解这句话. 一连串的问题从我脑海中冒出来了.

这GC是协程的问题还是大量的问题?

本质是迭代器是坏处? 基于unity声明周期是坏处? 大量开启是协程的问题不是使用者的问题?

同样的东西协程就比普通函数更容易GC吗?

不是只有引用类型会GC吗?

基于Unity生命周期就会GC吗?

垃圾到底在哪里?

垃圾的产生无法控制吗?

这前言不搭后语的一句话真是让人误会, 而且我还发现多处地方都引用了这句话, 却完全没人把这句话说明白.


疑惑存在, 实验开始.

1.当前版本的Unity使用协程是否会产生GC

实验代码:

public class TestMono : MonoBehaviour
{
    public int times = 100000;
    public bool useStr = true;

    void Start()
    {
        for (int i = 0; i < times; i++)
        {
            if (useStr)
                StartCoroutine("CoroutineFunc");
            else
                StartCoroutine(CoroutineFunc());
        }
    }

    IEnumerator CoroutineFunc()
    {
        yield return null;
    }
}

实验数据取 Unity Profiler 5秒内 GC Used Memory 最高点

项目/实验次数123456
不启动协程13.1M11.8M12.2M12.1M12.4M12.4M
传入字符串24.0M28.0M27.7M28.1M28.4M28.2M

传入IEnumerator

28.6M28.2M28.7M28.5M28.6M28.5M

小结: 使用协程的确会产生垃圾, 且两种方式产生的内存不相上下.

2. 协程的垃圾产生在迭代器还是调度器?

由于调度器调度不存在的函数会报错, 所以只能从迭代器入手进行测试.

实验代码:

public class TestMono : MonoBehaviour
{
    public int times = 100000;
    public bool useStr = true;


    // Start is called before the first frame update
    void Start()
    {
        for (int i = 0; i < times; i++)
        {
            if (useStr)
                enumerator = "CoroutineFunc";
            else
                CoroutineFunc();
        }
    }

    IEnumerator CoroutineFunc()
    {
        yield return null;
    }
}
项目/实验次数123456
不启动协程11.9M12.0M12.0M12.2M11.7M12.0M
传入字符串12.7M11.8M12.2M12.2M12.2M12.4M

传入IEnumerator

12.4M12.5M12.4M12.6M12.5M12.6M

小结: 通过对比一阶段的数据基本可以确定产生垃圾的主要位置在调度器. 但同时能发现迭代器对象也会造成微量的垃圾.

3. 是否只是单纯的使用调度器就会产生大量GC?

实验代码:

public class TestMono : MonoBehaviour
{
    public int times = 100000;
    private IEnumerator enumerator;


    // Start is called before the first frame update
    void Start()
    {
        enumerator = CoroutineFunc();
        for (int i = 0; i < times; i++)
        {
            StartCoroutine(enumerator);
        }
    }

    IEnumerator CoroutineFunc()
    {
        yield return null;
    }
}
项目/实验次数123456
不启动协程17.5M15.9M16.5M16.1M16.5M16.6M

传入IEnumerator

17.4M17.0M17.5M16.9M17.4M17.5M

小结: 只是单纯的调用StartCoroutine并不会产生大量垃圾, 那么第一轮的垃圾应该是一个可以遍历的迭代器和调度器共同作用产生的. 可惜IEnumerator禁止使用Reset, 不然可以测试的更全面.

结论

使用协程时会产生垃圾, 且此垃圾不可控制.

原因: 协程函数的调用会实例化一个接口对象, 而接口是引用类型.

         StartCoroutine对协程的调用会不可避免的产生较多垃圾.

         迭代器对象无法Reset, 想要重复执行相同逻辑只能再次创建迭代器对象.

有关Unity的协程详解的更多相关文章

  1. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  2. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  3. unity---接入Admob - 2

    目录1.AdmobSDK下载地址2.将下载好的unityPackagesdk导入到unity里​编辑 3.解析依赖到项目中

  4. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  5. Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板 - 2

    写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c

  6. 三分钟集成 TapTap 防沉迷 SDK(Unity 版) - 2

    三分钟集成Tap防沉迷SDK(Unity版)一、SDK介绍基于国家对上线所有游戏必须增加防沉迷功能的政策下,TapTap推出防沉迷SDK,供游戏开发者进行接入;允许未成年用户在周五、六、日以及法定节假日晚上8:00-9:00进行游戏,防沉谜时间段进入游戏会弹窗进行提示!开发环境要求:Unity2019.4或更高版本iOS10或更高版本Android5.0(APIlevel21)或更高版本🔗Unity集成Demo参考链接🔗UnityTapSDK功能体验APK下载链接二、集成前准备1.创建应用进入开发者后台,按照提示开始创建应用;2.开通服务在使用TDS实名认证和防沉迷服务之前,需要在上面创建的应

  7. 【Unity大气散射】GAMES104:3A中如何实现大气散射 - 2

    写在前面前两天学习并整理的大气散射基础知识:【Unity大气渲染】关于单次大气散射的理论知识,收获了很多,但不得不承认的是,这其实已经是最早的、90年代的非常古老的方法了,后来也出现了一些优化性的计算思路和方法。因此,我打算先不急着跟各种教程在Unity中实现大气散射,而是再花时间来看看最近的游戏是如何去实现大气渲染的:06.游戏中地形大气和云的渲染(下)|GAMES104-现代游戏引擎:从入门到实践接下来就跟着GAMES104讲地形大气和云渲染的部分学习并做简单的记录,涉及到之前没提到的Mie散射也只选择直接截图PPT的方式记录啦!毕竟对于做作品来说,之后实现出来才是重要的~当然,May佬的

  8. 物联网MQTT协议详解 - 2

    一、什么是MQTT协议MessageQueuingTelemetryTransport:消息队列遥测传输协议。是一种基于客户端-服务端的发布/订阅模式。与HTTP一样,基于TCP/IP协议之上的通讯协议,提供有序、无损、双向连接,由IBM(蓝色巨人)发布。原理:(1)MQTT协议身份和消息格式有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者。MQTT传输的消息分为:主题(Topic)和负载(payload)两部分Topic,可以理解为消息的类型,订阅者订阅(Su

  9. Tcl脚本入门笔记详解(一) - 2

    TCL脚本语言简介•TCL(ToolCommandLanguage)是一种解释执行的脚本语言(ScriptingLanguage),它提供了通用的编程能力:支持变量、过程和控制结构;同时TCL还拥有一个功能强大的固有的核心命令集。TCL经常被用于快速原型开发,脚本编程,GUI和测试等方面。•实际上包含了两个部分:一个语言和一个库。首先,Tcl是一种简单的脚本语言,主要使用于发布命令给一些互交程序如文本编辑器、调试器和shell。由于TCL的解释器是用C\C++语言的过程库实现的,因此在某种意义上我们又可以把TCL看作C库,这个库中有丰富的用于扩展TCL命令的C\C++过程和函数,所以,Tcl是

  10. ruby - 使用 Ruby 编写 Unity 游戏 - 2

    所以我看到unity支持c#、JS和Boo。我可以学习其中一个,但我想制作一个“编译器”或类似的东西,让我可以编写ruby​​代码并输出JS代码或制作一个可以被Unity编译器读取的层。这有可能吗?我愿意在这方面投入很多时间并且有相当多的经验。 最佳答案 如果您的问题实际上是“我如何将Ruby编译为JavaScript”,那么这更容易回答:Opal:RubytoJavaScriptcompiler但是,学习其中一种受支持的语言会更好。当运行的是用另一种语言解释的代码时,很难调试“您的”代码。

随机推荐