【Unity】UIの「ボタン」を自作する方法!拡張性の高いオリジナルのボタンを作ってみよう

オリジナルのボタンを作る方法 ゲーム開発

今回はUnityにおけるUIの実装方法に関する話題で、タイトルの通り

Unityのボタンを自作する方法

をサンプルスクリプト付きでご紹介するという内容になっています。

UnityのUIを使っているとよく思うのですが、標準のボタンってなんか使いづらくないですか?というのも

  • 標準のボタンは見た目はもちろん挙動がダサい
  • 標準のボタンは拡張性が低くて、自分好みに改造しようと思っても思い通りにならない

と個人的には思っています。もちろんプロトタイプとして使うなら標準のボタンでもいいのですが、きちんとしたゲームを作るならもうちょっとマシなボタンを使いたいところです。

そこでここではボタンのC#スクリプトを自作して

  • かっこよくて
  • 拡張性も高い

という良いことだらけのオリジナルのボタンを作る方法をご紹介しますね。

今回作るボタンのサンプル

まず、今回自作するボタンのサンプルは次のとおりです。

自作ボタンの例(1)

シンプル版。画像クリックでGIF再生

自作ボタンの例(2)

動きがついたよりゲームらしいボタン

1枚目のシンプル版をベースにして2枚目の動きが付いたボタンを作ろうと思います。

自作ボタンのC#スクリプト

ではお次は自作ボタンのサンプルスクリプトです。

注意:
下記のC#スクリプトは個人利用であれば自由に使っていただいて構いませんが、作者を偽るのは厳禁とします。例えば他の場所に掲載する場合等は必ずクレジットを明記してください。

自作ボタンの基底クラス(シンプルボタン)

下記はシンプルな自作ボタンのクラスで、より実用的なボタンの基底クラスとなるスクリプトです。

using System;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using DG.Tweening;
using TMPro;

[RequireComponent(typeof(Image))]
[RequireComponent(typeof(CanvasGroup))]
public class SimpleButton : MonoBehaviour, IPointerClickHandler, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler
{

    [SerializeField]
    Image image;
    [SerializeField]
    CanvasGroup canvasGroup;
    [SerializeField]
    TextMeshProUGUI text;

    [SerializeField]
    bool isInteractable = true;

    [SerializeField]
    ColorSettings buttonColorSettings = new();
    [SerializeField]
    ColorSettings textColorSettings = new();
    [SerializeField]
    ButtonEvents events = new();

    ColorSet currentButtonColorSet;
    ColorSet currentTextColorSet;

    public bool IsInteractable
    {
        get { return isInteractable; }
        set
        {
            if (value == isInteractable)
            {
                return;
            }

            SetButtonInteractable(value);
        }
    }

    public ButtonEvents Events { get { return events; } set { events = value; } }

    void Start()
    {
        InitButton();
    }

    protected virtual void InitButton()
    {
        SetButtonInteractable(isInteractable);
    }

    void SetButtonInteractable(bool value)
    {
        isInteractable = value;

        if (value)
        {
            ChangeButtonColor(buttonColorSettings.normalColor);
            ChangeTextColor(textColorSettings.normalColor);
        }
        else
        {
            ChangeButtonColor(buttonColorSettings.disabledColor);
            ChangeTextColor(textColorSettings.disabledColor);
        }
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        if (!isInteractable)
        {
            return;
        }

        ChangeButtonColor(buttonColorSettings.onPointerEnterColor);
        ChangeTextColor(textColorSettings.onPointerEnterColor);
        AdditionalOnPointerEnterProcess();

        events.onPointerEnter.Invoke();
    }

    protected virtual void AdditionalOnPointerEnterProcess()
    {

    }

    public void OnPointerExit(PointerEventData eventData)
    {
        if (!isInteractable)
        {
            return;
        }

        ChangeButtonColor(buttonColorSettings.normalColor);
        AdditionalOnPointerExitProcess();

        events.onPointerExit.Invoke();
    }

    protected virtual void AdditionalOnPointerExitProcess()
    {

    }

    public void OnPointerClick(PointerEventData eventData)
    {
        if (!isInteractable)
        {
            return;
        }

        AdditionalOnPointerClickProcess();

        events.onClick.Invoke();
    }

    protected virtual void AdditionalOnPointerClickProcess()
    {

    }

    public void OnPointerDown(PointerEventData eventData)
    {
        if (!isInteractable)
        {
            return;
        }

        ChangeButtonColor(buttonColorSettings.onPointerDownColor);
        AdditionalOnPointerDownProcess();

        events.onPointerDown.Invoke();
    }

    protected virtual void AdditionalOnPointerDownProcess()
    {

    }

    public void OnPointerUp(PointerEventData eventData)
    {
        if (!isInteractable)
        {
            return;
        }

        ChangeButtonColor(buttonColorSettings.normalColor);
        AdditionalOnPointerUpProcess();

        events.onPointerUp.Invoke();
    }

    protected virtual void AdditionalOnPointerUpProcess()
    {

    }

    public void ChangeButtonColor(Color targetColor, float duration, Ease ease = Ease.Linear)
    {
        if (duration < 0)
        {
            return;
        }

        image.DOColor(targetColor, duration).SetLink(gameObject).SetUpdate(true).SetEase(ease);
        canvasGroup.DOFade(targetColor.a, duration).SetLink(gameObject).SetUpdate(true).SetEase(ease);
    }

    void ChangeButtonColor(ColorSet colorSet)
    {
        if (currentButtonColorSet == colorSet)
        {
            return;
        }

        ChangeButtonColor(colorSet.color, colorSet.duration, colorSet.ease);

        currentButtonColorSet = colorSet;
    }

    public void ChangeTextColor(Color targetColor, float duration, Ease ease = Ease.Linear)
    {
        if (text == null || duration < 0)
        {
            return;
        }

        text.DOColor(targetColor, duration).SetLink(gameObject).SetUpdate(true).SetEase(ease);
    }

    void ChangeTextColor(ColorSet colorSet)
    {
        if (colorSet == currentTextColorSet)
        {
            return;
        }

        ChangeTextColor(colorSet.color, colorSet.duration, colorSet.ease);

        currentTextColorSet = colorSet;
    }

    [Serializable]
    sealed class ColorSettings
    {

        public ColorSet normalColor;
        public ColorSet disabledColor;
        public ColorSet onPointerEnterColor;
        public ColorSet onPointerDownColor;

    }

    [Serializable]
    sealed class ColorSet
    {

        public Color color = Color.white;
        [Min(0)]
        public float duration = 0;
        public Ease ease = Ease.Linear;

    }

}

[Serializable]
public sealed class ButtonEvents
{

    public UnityEvent onPointerEnter = new();
    public UnityEvent onPointerExit = new();
    public UnityEvent onClick = new();
    public UnityEvent onPointerDown = new();
    public UnityEvent onPointerUp = new();

}

このC#スクリプトの使い方

UIのImageに

  • 上記のC#スクリプト
  • Canvas Groupコンポーネント

をアタッチして、SimpleButtonコンポーネントの空欄に対応するコンポーネントを登録してください。また、「Button Color Settings」から各状態におけるボタンの色を設定してください。

自作ボタンのコンポーネントの設定例

C#スクリプトの解説

少し長いC#スクリプトで、イベントシステムのインターフェースも継承しているので分かりにくいかもしれませんのでスクリプトの内容を補足しておきます。主なポイントは次のとおりです。

  • イベントシステムのインターフェースについて
  • 色変更用の関数について
  • 色データ設定用のクラスについて
イベントシステムのインターフェースについて

イベントシステムのインターフェースを継承することで、例えば

  • ボタンのクリック
  • マウスカーソルの押下
  • Image上へのマウスカーソルの進入

などのイベントを拾うことができるようになります。ここでは

  1. IPointerClickHandler
  2. IPointerDownHandler
  3. IPointerUpHandler
  4. IPointerEnterHandler
  5. IPointerExitHandler

の5つを継承しており、ボタンの動作に必要なイベントを拾えるようにしています。各インターフェースについては公式マニュアルをご覧ください。

サポートされているイベント - Unity マニュアル
Event System は多くのイベントをサポートしており、Input Module を書くことでカスタマイズできます。

色変更用の関数について

ボタンやテキストの色変更を行う関数では、DOTweenという無料アセットを使って滑らかな色変更を行えるようにしています。

DOTweenの詳しい使い方については下記の記事をご覧ください。

【Unityアセット】DOTweenの使い方完全ガイド!超人気アセットを使いこなそう
今回はUnityユーザーの皆さんならおそらくご存じの神アセット「DOTween」の使い方を丁寧にご紹介するという内容です。 DOTweenは非常に人気が高いアセットなので既に持っていらっしゃる方も多いかと思いますが、正直なところ「使い方がよ...

色データ設定用のクラスについて

スクリプトの下の方にあるクラスは、ボタンの各状態における色や遷移時間等を設定するための内容となっています。このクラスをシリアライズして使うことで、SimpleButtonクラスにベタ書きするよりもインスペクターでの表示がスッキリします。

基底クラスを継承した実用的なボタンのクラス

さて上記のままでも十分ボタンとして使えますが、もう少し「ゲームらしい動き」をつけたくなることでしょう。そこで先ほどの基底クラスを継承して

  • カーソルがボタンに乗ったら滑らかに拡大
  • ボタンを押したら滑らかに縮小
  • カーソルがボタンから離れたらスケールを元に戻す

という動きをするボタンを作ります。スクリプトは下記のような簡単な内容となります。

using UnityEngine;
using DG.Tweening;

public class UsefulButton : SimpleButton
{

    [SerializeField]
    RectTransform rectTransform;
    [SerializeField]
    float onPointerEnterScale = 1;
    [SerializeField]
    float onPointerDownScale = 1;
    [SerializeField]
    float duration = 0.1f;
    [SerializeField]
    Ease ease = Ease.OutQuad;

    protected override void AdditionalOnPointerEnterProcess()
    {
        ChangeButtonScale(onPointerEnterScale);
    }

    protected override void AdditionalOnPointerExitProcess()
    {
        ChangeButtonScale(1);
    }

    protected override void AdditionalOnPointerDownProcess()
    {
        ChangeButtonScale(onPointerDownScale);
    }

    protected override void AdditionalOnPointerUpProcess()
    {
        ChangeButtonScale(1);
    }

    void ChangeButtonScale(float scale)
    {
        rectTransform.DOScale(Vector3.one * scale, duration).SetLink(gameObject).SetEase(ease).SetUpdate(true);
    }

}

基底クラスを拡張しやすいように書いておいたので、何か付け加えたいときはこのように簡単に拡張することができます。

おわりに

以上、Unityで自作のボタンを作る方法についてご説明しました。

一見すると標準で用意されているボタンをわざわざ自作するのは面倒くさいように見えますが、拡張性を考えると絶対自作したほうがいいのでおすすめです。普段は標準のボタンを使っているよ、という方もこの機会にぜひボタンを自作してみてください。

この記事がUnityでのゲーム開発のお役に立てば幸いです。