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

news2025/2/24 2:16:46

目录

协程的定义及简介

协程的用途

定时器

将复杂程序分帧执行

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

异步加载资源

协程的原理

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)模式,这是一种更现代的异步编程方法,可以提供更清晰的代码结构。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一文让你搞明白文本或代码中 \n 和 \r 的区别

我们使用printf打印时基本都会用到 \n 和 \r 之类控制字符&#xff0c;比如&#xff1a; printf("hello world!\r\n"); 那么&#xff0c;你知道 \n 和 \r 的区别吗&#xff1f; 一、关于 \n 和 \r 在ASCII码中&#xff0c;我们会看到有一类不可显示的字符&#x…

jenkins Job华为云EIP变更带宽

引言: 在数字化时代&#xff0c;云服务资源的弹性管理是企业降低运营成本、提高效率的关键手段。通过弹性公网IP&#xff08;EIP&#xff09;服务&#xff0c;企业可以实现按需计费&#xff0c;优化网络支出。然而&#xff0c;根据业务流量的不同阶段调整计费模式&#xff0c;…

【数据结构】什么是二叉树?

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 目录 &#x1f4cc;二叉树的定义 &#x1f4cc;二叉树的特点 &#x1f4cc;特殊二叉树 &#x1f4cc;二叉树的性质 &#x1f4cc;二叉树的存储结构 &#x1f4cc;二叉树…

路径规划最全综述+代码+可视化绘图(Dijkstra算法+A*算法+RRT算法等)

路径规划综述 1. 背景介绍 路径规划是指在给定的环境中找到从起点到终点的最佳路径的过程。它在现实生活中有着广泛的应用&#xff0c;包括无人驾驶、物流配送、机器人导航等领域。随着人工智能和计算机技术的发展&#xff0c;路径规划技术也在不断地得到改进和应用。 路径规划…

宠物智能喂养系统App重新定义养宠体验

​ 在科技蓬勃发展的当今世界&#xff0c;宠物照顾和护理的更多可能性也随之扩大。宠物智能喂养系统App正改变着我们对宠物看护的传统理解。 一、对宠物用品店的影响 作为一款集成了先进的摄像头、传感器和自动投喂功能的设备&#xff0c;智能喂养系统App使得宠物用品店可以…

龙迅LT8713SX适用于一路Type-C/DP1.4转三路Type-C/DP1.4/HDMI2.0应用方案,分辨率高达4K60HZ,支持SST/MST模式!

1. 概述 LT8713SX是一款高性能Type-C/DP1.4转Type-C/DP1.4/HDMI2.0转换器&#xff0c;具有三个可配置的DP1.4/HDMI2.0/DP输出接口和音频输出接口。LT8713SX支持 DisplayPort™ 单流传输 &#xff08;SST&#xff09; 模式和多流传输 &#xff08;MST&#xff09; 模式。当接收…

【SpringBoot】之Security进阶使用(登陆授权)

&#x1f389;&#x1f389;欢迎来到我的CSDN主页&#xff01;&#x1f389;&#x1f389; &#x1f3c5;我是君易--鑨&#xff0c;一个在CSDN分享笔记的博主。&#x1f4da;&#x1f4da; &#x1f31f;推荐给大家我的博客专栏《SpringBoot开发之Security系列》。&#x1f3af…

C# Onnx yolov8 pokemon detection

目录 效果 模型信息 项目 代码 下载 C# Onnx yolov8 pokemon detectio 效果 模型信息 Model Properties ------------------------- date&#xff1a;2023-12-25T17:55:44.583431 author&#xff1a;Ultralytics task&#xff1a;detect license&#xff1a;AGPL-3.0 h…

4.9【共享源】流的多生产者和消费者

当一个系统中存在多个生产者和消费者时&#xff0c;情况可能会变得复杂。 了解生产者和消费者流之间支持的基数非常重要。 本质上&#xff0c;一个生产者流可以与多个消费者流连接&#xff0c;但一个消费者流只能连接到一个生产者流。请注意&#xff0c;基数关系仅限于单个流&…

竞赛保研 基于RSSI的室内wifi定位系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; wifi室内定位系统 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;…

asp.net mvc 重定向问题的解决方式

前端ajax发起请求&#xff0c;在后端接口中重定向&#xff0c;结果报错&#xff0c;无法跳转 Ajax实际上是通过XMLHttpRequest来向服务器发送异步请求的&#xff0c;从服务器获取数据&#xff0c;然后使用JS来更新页面&#xff0c;这也就是常说的局部刷新实现方式&#xff0c;所…

Linux部署MeterSphere结合内网穿透实现远程访问服务管理界面

文章目录 前言1. 安装MeterSphere2. 本地访问MeterSphere3. 安装 cpolar内网穿透软件4. 配置MeterSphere公网访问地址5. 公网远程访问MeterSphere6. 固定MeterSphere公网地址 前言 MeterSphere 是一站式开源持续测试平台, 涵盖测试跟踪、接口测试、UI 测试和性能测试等功能&am…

华为数通方向HCIP-DataCom H12-831题库(多选题:241-249)

第241题 (NEW) 以下哪些操作可能会影响客户网络的正常运行? A、从设备上下载日志 B、软件升级 C、路由协议配置变更 D、debug核心交换机上转发的所有IP报文 答案:ABCD 解析: 第242题 对于防火墙的默认安全区 Trust 和 Untrust 的说法,正确的有 A、从 Trust 区域访问 Untr…

安卓开发--RecyclerView快速上手【上】

效果图展示: 下面三个kml文件名即动态从服务器获取并列表加载。 RecyclerView简称 RV, 是作为 ListView 和 GridView 的加强版出现的,目的是在有限的屏幕之上展示大量的内容,因此 RecyclerView 的复用机制的实现是它的一个核心部分。 一般在动态获取服务器数据进行…

K8S理论

kubernetes&#xff1a;8个字母省略&#xff0c;就是k8s 自动部署自动扩展和管理容器化部署的应用程序的一个开源系统 k8s是负责自动化运维管理多个容器化程序的集群&#xff0c;是一个功能强大的容器编排工具 分布式和集群化的方式进行容器化管理 版本有1.15 .1.18 .1.20 …

ES5语法数组遍历、字符串、对象新增方法

ES5数组遍历forEach\filter\some\every\map、字符串trim、对象keys\defineProperty新增方法   Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎&#xff0c;能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心&#xff0c;它集中存储您的数据&#xff…

ssh工具 向指定的ssh服务器配置公钥

此文分享一个python脚本,用于向指定的ssh服务器配置公钥,以达到免密登录ssh服务器的目的。 效果演示 🔥完整演示效果 👇第一步,显然,我们需要选择功能 👇第二步,确认 or 选择ssh服务器 👇第三步,输入ssh登录密码,以完成公钥配置 👇验证,我们通过ssh登录…

PHP+MySQL组合开发:万能在线预约小程序源码系统 附带完整的搭建教程

近年来&#xff0c;线上服务逐渐成为市场主流。特别是在预约服务领域&#xff0c;用户越来越倾向于选择方便快捷的线上预约方式。传统的预约方式如电话预约和到店预约不仅效率低下&#xff0c;而且在信息传达上存在很大的误差。这使得用户常常需要反复确认&#xff0c;浪费了大…

java实现矩阵谱峰搜索算法

矩阵谱峰搜索算法&#xff0c;也称为矩阵谱峰查找算法&#xff0c;是一种用于搜索二维矩阵中谱峰的方法。谱峰是指在矩阵中的一个元素&#xff0c;它比其上下左右四个相邻元素都大或相等。 该算法的基本思想是从矩阵的中间列开始&#xff0c;找到该列中的最大元素&#xff0c;…

使用ImageJ将Raw格式图片批量转换为JPG

自动方法&#xff1a; 1&#xff0c;创建一个txt文本文档&#xff0c;然后将下面的代码复制粘贴进去。 2&#xff0c;将代码的第一行path修改为你的raw图片所在的路径, 3&#xff0c;第二行out修改为转换后jpg图片存储路径。 4&#xff0c;完成前2步后&#xff0c;如果你是win…