【Unity入門】トグルボタンを作ろう！【UI】

**[グレンジ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/grenge)** 19日目担当のmikuri8です。
グレンジではUIの設計、基盤実装などをメインに担当しています。

今回はチェックボックスやラジオボタン、タブなどの2つの状態の見た目を切り替える機能を持ったトグルボタンについての記事になります。
また、明日は今回作成したトグルボタンを使用したタブグループの作成例を執筆予定なのでお楽しみに！

## トグルボタンについて
UIを作成するにあたり、クリックのコールバックのみを提供するボタンでは実装しにくいボタンも多くあります。
チェックボックスやラジオボタン、タブなどはクリックにより2つの状態を切り替える必要があります。
チェックボックスを例にすると下記のようになります。
- [ ] ←通常状態の見た目
- [x] ←選択状態の見た目

この見た目の切り替え処理を各ボタンの使用箇所で毎回毎回記述するのは面倒だし、バグの温床になります。
しかし、ボタンの機能を拡張し見た目の切り替えができるボタンを作成することでこのような問題は簡単に解決することができます。

## 設計方針
今回はシンプルにUnityのButtonクラスを継承してToggleButtonクラスを作成します。
（Buttonクラスを継承する都合上、InspectorでSerializeFieldを表示するためにToggleButtonEditorクラスの作成が必要ですが、本件ではエディタ拡張については詳しく触れないのでおまじないとさせてください）
見た目の切り替えはGameObjectのアクティブによって行う方針です。そのため、通常状態の見た目のルートと選択状態の見た目のルートの参照をSerializeFieldとして持つことによって操作を可能にします。
また、UniRxを扱うので使用する際はUniRxの導入が必要な点はご注意ください。

## 実装
```c#:ToggleButton.cs
using System;
using UniRx;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace Sample
{
    public class ToggleButton : Button
    {
        public enum State
        {
            Default,
            Selected,
        }
        
        [SerializeField] private GameObject _defaultObject;
        [SerializeField] private GameObject _selectedObject;
        
        private ReactiveProperty<State> _state = new();
        private Subject<State> _onStateChanged = new();

        /// <summary>
        /// ステート変更時発火するイベント
        /// </summary>
        public IObservable<State> OnStateChangedAsObservable()
        {
            return _onStateChanged.AsObservable();
        }
        
        private void Awake()
        {
            Initialize();
        }
        
        private void Initialize()
        {
            // ボタンの内部で状態変化時に行うこと
            _state.Subscribe(
                state =>
                {
                    _defaultObject.SetActive(state == State.Default);
                    _selectedObject.SetActive(state == State.Selected);
                    
                    _onStateChanged.OnNext(state);
                }).AddTo(this);

            _state.Value = State.Default;
        }
        
        public override void OnPointerClick(PointerEventData eventData)
        {
            base.OnPointerClick(eventData);

            SwitchToggleState();
        }
        
        public void SwitchToggleState()
        {
            if (interactable)
            {
                switch (_state.Value)
                {
                    case State.Default:
                        _state.Value = State.Selected;
                        break;
                    case State.Selected:
                        _state.Value = State.Default;
                        break;
                }
            }
        }
    }
}
```

```c#:ToggleButtonEditor.cs
using UnityEditor;
using UnityEditor.UI;

namespace Sample
{
    [CustomEditor(typeof(ToggleButton))]
    public class ToggleButtonEditor : SelectableEditor
    {
        SerializedProperty _defaultObject;
        SerializedProperty _selectedObject;

        protected override void OnEnable()
        {
            base.OnEnable();
            _defaultObject = serializedObject.FindProperty("_defaultObject");
            _selectedObject = serializedObject.FindProperty("_selectedObject");
        }

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();
            EditorGUILayout.Space();

            serializedObject.Update();
            EditorGUILayout.PropertyField(_defaultObject);
            EditorGUILayout.PropertyField(_selectedObject);
            serializedObject.ApplyModifiedProperties();
        }
    }
}
```

## 解説
Buttonクラスを継承し、OnPointerClickをoverrideしSwitchToggleStateを動かすことでボタンをクリックした際に見た目の切り替えが自動で行われるようにします。
Initialize時に状態が変化したときのToggleButtonクラス内で行う処理を記述します。ここで見た目の変更や状態変化時のコールバックを行っています。
状態が変化したときのIObservableを公開することで各トグルボタン使用箇所での処理を設定することができます。
このようにして**クリック時に状態の変化を行うボタン**として提供することで各トグルボタン使用箇所で見た目の切り替え処理を行うという冗長な記述を避けることができます。

Hierarchyでの構成例は下記のようになります。UnityのButtonを右クリックメニューから作成し、ButtonコンポーネントをToggleButtonに差し替えて内部にDefaultObjectをSelectedObjectを作成した簡単なものです。
![スクリーンショット 2023-12-15 15.51.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20374/c0995243-4cf5-1d31-9453-a526bedc8b75.png)
![スクリーンショット 2023-12-15 15.51.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20374/43909644-75b0-f6d9-6770-864d420d8430.png)

クリックしてみると見た目が切り替わることがわかります。
![toggle.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/20374/83da0d10-5368-5ef0-fd6c-ba2620b4f384.gif)

トグルボタンへの処理割り当て例は下記のようになります。
```c#
using UniRx;
using UnityEngine;

namespace Sample
{
    public class ToggleSample : MonoBehaviour
    {
        [SerializeField]
        private ToggleButton _toggleButton;
        
        void Start()
        {
            _toggleButton.OnClickAsObservable().Subscribe(
                _ =>
                {
                    Debug.Log("Click");
                }).AddTo(this);

            _toggleButton.OnStateChangedAsObservable().Subscribe(
                state =>
                {
                    Debug.Log($"StateChange : {state}");
                }).AddTo(this);
        }
    }
}
```

## まとめ
ボタンの機能拡張はUnityでのUI制作においては必須と言っても良い実装です。今回のようなトグルボタンの実装を行うことも多いと思うので参考にしていただければ幸いです！