深入探索Unity协程:揭开CSharp迭代器背后的神秘面纱

news2024/9/16 10:45:15

在这里插入图片描述

协程是一种特殊类型的迭代器方法,允许你在多个帧之间分段执行代码。可以用来处理时间延迟、异步操作和顺序执行的任务,而不阻塞主线程。Unity协程的实现依赖于C#语言提供的迭代器相关的语言特性,所以想要弄清楚Unity协程的底层原理,必须先了解C#的迭代器的基本功能。

深入探索Unity协程文章首发

C#迭代器

迭代器的基本概念

  1. 迭代器是什么? 迭代器是一种简化遍历集合或序列的工具。你可以用它来逐个访问集合中的每个元素,而不需要自己编写复杂的循环逻辑。迭代器通过生成一个可枚举的序列,让你逐个取出元素。

  2. yield 关键字 。在 C# 中,yield 关键字是迭代器的核心。它帮助你创建一个可以暂停和恢复的迭代过程。使用 yield 关键字,你可以逐步生成序列中的每个元素,而不是一次性生成所有元素。

    • yield return:用于返回序列中的一个元素,并暂停迭代器的执行,直到下一次请求。
    • yield break:用于结束序列的生成,不再返回更多的元素。

C#迭代器的作用

C#迭代器Enumerator提供了一种可以通过foreach遍历任何一个自定义类型的手段。对于任何一个实现了IEnumerable接口和IEnumerator接口的类型来说,都可以通过foreach语句来像遍历一个集合一样遍历一个对象。

定义一个班级类,由若干学生组成:

public class Student
{
    public string Name { get; set; }
    
	public override string ToString()
    {
        return Name;
    }
}

public class ClassRoom : IEnumerable
{
    private List<Student> students;

    public ClassRoom()
    {
        students = new List<Student>();
    }

    public void Add(Student student)
    {
        if (!students.Contains(student))
        {
            students.Add(student);
        }
    }

    public void Remvoe(Student student)
    {
        if (students.Contains(student))
        {
            students.Remove(student);
        }
    }

    public IEnumerator GetEnumerator()
    {
        return new StudentEnumerator(students);
    }
}

 public class StudentEnumerator : IEnumerator
 {
     public StudentEnumerator(List<Student> students)
     {
         this.students = students;
     }
 
     private List<Student> students;
     private int currentIndex = -1;
 
     public object Current
     {
         get
         {
             if(0 <= currentIndex && currentIndex<students.Count)
             {
                 return students[currentIndex];
             }
             return null;
         }
     }
 
     public bool MoveNext()
     {
         currentIndex++;
         return currentIndex<students.Count;
     }
 
     public void Reset()
     {
         currentIndex = -1;
     }
 }

当需要能够编写代码遍历ClassRoom类中的Student对象,如果不借助迭代器,就只能将ClassRoom内部的students集合暴露出来供调用方使用,这样就暴露了ClassRoom内部有关Student对象的存储细节,以后如果Student对象的存储结构变了(比如由List结构变成了数组或者字典等等),对应的调用方的所有代码也得跟着变更。除了直接将students成员暴露出来以外,还有一种方法就是可以让ClassRoom实现IEnumerable接口,这样就可以通过foreach语句来遍历其中的Student对象。

验证代码:

    ClassRoom c = new ClassRoom();
    c.Add(new Student() { Name = "zzz"});
    c.Add(new Student() { Name = "yyy"});

    foreach (Student s in c)
    {
        Debug.Log(s.ToString());
    }
    Debug.Log("......等价输出........");
	//foreach的等价写法
    IEnumerator enumerator = c.GetEnumerator();
    while (enumerator.MoveNext())
    {
        Debug.Log(((Student)(enumerator.Current)).ToString());
    }

控制台输出:
image.png

Unity协程

通常情况下,我们写的每一段代码,都会在Unity的更新逻辑中在同一帧全部执行完毕。如果我们需要将某一段代码包含的逻辑拆分到不同的帧来分段执行,除了自己手写状态机来实现该流程外,更简单方便的方法就是使用Unity协程。总的来说,Unity协程允许我们在保证整个应用在单线程模式不变的情况下通过编写协程函数并调用开启协程的方法(StartCoroutine)将一个任务分到不同的时间段异步执行。

Unity针对开关协程均提供了三个重载方法,以下表格中的方法均是一一对应的开关协程的用法,不能混用。

开启协程方法停止协程方法
StartCoroutine(string methodName)/StartCoroutine(string methodName, object value)StopCoroutine(string methodName)和StopCoroutine(Coroutine)
StartCoroutine(IEnumerator routine)/StartCoroutine(IEnumerator routine)StopCoroutine(Coroutine routine)和StopCoroutine(IEnumerator routine)

Yield Return延迟函数

Uniyt协程中的协程函数通过yield return后面的WaitForSeconds、WaitForEndOfFrame等可以控制延迟多少秒、多少帧之后再执行,诸如此类效果是如何实现的呢?关键点在于yield return语句后面的对象类型。我们知道,Unity协程中常见的yield return有这么几种:

  yield return new WaitForSeconds(1);

  yield return new WaitForEndOfFrame();

  yield return new WaitForFixedUpdate();

转到上诉三个函数定义源码处,不难看出它们均继承于YieldInstruction。Unity就是根据yield return返回的对象类型来判断到底应该延迟多长时间来执行下一段代码的。

总结

Unity的协程的实现原理是基于C#语言的迭代器特性,通过定义一个协程函数(通过yield return返回),将协程函数缓存为一个IEnumerator的对象,然后根据该对象的Current(是一个YieldInstruction对象或者null) 来判断下一次执行需要间隔的时间,等到间隔时间结束后执行MoveNext执行下一阶段的任务,并继续根据新的Current确定下一次等待的时间间隔,直到MoveNext返回false标志着协程终止。

大致可以用以下流程图来表示Unity协程的执行过程:
image.png

自定义实现一个有趣的协程方法

理解了Unity协程的实现原理之后,我们完全可以自己写代码来实现类似Unity中的StartCoroutine的效果。比如,我们编写一个自己的开启携程的方法:此方法规定能够接受一个返回IEnumerator的协程函数,并且可以根据yield return后面返回的字符串的长度来等待相应的秒数,比如yield return “1234”,那么就等待4秒之后再执行后面的代码,如果yield return “100”, 那么就等待3秒之后再执行后面的代码,如果yield return后面的对象不是string,则默认等待一帧之后再执行。有了前文的基础,我们很容易写出如下代码:

	/// <summary>
    /// 用来存储创建的迭代器对象
    /// </summary>
    private IEnumerator taskEnumerator = null;
    /// <summary>
    /// 用来记录任务是否完成的标记
    /// </summary>
    private bool isDone = false;
    private float currentDelayTime = 0f;
    private float currentPassedTime = 0f;
    private int delayFrameCount = 1;
    private bool delayFrame = false;
    private bool isCoroutineStarted = false;

    private void MyStartCoroutine(IEnumerator enumerator)
    {
        if (enumerator == null) return;
        isCoroutineStarted = true;
        taskEnumerator = enumerator;
        PushTaskToNextStep();
    }

    private void Start()
    {
        MyStartCoroutine(YieldFunction());
    }

    private IEnumerator YieldFunction()
    {
        //第一段代码
        Debug.Log("first step......");
        yield return 1;

        //第二段代码
        Debug.Log("second tep......");
        yield return 2;

        //第三段代码
        Debug.Log("third step......");
        yield return 3;

        //第四段代码
        Debug.Log("forth step......");
        yield return 4;
    }

    private void PushTaskToNextStep()
    {
        isDone = !taskEnumerator.MoveNext();
        if (!isDone)
        {
            if (taskEnumerator.Current is string)
            {
                currentDelayTime = (taskEnumerator.Current as string).Length;
                currentPassedTime = 0f;
                delayFrame = false;
            }
            else
            {
                delayFrame = true;
                delayFrameCount = 1;
            }
        }
        else
        {
            isCoroutineStarted = false;
        }
    }

    private void Update()
    {
        if (isCoroutineStarted)
        {
            if (delayFrame)
            {
                delayFrameCount--;
                if (delayFrameCount == 0)
                {
                    Debug.Log(string.Format("第{0}帧(运行数:{1})结果:阶段任务已完成!", Time.frameCount, Time.time));
                    PushTaskToNextStep();
                }
            }
            else
            {
                currentPassedTime += Time.deltaTime;
                if (currentPassedTime >= currentDelayTime)
                {
                    Debug.Log(string.Format("第{0}帧(运行数:{1})结果:阶段任务已完成!", Time.frameCount, Time.time));
                    PushTaskToNextStep();
                }
            }

        }
    }

控制台输出与预期一致:
image.png

挺有趣哈!

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

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

相关文章

web群集--nginx配置文件location匹配符的优先级顺序详解及验证

文章目录 前言优先级顺序优先级顺序(详解)1. 精确匹配&#xff08;Exact Match&#xff09;2. 正则表达式匹配&#xff08;Regex Match&#xff09;3. 前缀匹配&#xff08;Prefix Match&#xff09; 匹配规则的综合应用验证优先级 前言 location的作用 在 NGINX 中&#xff0…

Idea Mac代码调试常用快捷键~

Mac截图 commandShift4 idea英文大写转小写 commandShiftU 功能&#xff1a;查看类的实现和继承父类的方法 快捷键 fncommandF12 鼠标点击打开 功能&#xff1a;查看当前方法的上游方法 选中方法&#xff0c;controloptionH 功能&#xff1a;CommandB是查看本类的方法 功能&…

Matlab simulink建模与仿真 第十一章(端口及子系统库)【下】

参考视频&#xff1a;simulink1.1simulink简介_哔哩哔哩_bilibili 八、触发使能子系统 1、Enabled and Triggered Subsystem触发使能子系统概述 触发使能子系统其实是触发子系统和使能子系统二者的结合&#xff0c;当触发端口传来触发信号时&#xff0c;使能端口的输入需要大…

TitleBar:打造高效Android标题栏的新选择

在Android应用开发中&#xff0c;标题栏是用户界面的重要组成部分。一个好的标题栏不仅能够提升应用的专业感&#xff0c;还能增强用户体验。然而&#xff0c;传统的标题栏实现方式往往存在代码冗余、样式不统一、性能开销大等问题。今天&#xff0c;我们将介绍一个名为TitleBa…

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐&#xff1f; 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识&#xff0c;并举出了两个例子&#xff0c;我们再举出两个例子继续说明&…

python进阶篇-day08-数据结构与算法(线性结构介绍与链表实现)

数据的存储和组织形式 程序 数据结构 算法 一. 算法介绍 概述目的 都是可以提高程序的效率(性能), 面试高频考点 数据结构介绍 数据的存储和组织形式, 同样的空间, 不同的结构, 存储的数据不同, 操作方式也不同 算法介绍 为了解决实际的业务问题, 而考虑出来的方法和思路 …

龙芯+FreeRTOS+LVGL实战笔记(新)——06添加二级按钮

本专栏是笔者另一个专栏《龙芯+RT-Thread+LVGL实战笔记》的姊妹篇,主要的区别在于实时操作系统的不同,章节的安排和任务的推进保持一致,并对源码做了完善与优化,各位可以先到本人主页下去浏览另一专栏的博客列表(目前已撰写36篇,图1所示),再决定是否订阅。此外,也可以…

超强的截图工具:PixPin

你是否还在为寻找一款功能强大、操作简便的截图工具而烦恼&#xff1f;市面上那么多工具&#xff0c;常常让人无从选择。今天&#xff0c;想给大家安利一款神器——PixPin&#xff0c;一款真正解放双手的截图工具。 想象一下&#xff0c;你只需要按下快捷键就能轻松完成多种截…

雷电9模拟器安装magisk和lsposed

模拟器环境配置 1、开启root 2、开启System.vmdk可写入 安装magisk 1、新建模拟器、开启root权限、并安装debug版magisk 下载地址去上面吾爱论坛作者文章下载吧&#xff01;支持他一下&#xff01; 2、打开magisk的app&#xff0c;点击安装 如果弹出获取权限&#xff0c;直接…

【Socket网络编程原理实践】

socket 基于 TCP/IP协议实现&#xff0c;在网络模型中属于传输层 Java 网络编程中的核心概念 IP 地址&#xff1a;用于标识网络中的计算机端口号&#xff1a;用于标识计算机上的应用程序或进程Socket&#xff08;套接字&#xff09;&#xff1a;网络通信的基本单位&#xff0…

冒泡排序算法介绍

冒泡排序算法介绍 如果真的累了&#xff0c;就拉上窗帘关上手机关掉闹钟深呼吸一口气钻进被窝&#xff0c;好好地睡一觉&#xff0c;难熬的日子总需要一些温暖&#xff0c;而什么都不如被窝的温暖来的踏实。 冒泡排序是一种经典的排序算法&#xff0c;它通过重复遍历待排序的序…

如何恢复回收站中已删除/清空的文件

回收站清空后如何恢复已删除的文件&#xff1f;是否可以恢复永久删除的文件&#xff1f;或者最糟糕的是&#xff0c;如果文件直接被删除怎么办&#xff1f;本文将向您展示清空回收站后恢复已删除数据的最佳方法。 回收站清空后如何恢复已删除的文件&#xff1f; “回收站清空后…

从零开始搭建GPU深度学习环境(pytorch)

傻乎乎的我&#xff0c;突然发现我自己的笔记本电脑居然有gpu&#xff0c;这个电脑是我弟在2017年购入的。 电脑已经按照了cpu环境&#xff0c;现在增加gpu环境 参考torch的cpu版本和gpu版本有什么区别 torch与cuda版本_mob64ca13f6035c的技术博客_51CTO博客 前言&#xff1a…

Vue3使用Uni-ui的popup弹出层组件

由于uni-ui中有些组件文档的基于vue2编写的&#xff0c;比如popup组件 下面是vue3的写法 除了文档中要求的aleterDialog外&#xff0c;还得利用v-if设置一个isDialog判断 // template // script 解决

数学建模笔记——TOPSIS[优劣解距离]法

数学建模笔记——TOPSIS[优劣解距离法] TOPSIS(优劣解距离)法1. 基本概念2. 模型原理3. 基本步骤4. 典型例题4.1 矩阵正向化4.2 正向矩阵标准化4.3 计算得分并归一化4.4 python代码实现 TOPSIS(优劣解距离)法 1. 基本概念 C. L.Hwang和 K.Yoon于1981年首次提出 TOPSIS(Techni…

【Linux网络】详解TCP协议(1)

&#x1f389;博主首页&#xff1a; 有趣的中国人 &#x1f389;专栏首页&#xff1a; Linux网络 &#x1f389;其它专栏&#xff1a; C初阶 | C进阶 | 初阶数据结构 小伙伴们大家好&#xff0c;本片文章将会讲解 TCP协议 的相关内容。 如果看到最后您觉得这篇文章写得不错&am…

力扣每日一题 有序数组的平方 双指针 逆向思维

Problem: 977. 有序数组的平方 &#x1f468;‍&#x1f3eb; 灵神题解 class Solution {public int[] sortedSquares(int[] nums) {int n nums.length;int [] ans new int[n];int p n-1;int i 0;int j n-1;while(p > 0){int x nums[i] * nums[i];int y nums[j] * n…

结构体小知识

目录 前言1.结构体数组1.1结构体数组理解1.2结构体数组知识运用1.3 -> 操作符 2. 知识拓展 前言 本期blog是对上一期指针知识的知识补充&#xff0c;如果各位大佬感兴趣的话&#xff0c;可以结合起来一起看&#xff01; 1.结构体数组 1.1结构体数组理解 结构体数组在本…

关系的规范化与范式详解

在数据库设计中&#xff0c;关系的规范化是确保数据结构合理性、减少冗余和异常的关键步骤。如果你是一个数据库设计的初学者&#xff0c;这篇文章将为你深入浅出地讲解 关系规范化 和 范式 的核心概念&#xff0c;并通过简洁的示例帮助你加深理解。 关系的规范化&#xff1a;…

JavaScript进阶day1

目录 1.作用域 1.1 局部作用域 1.2 全局作用域 1.3 作用域链 1.4 JS垃圾回收机制 1.4.1 什么是垃圾回收机制&#xff1f; 1.4.2 内存的生命周期 1.4.3 算法说明 1.5 闭包 1.6 变量提升 2.函数进阶 2.1 函数提升 2.2 函数参数 2.2.1 动态参数 2.2.2 剩余参数 2.…