【Unity3D日常开发】Unity3D中协程的使用

news2024/9/23 15:21:33

推荐阅读

  • CSDN主页
  • GitHub开源地址
  • Unity3D插件分享
  • 简书地址
  • 我的个人博客

大家好,我是佛系工程师☆恬静的小魔龙☆,不定时更新Unity开发技巧,觉得有用记得一键三连哦。

一、前言

最近有小伙伴问协程怎么用、怎么写,我也是会用会写,但是原理不是很明白。

学习了一下,总结出来分享给看到这篇文章的人。

如果觉得本篇文章有用别忘了点个关注,关注不迷路,持续分享更多Unity干货文章。

二、正文

2-1、协程是什么

协程就相当于C#的线程。

Unity3D是支持多线程的,只是线程不能访问主线程中的对象,虽然说线程不能访问主线程中的对象,但是可以将一些复杂的算法计算、网络连接等逻辑抛给一个线程去处理,将处理的数据放到公共的内存模块中。

Unity3D主线程就可以使用了。

那么协程是什么呢,协程就是Unity针对上面的问题提出的解决方案,协程又叫做协同程序,使用的场景主要有资源、场景的异步加载,但是可以访问主线程中的对象。

协程的本质是迭代器,能够暂停协程执行,暂停后立即返回主函数,执行主函数剩余的部分,直到中断执行完成后,从中断指令的下一行继续执行协程剩余的函数,函数全部执行完成,协程结束。由于中断执行的出现,可以将一个函数分割成多个帧中去执行。

画了一个图来说明:
在这里插入图片描述
看起来没有什么难的,接下来就来解析一下执行顺序以及协程原理。

2-2、协程原理

执行顺序:

协程中的所有初始代码,从协程开始到中断执行的位置,可以中断,协程代码中的其他部分,也就是中断执行后面的代码将出现在Unity主循环DelayeCallManager中。

协程由 C# 编译器自动生成的类实例提供支持。

此对象用于跟踪单个方法的多次调用之间的协程状态。

因为协程中的局部作用域变量必须在 yield 调用中保持一致,所以这些局部作用域变量将被保存到上一级的生成的它们的类中,从而保证在协程的存活期内保留在堆上的地址分配。

该对象还会跟踪协程的内部状态:它会记住协程暂停后必须从代码中的哪一点恢复。

因此,启动协程引起的内存压力等于固定开销成本加上其局部变量的消耗。

启动协程的代码将构造并调用此对象,然后 Unity 的DelayedCallManager在每当满足协程的暂停条件时再次调用此对象。

由于协程通常在其他协程之外启动,因此它们的执行成本将分担到上述两个位置,这两个位置又叫做协程函数和协程调度器。

执行原理:

协程函数将执行成本分给了协程函数协程调度器协程函数使用的是C#的迭代器,协程调度器则使用了MonoBehaviour中的生命周期函数来实现。

协程函数实现了分步,协程调度器实现了分时。

再来了解一下迭代器:
在这里插入图片描述
迭代器中有一个MoveNext函数,协程函数实现了迭代器,那么协同程序就是一步步的执行迭代器对象中国男的MoveNext函数,调用MoveNext函数会执行下一个yield return之前的逻辑,并且根据MoveNext()的返回值判断是否全部执行完毕。

而yield return通过返回Current对象,来判断执行MoveNext()的时机,这部分的工作就是通过协程的另一个部分,也就是协程调度器来实现的,协程调度器是Unity引擎实现的,理论上我们可以自己去实现一个协程调度器,感兴趣的可以自己实现一个,能进一步加深对协程的理解。

2-3、实现协程原理的代码

我们已经清楚了协程的原理,以及协程的组成部分,也就是协程函数协程调度器,我们可以试着去实现一个,来加深对协程的理解:

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);
                }
            }
        }
    }
}

实现的代码主体就是这样了,当然还有一些GC回收没有做,感兴趣的可以继续优化。

2-4、使用协同程序

首先,来看一个简单的程序:

using System.Collections;
using UnityEngine;

public class TestCoroutine : MonoBehaviour
{
    void Start()
    {
        Debug.Log("在协程之前执行函数");
        StartCoroutine(m_Ien());
        Debug.Log("在协程之后执行函数");
    }

    IEnumerator m_Ien()
    {
        Debug.Log("执行函数");
        yield return new WaitForSeconds(1);//等待1秒
        Debug.Log("执行后面的函数");
    }
}

运行结果:
在这里插入图片描述

PS:这个例子演示了,协程的执行顺序,协程的写法,协程的调用

执行顺序:
(1)执行协程之前的代码
(2)执行协程中的协程函数直到中断程序
(3)执行协程之后的代码
(4)中断程序结束执行后面的协程函数

协程写法:
(1)声明是IEnumerator 迭代器类型返回值
(2)返回值为yield return new,也就是中断程序,就跟int的返回值是0123一样,没有返回值会报错
(3)执行中断程序后面的函数

协程的yield return new返回值:

返回值介绍
yield return null; yield retun x(x代表任意数字)下一帧再执行后续代码
yield break;结束该协程
yield return new WaitForSeconds(0.3f);等待固定时间执行后续代码
yield return FunctionName();函数执行完毕后执行后续代码
yield return AsyncOperation;异步执行完毕后执行后续代码
yield return Coroutine;协程执行完毕后执行后续代码
yield return new WaitForEndOfFrame();帧渲染完成后执行后续代码
yield return new WaitForFixedUpdate();物理帧更新后执行后续代码
yield return new WaitUntil(arg);参数为true时执行后续代码

协程的调用:
(1)调用协程使用StartCoroutine(m_Ien());
(2)调用协程还可以这么写StartCoroutine(“m_Ien”);
(3)终止协程用StopCoroutine(m_Ien());
(4)终止协程还可以这么写StopCoroutine(“m_Ien”);
(5)声明协程再终止协程,mCoroutine= StartCoroutine(m_Ien());StopCoroutine(mCoroutine);
(6)终止所有协程StopAllCoroutines();

2-5、实现一个自己的WaitForSeconds

协程的所能达到的效果就是在指定的时间点上执行需要执行的代码,Unity中开始一个协程的函数是StartCoroutine,而提供的延迟的类有以下几种分别是:

new WaitForEndOfFrame; //等待一帧
new WaitForFixedUpdate; //等待一个FixedUpdate(固定时间间隔)
new WaitForSeconds; //等待X秒
new WWW; //等待外部资源加载完毕

本节就针对其中的WaitForSeconds实现进行探究。

因为在开发过程中,很多时候会遇到一种情况就是,超时或者是符合某种条件就继续运行,使用系统提供WaitForSeconds已经无法满足要求了。

这时候有两种解决方法,一种是使用StopCoroutine来停止协程,但是对于Unity来说,这种行为会造成很大的开销;第二种就是可以采用重写WaitForSeconds,使它能达到我们的要求。

以下是重写WaitForSeconds的代码:

/// <summary>
/// 任务扩展
/// </summary>
static class CTaskExtend
{
    static public IEnumerator WaitForSeconds(float second)
    {
        DateTime init_dt = DateTime.Now;
        TimeSpan time;
        while (true)
        {
            time = DateTime.Now - init_dt;
            if (time.TotalSeconds <= second)
            {
                yield return null;
            }
            else
            {
                break;
            }
        }
    }
}

调用的方法与Unity差不多:

yield return CTaskExtend.WaitForSeconds(delayTime);

那么如果遇到之前说的那一种情况(超时或者是符合某种条件就继续运行),这里需要做怎么样的改动呢?如下:

/// <summary>
/// 任务扩展
/// </summary>
static class CTaskExtend
{
    public delegate bool CondDelegate();
    static public IEnumerator WaitForSeconds(float second, CondDelegate cond = null)
    {
        DateTime init_dt = DateTime.Now;
        TimeSpan time;
        while (true)
        {
            time = DateTime.Now - init_dt;
            if (time.TotalSeconds <= second && !cond())
            {
                yield return null;
            }
            else
            {
                break;
            }
        }
    }
}

加上了一个回调函数,每次都会检查这个函数是否为true,如果为true则停止等待。

2-6、自定义yield new return

我发现协程的返回值有这个:

yield return Coroutine;//协程执行完毕后执行后续代码

也就是协程类型的返回值,我在想,是不是可以通过扩展Coroutine,来写一个自定义的中断指令,也就是yield new return。

我们假设这样一种情况,当一个动画播放后,再执行其他函数。

参考代码如下:

using UnityEngine;
using System.Collections;
 
public class WaitForEndOfAnim : IEnumerator
{
    AnimationState m_animState;
 
    public WaitForEndOfAnim(AnimationState animState)
    {
        m_animState = animState;
    }
    //-- IEnumerator Interface
    public object Current
    {
        get
        {
            return null;
        }
    }
 
    //-- IEnumerator Interface
    public bool MoveNext()
    {
        return m_animState.enabled;
    }
 
    //-- IEnumerator Interface
    public void Reset()
    {
    }
}

这里面核心的逻辑就在“MoveNext”函数中,我通过m_animState.enabled来判断动画是否播放完了。

完整的测试代码如下:

using UnityEngine;
using System.Collections;
 
public class UnitTest : MonoBehaviour
{
 
    // Use this for initialization
    void Start()
    {
    }
 
    void OnGUI()
    {
        GUILayout.BeginArea(new Rect(6, 6, 200, 300));
        GUILayout.BeginVertical();
        GUILayout.Box("Conrountinue测试");
 
        if (GUILayout.Button("启动"))
        {
            StartCoroutine(DoTest());
        }
 
        GUILayout.EndVertical();
        GUILayout.EndArea();
    }
 
    IEnumerator DoTest()
    {
 
        Animation anim = GetComponentInChildren<Animation>();
        AnimationState animAttack = anim["attack"];
        animAttack.speed = 0.1f;
 
        AnimationState animHit = anim["hit"];
        animHit.speed = 0.1f;
 
        AnimationState animDie = anim["die"];
        animDie.speed = 0.1f;
 
        Debug.Log("1.开始播放攻击动画。" + Time.time * 1000);
        anim.Play(animAttack.name);
        yield return StartCoroutine(new WaitForEndOfAnim(animAttack));
 
        Debug.Log("2.开始播放受击动画。" + Time.time * 1000);
        anim.Play(animHit.name);
        yield return StartCoroutine(new WaitForEndOfAnim(animHit));
 
        Debug.Log("3.开始播放死亡动画。" + Time.time * 1000);
        anim.Play(animDie.name);
        yield return StartCoroutine(new WaitForEndOfAnim(animDie));
    }
}

三、后记

这篇文章详细讲解了Unity3D的协程的原理以及使用。

以及重写协程程序的返回值和自定义协程返回值。

对于某些代码来说难度比较高,推荐多理解多练习。


你的点赞就是对博主的支持,有问题记得留言:

博主主页有联系方式。

博主还有跟多宝藏文章等待你的发掘哦:

专栏方向简介
Unity3D开发小游戏小游戏开发教程分享一些使用Unity3D引擎开发的小游戏,分享一些制作小游戏的教程。
Unity3D从入门到进阶入门从自学Unity中获取灵感,总结从零开始学习Unity的路线,有C#和Unity的知识。
Unity3D之UGUIUGUIUnity的UI系统UGUI全解析,从UGUI的基础控件开始讲起,然后将UGUI的原理,UGUI的使用全面教学。
Unity3D之读取数据文件读取使用Unity3D读取txt文档、json文档、xml文档、csv文档、Excel文档。
Unity3D之数据集合数据集合数组集合:数组、List、字典、堆栈、链表等数据集合知识分享。
Unity3D之VR/AR(虚拟仿真)开发虚拟仿真总结博主工作常见的虚拟仿真需求进行案例讲解。
Unity3D之插件插件主要分享在Unity开发中用到的一些插件使用方法,插件介绍等
Unity3D之日常开发日常记录主要是博主日常开发中用到的,用到的方法技巧,开发思路,代码分享等
Unity3D之日常BUG日常记录记录在使用Unity3D编辑器开发项目过程中,遇到的BUG和坑,让后来人可以有些参考。

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

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

相关文章

Revit管理链接模型视图样式和链接CAD

一、Revit中如何管理链接模型的视图样式 Revit软件协同&#xff0c;无非就两种方式&#xff1a;1、工作集;2、链接文件。其中“工作集”属于软件内部的工作协调方式&#xff0c;不是我们本期问题汇总要说明的问题&#xff0c;这里我们着重说一下第二种关于“链接文件”方式协同…

我们的理性何处安放

每天工作压力和各种人相处都让我们非常忙碌&#xff0c;我们上大学&#xff0c;努力工作&#xff0c;都是想获得更好的人生场景&#xff0c;素养&#xff0c;提升自身的认知&#xff0c;这样就是对我们大多数人生最负责任。如何让自己理性与人为善&#xff0c;并能被人温柔以待…

大数据-学习实践-3HDFS

大数据-学习实践-2HDFS (大数据系列) 文章目录大数据-学习实践-2HDFS1知识点2具体内容2.1HDFS介绍2.2HDFS操作2.2.1基本操作2.2.2Java操作HDFS2.3HDFS体系结构2.3.1NameNode2.3.2SecondaryNameNode2.3.3DataNode2.3.4总结2.4HDFS回收站2.4HDFS安全模式2.5定时上传至HDFS2.6HDF…

java版工程项目管理系统 Spring Cloud+Spring Boot+Mybatis+Vue+ElementUI+前后端分离 功能清单

java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显…

Kalman Filter in SLAM (4) ——Iterated Extended Kalman Filter (IEKF, 迭代扩展卡尔曼滤波)

文章目录1. IEKF 概述2. IEKF 的核心思想2.1. The Motivation of Iteration2.2. IEKF 迭代线性化步骤3. IEKF 的推导3.1. 预测公式3.2. 校正公式1. IEKF 概述 由于非线性模型中做了线性化近似&#xff0c;当非线性程度越强时&#xff0c;误差就会较大&#xff0c;但是由于线性…

删库跑路现场还原

数据库是公司重要资产&#xff0c;在此类重要资产平台上&#xff0c;尤其是重要操作&#xff0c;应该保持敬畏心。数据库被删了&#xff1f;可怎么证明是某某某删了数据库&#xff1f;或者根本都不知道谁删除了数据库&#xff0c;又没抓现行&#xff0c;该怎么办&#xff1f;正…

品牌直播人气高达80w+,如何在B站打造品牌营销阵地?

2月9日&#xff0c;手机品牌REALME真我&#xff08;以下简称“真我”&#xff09;&#xff0c;在B站开启一场「发布会」盛宴。这场发布会正是为新机“真我GT Neo5”发布进行全面宣传&#xff0c;在当日&#xff0c;真我品牌官方号在B站开启了一场线上新机发布会。来源-B站官方号…

云和虚拟化有什么区别?

云和虚拟化概念容易被混淆&#xff0c;特别是因为它们都围绕着用抽象资源创建有用的环境。但是&#xff0c;虚拟化是一项允许您从单个物理硬件系统创建多个模拟环境或专用资源的技术&#xff0c;而云是可在整个网络中抽象&#xff0c;汇总和共享可伸缩资源的IT环境。简而言之&a…

优化UnRaid容器的WebUI端口设置实现应用快捷访问的方法

文章目录前言详细流程前言 自从入了UnRaid的坑&#xff0c;发现Docker真是个好东西&#xff0c;各种各样的应用工具层出不穷&#xff0c;可以大大提高生产效率。然而在安装Docker应用后&#xff0c;对于如何方便的访问该应用&#xff0c;各个应用服务提供者给出的解决方案不是…

ip-guard如何通过准入网关对指定的服务器进行通讯加密保护?

1、准入网关在高级配置设置受保护服务器; WEB管理界面【系统工具】,点击【配置管理】,点击参数设置,进入高级配置界面,输入配置内容即可。 [ControlServer]

最受欢迎的大数据可视化

大数据可视化是进行各种大数据分析的最重要组成部分之一。 一旦原始数据流被以图像形式表示时&#xff0c;以此做决策就变得容易多了。 为了满足并超越客户的期望&#xff0c;大数据可视化工具应该具备这些特征&#xff1a;能够处理不同种类型的传入数据能够应用不同种类的过滤…

【华为机试真题 Python实现】2023年1、2月高频机试题

文章目录2023年1季度最新机试题机考注意事项1. 建议提前刷题2. 关于考试设备3. 关于语言环境3.1. 编译器信息3.2. ACM 模式使用sys使用input&#xff08;推荐&#xff09;3. 关于题目分值及得分计算方式4. 关于做题流程5. 关于作弊2023年1季度最新机试题 两个专栏现在有200博文…

银河麒麟V10SP1高级服务器版本离线RPM方式升级openssl openssh 自动化升级系统补丁实战实例全网唯一

银河麒麟高级服务器操作系统简介&#xff1a; 银河麒麟高级服务器操作系统V10是针对企业级关键业务&#xff0c;适应虚拟化、云计算、大数据、工业互联网时代对主机系统可靠性、安全性、性能、扩展性和实时性等需求&#xff0c;依据CMMI5级标准研制的提供内生本质安全、云原生支…

主成分分析(PCA)原理

主成分分析&#xff08;PCA&#xff09;原理 在高维数据处理中&#xff0c;为了简化计算量以及储存空间&#xff0c;需要对这些高维数据进行一定程度上的降维&#xff0c;并尽量保证数据的不失真。PCA和ICA是两种常用的降维方法。 PCA&#xff1a;principal component analysi…

[思考进阶]06 养成“记笔记”的习惯,能够改变你的思考方式

目录前言一、10000张纸法则二、康奈尔笔记法三、东京大学笔记法四、养成“记笔记”的习惯除了要提升自己的技术能力&#xff0c;思维的学习和成长也非常非常重要&#xff0c;特推出此[思考进阶]系列&#xff0c;进行刻意练习&#xff0c;从而提升自己的认知。 前言 经常看到一…

BCELoss

1&#xff1a;单标签二分类&#xff1a;N是样本数&#xff0c;Yn是标签。范围在**[0,1]**。 公式&#xff1a; 举个例子&#xff1a; input torch.Tensor([[0.8, 0.9, 0.3]])#, [0.8, 0.9, 0.3], [0.8, 0.9, 0.3], [0.8, 0.9, 0.3]]) target torch.Tensor([[1, 1, 0]])#, […

weblogic-文件读取漏洞

weblogic-文件读取漏洞 0x00 前言 Weblogic存在管理后台&#xff0c;通过账号密码登录&#xff0c;由于管理员的疏忽&#xff0c;经常会使用弱口令&#xff0c;或者默认的账户名密码。因此存在弱口令爆破的风险。在本环境下模拟了一个真实的weblogic环境&#xff0c;其后台存…

Java:SpringBoot整合Spring Security实现认证与授权学习笔记

本文通过逐步学习Spring Security&#xff0c;由浅入深&#xff0c;SpringBoot整合Spring Security 分别实现自定义的HTTP Basic认证 和 Form表单认证。 本文是学习笔记&#xff0c;网上的教程五花八门&#xff0c;由于时间久远&#xff0c;很难拿来就用。 在此特别感谢IT老齐…

雪花算法(SnowFlake)

简介现在的服务基本是分布式、微服务形式的&#xff0c;而且大数据量也导致分库分表的产生&#xff0c;对于水平分表就需要保证表中 id 的全局唯一性。对于 MySQL 而言&#xff0c;一个表中的主键 id 一般使用自增的方式&#xff0c;但是如果进行水平分表之后&#xff0c;多个表…

使用三种方式创建servlet并配置访问成功

Servlet创建的方式 一、实现Servlet 接口 package com.openlab;import java.io.IOException;import javax.servlet.Servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.Servl…