4 분 소요

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.CancellationTokenAddTo(destroyCancellationToken).
    [AutoDispose] 불필요 (Unity MonoBehaviour + destroyCancellationToken 패턴).

  • AddTo.MonoBehaviourAddTo(this). [AutoDispose] 불필요.

효과 : “무엇을 구독할지”와 “무엇이 구독을 취소할지”를 한 어트리뷰트에 묶을 수 있습니다.


ReactiveProperty — 공개 읽기 필드 노출 작성 요구 X

[ReactiveProperty]ReactiveProperty<T> 필드에 두면 기본값 ReadOnly = trueReadOnlyReactiveProperty<T> 접근자가 생성됩니다. ReadOnly = falseReactiveProperty<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 : _disposableR3OnDestroy()가 생성되고, OnDestroy에서 R3OnDestroy()를 호출해야 합니다.

  • MonoBehaviour가 아닌 클래스 : IDisposableDispose() 등 정리 코드가 생성됩니다.

효과 : 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, OnDamagedObservable<int>, RespawnRequestedObservable<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] MonoBehaviourpartial 필수
R3Gen002 [ReactiveCommand] 필드 이름은 _camelCase 규칙
R3Gen003 [AutoSubscribe] 대상 필드 없음
R3Gen004 핸들러 매개변수 타입이 스트림과 불일치
R3Gen005 AddTo.Disposable[AutoDispose] 필요
R3Gen006 [ReactiveCommand] 필드는 ReactiveCommand<T> 여야 함
R3Gen007 필요 시 Awake() 존재
R3Gen008 Awake() 안에서 R3Awake() 호출
R3Gen009 [AutoDispose] MonoBehaviourOnDestroy() 필요
R3Gen010 OnDestroy() 안에서 R3OnDestroy() 호출

효과 : 구독 실수가 런타임 누수보다 빌드/생성 단계의 안정적인 오류로 떨어지기 쉽습니다.


요구 사항

  • Unity 2021.3+ (Roslyn Source Generator를 공식 지원하는 버전의 이후 모든 버전)

  • 프로젝트에 R3 런타임 패키지 — R3S는 R3 타입을 대체하지 않고, 그 타입을 쓰는 코드를 생성합니다.

  • 패키지에 포함된 Roslyn 소스 제너레이터 (Editor/R3Generator.dll 등) — 패키지 임포트만으로 동작 가정.


태그: C#, OpenSource, R3, Unity

카테고리:

업데이트: