รูปแบบผลตอบแทน StartCoroutine / yield ทำงานอย่างไรใน Unity


134

ฉันเข้าใจหลักการของโครูทีน ฉันรู้วิธีทำให้มาตรฐานStartCoroutine/ yield returnรูปแบบทำงานใน C # ใน Unity เช่นเรียกใช้วิธีการส่งคืนIEnumeratorผ่านStartCoroutineและในวิธีนั้นทำอะไรบางอย่างyield return new WaitForSeconds(1);รอสักครู่แล้วทำอย่างอื่น

คำถามของฉันคือเกิดอะไรขึ้นเบื้องหลัง? อะไรStartCoroutineทำจริงๆ? กลับมาIEnumeratorคืออะไรWaitForSeconds? การStartCoroutineควบคุมกลับไปยังส่วน "อย่างอื่น" ของวิธีการที่เรียกว่าอย่างไร ทั้งหมดนี้โต้ตอบกับโมเดลการทำงานพร้อมกันของ Unity อย่างไร (ซึ่งมีหลายสิ่งเกิดขึ้นพร้อมกันโดยไม่ต้องใช้โครูทีน)


3
คอมไพลเลอร์ C # แปลงวิธีการที่ส่งคืนIEnumerator/ IEnumerable(หรือเทียบเท่าทั่วไป) และที่มีyieldคีย์เวิร์ด ค้นหา iterators
Damien_The_Unbeliever

4
ตัววนซ้ำเป็นนามธรรมที่สะดวกมากสำหรับ "เครื่องสถานะ" ทำความเข้าใจก่อนแล้วคุณจะได้รับโครูทีน Unity เช่นกัน en.wikipedia.org/wiki/State_machine
Hans Passant

2
แท็กเอกภาพถูกสงวนไว้โดย Microsoft Unity กรุณาอย่าใช้ในทางที่ผิด
Lex Li

11
ฉันพบว่าบทความนี้สว่างไสว: Unity3D โดยละเอียด
Kay

5
@Kay - ฉันหวังว่าฉันจะซื้อเบียร์ให้คุณ บทความนั้นคือสิ่งที่ฉันต้องการ ฉันเริ่มตั้งคำถามถึงความมีสติสัมปชัญญะของฉันเพราะดูเหมือนว่าคำถามของฉันไม่สมเหตุสมผล แต่บทความนี้ตอบคำถามของฉันได้ดีกว่าที่ฉันจะจินตนาการได้โดยตรง บางทีคุณอาจเพิ่มคำตอบด้วยลิงค์นี้ซึ่งฉันยอมรับได้เพื่อประโยชน์ของผู้ใช้ SO ในอนาคต?
Ghopper21

คำตอบ:


109

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 แต่บางทีคุณอาจพบว่ามีประโยชน์ในการใช้ประโยชน์จากพวกเขาในลักษณะเดียวกัน


2
ขอบคุณสำหรับการผลิตซ้ำที่นี่ มันยอดเยี่ยมมากและช่วยฉันอย่างมาก
Naikrovek

96

หัวข้อแรกด้านล่างคือคำตอบที่ตรงสำหรับคำถาม สองหัวข้อหลังมีประโยชน์มากกว่าสำหรับโปรแกรมเมอร์ในชีวิตประจำวัน

รายละเอียดการใช้งาน Coroutines ที่น่าเบื่อ

Coroutines ได้รับการอธิบายในWikipediaและที่อื่น ๆ ที่นี่ฉันจะให้รายละเอียดบางส่วนจากมุมมองที่ใช้ได้จริง IEnumerator, yieldฯลฯ เป็นC # คุณสมบัติภาษาที่ใช้สำหรับบางส่วนของวัตถุประสงค์ที่แตกต่างกันในความสามัคคี

พูดง่ายๆก็คือการIEnumeratorอ้างว่ามีคอลเล็กชันของค่าที่คุณสามารถร้องขอได้ทีละรายการเช่นกList. ใน C # ฟังก์ชันที่มีลายเซ็นเพื่อส่งกลับIEnumeratorไม่จำเป็นต้องสร้างและส่งคืนจริง แต่สามารถให้ C # ระบุโดยนัยIEnumeratorได้ จากนั้นฟังก์ชั่นสามารถให้เนื้อหาของสิ่งที่ส่งคืนIEnumeratorในอนาคตในรูปแบบที่ขี้เกียจผ่านyield returnแถลงการณ์ ทุกครั้งที่ผู้โทรขอค่าอื่นจากนัยIEnumeratorนั้นฟังก์ชันจะทำงานจนถึงyield returnคำสั่งถัดไปซึ่งจะให้ค่าถัดไป เนื่องจากผลพลอยได้จากนี้ฟังก์ชันจะหยุดชั่วคราวจนกว่าจะมีการร้องขอค่าถัดไป

ใน Unity เราไม่ได้ใช้สิ่งเหล่านี้เพื่อให้คุณค่าในอนาคตเราใช้ประโยชน์จากข้อเท็จจริงที่ว่าฟังก์ชันหยุดชั่วคราว เนื่องจากการแสวงหาผลประโยชน์นี้หลายสิ่งเกี่ยวกับโครูทีนใน Unity จึงไม่สมเหตุสมผล ( IEnumeratorเกี่ยวข้องกับอะไรบ้างคือyieldอะไรทำไมnew WaitForSeconds(3)ฯลฯ ) สิ่งที่เกิดขึ้น "ภายใต้ประทุน" คือค่าที่คุณระบุผ่าน IEnumerator จะถูกใช้StartCoroutine()เพื่อตัดสินใจว่าจะขอค่าถัดไปเมื่อใดซึ่งจะกำหนดว่าโครูทีนของคุณจะยกเลิกการหยุดชั่วคราวอีกครั้งเมื่อใด

เกม Unity ของคุณเป็นแบบเธรดเดียว (*)

Coroutines ไม่ใช่เธรด มีลูปหลักหนึ่งวงของ Unity และฟังก์ชันทั้งหมดที่คุณเขียนจะถูกเรียกโดยเธรดหลักเดียวกันตามลำดับ คุณสามารถตรวจสอบได้โดยใส่while(true);ในฟังก์ชันหรือโคโรทีนของคุณ มันจะหยุดทุกอย่างแม้แต่โปรแกรมแก้ไข Unity นี่เป็นหลักฐานว่าทุกอย่างทำงานในเธรดหลักเดียว ลิงก์ที่ Kay กล่าวถึงในความคิดเห็นข้างต้นนี้เป็นแหล่งข้อมูลที่ยอดเยี่ยมเช่นกัน

(*) Unity เรียกใช้ฟังก์ชันของคุณจากเธรดเดียว ดังนั้นถ้าคุณไม่สร้างเธรดด้วยตัวเองโค้ดที่คุณเขียนจะเป็นเธรดเดียว แน่นอนว่า Unity ใช้เธรดอื่นและคุณสามารถสร้างเธรดด้วยตัวคุณเองได้หากต้องการ

คำอธิบายเชิงปฏิบัติของ Coroutines สำหรับโปรแกรมเมอร์เกม

โดยทั่วไปเมื่อคุณเรียกStartCoroutine(MyCoroutine())มันเหมือนกับการเรียกฟังก์ชันปกติเพื่อMyCoroutine()จนกว่าคนแรกyield return Xที่Xเป็นสิ่งที่ต้องการnull, new WaitForSeconds(3), StartCoroutine(AnotherCoroutine()), breakฯลฯ นี้คือเมื่อมันเริ่มแตกต่างจากฟังก์ชั่น Unity "หยุด" ฟังก์ชันนั้นทันทีที่yield return Xบรรทัดนั้นดำเนินการต่อกับธุรกิจอื่นและบางเฟรมก็ผ่านไปและเมื่อถึงเวลาอีกครั้ง Unity จะกลับมาใช้ฟังก์ชันนั้นทันทีหลังจากบรรทัดนั้น มันจำค่าสำหรับตัวแปรท้องถิ่นทั้งหมดในฟังก์ชัน ด้วยวิธีนี้คุณสามารถมีforลูปที่วนซ้ำทุก ๆ สองวินาทีเป็นต้น

เมื่อ Unity จะกลับมาใช้งานโครูทีนของคุณขึ้นอยู่กับสิ่งที่Xอยู่ในyield return Xไฟล์. ตัวอย่างเช่นหากคุณใช้yield return new WaitForSeconds(3);มันจะกลับมาทำงานต่อหลังจากผ่านไป 3 วินาที หากคุณใช้yield return StartCoroutine(AnotherCoroutine())มันจะกลับมาทำงานต่อหลังจากAnotherCoroutine()เสร็จสิ้นอย่างสมบูรณ์ซึ่งช่วยให้คุณสามารถซ้อนพฤติกรรมได้ทันเวลา หากคุณเพิ่งใช้ a yield return null;มันจะกลับมาทำงานในเฟรมถัดไป


2
มันแย่เกินไป UnityGems ดูเหมือนจะหยุดทำงานในขณะนี้ บางคนใน Reddit ได้รับไฟล์เก็บถาวรเวอร์ชันล่าสุด: web.archive.org/web/20140702051454/http://unitygems.com/…
ForceMagic

3
สิ่งนี้คลุมเครือและเสี่ยงต่อการไม่ถูกต้อง นี่คือวิธีการรวบรวมโค้ดจริง ๆ และเหตุใดจึงใช้งานได้ นอกจากนี้ยังไม่ตอบคำถามด้วย stackoverflow.com/questions/3438670/…
Louis Hong

ใช่ฉันเดาว่าฉันอธิบาย "coroutines ใน Unity ทำงานอย่างไร" จากมุมมองของโปรแกรมเมอร์เกม Quastion ที่แท้จริงกำลังถามว่าเกิดอะไรขึ้นภายใต้ประทุน หากคุณสามารถชี้ให้เห็นส่วนที่ไม่ถูกต้องในคำตอบของฉันฉันยินดีที่จะแก้ไข
Gazihan Alankus

4
ฉันเห็นด้วยเกี่ยวกับผลตอบแทนที่เป็นเท็จฉันเพิ่มเพราะมีคนวิพากษ์วิจารณ์ว่าคำตอบของฉันไม่มีมันและฉันก็รีบตรวจสอบว่ามันมีประโยชน์หรือไม่และเพิ่มลิงก์เข้าไป ฉันลบมันออกแล้ว อย่างไรก็ตามฉันคิดว่า Unity เป็นเธรดเดียวและ Coroutines เข้ากันได้อย่างไรนั้นไม่ชัดเจนสำหรับทุกคน โปรแกรมเมอร์ Unity มือใหม่หลายคนที่ฉันพูดคุยด้วยมีความเข้าใจที่คลุมเครือเกี่ยวกับเรื่องทั้งหมดและได้รับประโยชน์จากคำอธิบายดังกล่าว ฉันแก้ไขคำตอบของฉันเพื่อให้เป็นคำตอบตามความเป็นจริงสำหรับคำถาม ข้อเสนอแนะยินดีต้อนรับ
Gazihan Alankus

2
Unity ไม่ใช่ fwiw เธรดเดียว มันมีเธรดหลักที่เมธอดวงจรชีวิตของ MonoBehaviour ทำงานใน - แต่ก็มีเธรดอื่นด้วย คุณมีอิสระในการสร้างกระทู้ของคุณเอง
benthehutt

10

มันไม่ง่ายไปกว่านี้:

ความสามัคคี (และทุกเครื่องมือเกม) เป็นกรอบตาม

จุดรวมทั้งหมดคือ raison d'etre ของ Unity ทั้งหมดคือมันเป็นไปตามกรอบ เครื่องยนต์ทำ "แต่ละเฟรม" ให้คุณ (เคลื่อนไหวสร้างวัตถุทำฟิสิกส์และอื่น ๆ )

คุณอาจถามว่า .. "โอ้เยี่ยมมากถ้าฉันต้องการให้เครื่องยนต์ทำอะไรให้ฉันทีละเฟรมฉันจะบอกให้เครื่องยนต์ทำแบบนั้นในเฟรมได้อย่างไร"

คำตอบคือ ...

นั่นคือสิ่งที่ "โครูทีน" มีไว้สำหรับ

ง่ายๆแค่นั้นเอง

และพิจารณาสิ่งนี้ ....

คุณรู้จักฟังก์ชัน "อัปเดต" ค่อนข้างเพียงสิ่งที่คุณใส่ในนั้นจะทำทุกเฟรม มันเหมือนกันทุกประการไม่มีความแตกต่างเลยจากไวยากรณ์ coroutine-yield

void Update()
 {
 this happens every frame,
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 }

...in a coroutine...
 while(true)
 {
 this happens every frame.
 you want Unity to do something of "yours" in each of the frame,
 put it in here
 yield return null;
 }

ไม่มีความแตกต่างกันอย่างแน่นอน

เชิงอรรถ: เป็นคนมีการชี้ความสามัคคีก็มีไม่มีหัวข้อ "เฟรม" ใน Unity หรือในเกมเอนจิ้นใด ๆ ไม่มีส่วนเกี่ยวข้องกับเธรด แต่อย่างใด

Coroutines / yield เป็นเพียงวิธีที่คุณเข้าถึงเฟรมใน Unity แค่นั้นแหละ. (และแน่นอนมันเหมือนกับฟังก์ชั่น Update () ที่ Unity มีให้อย่างแน่นอน) นั่นคือทั้งหมดที่มีให้มันง่ายมาก


ขอบคุณ! แต่คำตอบของคุณอธิบายถึงวิธีการใช้โครูทีนไม่ใช่วิธีการทำงานเบื้องหลัง
Ghopper21

1
เป็นความสุขของฉันขอบคุณ ฉันเข้าใจว่าคุณหมายถึงอะไร - นี่อาจเป็นคำตอบที่ดีสำหรับผู้เริ่มต้นที่มักจะถามว่าอะไรคืออะไร ไชโย!
Fattie

1
อันที่จริง - ไม่มีคำตอบใด ๆ เลยแม้แต่เล็กน้อยที่อธิบายว่าเกิดอะไรขึ้น "เบื้องหลัง" (ซึ่งก็คือมันเป็น IEnumerator ที่ซ้อนอยู่ในตัวกำหนดตารางเวลา)
Fattie

คุณบอกว่า "ไม่มีความแตกต่างอย่างแน่นอน" แล้วทำไม Unity จึงสร้าง Coroutines ในเมื่อพวกเขามีการใช้งานที่แน่นอนอยู่แล้วUpdate()? ฉันหมายความว่าอย่างน้อยควรมีความแตกต่างเล็กน้อยระหว่างการใช้งานทั้งสองนี้กับกรณีการใช้งานซึ่งค่อนข้างชัดเจน
Leandro Gecozo

เฮ้ @LeandroGecozo - ฉันจะพูดมากกว่านี้ว่า "อัปเดต" เป็นเพียงการทำให้เข้าใจง่าย ("โง่ ๆ ") ที่พวกเขาเพิ่มเข้ามา (หลายคนไม่เคยใช้ แต่ใช้โครูทีน!) ฉันไม่คิดว่าจะมีคำตอบที่ดีสำหรับคำถามของคุณ แต่ความสามัคคีเป็นอย่างไร
Fattie

5

เมื่อเร็ว ๆ นี้ได้เจาะลึกเรื่องนี้เขียนโพสต์ที่นี่ - http://eppz.eu/blog/understand-ienumerator-in-unity-3d/ - ที่ให้ความกระจ่างเกี่ยวกับภายใน (พร้อมตัวอย่างโค้ดที่หนาแน่น) IEnumeratorอินเทอร์เฟซพื้นฐานและวิธีใช้สำหรับโครูทีน

การใช้ตัวนับคอลเลกชันเพื่อจุดประสงค์นี้ยังคงดูแปลกสำหรับฉัน มันเป็นสิ่งที่ตรงกันข้ามกับสิ่งที่ผู้แจงนับรู้สึกว่าออกแบบมาเพื่อ จุดของตัวแจงนับคือค่าที่ส่งคืนในทุกการเข้าถึง แต่จุดของ Coroutines คือรหัสที่อยู่ระหว่างการส่งคืนค่า ค่าที่ส่งคืนจริงไม่มีจุดหมายในบริบทนี้


0

ฟังก์ชันพื้นฐานใน Unity ที่คุณได้รับโดยอัตโนมัติคือฟังก์ชัน Start () และฟังก์ชัน Update () ดังนั้น Coroutine จึงเป็นฟังก์ชันหลักเช่นเดียวกับฟังก์ชัน Start () และ Update () ฟังก์ชันเก่า ๆ func () สามารถเรียกได้แบบเดียวกับที่ Coroutine สามารถเรียกได้ Unity ได้กำหนดขอบเขตที่แน่นอนสำหรับ Coroutines ที่ทำให้แตกต่างจากฟังก์ชันปกติ ความแตกต่างอย่างหนึ่งคือแทนที่จะเป็น

  void func()

ที่คุณเขียน

  IEnumerator func()

สำหรับโครูทีน และเช่นเดียวกับที่คุณสามารถควบคุมเวลาในฟังก์ชันปกติด้วยสายรหัสเช่น

  Time.deltaTime

โครูทีนมีที่จับเฉพาะเกี่ยวกับวิธีที่สามารถควบคุมเวลาได้

  yield return new WaitForSeconds();

แม้ว่านี่จะไม่ใช่สิ่งเดียวที่ทำได้ภายใน IEnumerator / Coroutine แต่ก็เป็นหนึ่งในสิ่งที่มีประโยชน์ที่ Coroutines ใช้ คุณจะต้องค้นคว้า API การเขียนสคริปต์ของ Unity เพื่อเรียนรู้การใช้งาน Coroutines เฉพาะอื่น ๆ


0

StartCoroutine เป็นวิธีการเรียกใช้ฟังก์ชัน IEnumerator มันคล้ายกับการเรียกใช้ฟังก์ชันโมฆะธรรมดา ๆ แต่ความแตกต่างคือคุณใช้กับฟังก์ชัน IEnumerator ฟังก์ชันประเภทนี้มีลักษณะเฉพาะเนื่องจากสามารถให้คุณใช้ฟังก์ชันผลตอบแทนพิเศษได้โปรดทราบว่าคุณต้องส่งคืนบางสิ่ง นั่นคือเท่าที่ฉันรู้ ที่นี่ฉันเขียนเกมสั่นไหวง่าย ๆวิธีOver textด้วยความสามัคคี

    public IEnumerator GameOver()
{
    while (true)
    {
        _gameOver.text = "GAME OVER";
        yield return new WaitForSeconds(Random.Range(1.0f, 3.5f));
        _gameOver.text = "";
        yield return new WaitForSeconds(Random.Range(0.1f, 0.8f));
    }
}

จากนั้นฉันก็เรียกมันออกจาก IEnumerator เอง

    public void UpdateLives(int currentlives)
{
    if (currentlives < 1)
    {
        _gameOver.gameObject.SetActive(true);
        StartCoroutine(GameOver());
    }
}

อย่างที่คุณเห็นว่าฉันใช้เมธอด StartCoroutine () อย่างไร หวังว่าฉันจะช่วยได้ ฉันเป็นผู้เริ่มต้นด้วยตัวเองดังนั้นหากคุณแก้ไขฉันหรือชื่นชมฉันคำติชมใด ๆ ก็จะดีมาก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.