UniRx에 대해
UniRx란?
C#의 공식 Rx는 Unity에서 작동하지 않고 iOS IL2CPP 호환성에 문제가 있다. UniRx(Reactive Extensions for Unity)는 이러한 문제를 해결하고 Unity에 대한 몇 가지 특정 유틸리티가 추가된 라이브러리이다
최근에는 Cysharp/R3가 배포되고 있어 이것으로 대체해달라는 문구가 올라온 상태이다
기본적인 Reactive Programming의 특징은 링크를 참고하자
UniRx가 제공하는 주요 기능
DefaultScheduler – UniRx의 기본 스케줄러
UniRx의 기본 시간 기반 연산자(Interval, Timer, Buffer(timeSpan), 등)는 Scheduler.MainThread 를 사용한다
이는 대부분의 연산자가 싱글 스레드에서 실행되도록 보장하며, 쓰레드 세이프티(thread safety) 문제를 신경 쓰지 않아도 됨을 의미한다
이는 .NET Rx의 기본 동작과는 다르지만, Unity 환경에 최적화된 방식이다
Observable Lifecycle Management – 옵저버블의 생명주기 관리
OnCompleted 호출 타이밍
ObservableTriggers는 해당 GameObject가 Destroy될 때 OnCompleted를 호출한다.
하지만 Observable.Timer, Observable.EveryUpdate 같은 정적(static) 생성 메서드로 생성된 옵저버블은 자동으로 종료되지 않으므로 직접 관리해야 한다
또한 TakeWhile, TakeUntil, TakeUntilDestroy, TakeUntilDisable*을 사용하여 종료 시점을 제어할 수 있다
Observable.IntervalFrame(30)
.TakeUntilDisable(this) // GameObject가 비활성화될 때 자동으로 종료
.Subscribe(x => Debug.Log(x), () => Debug.Log("완료됨!"));
IDisposable을 이용한 구독 관리
여러 개의 IDisposable을 관리할 때 CompositeDisposable을 사용할 수 있다.
AddTo(GameObject/Component)를 활용하면 해당 객체가 Destroy될 때 자동으로 Dispose된다.
// 여러 구독을 관리하는 CompositeDisposable
CompositeDisposable disposables = new CompositeDisposable();
void Start()
{
Observable.EveryUpdate().Subscribe(x => Debug.Log(x)).AddTo(disposables);
}
// 특정 이벤트 발생 시 모든 구독 해제
void OnTriggerEnter(Collider other)
{
disposables.Clear(); // 내부의 모든 IDisposable을 Dispose하고 리스트를 비움
}
// AddTo(GameObject/Component) 사용시
void Start()
{
Observable.IntervalFrame(30).Subscribe(x => Debug.Log(x)).AddTo(this);
}
// 해당 GameObject가 Destroy되면 자동으로 Dispose됨.
프레임 기반 시간 연산자 제공
UniRx는 프레임 단위로 동작하는 시간 연산자를 제공한다
이러한 연산자들을 사용하여 적절한 타이밍에 원하는 로직을 실행시킬 수 있다
UniRx와 Model-View-Reactive Presenter (MVRP) 패턴
UniRx는 Unity에서 Model-View-Reactive Presenter (MVRP) 패턴을 쉽게 구현할 수 있도록 도와준다
Unity는 MVVM에서 사용하는 데이터 바인딩(Data Binding) 기능을 기본적으로 제공하지 않는다. MVVM 스타일의 바인딩 레이어를 직접 구현하려면 복잡도가 증가하고 성능 손실이 발생할 가능성이 높기 때문이다
그러나 Presenter가 View의 UI 요소를 직접 업데이트하면, 바인딩 없이도 Rx를 활용하여 자동으로 UI를 갱신할 수 있다
MVRP 패턴 예제
Enemy HP UI를 업데이트하는 예제 코드이다
// ReactiveProperty를 활용하여 HP 변화를 감지하는 모델
using UniRx;
using UnityEngine;
public class Enemy
{
// HP 값이 변경될 때 자동으로 감지됨
public ReactiveProperty<int> CurrentHp { get; private set; }
public ReactiveProperty<bool> IsDead { get; private set; }
public Enemy(int initialHp)
{
CurrentHp = new ReactiveProperty<int>(initialHp);
IsDead = CurrentHp.Select(hp => hp <= 0).ToReactiveProperty();
}
// 데미지를 받는 메서드
public void TakeDamage(int damage)
{
CurrentHp.Value -= damage;
}
}
// Presenter와 연결되어 UI를 표시하는 View
using UnityEngine;
using UnityEngine.UI;
public class EnemyView : MonoBehaviour
{
public Text HpText;
public Button AttackButton;
}
// Model과 View를 연결하여 반응형으로 UI 업데이트하는 Presenter
using UniRx;
using UnityEngine;
using UnityEngine.UI;
public class EnemyPresenter : MonoBehaviour
{
public EnemyView View; // 인스펙터에서 할당
private Enemy enemy;
void Start()
{
// 초기 HP 100 설정
enemy = new Enemy(100);
// Model -> View: HP 변경 시 자동으로 UI 업데이트
enemy.CurrentHp.SubscribeToText(View.HpText);
// View -> Model: 버튼 클릭 시 에너미에게 데미지 입히기
View.AttackButton.OnClickAsObservable()
.Subscribe(_ => enemy.TakeDamage(10));
// Model -> View: HP가 0 이하가 되면 버튼 비활성화
enemy.IsDead.Subscribe(isDead => View.AttackButton.interactable = !isDead);
}
}
예제 코드의 실행 흐름은 다음과 같다
- 게임 시작 시 EnemyPresenter가 Enemy 모델을 생성 (초기 HP: 100)
- HP 값이 UI(HpText)에 자동으로 반영됨
- 버튼을 클릭하면 TakeDamage(10)이 호출되어 HP가 10 감소
- HP가 변경되면 SubscribeToText(View.HpText);를 통해 자동으로 UI 업데이트
- HP가 0이 되면 IsDead가 true가 되어 버튼이 비활성화됨
참고로 모델 생성을 프레젠터가 맡는건 예제코드 때문이다. 초기화는 프로젝트 혹은 팀의 코딩 스타일 방향성대로 진행하면 된다
마무리
UniRx는 Unity 환경에서 리액티브 프로그래밍을 쉽게 적용할 수 있도록 설계된 라이브러리이며, 데이터 스트림을 활용한 선언적 프로그래밍을 가능하게 해준다
최근에는 Cysharp/R3가 UniRx의 대체 라이브러리로 배포되고 있으므로, 새로운 프로젝트에서는 이를 고려해보자
Reference
UniRx - Reactive Extensions for Unity - Github(Read-Only)