R3S
R3S - Support R3 Scripting with Roslyn Source Generator
Current Version : 1.0.0
R3S는 R3에서 반복되던 구독과 정리, 그리고 프로퍼티 노출 같은 배선 코드를 선언적 어트리뷰트로 넘깁니다.
제너레이터가Subscribe+AddTo, 공개 접근자, dispose 연결을 생성하므로,
사용자의 코드는 “값이 바뀔 때 무엇을 할지”에만 집중할 수 있습니다.
이 글에서는 R3가 무엇인지, 리액티브 모델·연산자 등을 설명하지 않습니다.
다만 “R3 쪽 콜백·구독을 붙이는 방식을 덜 번거롭게 만든다”는 점만 다룹니다.
https://github.com/NightWish-0827/R3S.git?path=/com.nightwishlab.r3s
UPM Add Package from git URL
Table of Contents
명확한 개선 포인트
스크립트를 얇게 유지할 수 있는 구조
R3S 없이는 일회성 구독마다 비슷한 패턴이 반복됩니다.
dispose를 들고, Subscribe, AddTo, ReactiveProperty / Subject / ReactiveCommand 노출,
Tier Down 시 Dispose까지 하나 하나 사용자 스크립팅으로 맞춰야 합니다.
✦ R3S는 어트리뷰트로 이러한 부분을 잡습니다.
여전히 콜백 본문(값이 왔을 때 실행할 메서드)은 직접 작성하고, 제너레이터가 구독 한 줄, 필드용 공개 접근자,
그리고 선택에 따라 CompositeDisposable 생명주기를 뽑아 냅니다.
전·후 비교 (콜백 흐름)
이전 :
private void R3Awake()
{
_hp.Subscribe(OnHpChanged).AddTo(_disposable);
}
이후 (대상 필드 + 핸들러만 선언하면 자동으로 생성됨) :
[AutoSubscribe(nameof(_hp))]
private void OnHpChanged(int value)
{
// 여기에 로직만
}
이전 (프로퍼티 작성) :
private readonly ReactiveProperty<int> _hp = new(100);
public ReadOnlyReactiveProperty<int> Hp => _hp;
이후:
[ReactiveProperty]
private ReactiveProperty<int> _hp = new(100);
// 제너레이터: public ReadOnlyReactiveProperty<int> Hp => _hp;
이전 (ReactiveCommand 노출·실행 헬퍼) :
private readonly ReactiveCommand<Unit> _attack = new();
public Observable<Unit> Attack => _attack;
public void ExecuteAttack() => _attack.Execute(Unit.Default);
이후 :
[ReactiveCommand]
private ReactiveCommand<Unit> _attack;
// 제너레이터: 초기화 + public Observable + ExecuteAttack()
요지는 같습니다. R3 타입과 콜백 의도는 그대로, 그 주변 보일러플레이트만 사라집니다.
핵심 기능
AutoSubscribe — 메서드에 붙이면 Subscribe + AddTo 생성
메서드에 [AutoSubscribe(nameof(필드))]를 붙이면, 제너레이터가 해당 ReactiveProperty
또는 Subject 필드에 대해 Subscribe(해당 메서드)를 만들고, 선택한 AddTo 모드에 따라 AddTo(...)를 연결합니다.
효과 : 구독 한 줄을 매번 손으로 쓰지 않아도 되고,
핸들러 매개변수 타입이 스트림 항목과 맞지 않으면 생성 단계에서 잡히는 편이 됩니다.
AddTo 모드 — 같은 콜백, 다른 수명 앵커
-
AddTo.Disposable(기본) —AddTo(_disposable).
클래스에[AutoDispose]가 있어CompositeDisposable가 있어야 합니다. -
AddTo.CancellationToken—AddTo(destroyCancellationToken).
[AutoDispose]불필요 (UnityMonoBehaviour+destroyCancellationToken패턴). -
AddTo.MonoBehaviour—AddTo(this).[AutoDispose]불필요.
효과 : “무엇을 구독할지”와 “무엇이 구독을 취소할지”를 한 어트리뷰트에 묶을 수 있습니다.
ReactiveProperty — 공개 읽기 필드 노출 작성 요구 X
[ReactiveProperty]를 ReactiveProperty<T> 필드에 두면 기본값 ReadOnly = true로 ReadOnlyReactiveProperty<T> 접근자가 생성됩니다. ReadOnly = false면 ReactiveProperty<T>를 그대로 공개합니다.
효과 : 바인딩 가능한 상태를 한 줄로 노출하고,
_필드→PascalCase 프로퍼티규칙을 제너레이터가 유지합니다.
ReactiveCommand — 실행 헬퍼 + Observable 노출
[ReactiveCommand]를 ReactiveCommand<T>에 두면 :
-
T == Unit:Execute필드이름()형태와Observable<Unit>게터. -
T != Unit:Execute필드이름(T value)와Observable<T>게터.
효과 : UI·시스템은 다른 스트림처럼 “커맨드가 호출되었을 때”를 구독하고, 호출부는
Execute…만 쓰면 됩니다.
Subject — 내부 Subject, 밖에는 Observable만
[Subject]를 Subject<T>에 두면 public Observable<T> 이름 => _필드; 형태가 생성됩니다.
효과 : 이벤트 발생은 Subject로만 하고,
밖에는 Observable만 노출하는 R3 스타일 경계를 프로퍼티 한 줄을 안 써도 맞출 수 있습니다.
AutoDispose — CompositeDisposable + 정리 훅
[AutoDispose]를 클래스에 두면 :
-
MonoBehaviour:_disposable와R3OnDestroy()가 생성되고,OnDestroy에서R3OnDestroy()를 호출해야 합니다. -
MonoBehaviour가 아닌 클래스 :IDisposable와Dispose()등 정리 코드가 생성됩니다.
효과 :
AutoSubscribe(..., AddTo.Disposable)가 올바른 dispose 대상을 공유합니다.
예제 스크립트와 런타임 효과
제너레이터가 같은 클래스에 멤버를 끼워 넣으려면 partial이 필요합니다.
using System;
using R3;
using R3.Attributes;
using UnityEngine;
[AutoDispose]
public partial class CombatViewModel : MonoBehaviour
{
[ReactiveProperty]
private ReactiveProperty<int> _hp = new(100);
[ReactiveCommand]
private ReactiveCommand<Unit> _respawnRequested;
[Subject]
private Subject<int> _onDamaged;
private void Awake()
{
R3Awake();
}
private void OnDestroy()
{
R3OnDestroy();
}
[AutoSubscribe(nameof(_hp))]
private void OnHpChanged(int hp)
{
if (hp <= 0)
ExecuteRespawnRequested();
}
[AutoSubscribe(nameof(_onDamaged))]
private void OnDamaged(int amount)
{
Debug.Log($"Damaged: {amount}");
}
}
런타임
-
OnHpChanged는_hp가 값을 내보낼 때마다 호출됩니다._hp.Subscribe(...).AddTo(...)를 직접 쓰지 않습니다. -
OnDamaged는_onDamaged알림에 같은 방식으로 연결됩니다. -
바깥 코드는
Hp,OnDamaged를Observable<int>로,RespawnRequested를Observable<Unit>로 바인딩하고,
UI에서는ExecuteRespawnRequested()만 호출하면 됩니다. -
GameObject가 파괴되면R3OnDestroy()가CompositeDisposable를 정리해,
Disposable모드로 걸린AutoSubscribe구독이 함께 정리됩니다.
어트리뷰트의 각 효과
[AutoSubscribe(string targetField, AddTo addTo = AddTo.Disposable)]
| 작성하는 것 | 생성되는 것 (개념) |
|---|---|
| 메서드 + 어트리뷰트 | targetField.Subscribe(해당메서드).AddTo(...) |
AddTo : Disposable / CancellationToken / MonoBehaviour
[ReactiveProperty(ReadOnly = true)]
ReadOnly |
생성 프로퍼티 |
|---|---|
true (기본) |
public ReadOnlyReactiveProperty<T> 이름 => _이름; |
false |
public ReactiveProperty<T> 이름 => _이름; |
[ReactiveCommand]
| 필드 타입 | 생성 표면 |
|---|---|
ReactiveCommand<Unit> |
Observable<Unit> 게터 + Execute이름() |
ReactiveCommand<T> |
Observable<T> 게터 + Execute이름(T value) |
[Subject]
| 필드 타입 | 생성 표면 |
|---|---|
Subject<T> |
public Observable<T> 이름 => _이름; |
[AutoDispose]
| 호스트 | 생성 요소 |
|---|---|
MonoBehaviour |
CompositeDisposable + R3OnDestroy() |
| 그 외 클래스 | CompositeDisposable + IDisposable + Dispose() |
필수 수명주기
R3S가 Unity 메시지에 자동으로 훅을 걸지는 않습니다. MonoBehaviour에서는 제너레이터가 가정하는 브릿지를 직접 둡니다.
Awake()
------
R3Awake() 호출 (또는 생성 파이프라인상 R3Initialize)
OnDestroy() // [AutoDispose] MonoBehaviour일 때 필요
------------
R3OnDestroy() 호출
효과 :
Awake에서R3Awake()를 안 부르면 구독이 돌지 않습니다(R3Gen008).
OnDestroy에서R3OnDestroy()를 안 부르면CompositeDisposable가 정리되지 않습니다 (R3Gen009, R3Gen010).
제너레이터 규칙과 진단 코드
어트리뷰트 계약에 따라 안전망을 강제합니다.
각 에러 코드 :
| 코드 | 의미 (요약) |
|---|---|
| R3Gen001 | [AutoDispose] MonoBehaviour는 partial 필수 |
| R3Gen002 | [ReactiveCommand] 필드 이름은 _camelCase 규칙 |
| R3Gen003 | [AutoSubscribe] 대상 필드 없음 |
| R3Gen004 | 핸들러 매개변수 타입이 스트림과 불일치 |
| R3Gen005 | AddTo.Disposable은 [AutoDispose] 필요 |
| R3Gen006 | [ReactiveCommand] 필드는 ReactiveCommand<T> 여야 함 |
| R3Gen007 | 필요 시 Awake() 존재 |
| R3Gen008 | Awake() 안에서 R3Awake() 호출 |
| R3Gen009 | [AutoDispose] MonoBehaviour는 OnDestroy() 필요 |
| R3Gen010 | OnDestroy() 안에서 R3OnDestroy() 호출 |
효과 : 구독 실수가 런타임 누수보다 빌드/생성 단계의 안정적인 오류로 떨어지기 쉽습니다.
요구 사항
-
Unity 2021.3+ (Roslyn Source Generator를 공식 지원하는 버전의 이후 모든 버전)
-
프로젝트에 R3 런타임 패키지 — R3S는 R3 타입을 대체하지 않고, 그 타입을 쓰는 코드를 생성합니다.
-
패키지에 포함된 Roslyn 소스 제너레이터 (
Editor/R3Generator.dll등) — 패키지 임포트만으로 동작 가정.