Unityで個人的にいろいろ便利な自前Buttonを作ってみた

UnityのuGUIにはButtonコンポーネントがあり、これまではこのButtonを利用していました。

しかし、個人的にはこのuGUIの標準Button、使いづらいと感じる部分がありました。

uGUIのButtonに対する要望

どこが使いづらいかというと、個人的な要望としては

  • 連打しても処理は1回のみ実行されるように制限したい
  • 1つのボタンを押すと、他のボタンは押せないようにしたい
  • 全ボタンに共通の処理を書きたい
  • デフォルトで押下時に画像を差し替えたい
  • 押下画像に合わせてTextなどの子要素も移動させたい

というのがありました。

この5つの要望を大きく分けると上2つ、真ん中、下2つの項目に分けられます。

まずは、連打防止

例えばボタンを押すとシーン遷移するとき、即座にシーン遷移すれば問題ないのですが、だいたいシーン遷移には一定の時間がかかってしまいます。

この遷移処理中に他のボタンを押したり、同じボタンを連打したりしてしまうと、別の処理が重なって実行されてしまうという状態になってしまいます。

これまでは、ボタンを押したら画面全体を透明なImageで覆ってタッチを防いでからフェード、そしてシーン遷移、というような処理をすることが多くありました。

しかし、わざわざタッチ防止用のImageを作るのは無駄なので、Buttonの機能でこれができれば良いと感じていました。

次に、全ボタン共通の処理を書きたいという要望。

例えばボタンのSE再生処理はこれまで、各ボタンが実行するメソッド毎に記述したりだとか、ボタンSEを再生する処理を書いたメソッドを追加で登録したりしていました。

もっと簡単に共通処理を記述したい、という思いがありました。

最後に、押下アニメーションに関する要望。

uGUIのButtonのデフォルト押下アニメーションは、ButtonコンポーネントのTransitionで設定しているColor Tint(押下中は少しImageの色を暗くする)というものです。

しかし、これだと押下に対するプレイヤーへのフィードバックが少ないため、個人的にはいつもSprite Swap(押下中はImageのSpriteを差し替える)を使っています。

ほぼ毎回使っているので、まずはデフォルトでSprite Swapにしたいという要望。

また、SpriteSwapで押下画像に切り替えたときに、子要素のTextが取り残されてしまうため、押下中は子要素を指定した分だけ下に移動させたい、という要望もありました。

自前Buttonを実装

これらの要望を叶えるため、uGUIのButtonを使わない自前Buttonを実装しました。

コードはこちら。

using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using UnityEngine.Events;

[RequireComponent(typeof(Image))]
public class ButtonController : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler
{
    private Image image;
    private Sprite defaultSprite;
    [SerializeField] private Sprite pressedSprite;

    [SerializeField] private int id = 0;
    [SerializeField] private float pressOffsetY = 0f;
    [SerializeField] private UnityEvent onClick = null;

    private Transform child;
    private float defaultY;
    private ButtonController[] buttonControllers;

    private bool isPushed = false;


    void Awake()
    {
        image = GetComponent<Image>();
        defaultSprite = image.sprite;
        child = transform.GetChild(0);
        defaultY = child.localPosition.y;

        Transform canvas = GameObject.Find("Canvas").transform;
        buttonControllers = canvas.GetComponentsInChildren<ButtonController>();
    }

    void OnEnable()
    {
        ButtonActive(true);
    }

    public void ButtonActive(bool active)
    {
        isPushed = !active;
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        if (isPushed) return;
        Vector3 pos = child.localPosition;
        pos.y = defaultY - pressOffsetY;
        child.localPosition = pos;
        if (pressedSprite != null) image.sprite = pressedSprite;
        OnButtonDown();
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        if (isPushed) return;
        Vector3 pos = child.localPosition;
        pos.y = defaultY;
        child.localPosition = pos;
        image.sprite = defaultSprite;
        OnButtonUp();
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        if (id != -1)
        {
            foreach (var controller in buttonControllers)
            {
                controller.ButtonActive(controller.id != this.id);
            }
        }

        OnButtonClick();
        onClick?.Invoke();
    }

    public void AllButtonReset()
    {
        foreach (var controller in buttonControllers)
        {
            controller.ButtonActive(true);
        }
    }

    private void OnButtonDown()
    {
        // Down時の共通処理
    }

    private void OnButtonUp()
    {
        // Up時の共通処理
    }

    private void OnButtonClick()
    {
        // Click時の共通処理(SE鳴らすなど)
    }
}

これをImageオブジェクトにアタッチしてください。
Buttonコンポーネントは不要です。(鬱陶しいのでアタッチしないことをお勧めします)

インスペクタはこんな感じ。

Pressed Spriteに押下時のSpriteを指定してください。

通常時のSpriteはImageコンポーネントに指定されているものがそのまま使われます。

Idには、任意の整数を指定してください。

ボタンの処理を実行した際、そのボタンのIdと同じIdが割り当てられているボタンは(自分も含み)無効化されます。

もし連打を許容したい場合は、Idに-1を指定してください。

次にPress Offset Yですが、これは押下時に子要素を下に移動する距離です。

押下アニメーションでボタンが押し込まれるような画像切り替えをした場合、そのままだと子要素のTextが置いていかれてしまうので、押下画像に合わせて移動させる距離を指定してください。
(これは現状、自力で目視によって値を決める必要があります。)

最後にOn Clickの部分には、ボタンが押された場合に実行するメソッドを登録します。

これは通常のButtonと同じような感じです。

今回は個人的に使いやすいものを作ったので、人によって/プロジェクトによっては少しカスタマイズした方が良いかもしれません。

例えば同じIdのButtonを探す際、今回は “Canvas” というGameObjectの子要素にあるButtonを全て取得しています。

しかし、もしCanvasが複数あったり、Canvasの名前を変更していたりする場合は注意が必要です。

代替案としては、全てのButtonにタグを指定しておいて(もしくはAwake()で指定して)、GameObject.FindGameObjectsWithTag(“タグ名”)で全取得するとか、FindObjectsOfType(typeof(ButtonController))を使うとかが考えられます。

状況に合わせて変更してください。

他にもIPointerEnterを追加したりだとか、子要素がない場合はPressOffsetYが不要だったりとか。

各々カスタマイズをお願いします。

コメント