LoginSignup
14
5

More than 3 years have passed since last update.

【Unity】複雑な形のボタンの当たり判定をしっかり作れるようにする

Posted at

初めに

本記事はLife Is Tech ! #2 Advent Calendar 2020 11日目の記事です!
ぜひ最後まで見ていってください!

導入

この記事を読んでいる皆さんはUnityで様々なゲームを作っていると思います。
僕も同じようにとあるゲームを作っていたのですが、そんな時にとある問題に直面しました。

ゲームのUIを作っている時に星型のボタンを作ろうと思い、PhotoShopで星型の絵を用意しUnityに取り込んでボタンに設定しました。
そしてUnityでボタン機能の設定を行っていたら、ボタンの当たり判定つまりマウスでクリックできる範囲が四角形でしか作れませんでした。
小さいボタンであれば気にならないですが、今回僕が作ろうとしていたボタンは大きめだったのでこれは非常に問題でした。
Videotogif-2.gif
image.png

星のへこんでいる部分にもボタンのクリック判定ができてしまっている。
これを何とかするのが今回の目標です。

この記事での最終目標

上で書いた通り自分の好きな形のクリック判定のあるボタンを作ること

対象読者

  • Unityに詳しくてC#のコードも割と読める人
  • おしゃれなボタンを作りたい人

目次

この記事の目次は以下の通りになります
1と2に関してはコードの難しい部分に触れるので、飛ばして3と4だけでも読んでいってくれると嬉しいです!

1.Unityのマウスポインターの処理部分のコード解説

2.今回作成したコードの解説

3.今回作成したものの使い方

4.実際に作ってみたボタン紹介

1.Unityのマウスポインタ―の処理部分のコード解説

この章ではUnityのソースコードを読みながらマウスでボタンをクリックしている時の処理を知り、どのようなコードを書けば独自のクリック判定を実装できるかを解説していきたいと思います。

結構難しめなので、コードに慣れていない人は飛ばしてください。(結構自己満足なところがある)

今回読まなければいけないコードは以下のファイルです。

  • EventSystem.cs
  • BaseInputModule.cs
  • PointerInputModule.cs
  • StandaloneInputModule.cs
  • RaycasterManager.cs
  • BaseRaycaster.cs
  • GraphicRaycaster.cs
  • Image.cs
  • MaskableGraphic.cs
  • Graphic.cs
  • GraphicRegistry.cs

多いですね、、、
頑張っていきましょう!!

Unityのソースコードを読むには、例えばEventSystem.csが読みたければ画像のようにEventSystemの右上の三つポチからEdit Scriptを選択すれば開くことができます。
スクリーンショット (266).png
僕はこの方法と普段使っているエディターであるRiderの機能でクラスやメソッドの定義元や使用先を検索してそのコードを開いてくれる機能を使って読んでいきました。

一応Windowsのエクスプローラーからたどることも可能で僕の場合EventSystem.csは、

C:\Program Files\Unity\Hub\Editor\2019.4.16f1\Editor\Data\Resources\PackageManager\BuiltInPackages\com.unity.ugui\Runtime\EventSystem\EventSystem.cs

にありました。

では早速読んでいくのですが、まずこれらのクラスの関係性をまとめたいと思います。
UI.png

では実際に見ていきましょう!

まず見るべきはEventSystem.csです。
このスクリプトはUIのButtonなどをUnityで作ると自動でできるEventSystemというGameObjectにつけられています。そして、UnityのUI関連(マウスやキー入力)の管理をしています。
image.png

image.png

340行目にあるUpdate関数の中の一番下

m_CurrentInputModule.Process();

で毎週期のUIの処理をしています。
なので次にこの中を見に行きましょう。

m_CurrentInputModuleはEventSystemクラスのメンバ変数でBaseInputModule型です。
上の図で描いた通りBaseInputModuleの子クラスには各種入力処理に関連するものが存在し(例えばStandaloneInputModuleクラス)、毎週期のUpdateのそれぞれで現在処理すべき入力モジュール型のオブジェクトがm_CurrentInputModuleに入っています(親クラスの型に子クラスのオブジェクトは突っ込める)。

つまりここでのProcessは以下の画像のStandaloneInputModule.csで書かれているProcessメソッドが実行されています。
image.png
そしてこの中で、ProcessMouseEvent();でMouseの処理が書かれています。

ではこの関数の中を見ていきましょう!
この関数は同クラス内で宣言されています。
image.png
中身はProcessMouseEvent関数を呼んでいるだけです。

この関数も同クラス内にあり以下の画像の通りです。
image.png
この関数はマウスの処理がまとめられています。
その中で、マウスカーソルがUIのどこを選択しているかを確認する部分が最初の

var mouseData = GetMousePointerEventData(id);

のGetMousePointerEventDataメソッドです。
なのでこの中を見に行きましょう。

この関数はStandaloneInputModuleクラスの親クラスであるPointerInputModuleクラスのメンバ関数です。
なのでPointerInputModule.csを読みに行きます。
関数は以下のようになっています。
image.png

ここではマウスのポインターの位置からRayを飛ばしポインターの下にあるUIオブジェクトを探す部分とマウスのボタン入力の部分の処理が書かれています。
前者のRayを飛ばし下にあるUIオブジェクトをとってくるコードは277行目のeventSystem.RaycastAll(leftData, m_RaycastResultCache);です。

eventSystemはこのクラスの親クラスであるBaseInputModuleクラスのメンバ変数でEventSystem型です
なのでEventSystem.csにあるRaycastAll関数を読みに行きます。
image.png
RaycastAllでは登録されているUIオブジェクトに一つずつRayを飛ばし確認しマウスポインタの下にあれば第二引数のraycastResultsに追加します。

ここで一度UnityのUIオブジェクトの登録についてまとめます。
UnityでUIオブジェクトを作りEnableにするとRaycasterManagerクラスとGraphicRegistryクラスに登録されます。

RaycasterManagerクラスではメンバ変数にBseRaycaster型の配列であるs_RaycastersをもちUIにRayを飛ばす機能のオブジェクトを管理してます。
UnityでつくるButtonオブジェクトはBaseRaycasterの子クラスであるGraphicRaycaster型のオブジェクトとしてこの配列に追加されています。

GraphicRegistryクラスではメンバ変数にCanvas型とGraphic型のDictionary型であるm_Graphicsを持ち、UIオブジェクトを管理しています。
UnityでつくるButtonオブジェクトはGraphicクラスの子クラスであるImageクラスとしてこの配列に追加されます。

RaycastAllの内容に戻ると、
登録されているUIにRayを飛ばす機能のオブジェクトは最初の以下の部分で取得されています。

var modules = RaycasterManager.GetRaycasters();

RaycasterManager.GetRaycasters()はStatic関数でRaycasterManager.csを確認しに行くと以下の通りになっています。
image.png

よってRaycastAll関数内の各モジュールにRayを飛ばすmodule.Raycast(eventData, raycastResults);の部分でButtonオブジェクトがmoduleに登録されているとき、Raycast関数はGraphicRaycaster.csで書かれているものが呼び出されます。

なので、それを見に行きます。(このコードは長かったので画像はありません)

まず121行目のRaycast関数が呼ばれます。
その中の126行目のvar canvasGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);で先ほど説明したGraphicRegistryに登録されたUIオブジェクトを持ってきています。
そして、225行目でこのcanvasGraphicsも引数として渡して312行目で宣言されているRaycast関数を呼び出しています。

312行目のRaycast関数ではcanvasGraphicsに登録されているUIオブジェクトそれぞれにRayを飛ばしマウスポインタの下にあるか確認して下にあれば第五引数のresultsにそのGraphicクラスのオブジェクトを追加します。
Rayを実際に飛ばす部分は330行目の部分です。

ここまででようやく、PointerInputModule.csの277行目のeventSystem.RaycastAll(leftData, m_RaycastResultCache);の説明がおわり、結局この行が呼ばれればm_RaycastResultCacheにマウスが載っているUIオブジェクトが追加されていることがわかりました。

この下の行ではそれらの中から、UIで一番上に配置されているものを取り出して各種処理を行っています。

以上でUnityのマウスの処理の説明が終わりました。
よって今回UIのボタンの当たり判定を変更したいならば、GraphicRaycaster.csの330行目で呼び出している、GraphicクラスのRaycast関数を書き換える必要があります。

このRaycast関数の中ではGraphic.csの798行目でIsRaycastLocationValid関数が呼ばれていてこの関数の中で判定の処理が書かれています。
このIsRaycastLocationValid関数はGraphicクラスの子クラスであるImageクラスでvirtualで定義されています。

つまり今回はこのIsRaycastLocationValid関数を新たなスクリプトでoverrideしてあげ、そのなかに好きな形でも判定ができるようなコードをかいてあげれば成功です。

ここまで読んでくださって本当にありがとうございます!
そしてお疲れ様でした。

次の章ではこのIsRaycastLocationValid関数を書き換えるところを説明します。(多分力つきているので、大分雑になると思います、、、)

2.今回作成したコードの解説

今回作成したコードはこちら様の記事を参考(ほぼ丸パクリ)にさせていただきました。
先ほど説明したImageクラスはICanvasRaycastFilterクラスを継承しているので今回はICanvasRaycastFilterクラスを新たなコンポーネントとして実装しその中でIsRaycastLocationValid関数をoverrideするようにしています。

指定した範囲内にマウスがあるかなどのアルゴリズムに関してはそこまで難しいものは使っていません。ぜひ元記事の方を読まれてください。

範囲の指定の仕方ですが、SpriteのPhysics Shapeという機能を用いています。UnityのSprite Editorで好きに形を編集し、コード側でそれを取得し判定を行っています。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// SpriteのPhysicsShapeの内部のみ当たり判定を有効にするRaycastFilter
/// </summary>
[RequireComponent(typeof(Image))]
public class PhysicsShapeRaycastFilter : MonoBehaviour, ICanvasRaycastFilter //
{
    private readonly List<Vector2> _verts = new List<Vector2>();

    private Image _image;

    public bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
    {
        var image = _image;
        if (_image == null)
        {
            _image = GetComponent<Image>();
        }

        var rectTransform = transform as RectTransform;
        Vector2 local;
        if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local))
        {
            return false;
        }

        var rect = rectTransform.rect;

        // スプライト内の座標空間での位置を計算する
        var pivot = rectTransform.pivot;
        var sprite = image.sprite;
        var x = (local.x / rect.width + pivot.x - 0.5f) * sprite.rect.width / sprite.pixelsPerUnit;
        var y = (local.y / rect.height + pivot.y - 0.5f) * sprite.rect.height / sprite.pixelsPerUnit;
        var p = new Vector2(x, y);

        var physicsShapeCount = sprite.GetPhysicsShapeCount();
        for (var i = 0; i < physicsShapeCount; i++)
        {
            sprite.GetPhysicsShape(i, _verts);
            if (IsInPolygon(_verts, p))
            {
                // どれかの多角形の内部にあればtrueを返す
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// 非凸多角形の内部に点が存在するかどうか
    /// </summary>
    private static bool IsInPolygon(List<Vector2> polygon, Vector2 p)
    {
        // pからx軸の正方向への無限な半直線を考えて、多角形との交差回数によって判定する
        var n = polygon.Count;
        var isIn = false;
        for (var i = 0; i < n; i++)
        {
            var nxt = (i + 1);
            if (nxt >= n) nxt = 0;
            var a = polygon[i] - p;
            var b = polygon[nxt] - p;
            if (a.y > b.y)
            {
                // swap
                var t = a;
                a = b;
                b = t;
            }

            if (a.y <= 0 && 0 < b.y && CrossProduct(a, b) > 0)
            {
                isIn = !isIn;
            }
        }

        return isIn;
    }

    /// <summary>
    /// 外積
    /// </summary>
    private static float CrossProduct(Vector2 u, Vector2 v)
    {
        return u.x * v.y - u.y * v.x;
    }
}

解説に関しては元記事様の説明が非常に分かりやすいので是非読んでみて下さい

3.今回作成したものの使い方

使い方は非常に簡単です。

先ほどのコードをUnityにPhysicsShapeRaycastFilter.csとして作り目的のButtonオブジェクトに着けてあげるだけです。
image.png

そして、ButtonについているImageコンポーネントに登録している、スプライトをSprit Editorで編集していきます。

image.png
右下のSprite Editorをクリック

スクリーンショット (268).png
このようなウィンドウが立ち上がるので、左上のボタンをクリックしてCustom Physics Shapeを選択。

image.png
このように、ボタンの当たり判定になってほしい部分を描きます。
描けたら、右上のApplyボタンを押して閉じましょう。

以上で作業は終了です。
できたボタンを好きな場所に配置しましょう!

4.実際に作ってみたボタン紹介

実際にうまくいっているか確認してみましょう。
今回できたボタンが下の画像です。
Videotogif-3.gif

確かに星のへこんでいる部分では反応しないようになりました。

記事は以上になります!
お疲れ様でした。

感想

非常に疲れました。

コードって自分で理解できても人に説明するのってすごい難しいってことを改めて感じました。
Unity内部のコードはそこまで不可能なくらい難しくないので、勉強したい人はぜひ読んでみるといいと思いました。

14
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
5