推薦閱讀
- CSDN主頁(yè)
- GitHub開源地址
- Unity3D插件分享
- 簡(jiǎn)書地址
- 我的個(gè)人博客
大家好,我是佛系工程師☆恬靜的小魔龍☆,不定時(shí)更新Unity開發(fā)技巧,覺得有用記得一鍵三連哦。
一、前言
最近有小伙伴問協(xié)程怎么用、怎么寫,我也是會(huì)用會(huì)寫,但是原理不是很明白。
學(xué)習(xí)了一下,總結(jié)出來(lái)分享給看到這篇文章的人。
如果覺得本篇文章有用別忘了點(diǎn)個(gè)關(guān)注,關(guān)注不迷路,持續(xù)分享更多Unity干貨文章。
二、正文
2-1、協(xié)程是什么
協(xié)程就相當(dāng)于C#的線程。
Unity3D是支持多線程的,只是線程不能訪問主線程中的對(duì)象,雖然說(shuō)線程不能訪問主線程中的對(duì)象,但是可以將一些復(fù)雜的算法計(jì)算、網(wǎng)絡(luò)連接等邏輯拋給一個(gè)線程去處理,將處理的數(shù)據(jù)放到公共的內(nèi)存模塊中。
Unity3D主線程就可以使用了。
那么協(xié)程是什么呢,協(xié)程就是Unity針對(duì)上面的問題提出的解決方案,協(xié)程又叫做協(xié)同程序,使用的場(chǎng)景主要有資源、場(chǎng)景的異步加載,但是可以訪問主線程中的對(duì)象。
協(xié)程的本質(zhì)是迭代器,能夠暫停協(xié)程執(zhí)行,暫停后立即返回主函數(shù),執(zhí)行主函數(shù)剩余的部分,直到中斷執(zhí)行完成后,從中斷指令的下一行繼續(xù)執(zhí)行協(xié)程剩余的函數(shù),函數(shù)全部執(zhí)行完成,協(xié)程結(jié)束。由于中斷執(zhí)行的出現(xiàn),可以將一個(gè)函數(shù)分割成多個(gè)幀中去執(zhí)行。
畫了一個(gè)圖來(lái)說(shuō)明:
看起來(lái)沒有什么難的,接下來(lái)就來(lái)解析一下執(zhí)行順序以及協(xié)程原理。
2-2、協(xié)程原理
執(zhí)行順序:
協(xié)程中的所有初始代碼,從協(xié)程開始到中斷執(zhí)行的位置,可以中斷,協(xié)程代碼中的其他部分,也就是中斷執(zhí)行后面的代碼將出現(xiàn)在Unity主循環(huán)DelayeCallManager
中。
協(xié)程由 C# 編譯器自動(dòng)生成的類實(shí)例提供支持。
此對(duì)象用于跟蹤單個(gè)方法的多次調(diào)用之間的協(xié)程狀態(tài)。
因?yàn)閰f(xié)程中的局部作用域變量必須在 yield 調(diào)用中保持一致,所以這些局部作用域變量將被保存到上一級(jí)的生成的它們的類中,從而保證在協(xié)程的存活期內(nèi)保留在堆上的地址分配。
該對(duì)象還會(huì)跟蹤協(xié)程的內(nèi)部狀態(tài):它會(huì)記住協(xié)程暫停后必須從代碼中的哪一點(diǎn)恢復(fù)。
因此,啟動(dòng)協(xié)程引起的內(nèi)存壓力等于固定開銷成本加上其局部變量的消耗。
啟動(dòng)協(xié)程的代碼將構(gòu)造并調(diào)用此對(duì)象,然后 Unity 的DelayedCallManager
在每當(dāng)滿足協(xié)程的暫停條件時(shí)再次調(diào)用此對(duì)象。
由于協(xié)程通常在其他協(xié)程之外啟動(dòng),因此它們的執(zhí)行成本將分擔(dān)到上述兩個(gè)位置,這兩個(gè)位置又叫做協(xié)程函數(shù)和協(xié)程調(diào)度器。
執(zhí)行原理:
協(xié)程函數(shù)將執(zhí)行成本分給了協(xié)程函數(shù)
和協(xié)程調(diào)度器
,協(xié)程函數(shù)
使用的是C#的迭代器,協(xié)程調(diào)度器
則使用了MonoBehaviour中的生命周期函數(shù)來(lái)實(shí)現(xiàn)。
協(xié)程函數(shù)
實(shí)現(xiàn)了分步,協(xié)程調(diào)度器
實(shí)現(xiàn)了分時(shí)。
再來(lái)了解一下迭代器:
迭代器中有一個(gè)MoveNext函數(shù),協(xié)程函數(shù)
實(shí)現(xiàn)了迭代器,那么協(xié)同程序就是一步步的執(zhí)行迭代器對(duì)象中國(guó)男的MoveNext函數(shù),調(diào)用MoveNext函數(shù)會(huì)執(zhí)行下一個(gè)yield return之前的邏輯,并且根據(jù)MoveNext()的返回值判斷是否全部執(zhí)行完畢。
而yield return通過(guò)返回Current對(duì)象,來(lái)判斷執(zhí)行MoveNext()的時(shí)機(jī),這部分的工作就是通過(guò)協(xié)程的另一個(gè)部分,也就是協(xié)程調(diào)度器
來(lái)實(shí)現(xiàn)的,協(xié)程調(diào)度器
是Unity引擎實(shí)現(xiàn)的,理論上我們可以自己去實(shí)現(xiàn)一個(gè)協(xié)程調(diào)度器,感興趣的可以自己實(shí)現(xiàn)一個(gè),能進(jìn)一步加深對(duì)協(xié)程的理解。
2-3、實(shí)現(xiàn)協(xié)程原理的代碼
我們已經(jīng)清楚了協(xié)程的原理,以及協(xié)程的組成部分,也就是協(xié)程函數(shù)
和協(xié)程調(diào)度器
,我們可以試著去實(shí)現(xiàn)一個(gè),來(lái)加深對(duì)協(xié)程的理解:
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))
{
// 繼續(xù)指定執(zhí)行時(shí)機(jī)
}
else
{
list.RemoveAt(i);
}
}
else
{
list.RemoveAt(i);
}
}
}
}
}
實(shí)現(xiàn)的代碼主體就是這樣了,當(dāng)然還有一些GC回收沒有做,感興趣的可以繼續(xù)優(yōu)化。
2-4、使用協(xié)同程序
首先,來(lái)看一個(gè)簡(jiǎn)單的程序:
using System.Collections;
using UnityEngine;
public class TestCoroutine : MonoBehaviour
{
void Start()
{
Debug.Log("在協(xié)程之前執(zhí)行函數(shù)");
StartCoroutine(m_Ien());
Debug.Log("在協(xié)程之后執(zhí)行函數(shù)");
}
IEnumerator m_Ien()
{
Debug.Log("執(zhí)行函數(shù)");
yield return new WaitForSeconds(1);//等待1秒
Debug.Log("執(zhí)行后面的函數(shù)");
}
}
運(yùn)行結(jié)果:
PS:這個(gè)例子演示了,協(xié)程的執(zhí)行順序,協(xié)程的寫法,協(xié)程的調(diào)用
執(zhí)行順序:
(1)執(zhí)行協(xié)程之前的代碼
(2)執(zhí)行協(xié)程中的協(xié)程函數(shù)直到中斷程序
(3)執(zhí)行協(xié)程之后的代碼
(4)中斷程序結(jié)束執(zhí)行后面的協(xié)程函數(shù)
協(xié)程寫法:
(1)聲明是IEnumerator 迭代器類型返回值
(2)返回值為yield return new,也就是中斷程序,就跟int的返回值是0123一樣,沒有返回值會(huì)報(bào)錯(cuò)
(3)執(zhí)行中斷程序后面的函數(shù)
協(xié)程的yield return new返回值:
返回值 | 介紹 |
---|---|
yield return null; yield retun x(x代表任意數(shù)字) | 下一幀再執(zhí)行后續(xù)代碼 |
yield break; | 結(jié)束該協(xié)程 |
yield return new WaitForSeconds(0.3f); | 等待固定時(shí)間執(zhí)行后續(xù)代碼 |
yield return FunctionName(); | 函數(shù)執(zhí)行完畢后執(zhí)行后續(xù)代碼 |
yield return AsyncOperation; | 異步執(zhí)行完畢后執(zhí)行后續(xù)代碼 |
yield return Coroutine; | 協(xié)程執(zhí)行完畢后執(zhí)行后續(xù)代碼 |
yield return new WaitForEndOfFrame(); | 幀渲染完成后執(zhí)行后續(xù)代碼 |
yield return new WaitForFixedUpdate(); | 物理幀更新后執(zhí)行后續(xù)代碼 |
yield return new WaitUntil(arg); | 參數(shù)為true時(shí)執(zhí)行后續(xù)代碼 |
協(xié)程的調(diào)用:
(1)調(diào)用協(xié)程使用StartCoroutine(m_Ien());
(2)調(diào)用協(xié)程還可以這么寫StartCoroutine(“m_Ien”);
(3)終止協(xié)程用StopCoroutine(m_Ien());
(4)終止協(xié)程還可以這么寫StopCoroutine(“m_Ien”);
(5)聲明協(xié)程再終止協(xié)程,mCoroutine= StartCoroutine(m_Ien());StopCoroutine(mCoroutine);
(6)終止所有協(xié)程StopAllCoroutines();
2-5、實(shí)現(xiàn)一個(gè)自己的WaitForSeconds
協(xié)程的所能達(dá)到的效果就是在指定的時(shí)間點(diǎn)上執(zhí)行需要執(zhí)行的代碼,Unity中開始一個(gè)協(xié)程的函數(shù)是StartCoroutine,而提供的延遲的類有以下幾種分別是:
new WaitForEndOfFrame; //等待一幀
new WaitForFixedUpdate; //等待一個(gè)FixedUpdate(固定時(shí)間間隔)
new WaitForSeconds; //等待X秒
new WWW; //等待外部資源加載完畢
本節(jié)就針對(duì)其中的WaitForSeconds實(shí)現(xiàn)進(jìn)行探究。
因?yàn)樵陂_發(fā)過(guò)程中,很多時(shí)候會(huì)遇到一種情況就是,超時(shí)或者是符合某種條件就繼續(xù)運(yùn)行,使用系統(tǒng)提供WaitForSeconds已經(jīng)無(wú)法滿足要求了。
這時(shí)候有兩種解決方法,一種是使用StopCoroutine來(lái)停止協(xié)程,但是對(duì)于Unity來(lái)說(shuō),這種行為會(huì)造成很大的開銷;第二種就是可以采用重寫WaitForSeconds,使它能達(dá)到我們的要求。
以下是重寫WaitForSeconds的代碼:
/// <summary>
/// 任務(wù)擴(kuò)展
/// </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;
}
}
}
}
調(diào)用的方法與Unity差不多:
yield return CTaskExtend.WaitForSeconds(delayTime);
那么如果遇到之前說(shuō)的那一種情況(超時(shí)或者是符合某種條件就繼續(xù)運(yùn)行),這里需要做怎么樣的改動(dòng)呢?如下:
/// <summary>
/// 任務(wù)擴(kuò)展
/// </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;
}
}
}
}
加上了一個(gè)回調(diào)函數(shù),每次都會(huì)檢查這個(gè)函數(shù)是否為true,如果為true則停止等待。
2-6、自定義yield new return
我發(fā)現(xiàn)協(xié)程的返回值有這個(gè):
yield return Coroutine;//協(xié)程執(zhí)行完畢后執(zhí)行后續(xù)代碼
也就是協(xié)程類型的返回值,我在想,是不是可以通過(guò)擴(kuò)展Coroutine,來(lái)寫一個(gè)自定義的中斷指令,也就是yield new return。
我們假設(shè)這樣一種情況,當(dāng)一個(gè)動(dòng)畫播放后,再執(zhí)行其他函數(shù)。
參考代碼如下:
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”函數(shù)中,我通過(guò)m_animState.enabled來(lái)判斷動(dòng)畫是否播放完了。
完整的測(cè)試代碼如下:
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測(cè)試");
if (GUILayout.Button("啟動(dòng)"))
{
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.開始播放攻擊動(dòng)畫。" + Time.time * 1000);
anim.Play(animAttack.name);
yield return StartCoroutine(new WaitForEndOfAnim(animAttack));
Debug.Log("2.開始播放受擊動(dòng)畫。" + Time.time * 1000);
anim.Play(animHit.name);
yield return StartCoroutine(new WaitForEndOfAnim(animHit));
Debug.Log("3.開始播放死亡動(dòng)畫。" + Time.time * 1000);
anim.Play(animDie.name);
yield return StartCoroutine(new WaitForEndOfAnim(animDie));
}
}
三、后記
這篇文章詳細(xì)講解了Unity3D的協(xié)程的原理以及使用。
以及重寫協(xié)程程序的返回值和自定義協(xié)程返回值。
對(duì)于某些代碼來(lái)說(shuō)難度比較高,推薦多理解多練習(xí)。
你的點(diǎn)贊就是對(duì)博主的支持,有問題記得留言:
博主主頁(yè)有聯(lián)系方式。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-650457.html
博主還有跟多寶藏文章等待你的發(fā)掘哦:文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-650457.html
專欄 | 方向 | 簡(jiǎn)介 |
---|---|---|
Unity3D開發(fā)小游戲 | 小游戲開發(fā)教程 | 分享一些使用Unity3D引擎開發(fā)的小游戲,分享一些制作小游戲的教程。 |
Unity3D從入門到進(jìn)階 | 入門 | 從自學(xué)Unity中獲取靈感,總結(jié)從零開始學(xué)習(xí)Unity的路線,有C#和Unity的知識(shí)。 |
Unity3D之UGUI | UGUI | Unity的UI系統(tǒng)UGUI全解析,從UGUI的基礎(chǔ)控件開始講起,然后將UGUI的原理,UGUI的使用全面教學(xué)。 |
Unity3D之讀取數(shù)據(jù) | 文件讀取 | 使用Unity3D讀取txt文檔、json文檔、xml文檔、csv文檔、Excel文檔。 |
Unity3D之?dāng)?shù)據(jù)集合 | 數(shù)據(jù)集合 | 數(shù)組集合:數(shù)組、List、字典、堆棧、鏈表等數(shù)據(jù)集合知識(shí)分享。 |
Unity3D之VR/AR(虛擬仿真)開發(fā) | 虛擬仿真 | 總結(jié)博主工作常見的虛擬仿真需求進(jìn)行案例講解。 |
Unity3D之插件 | 插件 | 主要分享在Unity開發(fā)中用到的一些插件使用方法,插件介紹等 |
Unity3D之日常開發(fā) | 日常記錄 | 主要是博主日常開發(fā)中用到的,用到的方法技巧,開發(fā)思路,代碼分享等 |
Unity3D之日常BUG | 日常記錄 | 記錄在使用Unity3D編輯器開發(fā)項(xiàng)目過(guò)程中,遇到的BUG和坑,讓后來(lái)人可以有些參考。 |
到了這里,關(guān)于【Unity3D日常開發(fā)】Unity3D中協(xié)程的使用的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!