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.
IEnumerator
/IEnumerable
(hoặc tương đương chung) và có chứayield
từ khóa. Tra cứu lặp đi lặp lại.