Vào tháng 5 năm 2022, Alexandre Mutel và Kristyna Hougaard đã thông báo trong bài đăng của họ "Unity and .NET, what's next?" rằng Unity có kế hoạch áp dụng nhiều tính năng hơn của .NET, bao gồm cả sự tiện lợi khi sử dụng async-await. Và, có vẻ như Unity đang thực hiện đúng lời hứa của mình. Trong phiên bản alpha của Unity 2023.1, lớp Awaitable đã được giới thiệu, cung cấp nhiều cơ hội hơn để viết mã không đồng bộ.
phương pháp chờ đợi
Trong phần này, tôi sẽ không đi sâu vào các phương pháp mà theo tôi, đã có đầy đủ mô tả trong tài liệu chính thức của Unity. Tất cả chúng đều liên quan đến chờ đợi không đồng bộ.
Awaitable.WaitForSecondsAsync() cho phép bạn đợi trong một khoảng thời gian trò chơi cụ thể. Không giống như Task.Delay(), thực hiện chờ trong thời gian thực. Để giúp làm rõ sự khác biệt, tôi sẽ cung cấp một ví dụ nhỏ sau trong một khối mã.
private void Start() { Time.timeScale = 0; StartCoroutine(RunGameplay()); Task.WhenAll( WaitWithWaitForSecondsAsync(), WaitWithTaskDelay()); } private IEnumerator RunGameplay() { yield return new WaitForSecondsRealtime(5); Time.timeScale = 1; } private async Task WaitWithWaitForSecondsAsync() { await Awaitable.WaitForSecondsAsync(1); Debug.Log("Waiting WithWaitForSecondsAsync() ended."); } private async Task WaitWithTaskDelay() { await Task.Delay(1); Debug.Log("Waiting WaitWithTaskDelay() ended."); }
Trong ví dụ này, khi bắt đầu phương thức Start(), thời gian trò chơi bị dừng bằng cách sử dụng Time.timeScale. Vì lợi ích của thử nghiệm, một Coroutine sẽ được sử dụng để tiếp tục luồng của nó sau 5 giây trong phương thức RunGameplay(). Sau đó, chúng tôi khởi chạy hai phương thức chờ một giây. Một sử dụng Awaitable.WaitForSecondsAsync() và một sử dụng Task.Delay(). Sau một giây, chúng tôi sẽ nhận được thông báo trong bảng điều khiển "Đang chờ WaitWithTaskDelay() đã kết thúc". Và sau 5 giây sẽ xuất hiện thông báo “Đang chờ WaitWithTaskDelay() đã kết thúc”.
Các phương pháp thuận tiện khác cũng đã được thêm vào để giúp bạn linh hoạt hơn trong Vòng lặp trình phát cơ bản của Unity. Mục đích của chúng rõ ràng từ cái tên và tương ứng với sự tương tự của chúng khi sử dụng Coroutines:
- EndOfFrameAsync()
- Đã sửa lỗiUpdateAsync()
- NextFrameAsync()
Nếu bạn chưa quen làm việc với Coroutines, tôi khuyên bạn nên tự mình thử nghiệm với chúng để hiểu rõ hơn.
Một phương thức, Awaitable.FromAsyncOperation(), cũng đã được thêm vào để tương thích ngược với API cũ, AsyncOperation.
Sử dụng thuộc tính destroyCancellationToken
Một trong những tiện ích của việc sử dụng Coroutines là chúng sẽ tự động dừng nếu thành phần bị xóa hoặc bị vô hiệu hóa. Trong Unity 2022.2, thuộc tính destroyCancellationToken đã được thêm vào MonoBehaviour, cho phép bạn dừng thực thi không đồng bộ tại thời điểm xóa đối tượng. Điều quan trọng cần nhớ là việc dừng tác vụ thông qua việc hủy bỏ CancellationToken sẽ ném OperationCanceledException. Nếu phương thức gọi không trả về Tác vụ hoặc Có thể chờ đợi, thì ngoại lệ này sẽ bị bắt.
private async void Awake() { try { await DoAwaitAsync(); } catch (OperationCanceledException) { } } private async Awaitable DoAwaitAsync() { await Awaitable.WaitForSecondsAsync(1, destroyCancellationToken); Debug.Log("That message won't be logged."); } private void Start() { Destroy(this); }
Trong ví dụ này, đối tượng bị hủy ngay lập tức trong Start(), nhưng trước đó, Awake() quản lý để khởi chạy quá trình thực thi DoAwaitAsync(). Lệnh Awaitable.WaitForSecondsAsync(1, destroyCancellationToken) đợi trong 1 giây và sau đó sẽ xuất ra thông báo "Thông báo đó sẽ không được ghi lại." Vì đối tượng bị xóa ngay lập tức, nên hủy Hủy bỏToken dừng thực thi toàn bộ chuỗi bằng cách ném Ngoại lệ OperationCanceled. Theo cách này, destroyCancellationToken giúp chúng tôi không cần phải tạo CancellationToken theo cách thủ công.
Nhưng chúng ta vẫn có thể làm điều này, ví dụ như dừng thực thi tại thời điểm hủy kích hoạt đối tượng. Tôi sẽ lấy một ví dụ.
using System; using System.Threading; using UnityEngine; public class Example : MonoBehaviour { private CancellationTokenSource _tokenSource; private async void OnEnable() { _tokenSource = new CancellationTokenSource(); try { await DoAwaitAsync(_tokenSource.Token); } catch (OperationCanceledException) { } } private void OnDisable() { _tokenSource.Cancel(); _tokenSource.Dispose(); } private static async Awaitable DoAwaitAsync(CancellationToken token) { while (!token.IsCancellationRequested) { await Awaitable.WaitForSecondsAsync(1, token); Debug.Log("This message is logged every second."); } } }
Trong biểu mẫu này, thông báo "Thông báo này được ghi lại mỗi giây" sẽ được gửi miễn là đối tượng treo MonoBehaviour này được bật. Đối tượng có thể được tắt và bật lại.
Mã này có vẻ dư thừa. Unity đã có sẵn nhiều công cụ tiện lợi như Coroutines và InvokeRepeating() cho phép bạn thực hiện các tác vụ tương tự dễ dàng hơn nhiều. Nhưng đây chỉ là một ví dụ về việc sử dụng. Ở đây chúng tôi chỉ xử lý Awaitable.
Sử dụng thuộc tính Application.exitCancellationToken
Trong Unity, việc thực thi phương thức không đồng bộ không tự dừng ngay cả sau khi thoát Chế độ chơi trong trình chỉnh sửa. Hãy thêm một tập lệnh tương tự vào dự án.
using System.Threading.Tasks; using UnityEngine; public static class Boot { [RuntimeInitializeOnLoadMethod] public static async Awaitable LogAsync() { while (true) { Debug.Log("This message is logged every second."); await Task.Delay(1000); } } }
Trong ví dụ này, sau khi chuyển sang Chế độ phát, thông báo "Thông báo này được ghi lại mỗi giây" sẽ được xuất ra bảng điều khiển. Nó tiếp tục được phát ngay cả sau khi nhả nút Play. Trong ví dụ này, Task.Delay() được sử dụng thay vì Awaitable.WaitForSecondsAsync(), bởi vì ở đây, để hiển thị hành động, độ trễ không cần thiết trong thời gian trò chơi mà trong thời gian thực.
Tương tự để hủyCancellationToken, chúng ta có thể sử dụng Application.exitCancellationToken, làm gián đoạn việc thực thi các phương thức không đồng bộ khi thoát khỏi Chế độ chơi. Hãy sửa kịch bản.
using System.Threading.Tasks; using UnityEngine; public static class Boot { [RuntimeInitializeOnLoadMethod] public static async Awaitable LogAsync() { var cancellationToken = Application.exitCancellationToken; while (!cancellationToken.IsCancellationRequested) { Debug.Log("This message is logged every second."); await Task.Delay(1000, cancellationToken); } } }
Bây giờ tập lệnh sẽ thực thi như dự định.
Sử dụng với Chức năng sự kiện
Trong Unity, một số chức năng sự kiện có thể là Coroutine, ví dụ: Start, OnCollisionEnter hoặc OnCollisionExit. Nhưng bắt đầu từ Unity 2023.1, tất cả chúng đều có thể ở chế độ Chờ, bao gồm Update(), LateUpdate và thậm chí cả OnDestroy().
Chúng nên được sử dụng một cách thận trọng, vì không phải chờ đợi việc thực thi không đồng bộ của chúng. Ví dụ: đối với đoạn mã sau:
private async Awaitable Awake() { Debug.Log("Awake() started"); await Awaitable.NextFrameAsync(); Debug.Log("Awake() finished"); } private void OnEnable() { Debug.Log("OnEnable()"); } private void Start() { Debug.Log("Start()"); }
Trong giao diện điều khiển, chúng ta sẽ nhận được kết quả như sau:
Awake() started OnEnable() Start() Awake() finished
Cũng cần nhớ rằng bản thân MonoBehaviour hoặc thậm chí đối tượng trò chơi có thể không còn tồn tại trong khi mã không đồng bộ vẫn đang thực thi. Trong một tình huống như vậy:
private async Awaitable Awake() { Debug.Log(this != null); await Awaitable.NextFrameAsync(); Debug.Log(this != null); } private void Start() { Destroy(this); }
Trong khung tiếp theo, MonoBehaviour được coi là đã bị xóa. Trong giao diện điều khiển, chúng ta sẽ nhận được kết quả như sau:
True Flase
Điều này cũng áp dụng cho phương thức OnDestroy(). Nếu bạn tạo phương thức không đồng bộ, bạn nên tính đến việc sau câu lệnh chờ đợi, MonoBehaviour đã được coi là đã bị xóa. Khi bản thân đối tượng bị xóa, công việc của nhiều MonoBehaviour nằm trên đối tượng đó có thể không hoạt động chính xác vào thời điểm này.
Điều đáng chú ý là khi làm việc với các chức năng sự kiện, điều quan trọng là phải biết thứ tự thực hiện. Mã không đồng bộ có thể không thực thi theo thứ tự bạn mong đợi và điều cần thiết là phải ghi nhớ điều này khi thiết kế tập lệnh của bạn.
Các chức năng sự kiện có thể chờ đợi bắt tất cả các loại ngoại lệ
Điều đáng chú ý là các Hàm sự kiện có thể chờ đợi sẽ nắm bắt tất cả các loại ngoại lệ, điều này có thể xảy ra ngoài dự kiến. Tôi đã mong họ chỉ bắt được các Ngoại lệ OperationCanceled, điều này sẽ có ý nghĩa hơn. Nhưng việc nắm bắt tất cả các loại ngoại lệ khiến chúng không phù hợp để sử dụng vào lúc này. Thay vào đó, bạn có thể chạy các phương thức không đồng bộ và bắt các thông báo cần thiết theo cách thủ công, như minh họa trong ví dụ trước đó.
private async void Awake() { try { await DoAwaitAsync(); } catch (OperationCanceledException) { } } private async Awaitable DoAwaitAsync() { await Awaitable.WaitForSecondsAsync(1, destroyCancellationToken); Debug.Log("That message won't be logged"); } private void Start() { Destroy(this); }
Bởi vì thành phần bị xóa ngay khi bắt đầu, nên quá trình thực thi DoAwaitAsync() sẽ bị gián đoạn. Thông báo "Thư đó sẽ không được ghi" sẽ không xuất hiện trong bảng điều khiển. Chỉ có OperationCanceledException() bị bắt, tất cả các ngoại lệ khác có thể bị ném ra.
Tôi hy vọng phương pháp này sẽ được sửa chữa trong tương lai. Hiện tại, việc sử dụng Chức năng sự kiện chờ đợi không an toàn.
Chuyển động tự do trên các chủ đề
Như đã biết, tất cả các thao tác với đối tượng trò chơi và MonoBehaviours chỉ được phép trong luồng chính. Đôi khi cần phải thực hiện các tính toán lớn có thể dẫn đến đóng băng trò chơi. Tốt hơn là thực hiện chúng bên ngoài luồng chính. Awaitable cung cấp hai phương thức, BackgroundThreadAsync() và MainThreadAsync(), cho phép di chuyển khỏi luồng chính và quay lại luồng đó. Tôi sẽ cung cấp một ví dụ.
private async Awaitable DoAwaitAsync(CancellationToken token) { await Awaitable.BackgroundThreadAsync(); Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); Thread.Sleep(10000); await Awaitable.MainThreadAsync(); if (token.IsCancellationRequested) { return; } Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); gameObject.SetActive(false); await Awaitable.BackgroundThreadAsync(); Debug.Log($"Thread: {Thread.CurrentThread.ManagedThreadId}"); }
Ở đây, khi phương thức bắt đầu, nó sẽ chuyển sang một luồng bổ sung. Ở đây tôi xuất id của luồng bổ sung này ra bàn điều khiển. Nó sẽ không bằng 1, vì 1 là luồng chính.
Sau đó, luồng bị đóng băng trong 10 giây (Thread.Sleep(10000)), mô phỏng các phép tính lớn. Nếu bạn làm điều này trong luồng chính, trò chơi sẽ bị đóng băng trong suốt thời gian thực hiện. Nhưng trong tình huống này, mọi thứ vẫn tiếp tục hoạt động ổn định. Bạn cũng có thể sử dụng CancellationToken trong các tính toán này để dừng một thao tác không cần thiết.
Sau đó, chúng tôi quay trở lại chủ đề chính. Và bây giờ tất cả các chức năng của Unity đã có sẵn cho chúng tôi một lần nữa. Ví dụ: như trong trường hợp này, vô hiệu hóa một đối tượng trò chơi, điều này không thể thực hiện được nếu không có luồng chính.
Phần kết luận
Tóm lại, lớp Awaitable mới được giới thiệu trong Unity 2023.1 cung cấp cho các nhà phát triển nhiều cơ hội hơn để viết mã không đồng bộ, giúp dễ dàng tạo các trò chơi đáp ứng và hoạt động hiệu quả. Lớp Awaitable bao gồm nhiều phương thức chờ khác nhau, chẳng hạn như WaitForSecondsAsync(), EndOfFrameAsync(), FixedUpdateAsync() và NextFrameAsync(), cho phép linh hoạt hơn trong Player Loop cơ bản của Unity. Các thuộc tính destroyCancellationToken và Application.exitCancellationToken cũng cung cấp một cách thuận tiện để dừng thực thi không đồng bộ tại thời điểm xóa đối tượng hoặc thoát khỏi Chế độ phát.
Điều quan trọng cần lưu ý là mặc dù lớp Awaitable cung cấp một cách mới để viết mã không đồng bộ trong Unity, nhưng nó nên được sử dụng cùng với các công cụ Unity khác như Coroutines và InvokeRepeating để đạt được kết quả tốt nhất. Ngoài ra, điều quan trọng là phải hiểu những điều cơ bản về async-await và những lợi ích mà nó có thể mang lại cho quá trình phát triển trò chơi, chẳng hạn như cải thiện hiệu suất và khả năng phản hồi.
Tóm lại, lớp Awaitable là một công cụ mạnh mẽ dành cho các nhà phát triển Unity, nhưng nó nên được sử dụng cẩn thận và kết hợp với các công cụ và khái niệm Unity khác để đạt được kết quả tốt nhất. Điều quan trọng là phải thử nghiệm với nó để hiểu rõ hơn về khả năng và hạn chế của nó.