Unity协程的定义、使用及原理,与线程的区别、缺点全方面解析

目录

协程的定义及简介

协程的用途

定时器

将复杂程序分帧执行

等待某些条件完成后执行后续

异步加载资源

协程的原理

MonoBehaviour中每一帧的游戏循环

迭代器 IEnumerator 接口

具体执行过程

协程和线程的区别

协程的缺点

无法返回值

依赖于MonoBehaviour

维护困难与回调地狱


协程的定义及简介

先来看看Unity官方给出的定义:

A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame.

“协程类似于一个具有暂停执行并将控制权返回给Unity的功能的函数,但随后可以在下一帧继续从中断处继续执行。”

Unity中的协程是一种特殊的函数,它允许在执行过程中在特定点暂停,并在以后的时间点(如下一帧)从暂停的地方继续执行。这使得协程成为处理延时、等待和异步操作的强大工具。同时可以避免在主线程上进行长时间的阻塞操作。

协程的用途

定时器

当你需要延时执行一个方法或者是每隔一段时间就执行某项操作时,可以使用协程。

比如        yield return new WaitForSeconds(.1f),可以使得协程

以下是官方的案例:

游戏中的许多任务需要定期执行,最容易想到的方法是将任务包含在 Update 函数中。但是,通常情况下,每秒将多次调用该函数。不需要以这样的频繁程度重复任务时,可以将其放在协程中来进行定期更新,而不是每一帧都更新。这方面的一个示例可能是在附近有敌人时向玩家发出的警报。此代码可能如下所示:
 

bool ProximityCheck()
{
    for (int i = 0; i < enemies.Length; i++)
    {
        if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
                return true;
        }
    }

    return false;
}

如果有很多敌人,那么每帧都调用此函数可能会带来很大开销。但是,可以使用协程,每十分之一秒调用一次:

IEnumerator DoCheck()
{
    for(;;)
    {
        if (ProximityCheck())
        {
            // Perform some action here
        }
        yield return new WaitForSeconds(.1f);
    }
}

将复杂程序分帧执行

如果一个复杂的函数对于一帧的性能需求很大,我们就可以通过yield return null将步骤拆除,从而将性能压力分摊开来,最终获取一个流畅的过程,这就是一个简单的应用。

举一个案例,如果某一时刻需要使用Update读取一个列表,这样一般需要一个循环去遍历列表,这样每帧的代码执行量就比较大,就可以将这样的执行放置到协程中来处理:
 

//伪代码
​
IEnumerator ShowImageFromUrl(string url)
{
    Image image = null;
    yield return LoadImageAsync(url, image); //异步加载图像,加载完成后唤醒协程
    Show(image);
}

等待某些条件完成后执行后续

直到变量满足某些条件

         yield return new WaitUntil(()=> counter <= 0);

直到函数返回真

            yield return new WaitUntil(() => true);

直到某个协程开始执行后

            yield return StartCoroutine(Test());

异步加载资源

资源加载指的是通过IO操作,将磁盘或服务器上的数据加载成内存中的对象。资源加载一般是一个比较耗时的操作,如果直接放在主线程中会导致游戏卡顿,通常会放到异步线程中去执行。

举个例子,当你需要从服务器上加载一个图片并显示给用户,你需要做两件事情:

  1. 通过IO操作从服务器上加载图片数据到内存中。
  2. 当加载完成后,将图片显示在屏幕上。

其中,2操作必须等待1操作执行完毕后才能开始执行。

//伪代码
​
IEnumerator ShowImageFromUrl(string url)
{
    Image image = null;
    yield return LoadImageAsync(url, image); //异步加载图像,加载完成后唤醒协程
    Show(image);
}

以下是较为进阶的内容


协程的原理

基于迭代器:Unity协程的核心是C#的迭代器(Iterator)。迭代器允许函数在执行过程中暂停,并在未来某个时刻从上次暂停的地方继续执行。

yield关键字:在协程中,yield关键字用于指示暂停点。当协程运行到yield语句时,它会返回一个值,然后暂停执行,直到Unity决定继续执行这个协程。

协程的队列:Unity在内部维护了一个协程的执行队列。当你调用StartCoroutine()时,Unity会将协程加入这个队列。

MonoBehaviour中每一帧的游戏循环

协程的执行:在每一帧的游戏循环中,Unity会检查这些协程,并根据它们的yield返回值决定是否执行协程中的下一段代码。

在每一帧的游戏循环中可以见下表:

翻阅Unity官方文档中介绍MonoBehaviour生命周期的部分,会发现有很多yield阶段,在这些阶段中,Unity会检查MonoBehaviour中是否挂载了可以被唤醒的协程,如果有则唤醒它。

上面那段话的英文:

If a coroutine has yielded previously but is now due to resume then execution takes place during this part of the update.

“如果一个协程之前已经暂停了,但现在应该恢复执行,那么它的执行将在更新的这一部分进行。”

迭代器 IEnumerator 接口

在C#中,迭代器是通过实现 IEnumerator 接口来实现的。

IEnumerator接口:协程方法被编译成实现了IEnumerator接口的类。这个接口包含:

MoveNext() 方法:这个方法用于将迭代器推进到下一个元素。在协程中,每次调用 MoveNext() 会执行到下一个 yield 语句或协程结束。

Current属性:这个属性用于获取迭代器当前位置的元素。在协程中,它返回的是当前yield语句返回的对象。例如,考虑以下几种情况:

  • yield return null;:这种情况下,协程将在下一帧的Update方法之前恢复执行。Current属性返回的值是null,这告诉Unity协程在下一帧继续执行。

  • yield return new WaitForSeconds(n);:在这里,yield返回的是一个WaitForSeconds对象。Current属性返回的是这个对象,Unity使用它来确定协程应该在等待指定的秒数后继续执行。

Unity根据这个返回值来决定协程的下一步执行时机和方式。

具体执行过程

有了以上的基础知识后,我们就可以总结出协程的具体执行过程:
游戏循环中的执行:Unity内部维护了一个活动协程的队列。当你通过StartCoroutine启动一个协程时,该协程就会被添加到这个队列中。在每一帧的游戏循环中,Unity会遍历协程队列,调用每个协程的MoveNext()方法。在协程中,每次调用 MoveNext() 会执行到下一个 yield 语句,此时,协程执行yield return语句时,它会将控制权交还给Unity引擎,并暂停协程的执行。Unity随后决定何时再次恢复协程,这取决于yield return后的对象。

  • 如果yield return null,协程会在下一帧继续执行。
  • 如果yield return了一个WaitForSeconds对象,Unity会等待指定的时间再继续执行协程。
  • 其他yield return对象(如WaitForEndOfFrameWaitForFixedUpdate)会告诉Unity在特定的游戏循环阶段执行协程。

协程和线程的区别

是否并行执行:Unity的协程在主线程上执行。它们允许你将任务分解成多个步骤或等待,但这些步骤依然是在主线程上顺序执行的。

而线程是可以并行执行的,可以多线程并行执行。

轻量级:协程通常比线程更轻量级,因为它们不需要分配额外的操作系统资源。线程需要分配内存和操作系统线程资源,而协程只需要Unity的调度器来管理。

适用场景:协程适用于处理与游戏对象、Unity的生命周期和Unity API交互相关的任务,如延迟、动画序列、协作动作等。线程更适合处理需要并行执行的计算密集型任务,如物理模拟、复杂的算法计算等。

安全性:协程在Unity中的执行是协作的,可以安全地访问和修改Unity的游戏对象和组件。线程在多线程环境中需要额外的同步措施来确保数据的安全性,容易引入竞态条件和死锁。

调度和等待

  • 协程:协程可以方便地使用yield语句等待一段时间或等待其他协程完成,这使得状态管理和任务调度更容易。
  • 线程:线程通常需要使用诸如锁、信号量等机制来进行线程同步和等待操作,这可能会更复杂。

协程的缺点

无法返回值

使用协程的时候,协程本身无法像常规函数那样直接返回值。

如果你需要在异步操作完成后获取某个计算结果,需要寻找其他方式来传递这个结果。

  1. 使用回调函数:可以在协程结束时调用一个回调函数,并将结果作为参数传递给这个回调函数。

  2. 共享变量:协程可以修改外部定义的变量,这些变量的新值可以在协程结束后被读取。

  3. 事件系统:通过Unity的事件系统,可以在协程完成某项操作时触发事件,并传递相关数据。

依赖于MonoBehaviour

在Unity中,协程通常是在继承自MonoBehaviour的类中启动和运行的。这是因为StartCoroutineStopCoroutine方法是MonoBehaviour类的一部分。

依赖于 MonoBehavior 有什么不好的地方?就是我们在大型的商业项目当中,通常会自己去开发一些游戏框架。我们知道 Unity 为我们提供了脚本的一个基类叫做 MonoBehavior ,但实际上我们在商业项目开发当中很有可能根本就不从MonoBehavior 继承。

对MonoBehaviour依赖的考量

  1. 灵活性和控制MonoBehaviour是Unity提供的一个基础类,它与Unity的生命周期方法紧密绑定(如StartUpdate)。在某些情况下,开发者可能需要更多的控制权和灵活性,比如实现自定义的生命周期管理或更细粒度的控制。

  2. 性能优化MonoBehaviour会随Unity的生命周期进行调用,哪怕是空的生命周期方法也会占用调用时间。在大型项目中,这可能导致性能问题。自定义的游戏框架可以更精确地控制何时和如何执行这些方法。

  3. 代码组织和架构:大型游戏项目往往需要更复杂的代码组织和架构。直接依赖于MonoBehaviour可能限制了项目架构的设计,特别是在实现模块化和解耦方面。

虽然MonoBehaviour提供了许多方便的特性,但在某些情况下,它可能不适合所有的需求,特别是当涉及到高度定制的游戏架构和性能优化时。

维护困难与回调地狱

协程的异步性质可能使得调试变得更加困难。协程中的错误可能不会立即显现,而是在协程的执行过程中的某个点才出现,这可能导致问题的根源不明显。

回调地狱

"回调地狱"(Callback Hell)是一个编程术语,通常用于描述在异步编程中,多层嵌套的回调函数造成的代码结构复杂、难以理解和维护的情况。

在Unity中,当你过度依赖协程来处理复杂的异步逻辑时,可能陷入类似"回调地狱"的困境。具体表现为:

  1. 深层嵌套:一个协程等待另一个协程完成,而那个协程又等待另一个,这样层层嵌套下去,导致代码结构混乱,难以追踪协程之间的逻辑关系。

  2. 逻辑分散:异步逻辑分散在多个协程中,使得理解整个流程变得困难,尤其是当不同协程散布在不同的MonoBehaviour脚本中时。

  3. 维护困难:每增加一个新的异步步骤或更改现有步骤,都可能需要重新考虑整个协程链的结构,这增加了维护和调试的难度。

  4. 错误处理:在嵌套的协程中正确地处理错误和异常可能变得复杂。如果在协程的某个环节发生了错误或异常,那么这个错误信息需要被有效地传递回到协程调用的起点,以便可以被正确处理。在Unity中,当你使用协程处理异步任务或复杂的逻辑流时,你可能会有多个协程相互调用或嵌套。例如,一个协程等待另一个协程完成某个任务。如果在这个过程中的任何一个环节发生了错误(如一个协程中的操作失败),这个错误信息应该被捕获并传递给原始调用者,这样才能对错误做出适当的响应。

为了避免这种“回调地狱”,可以采取以下措施:

  • 逻辑分解:尽量将复杂的协程逻辑分解成更小、更可管理的部分。
  • 避免过深嵌套:避免无必要的嵌套协程,尽可能使用更直接的逻辑流程。
  • 状态机:对于复杂的异步流程,考虑使用状态机来管理状态转换,而不是深层嵌套的协程。
  • 异步/等待模式:在支持C# 6.0及以上版本的Unity项目中,考虑使用异步/等待(async/await)模式,这是一种更现代的异步编程方法,可以提供更清晰的代码结构。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/267577.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux学习笔记8-Uboot移植-网络设置和其他坑的解决

Linux之所以被称为操作系统&#xff0c;肯定是需要有引导程序来启动各个关键外设的运行&#xff0c;这里可以和个人电脑PC做个类比。我们在开机的时候是不是先要进入BIOS&#xff0c;BIOS在初始化硬盘、内存、USB接口、网口等之后&#xff0c;才可以进入Windows系统对吧&#x…

CUMT--Java复习--文件及IO流

目录 一、文件 1、文件系统和路径 2、File类 3、FilenameFilter接口 二、IO流 1、流的分类 2、流的体系结构 三、字节流 1、InputStream 2、OutputStream 四、字符流 1、Reader 2、Writer 五、过滤流和转换流 1、过滤流 2、转换流 六、序列化 1、对象序列化…

LeetCode 1954. 收集足够苹果的最小花园周长:数学O(1)的做法

【LetMeFly】1954.收集足够苹果的最小花园周长&#xff1a;数学O(1)的做法 力扣题目链接&#xff1a;https://leetcode.cn/problems/minimum-garden-perimeter-to-collect-enough-apples/ 给你一个用无限二维网格表示的花园&#xff0c;每一个 整数坐标处都有一棵苹果树。整数…

DRF之初识

一、序列化和反序列化 api接口开发&#xff0c;最核心最常见的一个过程就是序列化 【1】序列化 把我们能识别的数据结构(python的字典&#xff0c;列表&#xff0c;对象)转换成其他语言(程序)能识别的数据结构。例如&#xff1a; 我们在django中获取到的数据默认是模型对象(…

【论文解读】3D视觉标定的显式文本解耦和密集对齐(CVPR 2023)

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 论文链接&#xff1a;https://arxiv.org/abs/2209.14941 开源代码&#xff1a;https://github.com/yanmin-wu/EDA 图1所示。文本解耦&#xff0c;密集对齐的3D视觉标定。文本中的不同颜色对应不同的解耦分量。…

Windows 11中显示文件扩展名的方法与Windows 10大同小异,但前者更人性化

默认情况下&#xff0c;Windows 11会隐藏已知文件类型的文件扩展名。这可能会使在不首先打开文件的情况下很难识别文件类型。 幸运的是&#xff0c;你可以将Windows 11配置为显示已知文件类型的扩展名。该方法类似于Windows 10&#xff0c;但该选项现在组织在下拉菜单中&#…

[kubernetes]控制平面ETCD

什么是ETCD CoreOS基于Raft开发的分布式key-value存储&#xff0c;可用于服务发现、共享配置以及一致性保障&#xff08;如数据库选主、分布式锁等&#xff09;etcd像是专门为集群环境的服务发现和注册而设计&#xff0c;它提供了数据TTL失效、数据改变监视、多值、目录监听、…

cygwin64路径转换小工具

文章目录 cygwin64路径转换小工具改善效果实现函数END cygwin64路径转换小工具 改善 在cygwin64做实验呢, 用VSCODE自己加的cygwin64的启动命令行作为控制台. 如果是一个比较长的windows路径, 输入起来真的烦. 做了一个就几句代码的小工具, 让输入路径时, 可以从工具上拷贝到…

2023.12.22 关于 Redis 数据类型 String 常用命令

目录 引言 String 类型基本概念 SET & GET SET 命令 GET 命令 MSET & MGET MSET 命令 MGET 命令 SETNX & SETEX & PSETEX SETNX 命令 SETEX 命令 PSETEX 命令 计数命令 INCR 命令 INCRBY 命令 DECR 命令 DECRBY 命令 INCRBYFLOAT 命令 总结…

代码随想录27期|Python|Day24|回溯法|理论基础|77.组合

图片来自代码随想录 回溯法题目目录 理论基础 定义 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。 回溯是递归的副产品&#xff0c;只要有递归就会有回溯。回溯函数也就是递归函数&#xff0c;指的都是一个函数。 基本问题 组合问题&#xff08;无序&…

Spring(3)Spring从零到入门 - Spring整合技术及AOP事务管理

Spring&#xff08;3&#xff09;Spring从零到入门 - Spring整合技术及AOP事务管理 文章目录 Spring&#xff08;3&#xff09;Spring从零到入门 - Spring整合技术及AOP事务管理4 Spring整合技术示例4.1 Spring整合Mybatis4.1.1 Mybatis开发回顾4.1.2 整合Spring分析4.1.3 Spri…

AI项目十九:YOLOV8实现目标追踪

若该文为原创文章&#xff0c;转载请注明原文出处。 主要是学习一下实现目标追踪的原理&#xff0c;并测试一下效果。 目的是通过YOLOV8实现人员检测&#xff0c;并实现人员追踪&#xff0c;没个人员给分配一个ID&#xff0c;实现追踪的效果。 也可以统计人数。在小区办公楼…

JavaScript中的prototype和_proto_的关系是什么

JavaScript中的prototype和_proto_的关系是什么 __proto__ 是 JavaScript 中对象的一个内部属性&#xff0c;它指向该对象的原型。JavaScript 中每个对象都有一个 __proto__ 属性&#xff0c;通过它可以访问对象的原型。prototype 是函数对象特有的属性&#xff0c;每个函数都…

5. 创建型模式 - 单例模式

亦称&#xff1a; 单件模式、Singleton 意图 单例模式是一种创建型设计模式&#xff0c; 让你能够保证一个类只有一个实例&#xff0c; 并提供一个访问该实例的全局节点。 问题 单例模式同时解决了两个问题&#xff0c; 所以违反了单一职责原则&#xff1a; 保证一个类只有一…

C语言——关于数据在内存中存储的练习

大家好&#xff0c;我是残念&#xff0c;希望在你看完之后&#xff0c;能对你有所帮助&#xff0c;有什么不足请指正&#xff01;共同学习交流 本文由&#xff1a;残念ing原创CSDN首发&#xff0c;如需要转载请通知 个人主页&#xff1a;残念ing-CSDN博客&#xff0c;欢迎各位→…

gitattributes配置文件的作用

0 Preface/Foreword Git版本管控工具功能强大&#xff0c;在使用过程中&#xff0c;在多人合作的项目开发过程中&#xff0c;经常会遇到提交代码时出现的warning提醒&#xff0c;尤其是换行符。 Linux/Unix/Mac OS操作系统的换行符使用LF符号&#xff08;\n&#xff09;&…

基于python的selenium

一.安装 安装WebDriver 查看chrome版本号&#xff0c;设置-帮助-关于Google chrome&#xff0c;找到版本号。 可以到这个网站进行下载对应版本的chromedriver,如果chrome浏览器版本过高,可以下载最新版的chromedriver进行使用 Chrome for Testing availability 下载下来之后…

(附源码)SSM银行账目管理平台 计算机毕设43684

摘 要 当今社会已进入信息社会时代&#xff0c;信息已经受到社会的广泛关注&#xff0c;被看作社会和科学技术发展的三大支柱(材料,能源,信息)。信息是管理的基础&#xff0c;是进行决策的的基本依据。在一个组织里信息已作为人力,物力,财力之外的第四种能源,占有重要的地位。然…

Simulink元件

constant元件 输出常数/标量 这样我们就只输出了一个常数 输出一维数组/矢量 这样我们就输出了1-5一共5个数字 输出二维数组 这样我们就输出了4个数字 选择框Interpret vector parameters as 1-D 如果标量或者矩阵&#xff0c;勾与不勾都一样。 如果是向量&#xff0c;勾选…