【Unity】Vetor3用のInputFieldを作る


## 概要

Vector3 や配列等の複数の要素を持つ型に使用できる InputField が必要だったため、汎用的に使用できそうなものを作成した．カスタム InputField は、入力値を内部で ReactiveProperty<T>に変換して管理し、外部へはその変換後の値を通知する．また、Model との連携を容易にするためのインタフェースを実装しておく．

## 実装の詳細

**インタフェース定義**
ゲッタ/セッタと通知用 Observable を持つインタフェース．

```IDataHolder.cs
namespace nitou {

    /// <summary>
    /// <see cref="T"/>型のデータを保持できるオブジェクト．
    /// </summary>
    public interface IDataHolder<T> {
        public IObservable<T> OnValueChanged { get; }
        // ※必要に応じてバリデーションを差し込む
        public T GetValue();
        public void SetValue(T value);
    }

    public static class DataHolderExtensions {

        public static void BindTo<T>(this IReactiveProperty<T> property, IDataHolder<T> target, ICollection<IDisposable> disposables) {

            // Property → Target
            property.SubscribeWithState(target, (x, t) => t.SetValue(x)).AddTo(disposables);

            // Targer → Property
            target.OnValueChanged.SubscribeWithState(property, (x, p) => p.Value = x).AddTo(disposables);
        }
    }
}

```

**基本の InputField ビュークラス**

```InputFieldView.cs
using System;
using UniRx;
using UnityEngine;

namespace nitou.UI{
    public abstract class InputFieldView<T> : MonoBehaviour, IDataHolder<T>{

        protected readonly ReactiveProperty<T> _valueRP = new();
        public IObservable<T> OnValueChanged => _valueRP;

        // LifeCycle Events
        protected virtual void OnDestroy() {
            _valueRP.Dispose();
        }

        // Public Method
        public virtual T GetValue() => _valueRP.Value;
        public virtual void SetValue(T newValue) => _valueRP.Value = newValue;
        public virtual void ResetValue() => _valueRP.Value = default(T);
    }
}

```

**Vector3 用のカスタム InputField**

※入力文字列のバリデーションは別途 InputField インスペクタで設定する想定．

```Vector3InputFieldView.cs
using System;
using System.Globalization;
using UniRx;
using UnityEngine;
using TMPro;

namespace nitou.UI {

    public sealed class Vector3InputFieldView : InputFieldView<Vector3> {

        [Header("View")]
        [SerializeField] private TMP_InputField _xInputField;
        [SerializeField] private TMP_InputField _yInputField;
        [SerializeField] private TMP_InputField _zInputField;

        [Header("Settings")]
        [Range(0, 6)]
        [SerializeField] private int _decimalPlaces = 2;


        /// ----------------------------------------------------------------------------
        // LifeCycle Events

        private void Awake() {
            // ViewModelの監視
            _valueRP.Subscribe(v => ApplyValue(v)).AddTo(this);

            // Viewの監視
            Observable.Merge(
                _xInputField.onEndEdit.AsObservable().AsUnitObservable(),
                _yInputField.onEndEdit.AsObservable().AsUnitObservable(),
                _zInputField.onEndEdit.AsObservable().AsUnitObservable()
            )
            .Subscribe(_ => UpdateValue())
            .AddTo(this);
        }


        /// ----------------------------------------------------------------------------
        // Private Method

        private void UpdateValue() {
            Vector3 currentValue = _valueRP.Value;
            Vector3? parsedValue = TryParseValue();

            // パースが成功した場合のみReactivePropertyに反映
            if (parsedValue.HasValue) {
                _valueRP.Value = parsedValue.Value;
            } else {
                // ※パースに失敗した場合は元の値を再適用
                ApplyValue(currentValue);
            }
        }

        // Viewから値を読み取る．
        private Vector3? TryParseValue() {

            // parse values
            bool xParsed = float.TryParse(_xInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out var x);
            bool yParsed = float.TryParse(_yInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out var y);
            bool zParsed = float.TryParse(_zInputField.text, NumberStyles.Float, CultureInfo.InvariantCulture, out var z);

            // 全てのパースが成功した場合にのみ値を返す
            if (xParsed && yParsed && zParsed) {
                return new Vector3(x, y, z);
            }

            // いずれかのパースに失敗した場合はnullを返す
            return null;
        }

        // Viewに値を適用する．
        private void ApplyValue(Vector3 value) {
            _xInputField.text = value.x.ToFloatText(_decimalPlaces);
            _yInputField.text = value.y.ToFloatText(_decimalPlaces);
            _zInputField.text = value.z.ToFloatText(_decimalPlaces);
        }
    }
}

```