개발툴/Unity
UniRxWorkBook - Operator
가든_
2023. 12. 24. 22:54
1. Subscribe

using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class Lesson_1_Subscribe : MonoBehaviour
{
// _____() 부분을 올바른 형식으로 대체하여 큐브가 회전하도록 만들어보세요.
private void Start()
{
this.UpdateAsObservable().Subscribe(_=>RotateCube());
}
private void RotateCube()
{
this.transform.rotation = Quaternion.AngleAxis(1.0f, Vector3.up)*this.transform.rotation;
}
}
- **UniRx.Trigger**를 추가하면 **this.UpdateAsObservable()**를 통해 Unity의 Update() 메서드를 옵저버블로 다루어서 Update 메서드를 이벤트 스트림으로 다룰 수 있게 한다.
- **Subscribe**는 옵저버블을 구독하는 메서드로, 스트림에서 발생하는 각 이벤트에 대해 실행할 로직을 정의한다.
- 각 프레임마다 실행되는 Update 이벤트를 활용해서 Cube를 회전시키는 로직을 Subscribe 내부에 추가하면 큐브가 회전하게 된다.
2. Where


using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class Lesson_2_Where : MonoBehaviour
{
private void Start()
{
// _____() 부분을 올바른 형식으로 대체하여, 마우스의 왼쪽 클릭을 하는 동안에만 Cube가 회전하도록 만들어보세요.
this.UpdateAsObservable()
.Where(_ => Input.GetMouseButton(0))
.Subscribe(_ => RotateCube());
}
private void RotateCube()
{
this.transform.rotation = Quaternion.AngleAxis(1.0f, Vector3.up) * this.transform.rotation;
}
}
- **Where** 오퍼레이터를 추가해서, 특정 조건을 만족할 때만 스트림을 통과시킨다.
- 마우스의 왼쪽 클릭을 조건으로 설정해서, 클릭이 발생한 동안에만 스트림을 통과시켜 Cube가 회전하게된다.
3. SkipUntil


using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class Lesson_3_SkipUntil : MonoBehaviour
{
private void Start()
{
//마우스 클릭 스트림
var clickStream = this.UpdateAsObservable().Where(_ => Input.GetMouseButtonDown(0));
//_____() 부분을 올바른 형식으로 대체하여, 마우스 클릭이 한 번이라도 발생하면 회전이 시작되도록 만들어보세요.
this.UpdateAsObservable()
.SkipUntil(clickStream)
.Subscribe(_ => RotateCube());
}
private void RotateCube()
{
this.transform.rotation = Quaternion.AngleAxis(1.0f, Vector3.up) * this.transform.rotation;
}
}
this.UpdateAsObservable()
.SkipUntil(this.UpdateAsObservable().Where(_=>Input.GetMouseButtonDown(0)))
.Subscribe(_ => RotateCube());
- SkipUntil 오퍼레이터는 셔터(쉽게 말해 문)처럼 동작한다.
- SkipUntil은 인자로 전달된 스트림(clickStream)이 값을 보내기 전까지 셔터가 닫혀 있다가 **clickStream** 에 값이 도달하면 **SkipUntil**셔터를 열고 원래 스트림으로부터의 메세지를 받아서 후속 스트림으로 전달한다.
- **SkipUntil**이 닫혀 있는 동안에 도착한 모든 메시지는 폐기된다. 따라서 마우스 클릭이 처음으로 발생하면 회전이 시작되며, 그 이전에 있었던 이벤트는 무시된다.
4. Skip

using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class Lesson_4_Skip : MonoBehaviour
{
private void Start()
{
// _____() 부분을 올바른 형식으로 대체하여, 마우스가 3번 클릭되면 Cube가 회전하도록 만들어보세요.
var clickStream = this.UpdateAsObservable()
.Where(_ => Input.GetMouseButtonDown(0));
this.UpdateAsObservable()
.SkipUntil(clickStream.Skip(2))
.Subscribe(_ => RotateCube());
}
private void RotateCube()
{
this.transform.rotation = Quaternion.AngleAxis(1.0f, Vector3.up) * this.transform.rotation;
}
}
- Skip(n) 오퍼레이터는 n번의 메시지가 도착할 때까지 메시지를 차단한다.
- **Skip(2)**를 사용해서 처음 2번의 클릭 이벤트를 무시하고 3번째 이벤트부터 메시지를 전달하게할 수 있다. 이렇게 하면 처음 2번의 클릭은 무시되고 3번째 클릭부터 회전이 시작된다.
5. Buffer


using UnityEngine;
using UniRx;
using UnityEngine.UI;
public class Lesson_5_Buffer : MonoBehaviour
{
[SerializeField] private Text resultLabel;
[SerializeField] private Button buttonA;
[SerializeField] private Button buttonB;
[SerializeField] private Button buttonC;
private void Start()
{
var aStream = buttonA.OnClickAsObservable().Select(_ => "A");
var bStream = buttonB.OnClickAsObservable().Select(_ => "B");
var cStream = buttonC.OnClickAsObservable().Select(_ => "C");
// _____()를 수정하여
// 지난 3번의 눌린 버튼 이력을 표시해 보세요
// (각각 3번 눌릴 때마다 업데이트되는 형태로 구현해도 좋습니다)
Observable.Merge(aStream, bStream, cStream)
.Buffer(3)
.SubscribeToText(resultLabel, x => string.Join(", ",x));
// IEnmerable<String>를 하나의 String으로 합치려면
// strings.Aggregate((p, c) => p + c)와 Aggregate를 사용하면 간단하게 작성할 수 있습니다.
}
}
Observable.Merge(aStream, bStream, cStream)
.Buffer(3)
.SubscribeToText(resultLabel, x => x.Aggregate((p, c) => p + c));
- **Buffer(n)**를 사용하면 지난 n개의 메시지를 버퍼링할 수 있다.
- 몇 개가 쌓일 때 방출할지는 두 번째 매개변수로 지정할 수 있다 (기본값은 첫 번째 매개변수와 동일)

6. First


using UnityEngine;
using UniRx;
using UnityEngine.UI;
public class Lesson_6_First : MonoBehaviour
{
[SerializeField]
private Text resultLabel;
[SerializeField]
private Button buttonA;
[SerializeField]
private Button buttonB;
[SerializeField]
private Button buttonC;
private void Start()
{
var aStream = buttonA.OnClickAsObservable().Select(_ => "A");
var bStream = buttonB.OnClickAsObservable().Select(_ => "B");
var cStream = buttonC.OnClickAsObservable().Select(_ => "C");
// _____를 수정하여, 처음 1회 눌릴 때만 Text가 변경되도록 만들어보세요.
Observable.Merge(aStream, bStream, cStream)
.First()
.SubscribeToText(resultLabel);
}
}
- First, **FirstOrDefault**를 사용하면 가장 처음의 메시지만 통과시킬 수 있다.
- **First**는 가장 처음의 메시지를 통과시킨 후, **OnCompleted**를 발행한다.
- **First**와 **FirstOrDefault**에서 메시지가 한 번도 발행되지 않고 Dispose된 경우
- First : [OnError] 발행
- FirstOrDefault : [OnNext + OnCompleted] 발행
7. Zip


public class Lesson_7_Zip : MonoBehaviour
{
[SerializeField]
private Text resultLabel;
[SerializeField]
private Button buttonLeft;
[SerializeField]
private Button buttonRight;
private void Start()
{
var rightStream = buttonRight.OnClickAsObservable();
var leftStream = buttonLeft.OnClickAsObservable();
// _____를 수정하여, Left와 Right 버튼이 각각 최소한 1회씩 눌렸을 때에만 Text가 변경되도록 만들어보세요.
leftStream
.Zip(rightStream, (l, r) => new { l, r })
.First()
.SubscribeToText(resultLabel, _ => "OK");
}
}
Observable.Zip(leftStream, rightStream)
.First()
.SubscribeToText(resultLabel, _ => "OK");
- **Zip**은 여러 오퍼레이터의 값을 하나씩 조합하여 흐르게 하는 오퍼레이터
- **Zip**은 두 스트림에서 각각 한 번씩 메시지가 모일 때마다 메시지를 방출한다.
- **Zip**은 내부적으로 **Buffer**를 사용하고 있어, 내부적으로 버퍼링을 처리한다.
- 만약 **First**를 삭제하고 Left를 연속으로 3번 누른 후에, 그 후에 Right를 연속으로 3번 누르면..?
8. Repeat

public class Lesson_8_Repeat : MonoBehaviour
{
[SerializeField]
private Text resultLabel;
[SerializeField]
private Button buttonLeft;
[SerializeField]
private Button buttonRight;
private void Start()
{
var rightStream = buttonRight.OnClickAsObservable();
var leftStream = buttonLeft.OnClickAsObservable();
// _____를 수정하여, Left와 Right를 번갈아가며 1번씩 눌렀을 때 "OK"가 표시되도록 만들어봅시다.
//
// First()를 제거하는 것만으로는 Left와 Right를 번갈아가며 빠르게 눌렀을 때의 동작이 예측하기 어려우므로,
// 적절한 오퍼레이터를 First 뒤에 추가해봅시다.
leftStream
.Zip(rightStream, (l, r) => Unit.Default)
.First()
.Repeat()
.SubscribeToText(resultLabel, _ => resultLabel.text += "OK\\n");
}
}
- **Repeat**는 OnCompleted가 발생하면 스트림을 다시 Subscribe하는 오퍼레이터
- 흘러온 값들을 재현하여 반복하는 기능은 아님
- 숫자를 인수로 전달하여 지정한 횟수만큼 동작시킬 수도 있다 (기본값은 무한 동작)
- 적절하지 않게 사용하면 무한 Subscribe를 유발하여 프리즈를 일으킬 수 있다. 객체가 파괴되어 OnCompleted가 발생하는 타이밍을 주의해야 한다
- 다양한 Repeat 오퍼레이터
- RepeatSafe 짧은 기간에 Repeat가 여러 번 발생할 때 Dispose
- RepeatUntilDisable GameObject가 Disable되는 타이밍에 Dispose
- RepeatUntilDestory GameObject가 Destroy되는 타이밍에 Dispose
9. CombineLatest

public class Lesson_9_CombineLatest : MonoBehaviour
{
[SerializeField] private InputField leftInput;
[SerializeField] private InputField rightInput;
[SerializeField] private Text resultLabel;
private void Start()
{
var leftStream = leftInput.OnValueChangedAsObservable().Select(x => Int32.Parse(x));
var rightStream = rightInput.OnValueChangedAsObservable().Select(x => Int32.Parse(x));
// 아래의 오퍼레이터 체인은 두 개의 InputField에 입력된 숫자를 합산하여 표시하는 스트림을 생성하고 있습니다
// 그러나 Zip을 사용하면 동작이 이상하게 동작하기 때문에, Zip을 적절한 오퍼레이터로 변경하여 InputField의 변경이 즉시 반영되도록 해보겠습니다
leftStream
.CombineLatest(rightStream, (left, right) => left + right)
.SubscribeToText(resultLabel);
}
}
- **CombineLatest**는 모든 스트림 중 하나라도 값이 갱신되면 직전의 값으로 대체하여 결과를 방출합니다.
- **Zip**은 여러 스트림의 값이 동시에 갱신될 때만 값을 방출하므로 값이 갱신되지 않은 스트림이 있는 경우 업데이트가 지연되는 문제가 있습니다.
10. Throttle


public class Lesson_10_Throttle : MonoBehaviour
{
[SerializeField] private InputField inputField;
[SerializeField] private Text resultText;
private void Start()
{
// _____를 수정하여, 마지막으로 문자가 입력된 후 1초 후에 resultText에 반영되도록 만들어보겠습니다
inputField
.OnValueChangedAsObservable()
.Throttle(TimeSpan.FromSeconds(1))
.SubscribeToText(resultText);
}
}
- **Throttle**은 메시지가 연속적으로 발생할 때 일정 시간 동안 대기하는 오퍼레이터
- 지정한 시간 간격보다 짧은 간격으로 메시지가 대량으로 도착하면 그 메시지들을 모두 무시하고, 지정한 시간 이상이 경과한 후에 받은 메시지 중에서 마지막에 도착한 메시지를 방출한다.
11. ThrottleFirst

public class Lesson_11_ThrottleFirst : MonoBehaviour
{
[SerializeField] private GameObject bulletObject;
private void Start()
{
// 아래 스크립트는 "왼쪽 클릭을 유지하는 동안 총알을 발사하는" 스크립트입니다
// 그러나 현재 상태에서는 매 프레임마다 총알이 생성됩니다
// 그래서 ____을 수정하여 "왼쪽 클릭을 유지하는 동안 100ms마다 한 번씩 총알을 발사하는" 동작으로 변경하겠습니다
this.UpdateAsObservable()
.Where(_ => Input.GetMouseButton(0))
.ThrottleFirst(TimeSpan.FromMilliseconds(100))
.Subscribe(_ =>
{
var b = Instantiate(bulletObject, transform.position, Quaternion.identity) as GameObject;
Destroy(b, 2.0f);
});
}
}
- **ThrottleFirst**는 **Throttle**의 반대로, "첫 번째 메시지가 도착한 후 일정 기간 동안 메시지를 차단하는" 오퍼레이터
- 많은 메시지를 간편하게 간소화할 수 있다
- 프레임 수를 지정할 수 있는 **ThrottleFirstFrame**도 있음
12. TakeUntil


public class Lesson_12_TakeUntil : MonoBehaviour
{
[SerializeField] private Button onButton;
[SerializeField] private Button offButton;
[SerializeField] private GameObject cube;
private void Start()
{
// ____를 수정하여, OFF 버튼을 누르면 회전이 멈추도록 만들어보겠습니다
var onStream = onButton.OnClickAsObservable();
var offStream = offButton.OnClickAsObservable();
this.UpdateAsObservable()
.SkipUntil(onStream)
.TakeUntil(offStream)
.RepeatUntilDestroy(gameObject)
.Subscribe(_ => RotateCube());
}
private void RotateCube()
{
cube.transform.rotation = Quaternion.AngleAxis(1.0f, Vector3.up) * cube.transform.rotation;
}
}
- **TakeUntil**은 주어진 스트림에 메시지가 도달하면 OnCompleted를 발행
- **SkipUntil**과 조합하여 "이벤트 A가 발생한 후 이벤트 B가 발생할 때까지 처리"와 같은 작업을 간편하게 기술할 수 있다.
🚫 주의 TakeUntil은 메세지의 도달 여부와 상관없이 동작한다. → SkipUntil을 거지치 않고도 TakeUntil이 발동할 수 있다.
13. DistinctUntilChanged

this.UpdateAsObservable()
.Select(_ => controller.isGrounded)
.Throttle(TimeSpan.FromMilliseconds(5))
.Subscribe(isGrounded => StatusOutput(isGrounded));
- **DistinctUntilChanged**는 메시지의 값이 변경되었을 때에만 메시지를 통과시키는 연산자
- 어떤 값을 매 프레임 감시하고 변화가 있을 때만 처리
- ObserveEveryValueChanged 오퍼레이터
- **Observable.EveryUpdate().Select().DistinctUntilChanged()**와 동일
- ObserveEveryValueChanged 오퍼레이터