Làm thế nào để mô hình trả về StartCoroutine / suất thực sự hoạt động trong Unity?


134

Tôi hiểu nguyên tắc của coroutines. Tôi biết làm thế nào để có được những tiêu chuẩn StartCoroutine/ yield returnmẫu với công việc trong C # trong Unity, ví dụ như gọi một phương thức trả về IEnumeratorqua StartCoroutinevà trong phương pháp mà làm điều gì đó, làm yield return new WaitForSeconds(1);phải chờ một giây, sau đó làm cái gì khác.

Câu hỏi của tôi là: những gì thực sự xảy ra đằng sau hậu trường? Không gì StartCoroutinethực sự làm gì? Điều gì IEnumeratorđang WaitForSecondstrở lại? Làm thế nào để StartCoroutinetrả lại quyền kiểm soát cho phần "cái gì khác" của phương thức được gọi? Làm thế nào để tất cả những điều này tương tác với mô hình đồng thời của Unity (nơi có rất nhiều thứ đang diễn ra cùng một lúc mà không sử dụng coroutines)?


3
Trình biên dịch C # biến đổi các phương thức trả về IEnumerator/ IEnumerable(hoặc tương đương chung) và có chứa yieldtừ khóa. Tra cứu lặp đi lặp lại.
Damien_The_Unbeliever

4
Một iterator là một sự trừu tượng hóa rất thuận tiện cho một "máy trạng thái". Hiểu điều đó trước tiên và bạn cũng sẽ nhận được Unity coroutines. vi.wikipedia.org/wiki/State_machine
Hans Passant

2
Thẻ thống nhất được bảo lưu bởi Microsoft Unity. Xin đừng lạm dụng nó.
Lex Li

11
Tôi thấy bài viết này khá sáng sủa
Kay

5
@Kay - Ước gì tôi có thể mua cho bạn một cốc bia. Bài viết đó chính xác là những gì tôi cần. Tôi đã bắt đầu đặt câu hỏi về sự tỉnh táo của mình vì dường như câu hỏi của tôi thậm chí không có ý nghĩa gì, nhưng bài báo trực tiếp trả lời câu hỏi của tôi tốt hơn tôi có thể tưởng tượng. Có lẽ bạn có thể thêm câu trả lời với liên kết này mà tôi có thể chấp nhận, vì lợi ích của người dùng SO trong tương lai?
Ghopper21

Câu trả lời:


109

Các coroutines tham chiếu của Unity3D trong liên kết chi tiết đã chết. Vì nó được đề cập trong các ý kiến ​​và câu trả lời tôi sẽ đăng nội dung của bài viết ở đây. Nội dung này đến từ tấm gương này .


Thông tin chi tiết về Unity3D

Nhiều quá trình trong các trò chơi diễn ra trong quá trình nhiều khung hình. Bạn đã có các quy trình 'dày đặc', như tìm đường, làm việc chăm chỉ từng khung hình nhưng được phân chia thành nhiều khung để không ảnh hưởng quá nhiều đến tốc độ khung hình. Bạn đã có các quy trình 'thưa thớt', như kích hoạt trò chơi, không làm gì hầu hết các khung, nhưng đôi khi được yêu cầu thực hiện công việc quan trọng. Và bạn đã có các loại quy trình giữa hai.

Bất cứ khi nào bạn đang tạo một quy trình sẽ diễn ra trên nhiều khung hình - mà không cần đa luồng - bạn cần tìm một cách nào đó để chia công việc thành các phần có thể chạy từng khung hình. Đối với bất kỳ thuật toán nào có vòng lặp trung tâm, ví dụ khá rõ ràng: một đường dẫn A * có thể được cấu trúc sao cho nó duy trì danh sách nút của nó bán vĩnh viễn, chỉ xử lý một số nút từ danh sách mở mỗi khung, thay vì thử để làm tất cả công việc trong một lần. Có một số cân bằng được thực hiện để quản lý độ trễ - xét cho cùng, nếu bạn khóa tốc độ khung hình của mình ở mức 60 hoặc 30 khung hình mỗi giây, thì quá trình của bạn sẽ chỉ mất 60 hoặc 30 bước mỗi giây và điều đó có thể khiến quá trình chỉ mất quá dài tổng thể. Một thiết kế gọn gàng có thể cung cấp đơn vị công việc nhỏ nhất có thể ở một cấp độ - ví dụ: xử lý một nút A * duy nhất - và lớp trên cùng là cách nhóm nhóm làm việc với nhau thành các khối lớn hơn - ví dụ: tiếp tục xử lý các nút A * trong X mili giây. (Một số người gọi đây là 'thời gian', mặc dù tôi không).

Tuy nhiên, cho phép chia nhỏ công việc theo cách này có nghĩa là bạn phải chuyển trạng thái từ khung này sang khung khác. Nếu bạn đang phá vỡ một thuật toán lặp, thì bạn phải duy trì tất cả trạng thái được chia sẻ trên các lần lặp, cũng như một phương tiện để theo dõi lần lặp nào sẽ được thực hiện tiếp theo. Điều đó thường không quá tệ - thiết kế của một 'lớp tìm đường' là khá rõ ràng - nhưng cũng có những trường hợp khác, điều đó ít dễ chịu hơn. Đôi khi bạn sẽ phải đối mặt với các tính toán dài đang thực hiện các loại công việc khác nhau từ khung này sang khung khác; đối tượng chụp trạng thái của họ có thể kết thúc bằng một mớ hỗn độn 'hữu ích' bán hữu ích, được giữ để truyền dữ liệu từ khung này sang khung khác. Và nếu bạn đang xử lý một quy trình thưa thớt, cuối cùng bạn thường phải thực hiện một máy trạng thái nhỏ chỉ để theo dõi khi nào nên hoàn thành công việc.

Sẽ không gọn gàng nếu, thay vì phải theo dõi rõ ràng tất cả trạng thái này trên nhiều khung hình, và thay vì phải đa luồng và quản lý đồng bộ hóa và khóa, v.v., bạn chỉ có thể viết chức năng của mình dưới dạng một đoạn mã và đánh dấu những nơi cụ thể mà chức năng nên 'tạm dừng' và tiếp tục sau đó?

Unity - cùng với một số môi trường và ngôn ngữ khác - cung cấp điều này dưới dạng Coroutines.

Họ trông như thế nào? Trong bản Unityscript '(Javascript):

function LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield;
    }
}

Trong C #:

IEnumerator LongComputation()
{
    while(someCondition)
    {
        /* Do a chunk of work */

        // Pause here and carry on next frame
        yield return null;
    }
}

Họ làm việc như thế nào? Hãy để tôi nói nhanh rằng tôi không làm việc cho Unity Technologies. Tôi chưa thấy mã nguồn Unity. Tôi chưa bao giờ thấy sự can đảm của động cơ coroutine của Unity. Tuy nhiên, nếu họ đã thực hiện nó theo cách hoàn toàn khác với những gì tôi sắp mô tả, thì tôi sẽ khá ngạc nhiên. Nếu bất cứ ai từ UT muốn hòa nhập và nói về cách nó thực sự hoạt động, thì đó sẽ là điều tuyệt vời.

Các manh mối lớn là trong phiên bản C #. Đầu tiên, lưu ý rằng kiểu trả về cho hàm là IEnumerator. Và thứ hai, lưu ý rằng một trong những tuyên bố là lợi nhuận. Điều này có nghĩa là năng suất phải là một từ khóa và vì hỗ trợ C # của Unity là vanilla C # 3.5, nên nó phải là một từ khóa vanilla C # 3.5. Thật vậy, đây là MSDN - nói về một thứ gọi là 'khối lặp.' Vì vậy những gì đang xảy ra?

Đầu tiên, có loại IEnumerator này. Kiểu IEnumerator hoạt động giống như một con trỏ trên một chuỗi, cung cấp hai thành viên quan trọng: Hiện tại, là một thuộc tính cung cấp cho bạn phần tử con trỏ hiện tại và MoveNext (), một hàm di chuyển đến phần tử tiếp theo trong chuỗi. Bởi vì IEnumerator là một giao diện, nó không xác định chính xác cách thức các thành viên này được triển khai; MoveNext () chỉ có thể thêm một toC hiện tại hoặc nó có thể tải giá trị mới từ một tệp hoặc nó có thể tải xuống một hình ảnh từ Internet và băm nó và lưu trữ hàm băm mới trong Hiện tại hoặc thậm chí có thể làm một điều đầu tiên yếu tố trong chuỗi, và một cái gì đó hoàn toàn khác nhau cho lần thứ hai. Bạn thậm chí có thể sử dụng nó để tạo ra một chuỗi vô hạn nếu bạn muốn. MoveNext () tính giá trị tiếp theo trong chuỗi (trả về false nếu không còn giá trị nào nữa),

Thông thường, nếu bạn muốn triển khai một giao diện, bạn phải viết một lớp, triển khai các thành viên, v.v. Các khối Iterator là một cách thuận tiện để triển khai IEnumerator mà không gặp rắc rối nào - bạn chỉ cần tuân theo một vài quy tắc và việc triển khai IEnumerator được trình biên dịch tự động tạo ra.

Khối lặp là một hàm thông thường (a) trả về IEnumerator và (b) sử dụng từ khóa suất. Vậy từ khóa năng suất thực sự làm gì? Nó tuyên bố giá trị tiếp theo trong chuỗi là gì - hoặc không còn giá trị nào nữa. Điểm tại đó mã gặp phải lợi tức X hoặc phá vỡ lợi suất là điểm tại đó IEnumerator.MoveNext () sẽ dừng lại; lợi nhuận X làm cho MoveNext () trả về true vàC Hiện được gán giá trị X, trong khi ngắt năng suất làm cho MoveNext () trả về false.

Bây giờ, đây là mẹo. Nó không phải là vấn đề giá trị thực được trả về bởi chuỗi là gì. Bạn có thể gọi MoveNext () lặp lại và bỏ qua Hiện tại; các tính toán vẫn sẽ được thực hiện. Mỗi lần MoveNext () được gọi, khối lặp của bạn sẽ chạy đến câu lệnh 'suất' tiếp theo, bất kể biểu thức nào nó thực sự mang lại. Vì vậy, bạn có thể viết một cái gì đó như:

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

và những gì bạn thực sự đã viết là một khối lặp tạo ra một chuỗi dài các giá trị null, nhưng điều quan trọng là tác dụng phụ của công việc mà nó làm để tính toán chúng. Bạn có thể chạy coroutine này bằng một vòng lặp đơn giản như thế này:

IEnumerator e = TellMeASecret();
while(e.MoveNext()) { }

Hoặc, hữu ích hơn, bạn có thể kết hợp nó với công việc khác:

IEnumerator e = TellMeASecret();
while(e.MoveNext()) 
{ 
  // If they press 'Escape', skip the cutscene
  if(Input.GetKeyDown(KeyCode.Escape)) { break; }
}

Đó là tất cả trong thời gian Như bạn đã thấy, mỗi câu lệnh trả về lợi tức phải cung cấp một biểu thức (như null) để khối lặp có một cái gì đó thực sự được gán cho IEnumerator.C hiện tại. Một chuỗi dài các null không thực sự hữu ích, nhưng chúng tôi quan tâm nhiều hơn đến các tác dụng phụ. Có phải chúng ta không?

Thật sự có một cái gì đó hữu ích chúng ta có thể làm với biểu hiện đó. Điều gì sẽ xảy ra nếu, thay vì chỉ mang lại null và bỏ qua nó, chúng ta đã mang lại một cái gì đó chỉ ra khi chúng ta cần phải làm nhiều việc hơn? Thường thì chúng ta sẽ cần thực hiện thẳng vào khung hình tiếp theo, nhưng không phải lúc nào cũng vậy: sẽ có nhiều lúc chúng ta muốn tiếp tục sau khi hoạt hình hoặc âm thanh phát xong hoặc sau một khoảng thời gian cụ thể đã trôi qua. Những người trong khi (playingAnimation) mang lại lợi nhuận null; cấu trúc hơi tẻ nhạt, bạn nghĩ sao?

Unity tuyên bố loại cơ sở YieldIn cản và cung cấp một vài loại dẫn xuất cụ thể chỉ ra các loại chờ cụ thể. Bạn đã có WaitForSeconds, tiếp tục coroutine sau khi thời gian được chỉ định đã trôi qua. Bạn đã có WaitForEndOfFrame, tiếp tục coroutine tại một điểm cụ thể sau đó trong cùng một khung. Bạn đã có loại Coroutine, khi coroutine A sinh ra coroutine B, tạm dừng coroutine A cho đến khi coroutine B kết thúc.

Điều này trông như thế nào từ quan điểm thời gian chạy? Như tôi đã nói, tôi không làm việc cho Unity, vì vậy tôi chưa bao giờ thấy mã của họ; nhưng tôi tưởng tượng nó có thể trông hơi giống thế này:

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;

Không khó để tưởng tượng có thể thêm các kiểu con YieldInocate để xử lý các trường hợp khác - ví dụ, hỗ trợ ở mức động cơ cho tín hiệu, có thể được thêm vào, với YieldInocate ("SignalName") hỗ trợ nó. Bằng cách thêm nhiều YieldIn thi công, bản thân các coroutines có thể trở nên biểu cảm hơn - lợi nhuận mang lại WaitForSignal mới ("GameOver") sẽ dễ đọc hơn so với (! Signals.HasFired ("GameOver") mang lại lợi nhuận không, nếu bạn hỏi tôi, hoàn toàn khác thực tế là làm nó trong công cụ có thể nhanh hơn làm nó trong kịch bản.

Một vài phân nhánh không rõ ràng Có một vài điều hữu ích về tất cả những điều này mà mọi người đôi khi bỏ lỡ mà tôi nghĩ rằng tôi nên chỉ ra.

Thứ nhất, lợi nhuận mang lại chỉ là mang lại một biểu thức - bất kỳ biểu thức nào - và YieldIn cản là một loại thông thường. Điều này có nghĩa là bạn có thể làm những việc như:

YieldInstruction y;

if(something)
 y = null;
else if(somethingElse)
 y = new WaitForEndOfFrame();
else
 y = new WaitForSeconds(1.0f);

yield return y;

Các dòng cụ thể mang lại WaitForSeconds () mới, trả về WaitForEndOfFrame (), v.v., là phổ biến, nhưng chúng không thực sự là các hình thức đặc biệt theo cách riêng của chúng.

Thứ hai, bởi vì các coroutines này chỉ là các khối lặp, bạn có thể tự lặp lại chúng nếu bạn muốn - bạn không cần phải có động cơ làm điều đó cho bạn. Tôi đã sử dụng điều này để thêm các điều kiện ngắt vào coroutine trước đây:

IEnumerator DoSomething()
{
  /* ... */
}

IEnumerator DoSomethingUnlessInterrupted()
{
  IEnumerator e = DoSomething();
  bool interrupted = false;
  while(!interrupted)
  {
    e.MoveNext();
    yield return e.Current;
    interrupted = HasBeenInterrupted();
  }
}

Thứ ba, thực tế là bạn có thể mang lại các coroutine khác có thể cho phép bạn thực hiện YieldIn thi công của riêng bạn, mặc dù không thực hiện như thể chúng được thực hiện bởi động cơ. Ví dụ:

IEnumerator UntilTrueCoroutine(Func fn)
{
   while(!fn()) yield return null;
}

Coroutine UntilTrue(Func fn)
{
  return StartCoroutine(UntilTrueCoroutine(fn));
}

IEnumerator SomeTask()
{
  /* ... */
  yield return UntilTrue(() => _lives < 3);
  /* ... */
}

tuy nhiên, tôi thực sự không khuyến nghị điều này - chi phí để bắt đầu một Coroutine hơi nặng đối với sở thích của tôi.

Kết luận Tôi hy vọng điều này làm rõ một chút những gì thực sự xảy ra khi bạn sử dụng một Coroutine trong Unity. Các khối lặp của C # là một cấu trúc nhỏ bé và thậm chí nếu bạn không sử dụng Unity, có thể bạn sẽ thấy hữu ích khi tận dụng chúng theo cùng một cách.


2
Cảm ơn bạn đã tái tạo điều đó ở đây. Nó là tuyệt vời, và giúp tôi đáng kể.
Naikrovek

96

Tiêu đề đầu tiên dưới đây là một câu trả lời thẳng cho câu hỏi. Hai tiêu đề sau hữu ích hơn cho các lập trình viên hàng ngày.

Có thể nhàm chán Chi tiết thực hiện của Coroutines

Quân đoàn được giải thích trong Wikipedia và các nơi khác. Ở đây tôi sẽ chỉ cung cấp một số chi tiết từ quan điểm thực tế. IEnumerator, yieldv.v. là các tính năng ngôn ngữ C # được sử dụng cho mục đích khác trong Unity.

Nói một cách đơn giản, một IEnumeratortuyên bố có một tập hợp các giá trị mà bạn có thể yêu cầu từng cái một, giống như a List. Trong C #, một hàm có chữ ký để trả về IEnumeratorkhông thực sự phải tạo và trả lại một, nhưng có thể để C # cung cấp ẩn IEnumerator. Hàm này sau đó có thể cung cấp nội dung được trả về IEnumeratortrong tương lai một cách lười biếng, thông qua các yield returntuyên bố. Mỗi khi người gọi yêu cầu một giá trị khác từ ẩn đó IEnumerator, hàm sẽ thực thi cho đến yield returncâu lệnh tiếp theo , cung cấp giá trị tiếp theo. Là sản phẩm phụ của việc này, hàm tạm dừng cho đến khi giá trị tiếp theo được yêu cầu.

Trong Unity, chúng tôi không sử dụng những giá trị này để cung cấp các giá trị trong tương lai, chúng tôi khai thác thực tế là chức năng tạm dừng. Vì sự khai thác này, rất nhiều điều về coroutines trong Unity không có ý nghĩa gì (Phải IEnumeratorlàm gì với bất cứ điều gì? Cái gì yield? Tại sao new WaitForSeconds(3)? V.v.). Điều gì xảy ra "dưới mui xe" là, các giá trị bạn cung cấp thông qua IEnumerator được sử dụng StartCoroutine()để quyết định khi nào sẽ yêu cầu giá trị tiếp theo, xác định khi nào coroutine của bạn sẽ lại tạm dừng.

Trò chơi Unity của bạn là một luồng (*)

Quân đoàn không phải là chủ đề. Có một vòng lặp chính của Unity và tất cả các hàm mà bạn viết đang được gọi bởi cùng một luồng chính theo thứ tự. Bạn có thể xác minh điều này bằng cách đặt một while(true);trong bất kỳ chức năng hoặc coroutines nào của bạn. Nó sẽ đóng băng toàn bộ, ngay cả trình soạn thảo Unity. Đây là bằng chứng cho thấy mọi thứ chạy trong một chủ đề chính. Liên kết này mà Kay đề cập trong nhận xét trên của anh ấy cũng là một tài nguyên tuyệt vời.

(*) Unity gọi các chức năng của bạn từ một luồng. Vì vậy, trừ khi bạn tự tạo một luồng, mã mà bạn đã viết là luồng đơn. Tất nhiên, Unity sử dụng các chủ đề khác và bạn có thể tự tạo chủ đề nếu muốn.

Mô tả thực tế về Coroutines dành cho lập trình viên trò chơi

Về cơ bản, khi bạn gọi StartCoroutine(MyCoroutine()), nó giống hệt như một cuộc gọi chức năng thường xuyên để MyCoroutine(), cho đến khi là người đầu tiên yield return X, nơi mà Xlà một cái gì đó giống như null, new WaitForSeconds(3), StartCoroutine(AnotherCoroutine()), break, vv Điều này là khi nó bắt đầu khác nhau từ một hàm. Unity "tạm dừng" chức năng đó ngay tại yield return Xdòng đó , tiếp tục với doanh nghiệp khác và một số khung hình vượt qua và khi đến lúc, Unity tiếp tục chức năng đó ngay sau dòng đó. Nó ghi nhớ các giá trị cho tất cả các biến cục bộ trong hàm. Bằng cách này, bạn có thể có một forvòng lặp cứ sau hai giây.

Khi Unity sẽ tiếp tục coroutine của bạn phụ thuộc vào những gì Xđã có trong của bạn yield return X. Ví dụ: nếu bạn đã sử dụng yield return new WaitForSeconds(3);, nó sẽ tiếp tục sau 3 giây trôi qua. Nếu bạn đã sử dụng yield return StartCoroutine(AnotherCoroutine()), nó sẽ tiếp tục lại sau khi AnotherCoroutine()hoàn thành, điều này cho phép bạn thực hiện các hành vi lồng nhau kịp thời. Nếu bạn chỉ sử dụng một yield return null;, nó sẽ tiếp tục ngay ở khung tiếp theo.


2
Điều đó thật tệ, UnityGems dường như đã ngừng hoạt động trong một thời gian. Một số người trên Reddit quản lý để có được phiên bản cuối cùng của các kho lưu trữ: web.archive.org/web/20140702051454/http://unitygems.com/...
ForceMagic

3
Điều này rất mơ hồ và rủi ro là không chính xác. Đây là cách mã thực sự biên dịch và tại sao nó hoạt động. Ngoài ra, điều này cũng không trả lời câu hỏi. stackoverflow.com/questions 43238670 / trộm
Louis Hong

Vâng tôi đoán tôi đã giải thích "làm thế nào các coroutines trong Unity hoạt động" từ quan điểm của một lập trình viên trò chơi. Các quas thực tế đã hỏi những gì đang xảy ra dưới mui xe. Nếu bạn có thể chỉ ra những phần không chính xác trong câu trả lời của tôi, tôi sẽ vui lòng sửa nó.
Gazihan Alankus

4
Tôi đồng ý về lợi nhuận mang lại sai, tôi đã thêm nó bởi vì ai đó đã chỉ trích câu trả lời của tôi vì không có nó và tôi đã vội vàng xem xét nếu nó thậm chí hữu ích, và chỉ cần thêm liên kết. Tôi loại bỏ nó bây giờ. Tuy nhiên, tôi nghĩ rằng Unity là một luồng đơn và cách các coroutine phù hợp với điều đó không rõ ràng với mọi người. Rất nhiều lập trình viên Unity mới bắt đầu mà tôi đã nói chuyện có một sự hiểu biết rất mơ hồ về toàn bộ sự việc và được hưởng lợi từ một lời giải thích như vậy. Tôi đã chỉnh sửa câu trả lời của mình để cung cấp câu trả lời anctual cho câu hỏi. Gợi ý chào mừng.
Gazihan Alankus

2
Unity không phải là fwiw đơn luồng. Nó có một luồng chính mà các phương thức vòng đời của MonoBehaviour chạy trong-- nhưng nó cũng có các luồng khác. Bạn thậm chí có thể tự do tạo chủ đề của riêng bạn.
benthehutt

10

Nó không thể đơn giản hơn:

Unity (và tất cả các công cụ trò chơi) dựa trên khung .

Toàn bộ điểm, toàn bộ sự thống nhất của Unity, là nó dựa trên khung. Động cơ làm những việc "từng khung" cho bạn. (Hoạt hình, kết xuất đồ vật, vật lý, vân vân.)

Bạn có thể hỏi .. "Ồ, thật tuyệt. Điều gì sẽ xảy ra nếu tôi muốn động cơ làm điều gì đó cho tôi từng khung hình? Làm thế nào để tôi bảo động cơ làm điều tương tự trong một khung?"

Câu trả lời là ...

Đó chính xác là những gì một "coroutine" dành cho.

Nó thật đơn giản.

Và xem xét điều này ....

Bạn biết chức năng "Cập nhật". Rất đơn giản, bất cứ điều gì bạn đặt vào đó đều được thực hiện mọi khung hình . Nó hoàn toàn giống nhau, hoàn toàn không có sự khác biệt, từ cú pháp năng suất coroutine.

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

Hoàn toàn không có sự khác biệt.

Chú thích: như mọi người đã chỉ ra, Unity đơn giản là không có chủ đề . Các "khung" trong Unity hoặc trong bất kỳ công cụ trò chơi nào hoàn toàn không có kết nối với các luồng theo bất kỳ cách nào.

Coroutines /ield chỉ đơn giản là cách bạn truy cập vào các khung trong Unity. Đó là nó. (Và thực tế, nó hoàn toàn giống với chức năng Update () do Unity cung cấp.) Đó là tất cả những gì có, nó đơn giản.


Cảm ơn! Nhưng câu trả lời của bạn giải thích cách sử dụng coroutines - không phải cách chúng hoạt động đằng sau hậu trường.
Ghopper21

1
Đó là niềm vui của tôi, cảm ơn bạn. Tôi hiểu ý của bạn - đây có thể là một câu trả lời tốt cho những người mới bắt đầu, những người luôn hỏi những gì các coroutines là gì. Chúc mừng!
Fattie

1
Trên thực tế - không có câu trả lời nào, dù chỉ một chút, giải thích những gì đang diễn ra "đằng sau hậu trường". (Đó là một IEnumerator được xếp chồng lên lịch trình.)
Fattie

Bạn nói "Hoàn toàn không có sự khác biệt." Vậy thì tại sao Unity tạo ra Coroutines khi họ đã có một triển khai hoạt động chính xác như thế Update()nào? Tôi có nghĩa là nên có ít nhất một sự khác biệt nhỏ giữa hai triển khai này và các trường hợp sử dụng của chúng là khá rõ ràng.
Leandro Gecozo

hey @LeandroGecozo - Tôi muốn nói thêm rằng "Cập nhật" chỉ là một loại đơn giản hóa ("ngớ ngẩn") mà họ đã thêm. (Nhiều người không bao giờ sử dụng nó, chỉ sử dụng coroutines!) Tôi không nghĩ có câu trả lời hay nào cho câu hỏi của bạn, đó là Unity như thế nào.
Fattie

5

Gần đây, đã tìm hiểu về bài viết này, đã viết một bài đăng ở đây - http://eppz.eu/blog/under Hiểu - nienumerator -in- emp - 3d / - làm sáng tỏ nội bộ (với các ví dụ mã dày đặc), IEnumeratorgiao diện bên dưới , và làm thế nào nó được sử dụng cho coroutines.

Sử dụng bộ liệt kê bộ sưu tập cho mục đích này vẫn có vẻ hơi lạ đối với tôi. Nó là nghịch đảo của những gì các điều tra viên cảm thấy được thiết kế cho. Điểm của điều tra viên là giá trị được trả về trên mỗi lần truy cập, nhưng điểm của Coroutines là mã ở giữa giá trị trả về. Giá trị trả lại thực tế là vô nghĩa trong bối cảnh này.


0

Các hàm cơ bản trong Unity mà bạn nhận được tự động là hàm Start () và hàm Update (), do đó, Coroutine về cơ bản là các hàm giống như hàm Start () và Update (). Bất kỳ hàm cũ nào func () đều có thể được gọi giống như cách Coroutine có thể được gọi. Unity rõ ràng đã đặt ra các ranh giới nhất định cho các Coroutines khiến chúng khác biệt so với các chức năng thông thường. Một sự khác biệt là thay vì

  void func()

Bạn viết

  IEnumerator func()

cho các xác chết. Và giống như cách bạn có thể kiểm soát thời gian trong các chức năng bình thường với các dòng mã như

  Time.deltaTime

Một coroutine có một xử lý cụ thể về cách có thể kiểm soát thời gian.

  yield return new WaitForSeconds();

Mặc dù đây không phải là điều duy nhất có thể thực hiện bên trong IEnumerator / Coroutine, nhưng đây là một trong những điều hữu ích mà Coroutines được sử dụng cho. Bạn sẽ phải nghiên cứu API kịch bản của Unity để tìm hiểu cách sử dụng cụ thể khác của Coroutines.


0

StartCoroutine là một phương thức để gọi hàm IEnumerator. Nó tương tự như chỉ gọi một hàm void đơn giản, chỉ khác là bạn sử dụng nó trên các hàm IEnumerator. Loại hàm này là duy nhất vì nó có thể cho phép bạn sử dụng hàm sản lượng đặc biệt , lưu ý rằng bạn phải trả lại một cái gì đó. Đó là những gì tôi biết. Ở đây tôi đã viết một trò chơi nhấp nháy đơn giản Trên phương thức văn bản trong sự thống nhất

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

Sau đó tôi đã gọi nó ra khỏi IEnumerator

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

Như bạn có thể thấy tôi đã sử dụng phương thức StartCoroutine () như thế nào. Hy vọng tôi đã giúp bằng cách nào đó. Bản thân tôi là người cầu xin, vì vậy nếu bạn sửa tôi, hoặc đánh giá cao tôi, bất kỳ loại phản hồi nào cũng sẽ rất tuyệt.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.