paint-brush
Unity 2023.1에 Awaitable 클래스 도입~에 의해@deniskondratev
7,024 판독값
7,024 판독값

Unity 2023.1에 Awaitable 클래스 도입

~에 의해 Denis Kondratev10m2023/01/27
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

이 기사에서는 Unity 2023.1에 도입된 새로운 Awaitable 클래스에 대해 설명합니다. 이를 통해 Unity 게임 개발에서 비동기 코드를 작성할 수 있는 더 많은 기회가 제공됩니다. 대기 메서드, 비동기 실행을 중지하는 속성, 코루틴 및 InvokeRepeating과 같은 다른 Unity 도구와의 사용법을 다룹니다. async-await의 기본 사항을 이해하고 Awaitable 클래스를 실험하여 기능과 한계를 이해하는 것이 중요하다는 점을 강조합니다.
featured image - Unity 2023.1에 Awaitable 클래스 도입
Denis Kondratev HackerNoon profile picture
0-item

2022년 5월 Alexandre Mutel과 Kristyna Hougaard는 "Unity와 .NET, 다음은 무엇일까요?"라는 게시물을 통해 발표했습니다. Unity는 async-await 사용의 편의성을 포함하여 더 많은 .NET 기능을 채택할 계획입니다. 그리고 Unity는 그 약속을 이행하고 있는 것 같습니다. Unity 2023.1 알파 버전에는 Awaitable 클래스가 도입되어 비동기 코드를 작성할 수 있는 더 많은 기회를 제공합니다.

대기 방법

이 섹션에서는 공식 Unity 문서에 충분히 설명되어 있는 메서드에 대해서는 너무 깊이 다루지 않을 것입니다. 그것들은 모두 비동기 대기와 관련이 있습니다.


Awaitable.WaitForSecondsAsync()를 사용하면 지정된 게임 시간 동안 기다릴 수 있습니다. 실시간으로 대기를 수행하는 Task.Delay()와는 다릅니다. 차이점을 명확히 하는 데 도움이 되도록 나중에 코드 블록에서 작은 예를 제공하겠습니다.


 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."); }


이 예제에서는 Start() 메서드 시작 부분에서 Time.timeScale을 사용하여 게임 시간을 중지합니다. 실험을 위해 RunGameplay() 메서드에서 5초 후에 흐름을 재개하는 데 코루틴이 사용됩니다. 그런 다음 두 가지 1초 대기 방법을 시작합니다. 하나는 Awaitable.WaitForSecondsAsync()를 사용하고 다른 하나는 Task.Delay()를 사용합니다. 1초 후에 콘솔에 "Waiting WaitWithTaskDelay()가 끝났습니다"라는 메시지가 표시됩니다. 그리고 5초가 지나면 "Waiting WaitWithTaskDelay()가 종료되었습니다."라는 메시지가 나타납니다.


Unity의 기본 플레이어 루프에 더 많은 유연성을 제공하기 위해 다른 편리한 방법도 추가되었습니다. 그 목적은 이름에서 명확하며 코루틴을 사용할 때의 비유와 일치합니다.


  • EndOfFrameAsync()
  • 고정업데이트비동기화()
  • 다음프레임비동기화()


코루틴을 처음 사용하는 경우 더 나은 이해를 위해 직접 실험해 보는 것이 좋습니다.


이전 API인 AsyncOperation과의 하위 호환성을 위해 Awaitable.FromAsyncOperation() 메서드도 추가되었습니다.

destroyCancellationToken 속성 사용

코루틴 사용의 편리한 점 중 하나는 구성요소가 제거되거나 비활성화되면 자동으로 중지된다는 것입니다. Unity 2022.2에서는 destroyCancellationToken 속성이 MonoBehaviour에 추가되어 객체 삭제 시 비동기 실행을 중지할 수 있습니다. CancellationToken 취소를 통해 작업을 중지하면 OperationCanceledException이 발생한다는 점을 기억하는 것이 중요합니다. 호출 메서드가 Task 또는 Awaitable을 반환하지 않는 경우 이 예외를 포착해야 합니다.


 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); }


이 예에서 개체는 Start()에서 즉시 삭제되지만 그 전에 Awake()는 DoAwaitAsync()의 실행을 시작합니다. Awaitable.WaitForSecondsAsync(1, destroyCancellationToken) 명령은 1초 동안 기다린 후 "해당 메시지는 기록되지 않습니다."라는 메시지를 출력해야 합니다. 객체가 즉시 삭제되므로 destroyCancellationToken은 OperationCanceledException을 발생시켜 전체 체인의 실행을 중지합니다. 이러한 방식으로 destroyCancellationToken을 사용하면 CancellationToken을 수동으로 생성할 필요가 없습니다.


그러나 예를 들어 객체가 비활성화될 때 실행을 중지하는 등의 작업을 수행할 수 있습니다. 예를 들어 보겠습니다.


 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."); } } }


이 양식에서는 이 MonoBehaviour가 정지된 개체가 켜져 있는 한 "이 메시지는 매초 기록됩니다"라는 메시지가 전송됩니다. 개체를 끄고 다시 켤 수 있습니다.


이 코드는 중복된 것처럼 보일 수 있습니다. Unity에는 유사한 작업을 훨씬 쉽게 수행할 수 있는 코루틴 및 InvokeRepeating()과 같은 편리한 도구가 이미 많이 포함되어 있습니다. 그러나 이는 단지 사용 예일 뿐입니다. 여기서는 Awaitable을 다루고 있습니다.


Application.exitCancellationToken 속성 사용

Unity에서는 편집기에서 플레이 모드를 종료한 후에도 비동기 메서드 실행이 자체적으로 중지되지 않습니다. 프로젝트에 비슷한 스크립트를 추가해 보겠습니다.


 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); } } }


이 예에서는 플레이 모드로 전환한 후 "이 메시지는 매초 기록됩니다."라는 메시지가 콘솔에 출력됩니다. 재생 버튼을 놓은 후에도 계속 출력됩니다. 이 예제에서는 Awaitable.WaitForSecondsAsync() 대신 Task.Delay()를 사용합니다. 여기서는 작업을 표시하기 위해 게임 시간이 아닌 실시간 지연이 필요하기 때문입니다.


destroyCancellationToken과 유사하게, 플레이 모드 종료 시 비동기 메소드의 실행을 중단하는 Application.exitCancellationToken을 사용할 수 있습니다. 스크립트를 수정해 보겠습니다.


 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); } } }


이제 스크립트가 의도한 대로 실행됩니다.

이벤트 함수와 함께 사용

Unity에서 일부 이벤트 함수는 Start, OnCollisionEnter 또는 OnCollisionExit와 같은 코루틴일 수 있습니다. 하지만 Unity 2023.1부터는 Update(), LateUpdate, 심지어 OnDestroy()를 포함한 모든 항목을 Awaitable로 설정할 수 있습니다.


비동기 실행을 기다리지 않으므로 주의해서 사용해야 합니다. 예를 들어, 다음 코드의 경우:


 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()"); }


콘솔에서는 다음과 같은 결과를 얻게 됩니다.


 Awake() started OnEnable() Start() Awake() finished


또한 비동기 코드가 계속 실행되는 동안 MonoBehaviour 자체 또는 게임 개체가 더 이상 존재하지 않을 수 있다는 점을 기억할 가치가 있습니다. 그러한 상황에서는:


 private async Awaitable Awake() { Debug.Log(this != null); await Awaitable.NextFrameAsync(); Debug.Log(this != null); } private void Start() { Destroy(this); }


다음 프레임에서는 MonoBehaviour가 삭제된 것으로 간주됩니다. 콘솔에서는 다음과 같은 결과를 얻게 됩니다.


 True Flase


이는 OnDestroy() 메서드에도 적용됩니다. 메서드를 비동기식으로 만드는 경우 wait 문 이후 MonoBehaviour가 이미 삭제된 것으로 간주된다는 점을 고려해야 합니다. 개체 자체가 삭제되면 해당 개체에 있는 많은 MonoBehaviour의 작업이 이 시점에서 올바르게 작동하지 않을 수 있습니다.


이벤트 함수로 작업할 때 실행 순서를 아는 것이 중요하다는 점은 주목할 가치가 있습니다. 비동기 코드는 예상한 순서대로 실행되지 않을 수 있으므로 스크립트를 디자인할 때 이 점을 염두에 두는 것이 중요합니다.

대기 가능한 이벤트 함수는 모든 유형의 예외를 포착합니다.

Awaitable Event Functions는 예상치 못한 모든 유형의 예외를 포착한다는 점에 주목할 가치가 있습니다. 나는 그들이 OperationCanceledExceptions만 잡을 것으로 예상했는데, 이것이 더 합리적이었을 것입니다. 그러나 모든 유형의 예외를 포착하면 현재로서는 사용하기에 적합하지 않습니다. 대신 이전 예제에 표시된 대로 비동기 메서드를 실행하고 필요한 메시지를 수동으로 포착할 수 있습니다.


 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); }


구성 요소는 시작 시 즉시 삭제되므로 DoAwaitAsync() 실행이 중단됩니다. "해당 메시지는 기록되지 않습니다."라는 메시지가 콘솔에 표시되지 않습니다. OperationCanceledException()만 catch되고 다른 모든 예외는 throw될 수 있습니다.


앞으로는 이러한 접근 방식이 수정되기를 바랍니다. 현재로서는 Awaitable Event Functions를 사용하는 것이 안전하지 않습니다.

스레드 간 자유로운 이동

알려진 바와 같이 게임 개체 및 MonoBehaviour를 사용한 모든 작업은 기본 스레드에서만 허용됩니다. 때로는 게임이 중단될 수 있는 대규모 계산을 수행해야 하는 경우도 있습니다. 메인 스레드 외부에서 수행하는 것이 좋습니다. Awaitable은 기본 스레드에서 벗어나 메인 스레드로 돌아갈 수 있는 BackgroundThreadAsync() 및 MainThreadAsync()라는 두 가지 메서드를 제공합니다. 나는 예를 제시할 것이다.


 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}"); }


여기서 메서드가 시작되면 추가 스레드로 전환됩니다. 여기서는 이 추가 스레드의 ID를 콘솔에 출력합니다. 1이 메인 스레드이기 때문에 1과 같지 않습니다.


그런 다음 스레드가 10초 동안 정지되어(Thread.Sleep(10000)) 대규모 계산을 시뮬레이션합니다. 메인 스레드에서 이 작업을 수행하면 게임이 실행되는 동안 게임이 정지된 것처럼 보입니다. 하지만 이 상황에서도 모든 것이 계속해서 안정적으로 작동합니다. 이러한 계산에서 CancellationToken을 사용하여 불필요한 작업을 중지할 수도 있습니다.


그런 다음 다시 메인 스레드로 전환합니다. 이제 모든 Unity 기능을 다시 사용할 수 있습니다. 예를 들어, 이 경우와 같이 메인 스레드 없이는 불가능했던 게임 개체를 비활성화합니다.

결론

결론적으로, Unity 2023.1에 도입된 새로운 Awaitable 클래스는 개발자에게 비동기 코드를 작성할 수 있는 더 많은 기회를 제공하여 반응성이 뛰어나고 성능이 뛰어난 게임을 더 쉽게 만들 수 있게 해줍니다. Awaitable 클래스에는 Unity의 기본 플레이어 루프에 더 많은 유연성을 제공하는 WaitForSecondsAsync(), EndOfFrameAsync(), FixUpdateAsync() 및 NextFrameAsync()와 같은 다양한 대기 메서드가 포함되어 있습니다. destroyCancellationToken 및 Application.exitCancellationToken 속성은 객체 삭제 또는 재생 모드 종료 시 비동기 실행을 중지하는 편리한 방법도 제공합니다.


Awaitable 클래스는 Unity에서 비동기 코드를 작성하는 새로운 방법을 제공하지만 최상의 결과를 얻으려면 Coroutines 및 InvokeRepeating과 같은 다른 Unity 도구와 함께 사용해야 합니다. 또한 async-await의 기본 사항과 성능 및 응답성 향상과 같이 게임 개발 프로세스에 가져올 수 있는 이점을 이해하는 것이 중요합니다.


요약하자면, Awaitable 클래스는 Unity 개발자를 위한 강력한 도구이지만 최상의 결과를 얻으려면 다른 Unity 도구 및 개념과 함께 주의해서 사용해야 합니다. 기능과 한계를 더 잘 이해하려면 직접 실험해 보는 것이 중요합니다.