U
19 / 22 · 10 phút
Hỏi đáp

Coroutine: làm một việc dần dần theo thời gian mà không cần Update

câu trả lời ngắn

Làm sao cho một việc diễn ra dần theo thời gian, hoặc chờ rồi mới chạy?

Coroutine là một hàm bạn tạm dừng được: yield return new WaitForSeconds(1f) chờ một giây rồi chạy tiếp từ đúng dòng đó. Khởi động bằng StartCoroutine, giữ lại handle nếu sẽ có lúc cần dừng nó, và nhớ rằng coroutine sẽ dừng khi GameObject đang chạy nó bị tắt hoặc bị huỷ.

Update chạy một lần mỗi khung hình rồi kết thúc, nên việc gì cần thời gian đều phải dựng lại bằng những biến sống sót qua các khung hình — một bộ đếm, một cờ, một giai đoạn. Coroutine là cùng logic đó nhưng viết thành một mạch thẳng, và được phép dừng giữa chừng.

Cùng một việc, viết hai cách

Flash red for a moment, then go back
// Viết bằng Update: ba trường và một nhánh rẽ, logic nằm rải rác.
bool flashing;
float flashEnds;

void Update()
{
    if (flashing && Time.time >= flashEnds)
    {
        mat.color = Color.white;
        flashing = false;
    }
}

void Hit()
{
    mat.color = Color.red;
    flashing = true;
    flashEnds = Time.time + 0.1f;
}

// Viết bằng coroutine: cả câu chuyện đọc từ trên xuống, nằm gọn một chỗ.
IEnumerator Flash()
{
    mat.color = Color.red;
    yield return new WaitForSeconds(0.1f);
    mat.color = Color.white;
}

void Hit() => StartCoroutine(Flash());

Các quy tắc

  • Hàm trả về IEnumerator và tạm dừng bằng yield return. Chính yield biến nó thành coroutine chứ không phải một hàm bình thường.

  • Gọi nó suông thì không có gì xảy ra. Viết Flash() chỉ tạo ra coroutine rồi vứt đi; StartCoroutine(Flash()) mới là thứ chạy nó.

  • Nó chạy tiếp ở đúng vị trí trong khung hình nơi nó dừng — sau Update, trước LateUpdate — nên nó không phải luồng riêng, và không có gì chạy song song.

  • Nó thuộc về MonoBehaviour đã khởi động nó. Tắt GameObject đó hoặc huỷ nó là coroutine dừng ngang, không chạy nốt. Chỉ tắt component thì coroutine vẫn chạy tiếp.

Có thể chờ những gì

The four that cover almost everything
yield return null;                              //hai giây thời gian game
yield return new WaitForSeconds(2f);
yield return new WaitForSecondsRealtime(2f);    // hai giây thời gian thật
yield return new WaitUntil(() => player.isAlive);
yield return StartCoroutine(Other());           // chạy Other, chờ nó xong

Di chuyển thứ gì đó dần theo thời gian

The pattern behind every fade, slide and pop
IEnumerator MoveTo(Vector3 to, float seconds)
{
    Vector3 from = transform.position;
    float t = 0f;

    while (t < 1f)
    {
        t += Time.deltaTime / seconds;

        // Dùng SmoothStep thay Lerp thường: vào và ra đều mềm.
        transform.position = Vector3.Lerp(from, to, Mathf.SmoothStep(0f, 1f, t));
        yield return null;
    }

    // Đặt đúng đích: vòng lặp thoát ra khi t đã hơi lớn hơn 1.
    transform.position = to;
}

Dừng một coroutine

  • StopCoroutine("Flash") theo tên chỉ chạy nếu bạn cũng khởi động bằng tên, và chuỗi đó không bao giờ được trình biên dịch kiểm. Thay vào đó, hãy giữ lại handle.

  • Coroutine c = StartCoroutine(Flash()); rồi StopCoroutine(c). Cách này vẫn đúng dù bạn đổi tên hàm.

  • Khởi động cùng một coroutine hai lần là nó chạy hai bản. Với hiệu ứng chớp, hãy dừng handle cũ trước, không thì hai lần chớp chồng nhau để lại màu sai.

Khi nào đừng dùng coroutine

  • Thứ chạy mỗi khung hình mãi mãi thì thuộc về Update. Coroutine kiểu while (true) kèm yield return null chỉ là một Update viết vòng vo hơn.

  • Mọi thứ đụng tới vật lý thuộc về FixedUpdate, hoặc một coroutine yield WaitForFixedUpdate. Coroutine thường chạy tiếp theo nhịp khung hình, không phải nhịp vật lý.

  • Hàng trăm coroutine chạy cùng lúc tốn bộ nhớ thật: mỗi cái đều cấp phát. Với hàng trăm bộ đếm giống nhau, một Update duyệt qua danh sách rẻ hơn.

bài học làm ra thứ này

Hệ thống skill có hồi chiêu

Bài này là một công thức đứng riêng. Trong khoá học, cùng thứ đó được dựng như một phần của dự án xuyên suốt năm tab — Nhân vật & Chiến đấu, bài 04.