Unityのコンポーネントベースデザインを意識したカスタムボタンの実装サンプル

# はじめに
今回は、私がUnityのuGUIについて学んだ際に学んだことをサンプルコードとともに共有します。
UnityのuGUIを使用してUIを作成する際、標準のボタンを使用するのは簡単ですが、カスタマイズ性や管理性に課題が生じることがあります。
この記事では、*コンポーネントベースデザイン*を採用し、拡張性の高いカスタムボタンの実装方法を紹介します。

# 背景

Unityの標準ボタンは、以下の点で不便なことがあります。

* 機能不足: 
標準ボタンはシンプルな設計であるがゆえに、複雑なタッチ処理やアニメーションが実装しづらい。
* 管理の難しさ: 
イベントをインスペクターで設定できる点は便利ですが、特に大規模プロジェクトにおいて、全体の把握が困難になりがちです。
また、プロジェクト全体で共通の振る舞いや調整を行いたい場合、標準のボタンでは限界があるため独自の処理でボタンを作成した方が、全体的な調整がしやすくなる場合が多いです。

これらの課題を解決するために、カスタムボタンを実装しました。
さらに、Unityの構造を深く理解するため、そして個人的にコンポーネントベースデザインやコンポジション的なアプローチに魅力を感じていることから、このデザインを取り入れたカスタムボタンを作成しました。


# カスタムボタンの設計方針

今回の実装では、以下の2つのコンポーネントを組み合わせています。

1. カスタムボタン本体 (`CustomButton`クラス)
2. ボタンの表示と挙動 (`CommonButtonViewScale`クラス)

## CustomButtonクラス
`CustomButton`クラスは、ボタンのクリック、押下、リリースの各イベントをObservableとして提供し、またボタンのアクティブ状態を管理します。これにより、ボタンの状態を簡単に監視し、反応することができます。

### UniRxを使ったタッチイベント処理に関する注意点
実装に際して、`EventTrigger`を使ったタッチイベント処理の方法も検討しました。`EventTrigger`を利用すれば、複数のイベント（クリック、押下、リリースなど）を簡単に扱うことができるため、初期の段階ではこちらを使用することも考えました。

しかし、実際に試したところ、スクロールビューに配置した際にタッチイベントが通過しないといった問題が発生しました。
このため、最終的には`IPointerClickHandler`や`IPointerDownHandler`、`IPointerUpHandler`インターフェースを直接実装する形に落ち着きました。

これにより、スクロールビューなどの複雑なUIレイアウトでも期待通りに動作させることができました。
同様の問題に直面した場合は、EventTriggerの利用を避け、各イベントをハンドラーで直接実装する方法を検討してみてください。


```C#
using System;
using UniRx;
using UnityEngine;
using UnityEngine.EventSystems;

namespace Script.UI
{
    /// <summary>
    /// カスタムボタンクラス。ボタンのクリック、押下、リリースイベントをObservableとして提供します。
    /// また、ボタンはアクティブかどうかをフラグで管理しており、状態の変化もObservableとして提供します。
    /// </summary>
    public class CustomButton : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler
    {
        /// <summary>
        /// ボタンのアクティブ状態を保持するReactiveProperty
        /// </summary>
        public IReadOnlyReactiveProperty<bool> IsActive => _isActiveReactiveProperty;

        private readonly ReactiveProperty<bool> _isActiveReactiveProperty = new(true);

        /// <summary>
        /// ボタンがクリックされたときのイベントをObservableとして公開
        /// </summary>
        public IObservable<PointerEventData> OnClickAsObservable => _onClickSubject.AsObservable();

        /// <summary>
        /// ボタンが押されたときのイベントをObservableとして公開
        /// </summary>
        public IObservable<PointerEventData> OnDownAsObservable => _onPointerDownSubject.AsObservable();

        /// <summary>
        /// ボタンが放されたときのイベントをObservableとして公開
        /// </summary>
        public IObservable<PointerEventData> OnUpAsObservable => _onPointerUpSubject.AsObservable();

        private readonly Subject<PointerEventData> _onClickSubject = new Subject<PointerEventData>();
        private readonly Subject<PointerEventData> _onPointerDownSubject = new Subject<PointerEventData>();
        private readonly Subject<PointerEventData> _onPointerUpSubject = new Subject<PointerEventData>();

        /// <summary>
        /// ボタンがクリックされたときに呼び出されるメソッド
        /// </summary>
        /// <param name="eventData">クリックイベントのデータ</param>
        public void OnPointerClick(PointerEventData eventData)
        {
            if (IsActive.Value)
            {
                _onClickSubject.OnNext(eventData);
            }
        }

        /// <summary>
        /// ボタンが押されたときに呼び出されるメソッド
        /// </summary>
        /// <param name="eventData">押下イベントのデータ</param>
        public void OnPointerDown(PointerEventData eventData)
        {
            if (IsActive.Value)
            {
                _onPointerDownSubject.OnNext(eventData);
            }
        }

        /// <summary>
        /// ボタンが放されたときに呼び出されるメソッド
        /// </summary>
        /// <param name="eventData">放したイベントのデータ</param>
        public void OnPointerUp(PointerEventData eventData)
        {
            if (IsActive.Value)
            {
                _onPointerUpSubject.OnNext(eventData);
            }
        }

        private void OnDestroy()
        {
            _onClickSubject.OnCompleted();
            _onPointerDownSubject.OnCompleted();
            _onPointerUpSubject.OnCompleted();
            
            _isActiveReactiveProperty.Dispose();
        }

        /// <summary>
        /// ボタンのアクティブ状態を変更するメソッド
        /// </summary>
        /// <param name="isActive">新しいアクティブ状態</param>
        public void SetActive(bool isActive)
        {
            _isActiveReactiveProperty.Value = isActive;
        }
    }
}
```

## CommonButtonViewScaleクラス
CommonButtonViewScaleクラスは、CustomButtonの状態に応じてボタンの表示やアニメーションを管理します。例えば、ボタンが押された際にスケールを縮小したり、非アクティブ時に画像のアルファ値を変更する処理を実装しています。

### CommonButtonViewScaleの使い方についての注意点
CommonButtonViewScaleクラスは、その場しのぎの作りになっています。本格的なゲームを作成する際は、ボタンの表示方法は多岐にわたるため、各プロジェクトごとにカスタマイズが必要になります。このクラスは一時的な実装や動作確認に使うことを想定しているため、必要に応じて適宜カスタマイズを行ってください。


CommonButtonViewScaleクラスのコード例
```C#
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace Script.UI
{
    [RequireComponent(typeof(CustomButton))]
    public class CommonButtonViewScale : MonoBehaviour
    {
        /// <summary>
        /// スケールを変更する対象のゲームオブジェクト。
        /// </summary>
        [SerializeField] private GameObject scaleTarget;
        
        /// <summary>
        /// ボタンが通常状態のときのスケール係数
        /// </summary>
        [SerializeField] private float defaultScaleFactor = 1.0f;
        
        /// <summary>
        /// ボタンが押下状態のときのスケール係数
        /// </summary>
        [SerializeField] private float pressedScaleFactor = 0.8f;
        
        /// <summary>
        /// アクティブ状態に応じてアルファ値を変更する対象の画像。
        /// </summary>
        [SerializeField] private Image activeTarget;
        
        /// <summary>
        /// ボタンがアクティブ状態のときの画像のアルファ値
        /// </summary>
        [SerializeField] private float activeImageAlpha = 1.0f;
        
        /// <summary>
        /// ボタンが非アクティブ状態のときの画像のアルファ値
        /// </summary>
        [SerializeField] private float inactiveImageAlpha = 0.5f;

        private void Start()
        {
            if (scaleTarget == null || activeTarget == null)
            {
                Debug.LogError("scaleTarget または activeTarget が設定されていません。");
                return;
            }

            var button = GetComponent<CustomButton>();
            button.OnDownAsObservable
                .Subscribe(_ => scaleTarget.transform.localScale = Vector3.one * pressedScaleFactor)
                .AddTo(this.gameObject);

            button.OnUpAsObservable
                .Subscribe(_ => scaleTarget.transform.localScale = Vector3.one * defaultScaleFactor)
                .AddTo(this.gameObject);
            
            button.IsActive
                .Subscribe(SetButtonActive)
                .AddTo(this.gameObject);
        }

        /// <summary>
        /// ボタンのアクティブ状態に応じて対象画像のアルファ値を設定します。
        /// </summary>
        /// <param name="isActive">ボタンがアクティブかどうかを示すフラグ。</param>
        private void SetButtonActive(bool isActive)
        {
            activeTarget.color = new Color(1, 1, 1, isActive ? activeImageAlpha : inactiveImageAlpha);
        }
    }
}
```

# 使用例: クリックイベントの設定
カスタムボタンに対してクリックイベントを追加する方法の一例を以下に示します。この例では、ボタンがクリックされた際に、シーンを遷移する処理を呼び出していますが、実際にはクリック後に任意の処理を実装することができます。

```C#
// 取得したボタン押下時に次のシーン遷移を設定
_nextButton.OnClickAsObservable
    .Subscribe(_ => { LoadScene(); })  // LoadSceneはクリック後に呼び出される処理
    .AddTo(this.gameObject);
```
`LoadScene`メソッドは、クリック後に実行される処理の例です。ここをあなたのプロジェクトに応じた独自の処理に置き換えて利用してください。

# 実装手順
シーン内に空のGameObjectを作成し、CustomButtonとCommonButtonViewScaleをアタッチします。
必要なスケール対象や画像をインスペクターで設定します。
スクリプトを実行し、ボタンの押下やアクティブ状態に応じたスケールやアルファ値の変化を確認します。

# まとめ
このように、Reactive Extensions (UniRx) とコンポーネントベースの設計を用いることで、カスタマイズ性が高く、管理が容易なカスタムボタンを実装できます。また、EventTriggerを使ったタッチイベント処理には注意が必要で、特にスクロールビューなどのUIコンポーネントと併用する場合は、直接インターフェースを実装する方法をおすすめします。

Unityの標準ボタンに限界を感じている方は、ぜひこのアプローチを試してみてください。サンプルコードを活用し、自分のプロジェクトに合わせたボタンを作成してみましょう！

# 免責事項
この記事で紹介したサンプルコードは、私自身の学習と経験をもとに作成したものです。このコードを使用することで生じたトラブルや損害については、一切の責任を負いかねます。自己責任のもとでご利用ください。