Tại sao tôi không bị kẹt trong vòng lặp


8

Tôi mới đến Unity. Tôi đã học coroutines và tôi đã viết này.

private void Fire()
{
    if(Input.GetButtonDown("Fire1"))
    {
        StartCoroutine(FireContinuously());
    }
    if(Input.GetButtonUp("Fire1"))
    {
        StopAllCoroutines();
    }
}

IEnumerator FireContinuously()
{
    while(true)
    {
        GameObject laser = Instantiate(LaserPrefab, transform.position, Quaternion.identity) as GameObject;
        laser.GetComponent<Rigidbody2D>().velocity = new Vector2(0, 10f);
        yield return new WaitForSeconds(firetime);
    }
}

Khi nhấn nút, coroutine được gọi và nó sẽ vào vòng lặp 'while'. Khi tôi rời nút, nó dừng coroutine. Không phải nó bị kẹt trong vòng lặp 'while' vì nó là một vòng lặp vô hạn? Tại sao?


Gần đây tôi mới quay trở lại Unity, tôi nhận thấy rằng các phương thức Nhập liệu đang có trong một chuỗi "Fire1", đó có phải là thứ bạn có thể thiết lập trong công cụ để cho phép sửa lỗi thay vì gõ ra Keycode.Fookhông?
Mkalafut

1
Nó có thể giúp nhận ra rằng yieldnó thực sự ngắn gọn cho "Điều khiển năng suất cho người gọi cho đến khi mục tiếp theo trong Số đếm được yêu cầu."
3Dave

@Mkalafut nghe có vẻ giống như một câu hỏi trong bài đăng Câu hỏi mới nếu bạn không thể tìm thấy câu trả lời trong các trang tài liệu Unity , hướng dẫn hoặc thử nghiệm của riêng bạn.
DMGregory

Tôi không đề nghị StopAllCoroutines()trong trường hợp này. Sẽ ổn khi bạn chỉ sử dụng một coroutine, nhưng nếu bạn từng có nhiều hơn một, điều này sẽ có tác dụng không mong muốn. Thay vào đó, bạn nên sử dụng StopCoroutine()và chỉ dừng những thứ có liên quan thay vì tất cả chúng. ( StopAllCoroutines()sẽ hữu ích, ví dụ như khi kết thúc cấp độ hoặc tải một khu vực mới, v.v., nhưng không dành cho những thứ cụ thể như "Tôi sẽ không bắn nữa".)
Darrel Hoffman

Câu trả lời:


14

Lý do là từ khóayield có ý nghĩa cụ thể trong C #.

Khi gặp các từ, yield returnmột hàm trong C # trả về, như người ta mong đợi.

Sử dụng năng suất để định nghĩa một trình vòng lặp loại bỏ sự cần thiết của một lớp bổ sung rõ ràng

[...]

Khi đạt được một câu lệnh hoàn trả năng suất trong phương thức lặp, biểu thức được trả về và vị trí hiện tại trong mã được giữ lại. Thực thi được khởi động lại từ vị trí đó vào lần tiếp theo mà hàm lặp được gọi.

Vì vậy, không có vòng lặp vô hạn. Có một hàm / iterator có thể được gọi là vô số lần.

Hàm Unity StartCoroutine()làm cho khung Unity gọi hàm / iterator một lần trên mỗi khung.

Hàm Unity StopAllCoroutineslàm cho khung Unity dừng gọi hàm / iterator.

Và việc trả về WaitForSeconds(time)từ iterator làm cho khung Unity tạm dừng gọi hàm / iterator cho time.


Một nhận xét lẫn lộn và một upvote lẫn lộn không kém về nhận xét đó đã khuyến khích tôi giải thích thêm về những gì từ khóa yieldlàm và không làm.

Nếu bạn viết điều này:

IEnumerable<int> Count()
{
   int i = 0;
   yield return i++;
}

Thay vào đó, bạn cũng có thể viết điều này:

IEnumerator<int> Count() {
    return new CountEnumerator ();
}
class CountEnumerator : IEnumerator<int> {
    int i = 0;
    bool IEnumerator<int>.MoveNext() { i++; return true; }
    int IEnumerator<int>.Current { get { return i; }
    void IEnumerator<int>.Reset() { throw new NotSupportedException(); }
}

Theo sau đó, từ khóa yieldkhông liên quan đến đa luồng và hoàn toàn không gọi System.Threading.Thread.Yield().


1
" On encountering the words yield return a function in C# returns". Không nó không. Văn bản mà bạn trích dẫn giải thích nó, cũng như Wikipedia - " In computer science, yield is an action that occurs in a computer program during multithreading, of forcing a processor to relinquish control of the current running thread, and sending it to the end of the running queue, of the same scheduling priority.". Về cơ bản, "` làm ơn tạm dừng tôi đang ở đâu và để người khác chạy một lúc ".
Mawg nói rằng phục hồi Monica

2
@Mawg Tôi đã thêm phần thứ hai vào câu trả lời để giải quyết mối quan tâm của bạn.
Peter

Cảm ơn bạn rất nhiều vì đã làm rõ (nâng cao). Tôi chắc chắn đã học được một điều mới ngày hôm nay :-)
Mawg nói rằng phục hồi lại

8

Khi nút bắn được nhấc lên, câu lệnh if thứ hai được nhập và StopAllCoroutines được chạy. Điều này có nghĩa là Coroutine mà vòng lặp while đang chạy đã kết thúc, do đó không có vòng lặp vô hạn nữa. Coroutine giống như một thùng chứa để mã thực thi.

Tôi có thể đề xuất Hướng dẫn sử dụng UnityAPI Unity Scripting để hiểu rõ hơn về các coroutines là gì và chúng có thể mạnh đến mức nào.

Blog này và tìm kiếm bài đăng trên youtube cũng hữu ích cho tôi để sử dụng coroutines tốt hơn.


3

Quân đoàn là một con thú kỳ lạ. Lợi nhuận mang lại làm cho phương thức tạm dừng thực thi cho đến khi nó được thực hiện sau đó. Đằng sau hậu trường, nó có thể trông giống như thế này:

class FireContinuouslyData {
    int state;
    bool shouldBreak;
}

object FireContinuously(FireContinuouslyData data) {
    switch (data.state) {
        case 0:
            goto State_0;
    }
    while (true) {
        GameObject laser = ...;
        laser.GetComponent...
        //the next three lines handle the yield return
        data.state = 0;
        return new WaitForSeconds(fireTime);
        State_0:
    }
}

Và nội bộ cho Unity / C # (vì lợi nhuận lợi nhuận là một tính năng c # riêng), khi bạn gọi StartCoroutine, nó sẽ tạo một FireContinuouslyDatađối tượng và chuyển nó vào phương thức. Dựa trên giá trị trả về, nó xác định khi nào gọi lại sau, chỉ cần lưu trữ đối tượng FireContinuptlyData để chuyển nó trong lần tiếp theo.

Nếu bạn đã từng làm giảm năng suất, nó có thể được thiết lập bên trong data.shouldBreak = truevà sau đó Unity sẽ chỉ cần vứt bỏ dữ liệu và không lên lịch lại.

Và nếu có bất kỳ dữ liệu nào cần được lưu giữa các lần thực thi, thì nó cũng sẽ được lưu trong dữ liệu sau này.

Một ví dụ về cách Unity / C # có thể thực hiện chức năng coroutine:

//Internal to Unity/C#

class Coroutine {
    Action<object> method;
    object data;
}

Coroutine StartCoroutine(IEnumerator enumerator) {
    object data = CreateDataForEnumerator(method); //Very internal to C#
    Action<object> method = GetMethodForEnumerator(enumerator); //Also very internal to C#
    Coroutine coroutine = new Coroutine(method, data);
    RunCoroutine(coroutine);
    return coroutine;
}

//Called whenever this coroutine is scheduled to run
void RunCoroutine(Coroutine coroutine) {
    object yieldInstruction = coroutine.method(coroutine.data);
    if (!data.shouldBreak) {
        //Put this coroutine into a collection of coroutines to run later, by calling RunCoroutine on it again
        ScheduleForLater(yieldInstruction, coroutine);
    }
}

1

Một câu trả lời khác đề cập rằng bạn đang dừng các thói quen đồng thời khi "Fire1"kết thúc - điều này hoàn toàn chính xác, trong trường hợp tại sao coroutine không tiếp tục khởi tạo GameObjects sau lần nhấn đầu tiên "Fire1".

Tuy nhiên, trong trường hợp của bạn , mã này sẽ không bị "kẹt" trong một vòng lặp vô hạn, giống như bạn đang tìm câu trả lời cho - tức là while(true) {}vòng lặp, ngay cả khi bạn không dừng nó ở bên ngoài.

Nó sẽ không bị kẹt nhưng coroutine của bạn sẽ không kết thúc (mà không gọi StopCoroutine()hoặc StopAllCoroutines()). Điều này là do Unity coroutines kiểm soát năng suất cho người gọi của họ. yielding khác với returning:

  • một returncâu lệnh sẽ ngừng thực thi một hàm, ngay cả khi có nhiều mã theo sau nó
  • một yieldcâu lệnh sẽ tạm dừng chức năng, bắt đầu từ dòng tiếp theo sau yieldkhi được nối lại.

Thông thường, coroutines sẽ được nối lại từng khung nhưng bạn cũng trả lại một WaitForSecondsđối tượng.

Dòng này yield return new WaitForSeconds(fireTime)tạm dịch là "bây giờ đình chỉ tôi và không quay lại cho đến khi fireTimegiây trôi qua".

IEnumerator FireContinuously()
{
    // When started, this coroutine enters the below while loop...
    while(true)
    {
        // It does some things... (Infinite coroutine code goes here)

        // Then it yields control back to it's caller and pauses...
        yield return new WaitForSeconds(fireTime);
        // The next time it is called , it resumes here...
        // It finds the end of a loop, so will re-evaluate the loop condition...
        // Which passes, so control is returned to the top of the loop.
    }
}

Trừ khi dừng lại, đây là một coroutine, một khi đã bắt đầu, sẽ thực hiện toàn bộ vòng lặp một lần mỗi fireTimegiây.


1

Một lời giải thích đơn giản: dưới mui xe Unity đang lặp lại một bộ sưu tập (của YieldInXD s hoặc null hoặc bất cứ thứ gì bạn yield return) bằng cách sử dụng hàm IEnumeratormà hàm của bạn trả về.

Vì bạn sử dụng yieldtừ khóa, phương thức của bạn là một trình vòng lặp . Đây không phải là điều Unity, đó là tính năng ngôn ngữ C #. Làm thế nào nó hoạt động?

Đó là lười biếng và không tạo ra tất cả các bộ sưu tập cùng một lúc (và bộ sưu tập có thể là vô hạn và không thể được tạo ra cùng một lúc). Các yếu tố của bộ sưu tập được tạo ra khi cần thiết. Hàm của bạn trả về một trình vòng lặp để Unity hoạt động. Nó gọi MoveNextphương thức của nó để tạo ra một phần tử và thuộc Currenttính mới để truy cập nó.

Vì vậy, vòng lặp của bạn không phải là vô tận, nó chạy một số mã, trả về một phần tử và trả lại quyền điều khiển cho Unity để nó không bị kẹt và có thể thực hiện các công việc khác như xử lý dữ liệu đầu vào của bạn để dừng coroutine.


0

Hãy suy nghĩ về cách thức foreachhoạt động:

foreach (var number in Enumerable.Range(1, 1000000))
{
  if (number > 10) break;
}

Kiểm soát lặp lại là trên người gọi - nếu bạn dừng lặp lại (ở đây với break), đó là nó.

Các yieldtừ khóa là một cách đơn giản để thực hiện một đếm được trong C #. Tên gợi ý về điều này - yield returnmang lại quyền kiểm soát cho người gọi (trong trường hợp này là của chúng tôi foreach); đó là người gọi quyết định khi nào sẽ tiếp tục mục tiếp theo. Vì vậy, bạn có thể thực hiện một phương pháp như thế này:

IEnumerable<int> ToInfinity()
{
  var i = 0;
  while (true) yield return i++;
}

Điều này trông ngây thơ như nó sẽ chạy mãi mãi; nhưng trong thực tế, nó phụ thuộc hoàn toàn vào người gọi. Bạn có thể làm một cái gì đó như thế này:

var range = ToInfinity().Take(10).ToArray();

Điều này có thể hơi khó hiểu nếu bạn không quen với khái niệm này, nhưng tôi hy vọng cũng rõ ràng đây là một tài sản rất hữu ích. Đó là cách đơn giản nhất mà bạn có thể mang lại sự kiểm soát để người gọi của bạn, và khi người gọi quyết định để theo dõi, nó chỉ có thể thực hiện bước tiếp theo (nếu Unity đã được thực hiện ngày hôm nay, nó có lẽ sẽ sử dụng awaitthay vì yield; nhưng awaitđã không tồn tại trở lại sau đó).

Tất cả những gì bạn cần để thực hiện các coroutines của riêng bạn (không cần phải nói, các coroutines ngu ngốc đơn giản nhất) là đây:

List<IEnumerable> continuations = new List<IEnumerable>();

void StartCoroutine(IEnumerable coroutine) => continuations.Add(coroutine);

void MainLoop()
{
  while (GameIsRunning)
  {
    foreach (var continuation in continuations.ToArray())
    {
      if (!continuation.MoveNext()) continuations.Remove(continuation);
    }

    foreach (var gameObject in updateableGameObjects)
    {
      gameObject.Update();
    }
  }
}

Để thêm một cách WaitForSecondsthực hiện rất đơn giản , bạn chỉ cần một cái gì đó như thế này:

interface IDelayedCoroutine
{
  bool ShouldMove();
}

class Waiter: IDelayedCoroutine
{
  private readonly TimeSpan time;
  private readonly DateTime start;

  public Waiter(TimeSpan time)
  {
    this.start = DateTime.Now;
    this.time = time;
  }

  public bool ShouldMove() => start + time > DateTime.Now;
}

Và mã tương ứng trong vòng lặp chính của chúng tôi:

foreach (var continuation in continuations.ToArray())
{
  if (continuation.Current is IDelayedCoroutine dc)
  {
    if (!dc.ShouldMove()) continue;
  }

  if (!continuation.MoveNext()) continuations.Remove(continuation);
}

Ta-da - đó là tất cả một hệ thống coroutine đơn giản cần. Và bằng cách mang lại quyền kiểm soát cho người gọi, người gọi có thể quyết định bất kỳ số lượng nào; họ có thể có một bảng sự kiện được sắp xếp thay vì lặp qua tất cả các coroutines trên mỗi khung; họ có thể có các ưu tiên hoặc phụ thuộc. Nó cho phép thực hiện rất đơn giản của đa tác vụ hợp tác. Và chỉ cần nhìn vào cách đơn giản này, nhờ yield:)

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.