[Unity] UniRx 공부

2023. 8. 4. 17:08[Unity] 게임 개발

반응형

참고 원문 : 2022年現在におけるUniRxの使いみち

UniRx와 async/await와 UniTask

async/await, 이쪽은 간단하게 "편리하게 된 코루틴" 이라고 하는 인식으로 일단은 문제 없습니다. async/await 자체는 비동기 처리의 대기 처리를 간소하게 쓸 수 있도록 하기 위한, C#의 언어 기능입니다. (구조는 전혀 다르지만) async/await는 외형으로서는 Unity의 코루틴과 매우 비슷합니다.

UniRx에서도 코루틴과의 조합은 편리했지만, async/await에서도 UniRx와 조합하면 그 편리성이 높아집니다. 특히 UniTask라는 라이브러리가 강력하며, 여기를 도입함으로써 Unity에서 async/await의 취급을 대폭 강화할 수 있습니다. (async/await와 UniTask는 반드시 세트로 다루고 싶을 정도로 강력합니다)

여기서 오해하고 싶지 않지만, "async/await이 있기 때문에 UniRx는 불필요해졌다" 는 것은 아닙니다. UniRx는 UniRx에서 사용할 수 있는 경우도 아직도 있고, async/await또한 편리하게 사용할 수 있는 경우도 다수 있습니다. 옛날에는 UniRx 밖에 선택사항이 없었지만, 현대에 있어서는 UniRx를 사용하지 않아도 보다 간단하게 구현할 수 있는 수법이 늘어났다고 하는 것입니다.

 

UniRx가 편리한 장소, async/await가 편리한 장소

UniRx는 결코 다목적이 아닙니다. 상황에 따라서는 async/await(혹은, 완전히 다른 도구)를 사용하는 편이 깨끗하게 쓸 수 있는 경우도 많이 있습니다.

그럼 UniRx async/await를 어떻게 구사할까요, 기본적으로는 다음 구분으로 문제 없습니다.

  • UniRx가 편리하게 사용할 수 있는 경우
    • 불특정 다수에 여러 번 이벤트 메시지를 전달하는 경우
    • 특정 처리의 흐름(시퀀스)을 여러 번 반복 실행하는 경우
    • 종속성을 역전하려는 경우
    • Push형으로 구동할 필요가 있는 경우
  • async/await(및 UniTask조합)을 편리하게 사용할 수 있는 경우
    • "한 번만 실행되는 처리"를 기다리는 경우
    • 절차적 (if문 for문)으로 처리를 작성하고 싶은 경우
    • 처리의 동작이 Pull형으로 끝나는 경우

UniRx는 '반복 실행'에 특화되어 있습니다. 그 때문에 "몇번인가 발행되는 이벤트를 처리한다" "Update()의 루프 대신에 사용한다"라고 하는 용도에 매치하고 있습니다.

한편으로 async/await는 "뭔가의 처리를 1회만 기다린다"에 특화하고 있습니다. 그 때문에 "초기화 처리가 끝나는 것을 기다린다" "외부와의 통신을 기다린다"라고 하는 경우는 async/await에서 쓰는 편이 스마트하게 쓸 수 있습니다. (단발의 처리를 UniRx로 쓰는 것도 물론 할 수 있지만, 중복이 되기 쉽다)

 



UniRX 실제 적용 비교 예시

매 프레임 특정 Camera의 위치를 바라보게 하는 Billboard Code.

using UnityEngine;

public class LookAtCamera : MonoBehaviour
{
    public Camera targetCamera; // 특정 카메라를 지정하기 위한 변수

    private void Update()
    {
        if (targetCamera != null)
        {
            // 이미지가 특정 카메라를 바라보게 회전을 조절
            Vector3 lookDirection = targetCamera.transform.position - transform.position;
            transform.rotation = Quaternion.LookRotation(lookDirection, Vector3.up);
        }
    }
}

 

UniRX의 EveryValueChanged 기능을 활용하여 특정 Camera의 위치에 변경값이 있을때만 호출되도록 수정.

using UnityEngine;
using UniRx;

public class LookAtCamera : MonoBehaviour
{
    public Camera targetCamera; // 특정 카메라를 지정하기 위한 변수

    private void Start()
    {
        if (targetCamera != null)
        {
            // 카메라의 위치를 감시하고 위치가 변경될 때마다 이벤트를 발생시키는 Observable
            Observable.EveryValueChanged(() => targetCamera.transform.position)
                .Subscribe(_ => UpdateRotation()) // 위치가 변경될 때마다 UpdateRotation 함수 호출
                .AddTo(this); // 스크립트가 파괴될 때 구독을 해제하기 위해 AddTo(this) 사용
        }
    }

    private void UpdateRotation()
    {
        // 이미지가 특정 카메라를 바라보게 회전을 조절
        Vector3 lookDirection = targetCamera.transform.position - transform.position;
        transform.rotation = Quaternion.LookRotation(lookDirection, Vector3.up);
    }
}

 

  • 효과
    1. 성능 개선: Observable.EveryValueChanged()는 해당 속성 값이 변경될 때만 이벤트를 발생시킵니다. 따라서 속성의 값이 변하지 않는 경우에는 불필요한 호출을 막아 성능을 개선할 수 있습니다. Update문으로 처리할 경우 프레임마다 매번 카메라 위치를 확인하여 회전을 업데이트해야 합니다. 하지만 Observable.EveryValueChanged()를 사용하면 값이 변할 때만 이벤트를 발생시키기 때문에 성능상 이점이 있습니다.
    2. 코드 가독성: Observable.EveryValueChanged()를 사용하면 변화하는 값에 대한 감시를 명시적으로 표현할 수 있습니다. 코드가 더 명확해지고 가독성이 향상됩니다. "카메라의 위치를 감시하고 위치가 변경될 때만 이벤트를 발생시킨다"라는 의도가 코드에 명시적으로 드러나기 때문에 이해하기 쉽습니다.
    3. 유지보수성: Observable.EveryValueChanged()를 사용하면 감시하고자 하는 속성이나 값이 추가되거나 변경될 때 코드를 수정하기가 더욱 용이합니다. 코드가 명시적이고 모듈화되어 있기 때문에 유지보수가 간편해집니다.

 

 

요약

  • "UniRx 전용"해서 사용해야 되는 시대는 사라졌습니다.
    • 현대는 UniRx 이상에 편리한 라이브러리와 언어 기능이 갖추어져 있습니다.
    • 특히 async/await+UniTask는 매우 강력하며 UniRx 대신 사용할 수 있는 패턴이 많습니다.
  • 그러나 한편으로 UniRx의 용도가 완전히 없어진 것은 아니다.
    • 단순한 Observer 패턴의 구현 라이브러리로 사용해도 편리
    • ReactiveProperty는 특히 편리성이 높고 현역에서 사용할 수 있습니다.
  • UniRx를 너무 과신하지 말자
    • 사용 장소의 판별은 중요
    • async/await+UniTask쪽이 깨끗하게 쓸 수 있는 패턴도 있다
반응형