概要
UniRxを学習中であり,モデルとUIを連携する処理を書くことが度々あるため、備忘録としてまとめていきます.
使用環境
- Unity UI: 2.0.0
- UniRx: 7.1.0
対象UI
Text
単方向 (Model → View)
.cs
// [MEMO] SubscribeWithStateはクロージャが生成されない分、Subscribeより効率が良いらしい
public static IDisposable SubscribeToText(this IObservable<string> source, TextMeshProUGUI text)
{
return source.SubscribeWithState(text, (x, t) => t.text = x);
}
public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextMeshProUGUI text)
{
return source.SubscribeWithState(text, (x, t) => t.text = x.ToString());
}
public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextMeshProUGUI text, Func<T, string> selector)
{
return source.SubscribeWithState2(text, selector, (x, t, s) => t.text = s(x));
}
Slider
双方向 (Model → Veiw / View → Model)
.cs
public static void BindToSlider(this IReactiveProperty<float> property, Slider slider, ICollection<IDisposable> disposables)
{
// Model → View
property.SubscribeWithState(slider, (x, s) => s.value = x).AddTo(disposables);
// View → Model
slider.OnValueChangedAsObservable()
.SubscribeWithState(property, (x, p) => p.Value = x).AddTo(disposables);
}
Toggle
双方向 (Model → Veiw / View → Model)
.cs
public static void BindToToggle(this IReactiveProperty<bool> property, Toggle toggle, ICollection<IDisposable> disposables)
{
// Model → View
property.SubscribeWithState(toggle, (x, t) => t.isOn = x).AddTo(disposables);
// View → Model
toggle.OnValueChangedAsObservable()
.SubscribeWithState(property, (x, p) => p.Value = x).AddTo(disposables);
}
InputField
単方向 (View → Model)
.cs
/// <summary> Observe onEndEdit (Submit) event.</summary>
public static IObservable<string> OnEndEditAsObservable(this TMP_InputField inputField)
{
return inputField.onEndEdit.AsObservable();
}
/// <summary> Observe onValueChanged with current `text` value on subscribe. </summary>
public static IObservable<string> OnValueChangedAsObservable(this TMP_InputField inputField)
{
return Observable.CreateWithState<string, TMP_InputField>(inputField, (i, observer) => {
observer.OnNext(i.text);
return i.onValueChanged.AsObservable().Subscribe(observer);
});
}
双方向 (Model → Veiw / View → Model)
.cs
// String型
public static void BindToInputField(this IReactiveProperty<string> property, TMP_InputField inputField, ICollection<IDisposable> disposables)
{
// Model → View
property.SubscribeWithState(inputField, (x, i) => i.text = x).AddTo(disposables);
// View → Model
inputField.OnEndEditAsObservable()
.SubscribeWithState(property, (x, p) => p.Value = x).AddTo(disposables);
}
// Generic型
public static void BindToInputField<T>(this IReactiveProperty<T> property, TMP_InputField inputField, ICollection<IDisposable> disposables,
Func<string, T> parseFunc, Func<T, string> formatFunc)
{
// Model → View
property.SubscribeWithState2(inputField, formatFunc, (x, i, f) => i.text = f(x)).AddTo(disposables);
// View → Model
inputField.OnEndEditAsObservable()
.Subscribe(value => {
try
{
property.Value = parseFunc(value);
}
catch
{
// 変換失敗時に入力フィールドをリセット
inputField.text = formatFunc(property.Value);
}
})
.AddTo(disposables);
}
// Int型
public static void BindToInputField(this IReactiveProperty<int> property, TMP_InputField inputField, ICollection<IDisposable> disposables)
{
property.BindToInputField(inputField, disposables,
value => int.Parse(value),
value => value.ToString());
}
// Float型
public static void BindToInputFieldFloat(this IReactiveProperty<float> property, TMP_InputField inputField, ICollection<IDisposable> disposables)
{
property.BindToInputField(inputField, disposables,
value => float.Parse(value),
value => value.ToString("F2"));
}
Dropdown
単方向 (View → Model)
.cs
public static IObservable<int> OnValueChangedAsObservable(this TMP_Dropdown dropdown)
{
return Observable.CreateWithState<int, TMP_Dropdown>(dropdown, (d, observer) => {
observer.OnNext(d.value);
return d.onValueChanged.AsObservable().Subscribe(observer);
});
}
双方向 (Model → Veiw / View → Model)
.cs
public static void BindToDropdown(this IReactiveProperty<int> property, TMP_Dropdown dropdown, ICollection<IDisposable> disposables)
{
// Model → View
property.SubscribeWithState(dropdown, (x, d) => d.value = x).AddTo(disposables);
// View → Model
dropdown.OnValueChangedAsObservable()
.SubscribeWithState(property, (x, p) => p.Value = x).AddTo(disposables);
}
※オプションを列挙型を対応させるコンポーネント
DropdownEnumOptions.cs
[DisallowMultipleComponent]
[RequireComponent(typeof(TMP_Dropdown))]
public abstract class DropdownEnumOptions<TEnum> : MonoBehaviour where TEnum : Enum {
private TMP_Dropdown _dropdown;
private readonly ReactiveProperty<TEnum> _currentRP = new();
/// <summary>
/// 値の変化を通知するObservable
/// </summary>
public IObservable<TEnum> OnValueChanged => _currentRP;
private static readonly TEnum[] enumValues = (TEnum[])Enum.GetValues(typeof(TEnum));
private static int GetEnumIndex(TEnum type){
return Array.IndexOf(enumValues, type);
}
// LifeCycle Events
protected void Start(){
Setup();
// Viewの更新
_dropdown.onValueChanged.AsObservable()
.Subscribe(index => {
if (index.IsInRange(enumValues))
_currentRP.Value = enumValues[index];
else
Setup();
})
.AddTo(this);
// RPの更新
_currentRP
.Subscribe(type => {
_dropdown.value = GetEnumIndex(type);
_dropdown.RefreshShownValue();
}).AddTo(this);
}
private void OnDestroy() {
_currentRP?.Dispose();
}
private void OnValidate() {
if (_dropdown == null)
_dropdown = GetComponent<TMP_Dropdown>();
}
// Private Method
/// <summary> ゲッタ </summary>
public TEnum GetValue() {
return _currentRP.Value;
}
/// <summary> セッタ </summary>
public void SetValue(TEnum type) {
_currentRP.Value = type;
}
private void Setup() {
OnValidate();
// Enumの名前リストを取得してDropdownのオプションに設定
_dropdown.options.Clear();
_dropdown.options.AddRange(enumValues.Select(name => new TMP_Dropdown.OptionData(name.ToString())));
// 初期選択を最初の項目に設定
_dropdown.value = GetEnumIndex(_currentRP.Value);
_dropdown.RefreshShownValue();
}
}
// -----
// 使用例
public enum MyType { ModeA, ModeB, ModeC, ModeD,}
public sealed class TestDropdownOptions : DropdownEnumOptions<MyType> {}