Thứ Bảy, 20 tháng 3, 2021

Update, Couroutine và InvokeRepeating

Hello mí bạn, sau những bài toán toát mồ thì hôm nay chúng ta sẽ bàn về một số thứ cơ bản một tí, cụ thể là về Update, Coroutine và InvokeRepeating. Nói cơ bản thế thôi chứ bên trong mình có bonus một vài lưu ý cũng khá là nâng cao cho, vì vậy đây là bài viết cũng sẽ nặng lý thuyết nên các bạn lưu ý trước khi đọc.

Update

Đối với mấy bạn mới học, thường sẽ gọi tất cả mọi thứ chim chóc, múa lửa, … trong update một cách thường xuyên, nhiều hơn số lần mà ta cần nó thực thi. Cho ví dụ sau
void Update
{
    ProcessAI();
}
Trong ví dụ trên ta sẽ gọi ProcessAI trong mỗi frame, và giả định rằng đây là một task cực kì phức tạp, yêu cầu AI phải tìm đường đi với chi phí thấp nhất, kiểm tra trong grid system xem target đang ở vị trí nào hoặc cần di chuyển đến đâu…

Nếu bạn từng làm qua với NavMesh Component thì việc này cực kì ngốn frame rate của chúng ta (bạn có thể dùng A* thay vì NavMesh trong unity để cái tiến). Nhiều khi bạn sẽ nhận ra rằng không phải lúc nào cũng nên gọi ProcessAI(), ví dụ di chuyển đến 1 target thì chỉ cần gọi Process AI 1 lần là đủ nhưng bạn không biết thì khi nào nên gọi?

Có một trick nhỏ giúp bạn cái thiện performance như sau


private float _aiProcessDelay = 0.2f;
private float _timer = 0.0f;
void Update()
{
 
    _timer += Time.deltaTime;
 
    if (_timer > _aiProcessDelay)
    {
 
        ProcessAI();
 
        _timer -= _aiProcessDelay;
 
    }
}



Một tips nhỏ để cái thiện performance trên, tiêu tốn một ít memory cho floating-point. Mặc dù nhiều lúc nó vẫn sẽ gọi ProcessAI() dư thừa

Ví dụ trên cũng chính là điều kiện cực kì tốt cho ta gọi Couroutine để delay ProcessAI()


Coroutine


Couroutine thường dùng để gọi một chuỗi sự kiện ngắn, các hành động được gọi một lần hoặc lặp lại


Một điều lưu ý là Coroutine không phải là đa luồng, thứ mà sẽ chạy trên các cpu core khác nhau và đồng thời. Ngược lại Couroutine chạy trên main thread theo cách tuần tự và mỗi couroutine sẽ được xử lý tại bất kỳ thời điểm nào. Mỗi couroutine đều có thể tùy biến pause hay resume bằng cách dùng các câu lệnh yield



Ví dụ cho đoạn code dưới đây
void Start()
 
{
    StartCoroutine(ProcessAICoroutine());
}
IEnumerator ProcessAICoroutine()
{
    while (true)
    {
 
        ProcessAI();
 
        yield return new WaitForSeconds(_aiProcessDelay);
    }
}

Đoạn code trên chính là 1 courouine, nó call ProcessAI và sau đó pause vài giây bằng câu lệnh yield sau đó lặp lại mãi mãi trừ khi dev cho nó dừng lại bằng một điều kiện nào đó.

Lợi ích của cách làm trên là nó sẽ dừng lại cho đến khi câu lệnh yield hoàn thành, giảm bớt đi sự truy cập liên tục trong hầu hết các frame của chúng ta. Tuy nhiên cách làm này cũng có một số mặt hạn chế


Khi bắt đầu mỗi coroutine, nó sẽ đi kèm với một overhead cost (chi phí chung) cũng như là memory allocation để lưu trữ trạng thái hiện tại cho đến lần invoke kế tiếp, và tất nhiên chi phí này không phải là chi phí 1 lần duy nhất, mà nó sẽ allocate liên tục mỗi khi coroutine bắt đầu (vì nó yield liên tục), nó sẽ cung cấp chi phí tương đương với các lần gọi trước, cứ liên tục như vậy => GC phải hoại động liên tục, vì vậy chúng ta phải đảm bảo rằng khi ta giảm tần suất gọi của ProcessAI thì lợi ích của nó phải outweight lượng chi phí này (hay gọi là garbage do Coroutine sinh ra cũng được)


Fact: 1000 empty Update 1.1 milisecond và 1000 Coroutine WaitEndOfFrame 2.9 miliseconds
=> chi phí tương đối hầu như là gấp 3 lần

Điều đặc biệt là coroutine sẽ phụ thuộc vào Monobehavior chứa nó, khi disable component (hay inactive gameobject) => coroutine sẽ dừng hẳn luôn.

Có vài loại yield chúng ta hay dùng đó là
  • WaitForSecond (Coroutine sẽ pause và thời gian chờ là tham số đầu vào)
  • WaitForSecondRealTime: Một loại khác của WaitForSecond nhưng nó không phụ thuộc vào Timescale
  • WaitForEndOfFrame: nó sẽ chờ cho đến khi Update() kết thúc hoặc bắt đầu vào lần tiếp theo
  • WaitForFixedUpdate: chờ cho đến khi FixedUpdate() kết thúc hoặc bắt đầu vào lần tiếp theo
  • v.vvv

Bonus: Từ unity 5.3 đã cung cấp cho chúng ta WailUntil WaitWhile, chúng ta có thể cung cấp đầu vào cho nó và coroutine sẽ pause cho đến khi delegate trả về true or false và tất nhiên đừng cung cấp delegate có chi phí quá đắt đỏ.

Tham khảo bài delegate Tại đây 

Cuối cùng, việc chuyển method chúng ta vào coroutine sẽ làm giảm việc gọi liên tục các method vào mỗi frame , tuy nhiên khi chi phí thực hiện của một method quá nhiều so với việc chờ đợi thì nó sẽ vượt quá thời gian chờ mong muốn của chúng ta. Và tránh thực hiện một complex task trong một coroutine vì coroutine sẽ thực hiện tại thời điểm nó được gọi và không tuân theo callback => dẫn đến cực kì khó khăn trong việc debug. Lời khuyên từ mình đó là giữ couroutine của bạn thật đơn giản và đừng để nó phụ thuộc vào bất kì complex subsystem nào

InvokeRepeating

Khi coroutine của bạn đủ đơn giản để nó tạo thành 1 while loop, chúng ta có thể thay thế nó bằng InvokeRepeating, thứ mà cực kì đơn giản để setup mà overhead cost chả đáng bao nhiêu. Ta thay ví dụ trên bằng cách
void Start()
{
    InvokeRepeating("ProcessAI", 0f, _delayTime);
}

Một điều khác biệt quan trọng giữa InvokeRepeating và Coroutine đó là InvokeRepeating không phụ thuộc vào Monobehavior hay trạng thái của GameObject. Chúng ta có thể dừng chúng bằng CancelInvoke(), thứ sẽ stop toàn bộ InvokeRepeating() hoặc destroy MonoBehavior hay parent GameObject của nó. Lưu ý: Disable MonoBehavior hay GameObject không stop InvokeRepeating

Fact: 1 chạy 1000 InvokeRepeating xử lý trong 2.6 ms tuy nhiên 1000 Coroutine thì trong 2.9ms

Performance của Invoke/InvokeRepeating luôn tốt hơn Coroutine, tuy nhiên Invoke thì không thể truyền tham số nhưng Coroutine thì có thể, cho nên tùy trường hợp mà dùng nhé

Đọc thêm bài về Invoke và Coroutine của sư phụ mình Tại đây

Tổng kết

Đó là những gì cơ bản và một chút nâng cao cho các bạn về các kiến thức liên quan đến Update, Coroutine và InvokeRepeating. Có khi các bạn phải đọc 2 - 3 lần mới load hết được :)). Oke nhé, nếu có gì thắc mắc hay câu hỏi gì thì hãy comment dưới bài này để mình giải đáp, cám ơn các bạn đã đọc/

Thói quen

ủy nhiệm


noun

đại biểu, đại diện, người được ủy nhiệm


verb

giao quyền, ủy nhiệm

Share: