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ụ sauvoid 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
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 và 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
0 nhận xét:
Đăng nhận xét