Một số tác vụ trong quá trình phát triển trò chơi không đồng bộ — chúng không đồng bộ. Điều này có nghĩa là chúng không được thực thi tuyến tính trong mã trò chơi. Một số tác vụ không đồng bộ này có thể yêu cầu thời gian khá dài để hoàn thành, trong khi những tác vụ khác liên quan đến tính toán chuyên sâu. Một số tác vụ không đồng bộ chơi game phổ biến nhất như sau: Thực hiện yêu cầu mạng Đang tải cảnh, tài nguyên và các nội dung khác Đọc và ghi tệp Trí tuệ nhân tạo để ra quyết định Chuỗi hoạt hình dài Xử lý một lượng lớn dữ liệu tìm đường Bây giờ, điều quan trọng là, vì tất cả mã Unity chạy trong một luồng, nên bất kỳ tác vụ nào giống như một trong những tác vụ được đề cập ở trên, nếu chúng được thực hiện đồng bộ, sẽ dẫn đến luồng chính bị chặn và do đó, khung hình bị giảm. Xin chào mọi người, tên tôi là Dmitrii Ivashchenko và tôi là Trưởng nhóm phát triển tại MY.GAMES. Trong bài viết này, chúng ta sẽ nói về việc tránh bất kỳ vấn đề nào như vậy. Chúng tôi sẽ đề xuất các kỹ thuật lập trình không đồng bộ để thực hiện các tác vụ này trong một luồng riêng biệt, do đó để luồng chính tự do thực hiện các tác vụ khác. Điều này sẽ giúp đảm bảo quá trình chơi mượt mà và nhạy bén, đồng thời (hy vọng) làm hài lòng các game thủ. quân đoàn Đầu tiên, hãy nói về Chúng được giới thiệu trong Unity vào năm 2011, thậm chí trước khi async/await xuất hiện trong .NET. Trong Unity, các coroutine cho phép chúng ta thực hiện một tập hợp các hướng dẫn trên nhiều khung, thay vì thực hiện tất cả chúng cùng một lúc. Chúng tương tự như các luồng, nhưng nhẹ hơn và được tích hợp vào vòng cập nhật của Unity, khiến chúng rất phù hợp để phát triển trò chơi. coroutines. (Nhân tiện, về mặt lịch sử, coroutine là cách đầu tiên để thực hiện các hoạt động không đồng bộ trong Unity, vì vậy hầu hết các bài báo trên Internet đều nói về chúng.) Để tạo một coroutine, bạn cần khai báo một hàm với kiểu trả về . Hàm này có thể chứa bất kỳ logic nào mà bạn muốn coroutine thực thi. IEnumerator Để bắt đầu một coroutine, bạn cần gọi phương thức trên một cá thể và chuyển hàm coroutine làm đối số: StartCoroutine MonoBehaviour public class Example : MonoBehaviour { void Start() { StartCoroutine(MyCoroutine()); } IEnumerator MyCoroutine() { Debug.Log("Starting coroutine"); yield return null; Debug.Log("Executing coroutine"); yield return null; Debug.Log("Finishing coroutine"); } } Có một số hướng dẫn năng suất có sẵn trong Unity, chẳng hạn như , , , , cũng như một số hướng dẫn khác. Điều quan trọng cần nhớ là việc sử dụng chúng dẫn đến phân bổ, vì vậy chúng nên được sử dụng lại bất cứ khi nào có thể. WaitForSeconds WaitForEndOfFrame WaitForFixedUpdate WaitForSecondsRealtime WaitUntil Ví dụ: xem xét phương pháp này từ tài liệu: IEnumerator Fade() { Color c = renderer.material.color; for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return new WaitForSeconds(.1f); } } Với mỗi lần lặp lại vòng lặp, một phiên bản mới của sẽ được tạo. Thay vì điều này, chúng ta có thể di chuyển việc tạo ra bên ngoài vòng lặp và tránh phân bổ: new WaitForSeconds(.1f) IEnumerator Fade() { Color c = renderer.material.color; **var waitForSeconds = new WaitForSeconds(0.2f);** for (float alpha = 1f; alpha >= 0; alpha -= 0.1f) { ca = alpha; renderer.material.color = c; yield return **waitForSeconds**; } } Một thuộc tính quan trọng khác cần lưu ý là có thể được sử dụng với tất cả các phương thức do Unity cung cấp vì s là hậu duệ của : yield return Async AsyncOperation YieldInstruction yield return SceneManager.LoadSceneAsync("path/to/scene.unity"); Một số cạm bẫy có thể có của coroutines Tất cả điều này đã được nói, coroutines cũng có một vài nhược điểm cần lưu ý: Không thể trả lại kết quả của một hoạt động dài. Bạn vẫn cần các cuộc gọi lại sẽ được chuyển đến coroutine và được gọi khi nó kết thúc để trích xuất bất kỳ dữ liệu nào từ nó. Một coroutine được liên kết chặt chẽ với khởi chạy nó. Nếu bị tắt hoặc bị hủy, coroutine sẽ ngừng xử lý. MonoBehaviour GameObject Cấu trúc không thể được sử dụng do sự hiện diện của cú pháp năng suất. try-catch-finally Ít nhất một khung sẽ vượt qua sau khi trước khi mã tiếp theo bắt đầu thực thi. yield return Phân bổ lambda và chính coroutine lời hứa là một mẫu để tổ chức và làm cho các hoạt động không đồng bộ dễ đọc hơn. Chúng đã trở nên phổ biến do được sử dụng trong nhiều thư viện JavaScript của bên thứ ba và kể từ ES6, chúng đã được triển khai nguyên bản. Lời hứa Khi sử dụng Lời hứa, chúng tôi ngay lập tức trả về một đối tượng từ chức năng không đồng bộ của bạn. Điều này cho phép người gọi đợi giải pháp (hoặc lỗi) của thao tác. Về cơ bản, điều này làm cho các phương thức không đồng bộ có thể trả về các giá trị và “hành động” giống như các phương thức đồng bộ: thay vì trả về giá trị cuối cùng ngay lập tức, chúng đưa ra một “lời hứa” rằng chúng sẽ trả lại một giá trị trong tương lai. Có một số triển khai Promise cho Unity: C-Sharp-Promise UnityFx.Async Lời hứa C# uPromise Cách chính để tương tác với Promise là thông qua . các hàm gọi lại Bạn có thể xác định hàm gọi lại sẽ được gọi khi Lời hứa được giải quyết và một hàm gọi lại khác sẽ được gọi nếu Lời hứa bị từ chối. Các cuộc gọi lại này nhận kết quả của hoạt động không đồng bộ làm đối số, sau đó có thể được sử dụng để thực hiện các hoạt động tiếp theo. Theo này từ chức Promises/A+, một Promise có thể ở một trong ba trạng thái: các thông số kỹ thuật tổ : trạng thái ban đầu, điều này có nghĩa là hoạt động không đồng bộ vẫn đang được tiến hành và kết quả của hoạt động vẫn chưa được biết. Pending ( ): trạng thái đã giải quyết được kèm theo một giá trị đại diện cho kết quả của hoạt động. Fulfilled Resolved : nếu hoạt động không đồng bộ không thành công vì bất kỳ lý do gì, Lời hứa được cho là "bị từ chối". Trạng thái bị từ chối kèm theo lý do thất bại. Rejected Thông tin thêm về Lời hứa Ngoài ra, các lời hứa có thể được xâu chuỗi lại với nhau để kết quả của một Lời hứa có thể được sử dụng để xác định kết quả của một Lời hứa khác. Ví dụ: bạn có thể tạo một Lời hứa tìm nạp một số dữ liệu từ máy chủ, sau đó sử dụng dữ liệu đó để tạo một Lời hứa khác thực hiện một số tính toán và các hành động khác: var promise = MakeRequest("https://some.api") .Then(response => Parse(response)) .Then(result => OnRequestSuccess(result)) .Then(() => PlaySomeAnimation()) .Catch(exception => OnRequestFailed(exception)); Đây là một ví dụ về cách tổ chức một phương thức thực hiện thao tác không đồng bộ: public IPromise<string> MakeRequest(string url) { // Create a new promise object var promise = new Promise<string>(); // Create a new web client using var client = new WebClient(); // Add a handler for the DownloadStringCompleted event client.DownloadStringCompleted += (sender, eventArgs) => { // If an error occurred, reject the promise if (eventArgs.Error != null) { promise.Reject(eventArgs.Error); } // Otherwise, resolve the promise with the result else { promise.Resolve(eventArgs.Result); } }; // Start the download asynchronously client.DownloadStringAsync(new Uri(url), null); // Return the promise return promise; } Chúng ta cũng có thể bọc các coroutine trong một : Promise void Start() { // Load the scene and then show the intro animation LoadScene("path/to/scene.unity") .Then(() => ShowIntroAnimation()) .Then( ... ); } // Load a scene and return a promise Promise LoadScene(string sceneName) { // Create a new promise var promise = new Promise(); // Start a coroutine to load the scene StartCoroutine(LoadSceneRoutine(promise, sceneName)); // Return the promise return promise; } IEnumerator LoadSceneRoutine(Promise promise, string sceneName) { // Load the scene asynchronously yield return SceneManager.LoadSceneAsync(sceneName); // Resolve the promise once the scene is loaded promise.Resolve(); } Và tất nhiên, bạn có thể tổ chức bất kỳ sự kết hợp nào của thứ tự thực hiện lời hứa bằng cách sử dụng / và / : ThenAll Promise.All ThenRace Promise.Race // Execute the following two promises in sequence Promise.Sequence( () => Promise.All( // Execute the two promises in parallel RunAnimation("Foo"), PlaySound("Bar") ), () => Promise.Race( // Execute the two promises in a race RunAnimation("One"), PlaySound("Two") ) ); Những phần “không hứa hẹn” của lời hứa Mặc dù có tất cả sự tiện lợi khi sử dụng, nhưng những lời hứa cũng có một số nhược điểm: : Tạo Lời hứa liên quan đến chi phí bổ sung so với việc sử dụng các phương pháp lập trình không đồng bộ khác, như coroutines. Trong một số trường hợp, điều này có thể dẫn đến giảm hiệu suất. Chi phí chung : Gỡ lỗi Lời hứa có thể khó hơn gỡ lỗi các mẫu lập trình không đồng bộ khác. Có thể khó theo dõi luồng thực thi và xác định nguồn gốc của lỗi. Gỡ lỗi : Xử lý ngoại lệ có thể phức tạp hơn với Lời hứa so với các mẫu lập trình không đồng bộ khác. Có thể khó quản lý các lỗi và ngoại lệ xảy ra trong chuỗi Promise. Xử lý ngoại lệ Tác vụ không đồng bộ/đang chờ Tính năng async/await đã là một phần của C# kể từ phiên bản 5.0 (2012) và được giới thiệu trong Unity 2017 với việc triển khai thời gian chạy .NET 4.x. Trong lịch sử của .NET, có thể phân biệt các giai đoạn sau: (Mẫu không đồng bộ dựa trên sự kiện): Cách tiếp cận này dựa trên các sự kiện được kích hoạt khi hoàn thành một thao tác và một phương thức thông thường gọi thao tác này. EAP (Mô hình lập trình không đồng bộ): Cách tiếp cận này dựa trên hai phương pháp. Phương thức trả về giao diện . Phương thức lấy ; nếu hoạt động không được hoàn thành tại thời điểm cuộc gọi , luồng sẽ bị chặn. APM BeginSmth IAsyncResult EndSmth IAsyncResult EndSmth (Mẫu không đồng bộ dựa trên tác vụ): Khái niệm này đã được cải thiện bằng cách giới thiệu async/await và các loại và . TAP Task Task<TResult> Các phương pháp trước đó đã trở nên lỗi thời do sự thành công của phương pháp cuối cùng. Để tạo một phương thức không đồng bộ, phương thức đó phải được đánh dấu bằng từ khóa , chứa một bên trong và giá trị trả về phải là , hoặc (không được khuyến nghị). async await Task Task<T> void public async Task Example() { SyncMethodA(); await Task.Delay(1000); // the first async operation SyncMethodB(); await Task.Delay(2000); // the second async operation SyncMethodC(); await Task.Delay(3000); // the third async operation } Trong ví dụ này, việc thực thi sẽ diễn ra như sau: Đầu tiên, mã trước cuộc gọi đến hoạt động không đồng bộ đầu tiên ( ) sẽ được thực thi. SyncMethodA Hoạt động không đồng bộ đầu tiên được khởi chạy và dự kiến sẽ được thực thi. Trong khi đó, mã được gọi khi hoạt động không đồng bộ hoàn tất ("tiếp tục") sẽ được lưu. await Task.Delay(1000) Sau khi hoàn thành thao tác không đồng bộ đầu tiên, "sự tiếp tục" — mã cho đến khi thao tác không đồng bộ tiếp theo ( ) sẽ bắt đầu thực thi. SyncMethodB Hoạt động không đồng bộ thứ hai ( ) được khởi chạy và dự kiến sẽ được thực thi. Đồng thời, phần tiếp theo — mã sau thao tác không đồng bộ thứ hai ( ) sẽ được giữ nguyên. await Task.Delay(2000) SyncMethodC Sau khi hoàn thành hoạt động không đồng bộ thứ hai, sẽ được thực thi, tiếp theo là thực thi và chờ hoạt động không đồng bộ thứ ba . SyncMethodC await Task.Delay(3000) Đây là một lời giải thích đơn giản hóa, vì trên thực tế, async/await là cú pháp đường để cho phép gọi các phương thức không đồng bộ một cách thuận tiện và chờ chúng hoàn thành. Bạn cũng có thể tổ chức bất kỳ tổ hợp lệnh thực thi nào bằng cách sử dụng và : WhenAll WhenAny var allTasks = Task.WhenAll( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); allTasks.ContinueWith(t => { Console.WriteLine("All the tasks are completed"); }); var anyTask = Task.WhenAny( Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }), Task.Run(() => { /* ... */ }) ); anyTask.ContinueWith(t => { Console.WriteLine("One of tasks is completed"); }); IAsyncStateMachine Trình biên dịch chuyển đổi các cuộc gọi không đồng bộ/chờ đợi thành một máy trạng thái , đây là một tập hợp các hành động tuần tự phải được thực hiện để hoàn thành thao tác không đồng bộ. C# IAsyncStateMachine Mỗi khi bạn gọi một hoạt động đang chờ, máy trạng thái sẽ hoàn thành công việc của nó và chờ hoàn thành hoạt động đó, sau đó nó tiếp tục thực hiện hoạt động tiếp theo. Điều này cho phép các hoạt động không đồng bộ được thực hiện trong nền mà không chặn luồng chính và cũng làm cho các lệnh gọi phương thức không đồng bộ trở nên đơn giản và dễ đọc hơn. Do đó, phương thức được chuyển thành tạo và khởi tạo một máy trạng thái với chú thích và bản thân máy trạng thái có một số trạng thái bằng với số lần gọi chờ. Example [AsyncStateMachine(typeof(ExampleStateMachine))] Ví dụ về phương thức được biến đổi Example [AsyncStateMachine(typeof(ExampleStateMachine))] public /*async*/ Task Example() { // Create a new instance of the ExampleStateMachine class ExampleStateMachine stateMachine = new ExampleStateMachine(); // Create a new AsyncTaskMethodBuilder and assign it to the taskMethodBuilder property of the stateMachine instance stateMachine.taskMethodBuilder = AsyncTaskMethodBuilder.Create(); // Set the currentState property of the stateMachine instance to -1 stateMachine.currentState = -1; // Start the stateMachine instance stateMachine.taskMethodBuilder.Start(ref stateMachine); // Return the Task property of the taskMethodBuilder return stateMachine.taskMethodBuilder.Task; } Ví dụ về máy trạng thái được tạo ExampleStateMachine [CompilerGenerated] private sealed class ExampleStateMachine : IAsyncStateMachine { public int currentState; public AsyncTaskMethodBuilder taskMethodBuilder; private TaskAwaiter taskAwaiter; public int paramInt; private int localInt; void IAsyncStateMachine.MoveNext() { int num = currentState; try { TaskAwaiter awaiter3; TaskAwaiter awaiter2; TaskAwaiter awaiter; switch (num) { default: localInt = paramInt; // Call the first synchronous method SyncMethodA(); // Create a task awaiter for a delay of 1000 milliseconds awaiter3 = Task.Delay(1000).GetAwaiter(); // If the task is not completed, set the current state to 0 and store the awaiter if (!awaiter3.IsCompleted) { currentState = 0; taskAwaiter = awaiter3; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine); return; } // If the task is completed, jump to the label after the first await goto Il_AfterFirstAwait; case 0: // Retrieve the awaiter from the taskAwaiter field awaiter3 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the first await goto Il_AfterFirstAwait; case 1: // Retrieve the awaiter from the taskAwaiter field awaiter2 = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; // Jump to the label after the second await goto Il_AfterSecondAwait; case 2: // Retrieve the awaiter from the taskAwaiter field awaiter = taskAwaiter; // Reset the taskAwaiter field taskAwaiter = default(TaskAwaiter); currentState = -1; break; Il_AfterFirstAwait: awaiter3.GetResult(); // Call the second synchronous method SyncMethodB(); // Create a task awaiter for a delay of 2000 milliseconds awaiter2 = Task.Delay(2000).GetAwaiter(); // If the task is not completed, set the current state to 1 and store the awaiter if (!awaiter2.IsCompleted) { currentState = 1; taskAwaiter = awaiter2; // Store the current state machine ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine); return; } // If the task is completed, jump to the label after the second await goto Il_AfterSecondAwait; Il_AfterSecondAwait: // Get the result of the second awaiter awaiter2.GetResult(); // Call the SyncMethodC SyncMethodC(); // Create a new awaiter with a delay of 3000 milliseconds awaiter = Task.Delay(3000).GetAwaiter(); // If the awaiter is not completed, set the current state to 2 and store the awaiter if (!awaiter.IsCompleted) { currentState = 2; taskAwaiter = awaiter; // Set the stateMachine to this ExampleStateMachine stateMachine = this; // Await the task and pass the state machine taskMethodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine); return; } break; } // Get the result of the awaiter awaiter.GetResult(); } catch (Exception exception) { currentState = -2; taskMethodBuilder.SetException(exception); return; } currentState = -2; taskMethodBuilder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { /*...*/ } } Đồng bộ hóaBối cảnh Trong lệnh gọi , bối cảnh đồng hóa hiện tại sẽ được lấy. là một khái niệm trong C# được sử dụng để biểu diễn ngữ cảnh kiểm soát việc thực hiện một tập hợp các hoạt động không đồng bộ. Nó được sử dụng để điều phối việc thực thi mã trên nhiều luồng và để đảm bảo rằng mã được thực thi theo một thứ tự cụ thể. Mục đích chính của SynchronizationContext là cung cấp một cách để kiểm soát việc lên lịch và thực hiện các hoạt động không đồng bộ trong môi trường đa luồng. AwaitUnsafeOnCompleted SynchronizationContext SynchronizationContext Trong các môi trường khác nhau, có các triển khai khác nhau. Ví dụ, trong .NET, có: SynchronizationContext : WPF System.Windows.Threading.DispatcherSynchronizationContext : WinForms System.Windows.Forms.WindowsFormsSynchronizationContext : WinRT System.Threading.WinRTSynchronizationContext : ASP.NET System.Web.AspNetSynchronizationContext cũng có bối cảnh đồng bộ hóa riêng, , cho phép chúng tôi sử dụng các hoạt động không đồng bộ với liên kết với API PlayerLoop. Ví dụ mã sau đây cho thấy cách xoay một đối tượng trong mỗi khung bằng cách sử dụng : Unity UnitySynchronizationContext Task.Yield() private async void Start() { while (true) { transform.Rotate(0, Time.deltaTime * 50, 0); await Task.Yield(); } } Một ví dụ khác về việc sử dụng async/await trong Unity để thực hiện yêu cầu mạng: using UnityEngine; using System.Net.Http; using System.Threading.Tasks; public class NetworkRequestExample : MonoBehaviour { private async void Start() { string response = await GetDataFromAPI(); Debug.Log("Response from API: " + response); } private async Task<string> GetDataFromAPI() { using (var client = new HttpClient()) { var response = await client.GetStringAsync("https://api.example.com/data"); return response; } } } Nhờ , chúng ta có thể sử dụng các phương thức một cách an toàn (chẳng hạn như ) ngay sau khi hoàn thành thao tác không đồng bộ, vì việc thực thi mã này sẽ tiếp tục trong chuỗi Unity chính. UnitySynchronizationContext UnityEngine Debug.Log() TaskCompletitionSource<T> Lớp này cho phép bạn quản lý một đối tượng . Nó được tạo ra để điều chỉnh các phương thức không đồng bộ cũ thành TAP, nhưng nó cũng rất hữu ích khi chúng ta muốn bao bọc một xung quanh một số hoạt động chạy dài dựa trên một số sự kiện. Task Task Trong ví dụ sau, đối tượng bên trong sẽ hoàn thành sau 3 giây kể từ khi bắt đầu và chúng ta sẽ nhận được kết quả của nó trong phương thức : Task taskCompletionSource Update using System.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private TaskCompletionSource<int> taskCompletionSource; private void Start() { // Create a new TaskCompletionSource taskCompletionSource = new TaskCompletionSource<int>(); // Start a coroutine to wait 3 seconds // and then set the result of the TaskCompletionSource StartCoroutine(WaitAndComplete()); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the TaskCompletionSource taskCompletionSource.SetResult(10); } private async void Update() { // Await the result of the TaskCompletionSource int result = await taskCompletionSource.Task; // Log the result to the console Debug.Log("Result: " + result); } } Mã thông báo hủy được sử dụng trong C# để báo hiệu rằng một tác vụ hoặc hoạt động sẽ bị hủy bỏ. Mã thông báo được chuyển đến nhiệm vụ hoặc hoạt động và mã trong nhiệm vụ hoặc hoạt động có thể kiểm tra mã thông báo định kỳ để xác định xem có nên dừng nhiệm vụ hoặc hoạt động hay không. Điều này cho phép hủy bỏ một nhiệm vụ hoặc hoạt động một cách gọn gàng và nhẹ nhàng, thay vì chỉ giết chết nó một cách đột ngột. Mã hủy bỏ Mã thông báo hủy thường được sử dụng trong các tình huống mà người dùng có thể hủy tác vụ chạy dài hoặc nếu tác vụ không còn cần thiết nữa, chẳng hạn như nút hủy trong giao diện người dùng. Mẫu tổng thể tương tự như việc sử dụng . Đầu tiên, một được tạo, sau đó của nó được chuyển sang hoạt động không đồng bộ: TaskCompletionSource CancellationTokenSource Token public class ExampleMonoBehaviour : MonoBehaviour { private CancellationTokenSource _cancellationTokenSource; private async void Start() { // Create a new CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); // Get the token from the CancellationTokenSource CancellationToken token = _cancellationTokenSource.Token; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } private void OnDestroy() { // Cancel the token when the object is destroyed _cancellationTokenSource.Cancel(); } } Khi thao tác bị hủy, một sẽ được ném ra và thuộc tính sẽ được đặt thành . OperationCanceledException Task.IsCanceled true Các tính năng không đồng bộ mới trong Unity 2022.2 Điều quan trọng cần lưu ý là các đối tượng được quản lý bởi thời gian chạy .NET chứ không phải bởi Unity và nếu đối tượng thực thi tác vụ bị hủy (hoặc nếu trò chơi thoát khỏi chế độ chơi trong trình chỉnh sửa), thì tác vụ sẽ tiếp tục chạy như Unity có không có cách nào để hủy bỏ nó. Task Bạn luôn cần đi kèm với với tương ứng. Điều này dẫn đến một số mã dự phòng và trong Unity 2022.2, các mã thông báo tích hợp ở cấp và toàn bộ cấp đã xuất hiện. await Task CancellationToken MonoBehaviour Application Hãy xem ví dụ trước thay đổi như thế nào khi sử dụng của đối tượng : destroyCancellationToken MonoBehaviour using System.Threading; using System.Threading.Tasks; using UnityEngine; public class ExampleMonoBehaviour : MonoBehaviour { private async void Start() { // Get the cancellation token from the MonoBehaviour CancellationToken token = this.destroyCancellationToken; try { // Start a new Task and pass in the token await Task.Run(() => DoSomething(token), token); } catch (OperationCanceledException) { Debug.Log("Task was cancelled"); } } private void DoSomething(CancellationToken token) { for (int i = 0; i < 100; i++) { // Check if the token has been cancelled if (token.IsCancellationRequested) { // Return if the token has been cancelled return; } Debug.Log("Doing something..."); // Sleep for 1 second Thread.Sleep(1000); } } } Chúng tôi không còn cần phải tạo thủ công và hoàn thành tác vụ trong phương thức . Đối với các tác vụ không được liên kết với một cụ thể, chúng ta có thể sử dụng . Thao tác này sẽ chấm dứt tác vụ khi thoát Chế độ phát (trong Trình chỉnh sửa) hoặc khi thoát ứng dụng. CancellationTokenSource OnDestroy MonoBehaviour UnityEngine.Application.exitCancellationToken UniTask Bất chấp sự tiện lợi khi sử dụng và các khả năng do .NET Tasks cung cấp, chúng có những hạn chế đáng kể khi được sử dụng trong Unity: Các đối tượng quá cồng kềnh và gây ra nhiều phân bổ. Task không khớp với luồng Unity (luồng đơn). Task Thư viện bỏ qua những hạn chế này mà không cần sử dụng các luồng hoặc . Nó đạt được sự vắng mặt của phân bổ bằng cách sử dụng loại dựa trên cấu trúc . UniTask SynchronizationContext UniTask<T> UniTask yêu cầu phiên bản thời gian chạy tập lệnh .NET 4.x, với Unity 2018.4.13f1 là phiên bản chính thức được hỗ trợ thấp nhất. Ngoài ra, bạn có thể chuyển đổi tất cả thành bằng các phương thức mở rộng: AsyncOperations UnitTask using UnityEngine; using UniTask; public class AssetLoader : MonoBehaviour { public async void LoadAsset(string assetName) { var loadRequest = Resources.LoadAsync<GameObject>(assetName); await loadRequest.AsUniTask(); var asset = loadRequest.asset as GameObject; if (asset != null) { // Do something with the loaded asset } } } Trong ví dụ này, phương thức sử dụng để tải nội dung không đồng bộ. Sau đó, phương thức được sử dụng để chuyển đổi do trả về thành , có thể chờ đợi. LoadAsset Resources.LoadAsync AsUniTask AsyncOperation LoadAsync UniTask Như trước đây, bạn có thể tổ chức bất kỳ sự kết hợp thứ tự thực thi nào bằng cách sử dụng và : UniTask.WhenAll UniTask.WhenAny using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; public class Example : MonoBehaviour { private async void Start() { // Start two Tasks and wait for both to complete await UniTask.WhenAll(Task1(), Task2()); // Start two Tasks and wait for one to complete await UniTask.WhenAny(Task1(), Task2()); } private async UniTask Task1() { // Do something } private async UniTask Task2() { // Do something } } Trong UniTask, có một triển khai khác gọi là có thể được sử dụng để thay thế để có hiệu suất tốt hơn. SynchronizationContext UniTaskSynchronizationContext UnitySynchronizationContext API có thể chờ đợi Trong phiên bản alpha đầu tiên của Unity 2023.1, lớp đã được giới thiệu. Các Coroutine có thể chờ đợi là các loại giống như Tác vụ không đồng bộ/tương thích với chờ đợi được thiết kế để chạy trong Unity. Không giống như .NET Tasks, chúng được quản lý bởi engine chứ không phải runtime. Awaitable private async Awaitable DoSomethingAsync() { // awaiting built-in events await Awaitable.EndOfFrameAsync(); await Awaitable.WaitForSecondsAsync(); // awaiting .NET Tasks await Task.Delay(2000, destroyCancellationToken); await Task.Yield(); // awaiting AsyncOperations await SceneManager.LoadSceneAsync("path/to/scene.unity"); // ... } Chúng có thể được chờ đợi và được sử dụng làm kiểu trả về của phương thức không đồng bộ. So với , chúng ít phức tạp hơn nhưng có các phím tắt nâng cao hiệu suất dựa trên các giả định dành riêng cho Unity. System.Threading.Tasks Dưới đây là những điểm khác biệt chính so với .NET Tasks: Đối tượng chỉ có thể được chờ đợi một lần; nó không thể được chờ đợi bởi nhiều chức năng không đồng bộ. Awaitable sẽ không chặn cho đến khi hoàn thành. Gọi nó trước khi hoạt động kết thúc là hành vi không xác định. Awaiter.GetResults() Không bao giờ nắm bắt một . Vì lý do bảo mật, Nhiệm vụ .NET nắm bắt ngữ cảnh thực thi khi đang chờ để truyền bá ngữ cảnh mạo danh qua các cuộc gọi không đồng bộ. ExecutionContext Không bao giờ nắm bắt . Các phần tiếp theo của Coroutine được thực thi đồng bộ từ mã làm tăng phần hoàn thành. Trong hầu hết các trường hợp, đây sẽ là từ khung chính của Unity. SynchronizationContext Awaitables là các đối tượng gộp lại để ngăn phân bổ quá mức. Đây là các loại tham chiếu, vì vậy chúng có thể được tham chiếu qua các ngăn xếp khác nhau, được sao chép một cách hiệu quả, v.v. đã được cải thiện để tránh kiểm tra giới hạn trong các trình tự nhận/phát hành thông thường được tạo bởi các máy trạng thái không đồng bộ. ObjectPool Stack<T> Để có được kết quả của một thao tác dài, bạn có thể sử dụng loại . Bạn có thể quản lý việc hoàn thành bằng cách sử dụng và tương tự như : Awaitable<T> Awaitable AwaitableCompletionSource AwaitableCompletionSource<T> , TaskCompletitionSource using UnityEngine; using Cysharp.Threading.Tasks; public class ExampleBehaviour : MonoBehaviour { private AwaitableCompletionSource<bool> _completionSource; private async void Start() { // Create a new AwaitableCompletionSource _completionSource = new AwaitableCompletionSource<bool>(); // Start a coroutine to wait 3 seconds // and then set the result of the AwaitableCompletionSource StartCoroutine(WaitAndComplete()); // Await the result of the AwaitableCompletionSource bool result = await _completionSource.Awaitable; // Log the result to the console Debug.Log("Result: " + result); } private IEnumerator WaitAndComplete() { yield return new WaitForSeconds(3); // Set the result of the AwaitableCompletionSource _completionSource.SetResult(true); } } Đôi khi, cần phải thực hiện các phép tính lớn có thể dẫn đến đóng băng trò chơi. Đối với điều này, tốt hơn là sử dụng các phương thức Có thể chờ đợi: và . Chúng cho phép bạn thoát khỏi luồng chính và quay lại luồng đó. BackgroundThreadAsync() MainThreadAsync() private async Awaitable DoCalculationsAsync() { // Awaiting execution on a ThreadPool background thread. await Awaitable.BackgroundThreadAsync(); var result = PerformSomeHeavyCalculations(); // Awaiting execution on the Unity main thread. await Awaitable.MainThreadAsync(); // Using the result in main thread Debug.Log(result); } Bằng cách này, Awaitables loại bỏ những nhược điểm của việc sử dụng .NET Tasks và cũng cho phép chờ đợi các sự kiện PlayerLoop và AsyncOperations. Phần kết luận Như chúng ta có thể thấy, với sự phát triển của Unity, ngày càng có nhiều công cụ để tổ chức các hoạt động không đồng bộ: Đoàn kết quân đoàn lời hứa Nhiệm vụ .NET UniTask Mã thông báo hủy tích hợp API có thể chờ đợi 5.6 ✅ ✅ 2017.1 ✅ ✅ ✅ 2018.4 ✅ ✅ ✅ ✅ 2022.2 ✅ ✅ ✅ ✅ ✅ 2023.1 ✅ ✅ ✅ ✅ ✅ ✅ Chúng tôi đã xem xét tất cả các cách lập trình không đồng bộ chính trong Unity. Tùy thuộc vào mức độ phức tạp của nhiệm vụ và phiên bản Unity bạn đang sử dụng, bạn có thể sử dụng nhiều loại công nghệ từ Coroutines và Promise cho đến Nhiệm vụ và Awaitables, để đảm bảo quá trình chơi mượt mà và liền mạch trong trò chơi của bạn. Cảm ơn bạn đã đọc, và chúng tôi chờ đợi những kiệt tác tiếp theo của bạn.