【Unity】汎用的に扱えるボタンの個人的な実装アプローチの紹介

本記事は、[Unityゲーム開発者ギルド Advent Calendar 2023](https://adventar.org/calendars/8582) 2日目の記事です。
Unityゲーム開発者ギルド（以下、UGDG）については、下記リンクをご覧ください。UGDGはいいぞ！

https://scrapbox.io/unity-game-dev-guild-pr/Unity%E3%82%B2%E3%83%BC%E3%83%A0%E9%96%8B%E7%99%BA%E8%80%85%E3%82%AE%E3%83%AB%E3%83%89


# 1. はじめに

皆さんはボタン周りの実装を行う際、どのようなアプローチを取っていますか？

既存の`Button(UI)`を使って`Inspector`上でクリック時の呼び出しを設定したり、
スクリプトからイベント購読したりと方法は多岐にわたると思います。

本記事では、Unityにおいて汎用的に扱えるボタンの個人的な実装アプローチを紹介します。
ゲームの仕様によっては上手く当てはまらない場合や、より良いアプローチがある可能性があるため、記事の内容を**鵜呑みにせず**参考にして頂けると幸いです。

なお、UIパーツに関する用語については以下記事を参考にしています。

https://note.com/wiz_creative/n/n7d8f971daa6a

## 今回作るもの

今回は以下３種類のUIパーツの実装アプローチを紹介します。

- 汎用ボタン
- 汎用ラジオボタン（選択肢のいずれか１つを選択出来る機能）
- 汎用チェックボックス（選択肢を複数選択出来る機能）

１つ目の「汎用ボタン」がメインで、残りの２つはおまけです。

<details><summary>最終的に出来上がるクラス図</summary>

最終的には以下のような構成になります。
一部`public`レベルとすべての`private`レベルの内容は隠している不完全なクラス図です。

:::note info
コードは全て記事後半にて紹介しているのでここはさらっと流して問題ありません。
:::


![CommonViewParts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/84659429-18a5-bc18-5493-e552a245eab7.png)

（これで理解出来てしまったら多分もうこの記事から得られるものはありません...）

</details>


## 環境

- Unity2022.3.9f1
- Rider2023.2.3
- UniRx v7.1.0
    - https://github.com/neuecc/UniRx

# 2. 実装方針

大まかな方針としては、ボタンを扱うクラスを`入力・イベント発行部分`と`見た目・演出部分`に分けて実装します。ボタンも既存のものではなく、`Image`を使用します。

![button.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/72fad9eb-61fa-3dc5-040e-d19435834bf1.png)

`入力・イベント発行部分`には、
- ボタンを押下などのプレイヤーからの入力受け取り
- ボタンに対してどのような操作が行われたかを**イベントとして発行する機能**

`見た目・演出部分`には、
- `入力・イベント発行部分`が発行するイベントを購読して**見た目を切り替えたり、演出を与えたりする機能**（ロジックには影響を与えない）

を持たせます。

なお外部からは、同様にボタンの入力イベントを購読することで、ボタンを押した時のロジック処理を各々実装していく形になります。

## なぜ機能（責務）を分けるのか

どうしてわざわざボタンの役割を２つに分離したのかについて軽く話します。

このアプローチには少なくとも２つのメリットがあります。
それは、

- ボタンの見た目や演出を変えたくなった時に**柔軟に**切り替えられる
- 仮置きのボタンでもボタン使用側は**気にせず**実装を進められる

ところにあります。

### 1つにまとめる欠点

基本的にボタンが押されたときの演出は同じであることが多いですが、一部特殊な場面で異なった演出を行いたいときなど、ボタン押下に対して**複数種類の演出を与えたい**こともあります。

:::note info
演出Aでは、ボタン押下時にボタンUIが**小さく**なる
    演出Bでは、ボタン押下時にボタンUIを**爆発させる**　　などなど...
:::


しかしそのような時、ボタンに関する機能を１つにまとめてしまっていると、
演出の種類ごとにクラスを切り分ける必要ができ、外部から使用するときに**ボタンのクラスを使い分けなければならない**という問題が発生します。

![button_func_only_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/b731161a-fa87-d117-6a79-d3d872eac653.png)

これはつまり、ボタンを使う側（外部側）が**どの演出のボタンであるかを意識する必要がある**ということでもあります。

「ボタンの演出を後から切り替えたい！」となった時にこの実装だと、**使う側のクラスの中身を書き替える必要**があり、あまり好ましい実装とは言えません。

### 機能を分離して扱いやすく

そこで、見た目・演出部分を分離させました。

ボタンを使う側にとっては、`ボタンがクリックされたか`といったイベントを受け取ることが出来れば十分です。つまり、外部にはそのイベント情報のみが分かればよいのです。

![button_func_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/8ba4f907-b8b6-136f-5573-942e311f5379.png)

これにより、外部からは**どの演出のボタンであるか**といった意識をする必要は無くなり、仮置きのボタンUIでも気にせず外部の実装を進めることが出来ます。

また、ボタンを見た目や演出を差し替えるときも演出を管理するクラスを切り替えるだけで済むようになります。

# 3. 汎用ボタンの実装

まずはボタン入力周りのイベントを公開するクラスを作成します。
これが、`2. 実装方針`における`入力・イベント発行部分`にあたる実装です。

また、ライブラリとして`UniRx`を導入しています。

```CustomButton.cs
using System;
using UniRx;
using UniRx.Triggers;
using UnityEngine;

namespace CommonViewParts
{
    /// <summary>
    /// 汎用ボタン
    /// </summary>
    [RequireComponent(typeof(ObservableEventTrigger))]
    public class CustomButton : MonoBehaviour
    {
        private ObservableEventTrigger _observableEventTrigger;

        /// <summary>
        /// ボタンクリック時
        /// </summary>
        public IObservable<Unit> OnButtonClicked => _observableEventTrigger
            .OnPointerClickAsObservable().AsUnitObservable().Where(_ => _isActiveRP.Value);
        
        /// <summary>
        /// ボタンを押した時
        /// </summary>
        public IObservable<Unit> OnButtonPressed => _observableEventTrigger
            .OnPointerDownAsObservable().AsUnitObservable().Where(_ => _isActiveRP.Value);
        
        /// <summary>
        /// ボタンを離した時
        /// </summary>
        public IObservable<Unit> OnButtonReleased => _observableEventTrigger
            .OnPointerUpAsObservable().AsUnitObservable().Where(_ => _isActiveRP.Value);
        
        /// <summary>
        /// ボタンの領域にカーソルが入った時
        /// </summary>
        public IObservable<Unit> OnButtonEntered => _observableEventTrigger
            .OnPointerEnterAsObservable().AsUnitObservable().Where(_ => _isActiveRP.Value);
        
        /// <summary>
        /// ボタンの領域からカーソルが出た時
        /// </summary>
        public IObservable<Unit> OnButtonExited => _observableEventTrigger
            .OnPointerExitAsObservable().AsUnitObservable().Where(_ => _isActiveRP.Value);

        /// <summary>
        /// ボタンのアクティブ状態を保持するReactiveProperty
        /// </summary>
        public IReadOnlyReactiveProperty<bool> IsActiveRP => _isActiveRP;
        
        private readonly ReactiveProperty<bool> _isActiveRP = new(true);

        protected virtual void OnDestroy()
        {
            _isActiveRP.Dispose();
        }

        protected virtual void Awake()
        {
            _observableEventTrigger = GetComponent<ObservableEventTrigger>();
        }

        /// <summary>
        /// ボタンのアクティブ状態を取得する
        /// </summary>
        public bool GetIsActive() => _isActiveRP.Value;

        /// <summary>
        /// アクティブ状態を変更する
        /// </summary>
        public void SetActive(bool isActive)
        {
            _isActiveRP.Value = isActive;
        }
    }
}
```

長く書いてますが簡潔に説明すると、汎用ボタンでは、
- ボタンクリック時
- ボタン押下・解放時
- ボタン領域へ入った・出た時
- ボタンのアクティブ状態が変化した時

以上のタイミングでそれぞれのイベントを発行しています。
非アクティブ時にはイベントを発行しません。

これらを外部から購読することでボタンクリック処理を実装します。

<details><summary>ボタンクリック処理の実装例</summary>

ここでは例として２つのボタンを用意し、
クリックしたらそれぞれの`Debug.Log()`が呼び出される仕組みを作ります。

```HogeWindowView.cs
using CommonViewParts;
using UniRx;
using UnityEngine;

namespace Debugs
{
    /// <summary>
    /// 何かのウィンドウのViewクラスがあったとする
    /// そして２つのボタンを持っていたとする
    /// </summary>
    public class HogeWindowView : MonoBehaviour
    {
        [SerializeField] private CustomButton _startButton;
        [SerializeField] private CustomButton _optionButton;
        
        private void Start()
        {
            _startButton.OnButtonClicked
                .Subscribe(_ => Debug.Log("Start!"))
                .AddTo(this.gameObject);
            
            _optionButton.OnButtonClicked
                .Subscribe(_ => Debug.Log("Option!"))
                .AddTo(this.gameObject);
        }
    }
}
```

`Start()`内にて、それぞれの`CustomButton`がクリックされたときのイベントを購読しています。

</details>

---

また、クリックやタッチの入力検知のために`UniRx.Triggers`の`ObservableEventTrigger`を使用して入力イベントを取得しています。

このクラスは`UnityEngine.EventSystems`の`IPointerClickHandler`などのインターフェースを実装しています。

これらインターフェースに関する詳細は公式ドキュメントをご覧ください。

https://docs.unity3d.com/2018.3/Documentation/ScriptReference/EventSystems.IPointerClickHandler.html

### 見た目・演出部分

続いて、`見た目・演出部分`にあたるクラスを作成します。
演出の与え方が複数あるのであれば、それぞれでクラスを用意します。

今回は以下の仕様に沿ったボタンUIを作ってみます。

- ボタン押下時にボタンUIの`Scale`が `0.9倍` に
- ボタン解放時に`Scale`は元に戻る
- ボタン無効時には不透明度(`alpha`)が `50%` になる

```ScaledCommonButtonView.cs
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace CommonViewParts
{
    /// <summary>
    /// 選択すると拡縮するようなボタンView
    /// </summary>
    [RequireComponent(typeof(CustomButton))]
    public class ScaledCommonButtonView : MonoBehaviour
    {
        private const float DefaultScale = 1f;
        private const float PressedScale = 0.9f;
        
        private const float ActiveImageAlpha = 1f;
        private const float InactiveImageAlpha = 0.5f;
        
        private CustomButton _button;
        [SerializeField] private Image _image;

        private void Start()
        {
            _button = GetComponent<CustomButton>();
            
            _button.OnButtonPressed
                .Subscribe(_ => SetScale(PressedScale))
                .AddTo(this.gameObject);

            _button.OnButtonReleased
                .Subscribe(_ => SetScale(DefaultScale))
                .AddTo(this.gameObject);
            
            _button.IsActiveRP
                .Subscribe(SetButtonActive)
                .AddTo(this.gameObject);
        }

        private void SetScale(float scale)
        {
            _image.rectTransform.localScale = Vector3.one * scale;
        }

        private void SetButtonActive(bool isActive)
        {
            float alpha = isActive ? ActiveImageAlpha : InactiveImageAlpha;
            _image.color = new Color(1, 1, 1, alpha);
        }
    }
}
```

`Start()`に直書きで`CustomButton`のイベントのうち「押下時」「解放時」「アクティブ状態変化時」の３種類を購読し、その際に行う内容を`Subscribe()`の中に記述しています。

また、このクラスを使うにあたって`CustomButton`は必須であるため、`RequireComponent`属性を用いてコンポーネント追加時に`CustomButton`コンポーネントが同時に追加されるようにしています。

今回はこの３種類ですが、カーソルがUIに入った時に演出を加えたいのであれば`_button.OnButtonEntered`や`_button.OnButtonExited`のイベントを購読し、その時の演出を追加実装してあげると良いでしょう。

もし、ここからさらに`Animator`を用いてボタンUIをアニメーションさせたいという要望が出た場合でも、それ用のクラスを別で用意すれば良いですし、演出クラスを差し替えても特に`CustomButton`を使う外部に影響を与えることも無く済みます。

---

最後に、この`CustomButton`と`ScaledCommonButtonView`を`Image`コンポーネントを持つ`GameObject`に追加し、コンポーネントのアタッチをすれば、以下動画のようなものが出来上がります。

![ezgif.com-video-to-gif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/437da696-56c5-ddeb-0a55-5b5dde82d55d.gif)

（左：非アクティブ　右：アクティブ）（素材は過去に作ったもの）

これで、汎用的に扱えるボタンの実装が出来ました。

# 4. 汎用ラジオボタンの実装

ここからは、汎用ボタンの応用した扱い方を紹介するおまけです。

今回は前節で作成した`CustomButton`を用いてラジオボタンを実装します。
ラジオボタンとは、**複数の選択肢の中から１つだけを選択出来る**ようなUIパーツです。

そのため、ボタンに新たに「**選択されているか**」という状態を持たせます。
しかし、通常のボタンに選択状態は不要な機能なので、`CustomButton`を継承した選択可能なボタンクラスを新たに作成します。

:::note info
`CustomButton`を継承するのではなく、新しいクラスに保持させるのも良いと思いますが、
今回は継承を用いたアプローチで紹介します。
:::

```CustomSelectableButton.cs
using UniRx;

namespace CommonViewParts
{
    /// <summary>
    /// 選択状態を保持できるボタン
    /// </summary>
    public class CustomSelectableButton : CustomButton
    {
        /// <summary>
        /// 選択状態かどうかを示すReactiveProperty
        /// </summary>
        public IReadOnlyReactiveProperty<bool> IsSelectedRP => _isSelectedRP;
        private readonly ReactiveProperty<bool> _isSelectedRP = new();

        protected override void OnDestroy()
        {
            base.OnDestroy();
            _isSelectedRP.Dispose();
        }

        /// <summary>
        /// 選択状態をセットする
        /// </summary>
        public void SetSelected(bool isSelected)
        {
            _isSelectedRP.Value = isSelected;
        }
    }
}
```

`CustomButton`を継承して、新たに`IsSelectedRP`という**選択状態が変化した時にイベントが発行される**機能を追加しました。

### 見た目・演出部分の実装について

前節にて、`CustomButton`を用いる際の`見た目・演出部分`の実装として`ScaledCommonButtonView`を作成しましたが、ここではその`CustomSelectableButton`版を作成します。

以下は、選択時に色が赤色に変化するだけの処理を行っています。

```CommonSelectableButtonView.cs
using UniRx;
using UnityEngine;
using UnityEngine.UI;

namespace CommonViewParts
{
    /// <summary>
    /// 選択可能なボタンのView
    /// </summary>
    [RequireComponent(typeof(CustomSelectableButton))]
    public class CommonSelectableButtonView : MonoBehaviour
    {
        private static readonly Color s_selectedColor = Color.red;
        private static readonly Color s_unselectedColor = Color.white;
        
        private CustomSelectableButton _button;
        [SerializeField] private Image _image;

        private void Start()
        {
            _button = GetComponent<CustomSelectableButton>();
            
            // 今回は選択時に色を変えてみる
            _button.IsSelectedRP
                .Subscribe(SetColor)
                .AddTo(this.gameObject);
        }
        
        private void SetColor(bool isActive)
        {
            Color color = isActive ? s_selectedColor : s_unselectedColor;
            _image.color = color;
        }
    }
}
```

---

これらを用いてラジオボタンの実装を行います。
なお、ラジオボタンのコンポーネントをアタッチする`GameObject`の親子関係は以下のようになります。

- ラジオボタン - `CustomRadioButton`
    - 選択肢ボタンA - `CustomSelectableButton`
    - 選択肢ボタンB - `CustomSelectableButton`
    - 選択肢ボタンC - `CustomSelectableButton`

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

namespace CommonViewParts
{
    /// <summary>
    /// 複数のボタンから１つだけを選択できるもの（ラジオボタン）
    /// </summary>
    public class CustomRadioButton : MonoBehaviour
    {
        [Header("それぞれの選択肢ボタン")]
        [SerializeField] private CustomSelectableButton[] _buttons = Array.Empty<CustomSelectableButton>();

        private int _beforeSelectedButtonIndex;
        
        /// <summary>
        /// ボタンがアクティブ状態になった時に対象indexを返す
        /// </summary>
        public IObservable<int> OnButtonActivated => _buttonActivatedSubject;
        private readonly Subject<int> _buttonActivatedSubject = new();

        private void OnDestroy()
        {
            _buttonActivatedSubject.Dispose();
        }

        /// <summary>
        /// 初期化処理を実行する
        /// </summary>
        public void Initialize(int startIndex)
        {
            if (startIndex >= _buttons.Length)
            {
                throw new IndexOutOfRangeException();
            }
            
            Activate(startIndex);
            _beforeSelectedButtonIndex = startIndex;
            
            SetEvent();
        }

        /// <summary>
        /// スクリプトからボタン情報を設定するときに使用する初期化処理
        /// </summary>
        public void Initialize(CustomSelectableButton[] buttons, int startIndex)
        {
            _buttons = buttons;
            Initialize(startIndex);
        }
        
        /// <summary>
        /// 対象のボタンが選択されているかどうか
        /// </summary>
        public bool GetIsSelected(int targetIndex)
        {
            return _buttons[targetIndex].IsSelectedRP.Value;
        }
        
        /// <summary>
        /// 現在選択されているボタンのindexを取得する
        /// </summary>
        public int GetSelectedButtonIndex()
        {
            return _beforeSelectedButtonIndex;
        }

        private void SetEvent()
        {
            for (int i = 0; i < _buttons.Length; i++)
            {
                int index = i;
                _buttons[index].OnButtonClicked
                    .Subscribe(_ => OnAnyButtonSelected(index))
                    .AddTo(this.gameObject);
            }
        }

        private void OnAnyButtonSelected(int selectedIndex)
        {
            Deactivate(_beforeSelectedButtonIndex);
            Activate(selectedIndex);
            _beforeSelectedButtonIndex = selectedIndex;
        }

        private void Activate(int targetIndex)
        {
            _buttons[targetIndex].SetSelected(true);
            _buttonActivatedSubject?.OnNext(targetIndex);
        }

        private void Deactivate(int targetIndex)
        {
            _buttons[targetIndex].SetSelected(false);
        }

#if UNITY_EDITOR
        private void OnValidate()
        {
            if (_buttons.Length == 0)
            {
                _buttons = transform.GetComponentsInChildren<CustomSelectableButton>();
            }
        }
#endif
    }
}
```

:::note warn
このクラスは使用する前に初期化処理として`Initialize()`を呼び出す必要があります。
:::

`CustomSelectableButton`の配列を用いてボタンの管理を行い、`OnValidate()`で選択肢ボタンのコンポーネントを自動的にアタッチさせます。

そして選択されていないボタンがクリックされると、その`index`値を引数に持ったイベントが発行されます。また、対象の`index`値のボタンが選択されているか、あるいは今選択されているボタンの`index`は何かを取得することも出来ます。

これで以下GIF動画のようにボタンを選択出来るようになりました。

![ezgif.com-video-to-gif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/e6868571-12e4-9718-4850-c4d68913f6bf.gif)

### `index`で選択情報を返すことについて

`index`でどれが選択されたかを管理しているという点については、コードが読みづらくなるという観点から、自分でも完全に納得は出来ていません。（誰か良案があれば教えて頂けると幸いです！）

一応は`Initialize()`を用いて初期化する際に、オーバーロードとして`CustomSelectableButton`の配列を引数に与えることでボタンの種類の管理を外部に譲るという方法を取れるような実装にしてあります。

また、別のアプローチとして、以下のように`enum`を定義して各`index`と関連付けすることで、外部から利用する際に各選択肢を`enum`で管理出来るようにする方法もあります。

```cs
private void Start()
{
    _radioButton.OnButtonActivated
        .Subscribe(index => LogFruitType((FruitType) index))
        .AddTo(this.gameObject);
}

private void LogFruitType(FruitType fruitType)
{
    Debug.Log($"{fruitType}が選択されました");
}

private enum FruitType
{
    Apple = 0,
    Orange = 1,
    Lemon = 2,
}
```

（`index = 1`なら、`Orangeが選択されました`と出力されます）

# 5. 汎用チェックボックスの実装

最後はチェックボックスの機能を実装します。
チェックボックスとは、**複数の選択肢から複数を選択出来る**UIパーツです。

ラジオボタンと同様に、`CustomSelectableButton`を用いて実装します。
また、`GameObject`の親子関係も同様になります。

- チェックボックス - `CustomCheckbox`
    - 選択肢ボタンA - `CustomSelectableButton`
    - 選択肢ボタンB - `CustomSelectableButton`
    - 選択肢ボタンC - `CustomSelectableButton`

```CustomCheckbox.cs
using System;
using System.Collections.Generic;
using UniRx;
using UnityEngine;

namespace CommonViewParts
{
    /// <summary>
    /// 複数のボタンから複数選択できるもの（チェックボックス）
    /// </summary>
    public class CustomCheckbox : MonoBehaviour
    {
        [Header("それぞれの選択肢ボタン")]
        [SerializeField] private CustomSelectableButton[] _buttons = Array.Empty<CustomSelectableButton>();

        private bool[] _activeIndexArray;

        /// <summary>
        /// ボタンがアクティブ状態になった時に対象indexを返す
        /// </summary>
        public IObservable<int> OnButtonActivated => _buttonActivatedSubject;
        
        /// <summary>
        /// ボタンが非アクティブ状態になった時に対象indexを返す
        /// </summary>
        public IObservable<int> OnButtonDeactivated => _buttonDeactivatedSubject;
        
        private readonly Subject<int> _buttonActivatedSubject = new();
        private readonly Subject<int> _buttonDeactivatedSubject = new();

        private void OnDestroy()
        {
            _buttonActivatedSubject.Dispose();
            _buttonDeactivatedSubject.Dispose();
        }

        /// <summary>
        /// 初期化処理を実行する
        /// </summary>
        public void Initialize()
        {
            _activeIndexArray = new bool[_buttons.Length];
            SetEvent();
        }

        /// <summary>
        /// スクリプトからボタン情報を設定するときに使用する初期化処理
        /// </summary>
        public void Initialize(CustomSelectableButton[] buttons)
        {
            if (buttons.Length == 0)
            {
                throw new ArgumentOutOfRangeException();
            }
        
            _buttons = buttons;
            Initialize();
        }

        /// <summary>
        /// 対象のボタンのアクティブ状態をセットする
        /// </summary>
        public void SetButtonActive(int targetIndex, bool isActive)
        {
            _activeIndexArray[targetIndex] = isActive;
            _buttons[targetIndex].SetSelected(isActive);

            if (isActive)
            {
                _buttonActivatedSubject?.OnNext(targetIndex);
            }
            else
            {
                _buttonDeactivatedSubject?.OnNext(targetIndex);
            }
        }
        
        /// <summary>
        /// それぞれの選択肢が選択されているかのリストを返す
        /// </summary>
        public IReadOnlyList<bool> GetIsButtonSelectedList()
        {
            return Array.AsReadOnly(_activeIndexArray);
        }

        /// <summary>
        /// 対象のボタンが選択されているかどうか
        /// </summary>
        public bool GetIsSelected(int targetIndex)
        {
            return _activeIndexArray[targetIndex];
        }

        private void SetEvent()
        {
            for (int i = 0; i < _buttons.Length; i++)
            {
                int index = i;
                _buttons[index].OnButtonClicked
                    .Subscribe(_ => SwitchButtonActive(index))
                    .AddTo(this.gameObject);
            }
        }

        private void SwitchButtonActive(int targetIndex)
        {
            SetButtonActive(targetIndex, !_activeIndexArray[targetIndex]);
        }
        
#if UNITY_EDITOR
        private void OnValidate()
        {
            if (_buttons.Length == 0)
            {
                _buttons = transform.GetComponentsInChildren<CustomSelectableButton>();
            }
        }
#endif
    }
}
```

:::note warn
このクラスも使用する前に初期化処理として`Initialize()`を呼び出す必要があります。
:::

`CustomCheckbox`では、「**ボタンが選択された時**」と「**ボタンの選択が解除された時**」にイベントを発行させます。また、現在選択されているボタンの`index`のリスト、対象`index`が選択されているかを取得することが出来ます。

内部で行っている処理としては、選択されたボタンの選択状態を反転させ、イベントを発行しているのみです。

これで、以下GIF動画のようなものが出来上がります。

![ezgif.com-video-to-gif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1496490/129465d1-b2ee-448e-fa04-cbb70e836345.gif)


# 6. まとめ

本記事では、
- 汎用ボタン - `CustomButton`
- 汎用ラジオボタン - `CustomRadioButton`
- 汎用チェックボックス - `CustomCheckbox`

の個人的な実装アプローチを紹介しました。

ボタンを実装するにあたって、以下の２つに**機能（責務）を分け**ました。
- ボタンとしての機能を持つ`入力・イベント発行部分`
- 見た目を管理する`見た目・演出部分`

これにより、演出を差し替えてもボタンを扱う外部は**全く気にせず実装を進める**ことが出来るようになりました。

例えばUnity1Weekなど開発期間が短い場合は特に、自分用のライブラリとして用意しておくことで、ボタン周りの実装を効率的に行えるようになります。

他にも、チーム開発でボタンUI素材がまだ来ていない場合でも、**仮置きで気にせず進められる**ので、手を止めることなく開発を進めることが出来ます。

良かったら試してみてね！