Coroutines Unity3D ที่อ้างถึงบ่อยครั้งในลิงก์รายละเอียดนั้นตายแล้ว เนื่องจากมีการกล่าวถึงในความคิดเห็นและคำตอบฉันจะโพสต์เนื้อหาของบทความที่นี่ เนื้อหานี้มาจากกระจกเงานี้
Unity3D แสดงรายละเอียด
กระบวนการมากมายในเกมเกิดขึ้นในหลาย ๆ เฟรม คุณมีกระบวนการที่ 'หนาแน่น' เช่นการค้นหาเส้นทางซึ่งทำงานอย่างหนักในแต่ละเฟรม แต่แยกออกเป็นหลาย ๆ เฟรมเพื่อไม่ให้ส่งผลกระทบต่อเฟรมเรตมากเกินไป คุณมีกระบวนการที่ 'กระจัดกระจาย' เช่นทริกเกอร์การเล่นเกมที่ไม่ได้ทำอะไรเลยเฟรมส่วนใหญ่ แต่บางครั้งก็ถูกเรียกร้องให้ทำงานที่สำคัญ และคุณมีกระบวนการต่างๆระหว่างทั้งสอง
เมื่อใดก็ตามที่คุณกำลังสร้างกระบวนการที่จะเกิดขึ้นในหลาย ๆ เฟรมโดยไม่ต้องใช้มัลติเธรดคุณต้องหาวิธีแบ่งงานออกเป็นชิ้น ๆ ที่สามารถเรียกใช้แบบหนึ่งต่อเฟรมได้ สำหรับอัลกอริทึมใด ๆ ที่มีลูปกลางมันค่อนข้างชัดเจนเช่น A * pathfinder สามารถจัดโครงสร้างเพื่อให้โหนดของมันแสดงรายการแบบกึ่งถาวรโดยประมวลผลโหนดเพียงไม่กี่โหนดจากรายการที่เปิดในแต่ละเฟรมแทนที่จะพยายาม เพื่อทำงานทั้งหมดในครั้งเดียว มีการปรับสมดุลบางอย่างเพื่อจัดการความหน่วงแฝง - หากคุณล็อกอัตราเฟรมไว้ที่ 60 หรือ 30 เฟรมต่อวินาทีกระบวนการของคุณจะใช้เวลาเพียง 60 หรือ 30 ขั้นตอนต่อวินาทีและนั่นอาจทำให้กระบวนการใช้เวลาเพียง โดยรวมยาวเกินไป การออกแบบที่ประณีตอาจนำเสนอหน่วยงานที่เล็กที่สุดเท่าที่จะทำได้ในระดับหนึ่งเช่น ประมวลผลโหนด A * เดียวและวางเลเยอร์ไว้ด้านบนวิธีการจัดกลุ่มการทำงานร่วมกันเป็นกลุ่มขนาดใหญ่เช่นประมวลผลโหนด A * ต่อไปเป็นเวลา X มิลลิวินาที (บางคนเรียกสิ่งนี้ว่า 'timeslicing' แม้ว่าฉันจะไม่ทำก็ตาม)
ถึงกระนั้นการปล่อยให้งานถูกแบ่งออกด้วยวิธีนี้หมายความว่าคุณต้องโอนสถานะจากเฟรมหนึ่งไปยังอีกเฟรมหนึ่ง หากคุณกำลังทำลายอัลกอริทึมแบบวนซ้ำคุณจะต้องรักษาสถานะทั้งหมดที่ใช้ร่วมกันในการทำซ้ำรวมถึงวิธีการติดตามที่จะดำเนินการซ้ำต่อไป นั่นไม่ใช่เรื่องเลวร้ายนักการออกแบบ 'A * pathfinder class' นั้นค่อนข้างชัดเจน - แต่ก็มีกรณีอื่นเช่นกันที่ไม่ค่อยน่าพอใจนัก บางครั้งคุณจะต้องเผชิญกับการคำนวณที่ยาวนานซึ่งกำลังทำงานหลายประเภทจากเฟรมหนึ่งไปอีกเฟรมหนึ่ง วัตถุที่จับสถานะของพวกเขาสามารถจบลงด้วย 'ชาวบ้าน' ที่มีประโยชน์ขนาดใหญ่เก็บไว้เพื่อส่งผ่านข้อมูลจากเฟรมหนึ่งไปยังอีกเฟรมหนึ่ง และหากคุณกำลังจัดการกับกระบวนการที่เบาบางคุณมักจะต้องติดตั้งเครื่องจักรขนาดเล็กเพื่อติดตามว่าเมื่อใดควรทำงานให้เสร็จ
จะไม่เรียบร้อยหรือไม่ถ้าแทนที่จะต้องติดตามสถานะทั้งหมดนี้อย่างชัดเจนในหลาย ๆ เฟรมและแทนที่จะต้องมัลติเธรดและจัดการการซิงโครไนซ์และการล็อกเป็นต้นคุณสามารถเขียนฟังก์ชันของคุณเป็นโค้ดชิ้นเดียวและ ทำเครื่องหมายตำแหน่งเฉพาะที่ฟังก์ชันควร "หยุดชั่วคราว" และดำเนินการต่อในภายหลัง?
Unity - พร้อมกับสภาพแวดล้อมและภาษาอื่น ๆ อีกมากมาย - จัดเตรียมสิ่งนี้ในรูปแบบของ Coroutines
พวกเขามีลักษณะอย่างไร? ใน“ Unityscript” (Javascript):
function LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield;
}
}
ใน C #:
IEnumerator LongComputation()
{
while(someCondition)
{
/* Do a chunk of work */
// Pause here and carry on next frame
yield return null;
}
}
พวกเขาทำงานอย่างไร? ให้ฉันพูดเร็ว ๆ ว่าฉันไม่ได้ทำงานให้กับ Unity Technologies ฉันไม่เห็นซอร์สโค้ด Unity ฉันไม่เคยเห็นความกล้าของเครื่องยนต์ Coroutine ของ Unity อย่างไรก็ตามหากพวกเขานำมันไปใช้ในลักษณะที่แตกต่างไปจากที่ฉันกำลังจะอธิบายอย่างสิ้นเชิงฉันจะต้องประหลาดใจทีเดียว หากใครจาก UT ต้องการตีระฆังและพูดคุยเกี่ยวกับวิธีการใช้งานจริงก็จะดีมาก
เบาะแสใหญ่อยู่ในรุ่น C # ประการแรกโปรดทราบว่าประเภทการส่งคืนสำหรับฟังก์ชันคือ IEnumerator และประการที่สองโปรดทราบว่าหนึ่งในข้อความคือผลตอบแทนที่ได้รับ ซึ่งหมายความว่าผลตอบแทนต้องเป็นคีย์เวิร์ดและเนื่องจากการสนับสนุน C # ของ Unity คือวานิลลา C # 3.5 จึงต้องเป็นคีย์เวิร์ดวานิลลา C # 3.5 อันที่จริงมันอยู่ใน MSDN - พูดถึงสิ่งที่เรียกว่า 'iterator Blocks' เกิดอะไรขึ้น?
ประการแรกมีประเภท IEnumerator นี้ ประเภท IEnumerator ทำหน้าที่เหมือนเคอร์เซอร์เหนือลำดับโดยให้สมาชิกที่สำคัญสองตัว: ปัจจุบันซึ่งเป็นคุณสมบัติที่ให้องค์ประกอบที่เคอร์เซอร์อยู่เหนือปัจจุบันและ MoveNext () ซึ่งเป็นฟังก์ชันที่ย้ายไปยังองค์ประกอบถัดไปในลำดับ เนื่องจาก IEnumerator เป็นอินเทอร์เฟซจึงไม่ได้ระบุวิธีการใช้งานสมาชิกเหล่านี้อย่างชัดเจน MoveNext () สามารถเพิ่มหนึ่ง toCurrent หรืออาจโหลดค่าใหม่จากไฟล์หรืออาจดาวน์โหลดภาพจากอินเทอร์เน็ตและแฮชและจัดเก็บแฮชใหม่ในปัจจุบัน ... หรืออาจทำสิ่งหนึ่งในสิ่งแรก องค์ประกอบในลำดับและสิ่งที่แตกต่างอย่างสิ้นเชิงสำหรับวินาที คุณยังสามารถใช้เพื่อสร้างลำดับที่ไม่มีที่สิ้นสุดหากคุณต้องการ MoveNext () คำนวณค่าถัดไปในลำดับ (ส่งคืนค่าเท็จหากไม่มีค่าเพิ่มเติม)
โดยปกติถ้าคุณต้องการใช้อินเทอร์เฟซคุณจะต้องเขียนคลาสใช้งานสมาชิกและอื่น ๆ บล็อก Iterator เป็นวิธีที่สะดวกในการใช้งาน IEnumerator โดยไม่ต้องยุ่งยากเพียงแค่ทำตามกฎไม่กี่ข้อและการใช้งาน IEnumerator จะถูกสร้างขึ้นโดยอัตโนมัติโดยคอมไพเลอร์
บล็อกตัววนซ้ำเป็นฟังก์ชันปกติที่ (a) ส่งคืน IEnumerator และ (b) ใช้คีย์เวิร์ดผลตอบแทน แล้วคำหลักผลตอบแทนทำหน้าที่อะไร? มันประกาศว่าค่าถัดไปในลำดับคืออะไร - หรือไม่มีค่าเพิ่มเติม จุดที่โค้ดพบผลตอบแทน X หรือตัวแบ่งผลตอบแทนคือจุดที่ IEnumerator.MoveNext () ควรหยุด; ผลตอบแทน X ทำให้ MoveNext () ส่งคืนจริงและปัจจุบันเพื่อกำหนดค่า X ในขณะที่การแบ่งผลตอบแทนทำให้ MoveNext () ส่งคืนค่าเท็จ
ตอนนี้นี่คือเคล็ดลับ ไม่จำเป็นต้องสำคัญว่าค่าที่แท้จริงที่ส่งคืนตามลำดับคืออะไร คุณสามารถเรียก MoveNext () ซ้ำ ๆ และละเว้นปัจจุบัน การคำนวณจะยังคงดำเนินการอยู่ ทุกครั้งที่มีการเรียก MoveNext () บล็อกตัววนซ้ำของคุณจะทำงานไปยังคำสั่ง 'yield' ถัดไปโดยไม่คำนึงถึงนิพจน์ที่ให้ผลจริง ดังนั้นคุณสามารถเขียนสิ่งต่างๆเช่น:
IEnumerator TellMeASecret()
{
PlayAnimation("LeanInConspiratorially");
while(playingAnimation)
yield return null;
Say("I stole the cookie from the cookie jar!");
while(speaking)
yield return null;
PlayAnimation("LeanOutRelieved");
while(playingAnimation)
yield return null;
}
และสิ่งที่คุณเขียนจริงๆคือบล็อกตัววนซ้ำที่สร้างลำดับค่าว่างยาว ๆ แต่สิ่งที่สำคัญคือผลข้างเคียงของงานที่คำนวณค่าเหล่านี้ คุณสามารถเรียกใช้โครูทีนนี้ได้โดยใช้ลูปง่ายๆดังนี้:
IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }
หรือมีประโยชน์มากกว่านั้นคุณสามารถผสมผสานกับงานอื่น ๆ :
IEnumerator e = TellMeASecret();
while(e.MoveNext())
{
// If they press 'Escape', skip the cutscene
if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}
ทุกอย่างอยู่ในช่วงเวลาดังที่คุณเห็นแต่ละคำสั่งผลตอบแทนผลตอบแทนต้องมีนิพจน์ (เช่น null) เพื่อให้บล็อกตัววนซ้ำมีบางอย่างที่จะกำหนดให้กับ IEnumerator ปัจจุบัน ลำดับค่าว่างที่ยาวไม่ได้มีประโยชน์ แต่เราสนใจผลข้างเคียงมากกว่า เราไม่ใช่เหรอ
มีบางอย่างที่เป็นประโยชน์ที่เราสามารถทำได้กับสำนวนนั้น จะเป็นอย่างไรถ้าแทนที่จะยอมเป็นโมฆะและเพิกเฉยเรากลับให้สิ่งที่ระบุเมื่อเราคาดว่าจะต้องทำงานเพิ่มขึ้น บ่อยครั้งที่เราจำเป็นต้องดำเนินการต่อในเฟรมถัดไปอย่างแน่นอน แต่ก็ไม่เสมอไป: จะมีหลายครั้งที่เราต้องการดำเนินการต่อหลังจากที่ภาพเคลื่อนไหวหรือเสียงเล่นเสร็จหรือหลังจากเวลาผ่านไประยะหนึ่ง ขณะที่ (playingAnimation) ให้ผลตอบแทนเป็นโมฆะ โครงสร้างค่อนข้างน่าเบื่อคุณไม่คิดเหรอ?
Unity ประกาศประเภทของฐาน YieldInstruction และมีคอนกรีตที่ได้รับมาสองสามประเภทซึ่งระบุประเภทของการรอโดยเฉพาะ คุณมี WaitForSeconds ซึ่งจะเรียกคืนโครูทีนหลังจากระยะเวลาที่กำหนดผ่านไป คุณมี WaitForEndOfFrame ซึ่งจะเรียกคืนโครูทีน ณ จุดใดจุดหนึ่งในภายหลังในเฟรมเดียวกัน คุณมีประเภท Coroutine เองซึ่งเมื่อโครูทีน A ให้โครูทีน B จะหยุดโครูทีน A ชั่วคราวจนกว่าโครูทีน B จะเสร็จสิ้น
สิ่งนี้มีลักษณะอย่างไรจากมุมมองรันไทม์ อย่างที่บอกฉันไม่ได้ทำงานเพื่อ Unity ดังนั้นฉันจึงไม่เคยเห็นรหัสของพวกเขา แต่ฉันคิดว่ามันอาจจะเป็นแบบนี้:
List<IEnumerator> unblockedCoroutines;
List<IEnumerator> shouldRunNextFrame;
List<IEnumerator> shouldRunAtEndOfFrame;
SortedList<float, IEnumerator> shouldRunAfterTimes;
foreach(IEnumerator coroutine in unblockedCoroutines)
{
if(!coroutine.MoveNext())
// This coroutine has finished
continue;
if(!coroutine.Current is YieldInstruction)
{
// This coroutine yielded null, or some other value we don't understand; run it next frame.
shouldRunNextFrame.Add(coroutine);
continue;
}
if(coroutine.Current is WaitForSeconds)
{
WaitForSeconds wait = (WaitForSeconds)coroutine.Current;
shouldRunAfterTimes.Add(Time.time + wait.duration, coroutine);
}
else if(coroutine.Current is WaitForEndOfFrame)
{
shouldRunAtEndOfFrame.Add(coroutine);
}
else /* similar stuff for other YieldInstruction subtypes */
}
unblockedCoroutines = shouldRunNextFrame;
ไม่ยากที่จะจินตนาการว่าสามารถเพิ่มประเภทย่อย YieldInstruction เพื่อจัดการกับกรณีอื่น ๆ ได้อย่างไรเช่นสามารถเพิ่มการสนับสนุนระดับเครื่องยนต์สำหรับสัญญาณโดยมี YieldInstruction ("SignalName") WaitForSignal ("SignalName") รองรับ ด้วยการเพิ่ม YieldInstructions ให้มากขึ้นการแก้ไขตัวเองสามารถแสดงออกได้มากขึ้น - ให้ผลตอบแทน WaitForSignal ("GameOver") ใหม่ที่อ่านได้ดีกว่าในขณะเดียวกัน (! Signals.HasFired ("GameOver")) ให้ผลตอบแทนเป็นโมฆะถ้าคุณถามฉันค่อนข้างนอกเหนือจาก ความจริงที่ว่าการทำในเครื่องยนต์อาจเร็วกว่าการทำในสคริปต์
การแบ่งส่วนที่ไม่ชัดเจนสองสามอย่างมีประโยชน์สองสามประการเกี่ยวกับทั้งหมดนี้ที่บางครั้งผู้คนพลาดที่ฉันคิดว่าฉันควรชี้ให้เห็น
ประการแรกผลตอบแทนผลตอบแทนเป็นเพียงการให้นิพจน์ - นิพจน์ใด ๆ - และ YieldInstruction เป็นประเภทปกติ ซึ่งหมายความว่าคุณสามารถทำสิ่งต่างๆเช่น:
YieldInstruction y;
if(something)
y = null;
else if(somethingElse)
y = new WaitForEndOfFrame();
else
y = new WaitForSeconds(1.0f);
yield return y;
บรรทัดเฉพาะจะให้ผลตอบแทน WaitForSeconds () ใหม่ให้ผลตอบแทน WaitForEndOfFrame ใหม่ () ฯลฯ เป็นเรื่องปกติ แต่ก็ไม่ได้มีรูปแบบพิเศษตามสิทธิของตนเอง
ประการที่สองเนื่องจากโครูทีนเหล่านี้เป็นเพียงตัวทำซ้ำบล็อกคุณสามารถทำซ้ำได้ด้วยตัวเองหากคุณต้องการคุณไม่จำเป็นต้องให้เอ็นจิ้นทำเพื่อคุณ ฉันเคยใช้สิ่งนี้เพื่อเพิ่มเงื่อนไขการขัดจังหวะให้กับโครูทีนมาก่อน:
IEnumerator DoSomething()
{
/* ... */
}
IEnumerator DoSomethingUnlessInterrupted()
{
IEnumerator e = DoSomething();
bool interrupted = false;
while(!interrupted)
{
e.MoveNext();
yield return e.Current;
interrupted = HasBeenInterrupted();
}
}
ประการที่สามความจริงที่ว่าคุณสามารถให้ Coroutines อื่น ๆ สามารถช่วยให้คุณสามารถใช้ YieldInstructions ของคุณเองได้แม้ว่าจะไม่ได้มีประสิทธิภาพเหมือนกับที่ใช้เครื่องยนต์ ตัวอย่างเช่น:
IEnumerator UntilTrueCoroutine(Func fn)
{
while(!fn()) yield return null;
}
Coroutine UntilTrue(Func fn)
{
return StartCoroutine(UntilTrueCoroutine(fn));
}
IEnumerator SomeTask()
{
/* ... */
yield return UntilTrue(() => _lives < 3);
/* ... */
}
อย่างไรก็ตามฉันไม่อยากแนะนำสิ่งนี้จริงๆ - ค่าใช้จ่ายในการเริ่มต้น Coroutine นั้นค่อนข้างหนักสำหรับความชอบของฉัน
บทสรุปฉันหวังว่าสิ่งนี้จะช่วยชี้แจงสิ่งที่เกิดขึ้นจริงเล็กน้อยเมื่อคุณใช้ Coroutine ใน Unity บล็อกตัววนซ้ำของ C # เป็นโครงสร้างเล็ก ๆ ที่น่าสนใจและแม้ว่าคุณจะไม่ได้ใช้ Unity แต่บางทีคุณอาจพบว่ามีประโยชน์ในการใช้ประโยชน์จากพวกเขาในลักษณะเดียวกัน
IEnumerator
/IEnumerable
(หรือเทียบเท่าทั่วไป) และที่มีyield
คีย์เวิร์ด ค้นหา iterators