Unityを触り始めた頃、「とりあえずクリック処理は全部Updateに書いておけばいいや」と思って、こんなコードを書いてしまいがちですよね。

// よくある「なんでもUpdate」スタイル(あまり良くない例)
void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        // 画面クリック → レイキャスト → 何かしらの判定 → 直接処理を書く…
        // プレイヤークリック処理
        // 敵クリック処理
        // UIクリック処理
        // ...
    }
}

この書き方だと、

  • クリック対象が増えるたびに巨大なif/elseやswitchが増える
  • プレイヤー、敵、UIなどの責務が1つのスクリプトに密結合する
  • プレハブを差し替えたときに、中央のクリック管理クラスを毎回修正する

といった問題が出てきて、すぐに「Godクラス」化してしまいます。

そこでこの記事では、「クリックされる側」に小さなコンポーネントを付けるだけで、クリックイベントを受け取り、シンプルな「シグナル(イベント)」として他コンポーネントに通知できるようにする 「ClickableObject」コンポーネントを用意してみます。

クリック判定は各オブジェクトに任せて、何が起こるかは別コンポーネントに委ねる――そんなコンポーネント指向な設計にしていきましょう。

【Unity】クリック検知をコンポーネント化!「ClickableObject」コンポーネント

ここでは、以下のような要件を満たす ClickableObject を作ります。

  • マウス(あるいはタップ)でクリックされたことを検知する
  • 「クリックされた」というシグナルを C#イベントUnityEvent の両方で発火する
  • クリック処理本体(何をするか)は、このコンポーネントに書かず、他コンポーネントに任せる

この記事のコードだけで動くように、クリック用の簡易レイキャストマネージャも一緒に用意します。

フルコード:ClickableObject と ClickInputManager

using System;
using UnityEngine;
using UnityEngine.Events;

namespace ClickSample
{
    /// <summary>
    /// クリックされたことを通知するコンポーネント。
    /// このコンポーネント自体は「クリックされた」という事実だけをシグナルとして提供し、
    /// 何が起こるかは他のコンポーネント(購読者)に任せます。
    /// 
    /// - C#イベント: Clicked
    /// - UnityEvent: onClicked(インスペクターから設定可能)
    /// 
    /// ※ クリック検出そのものは ClickInputManager が行い、
    ///    レイキャストでヒットしたオブジェクトに対して NotifyClicked を呼び出します。
    /// </summary>
    public class ClickableObject : MonoBehaviour
    {
        // インスペクターから設定可能なUnityEvent
        [SerializeField]
        private UnityEvent onClicked = new UnityEvent();

        /// <summary>
        /// C# のイベントで通知したい場合はこちらを購読します。
        /// 例:
        ///   clickableObject.Clicked += OnClicked;
        /// </summary>
        public event Action<ClickableObject> Clicked;

        /// <summary>
        /// ClickInputManager から呼び出される「クリックされたよ」通知。
        /// 通常は外部から直接呼ばず、レイキャスト結果として呼ばれる想定です。
        /// </summary>
        public void NotifyClicked()
        {
            // UnityEvent を発火(インスペクター設定用)
            onClicked?.Invoke();

            // C#イベントを発火(コードから購読用)
            Clicked?.Invoke(this);
        }

        // デバッグ用に、クリックされたことをログに出すかどうか
        [SerializeField]
        private bool logOnClick = false;

        private void Awake()
        {
            if (logOnClick)
            {
                // 自分自身の C#イベントに購読してログを出す
                Clicked += self =>
                {
                    Debug.Log($"[ClickableObject] Clicked: {self.gameObject.name}", self);
                };
            }
        }
    }

    /// <summary>
    /// 画面クリック(またはタップ)を検出して、レイキャストを行い、
    /// ヒットしたオブジェクトに付いている ClickableObject を探して NotifyClicked を呼ぶマネージャ。
    /// 
    /// シーンに1つ置いておくだけで、すべての ClickableObject を一括で扱えます。
    /// </summary>
    public class ClickInputManager : MonoBehaviour
    {
        [Header("レイキャスト設定")]

        [SerializeField]
        [Tooltip("クリック判定を行うカメラ。未指定の場合は Camera.main を使用します。")]
        private Camera targetCamera;

        [SerializeField]
        [Tooltip("クリック判定に使うレイヤーマスク。必要に応じて絞り込みましょう。")]
        private LayerMask clickableLayerMask = ~0; // デフォルトですべてのレイヤー

        [Header("入力設定")]

        [SerializeField]
        [Tooltip("PC向け: 左クリックを有効にするか")]
        private bool useMouseLeftButton = true;

        [SerializeField]
        [Tooltip("タッチ入力を有効にするか(モバイル向け)")]
        private bool useTouch = true;

        private void Awake()
        {
            if (targetCamera == null)
            {
                targetCamera = Camera.main;
            }

            if (targetCamera == null)
            {
                Debug.LogWarning("[ClickInputManager] 対象カメラが見つかりません。レイキャストが行えません。", this);
            }
        }

        private void Update()
        {
            if (targetCamera == null)
            {
                return;
            }

            // マウス左クリック
            if (useMouseLeftButton && Input.GetMouseButtonDown(0))
            {
                Vector3 mousePosition = Input.mousePosition;
                TryRaycastAndNotify(mousePosition);
            }

            // タッチ入力(シンプルに最初の1本のみを見る)
            if (useTouch && Input.touchCount > 0)
            {
                Touch touch = Input.GetTouch(0);
                if (touch.phase == TouchPhase.Began)
                {
                    Vector2 touchPosition = touch.position;
                    TryRaycastAndNotify(touchPosition);
                }
            }
        }

        /// <summary>
        /// スクリーン座標からレイキャストを飛ばし、ヒットしたオブジェクトに
        /// ClickableObject があれば NotifyClicked を呼び出す。
        /// </summary>
        /// <param name="screenPosition">マウスまたはタッチのスクリーン座標</param>
        private void TryRaycastAndNotify(Vector2 screenPosition)
        {
            Ray ray = targetCamera.ScreenPointToRay(screenPosition);
            if (Physics.Raycast(ray, out RaycastHit hitInfo, float.MaxValue, clickableLayerMask))
            {
                // 3Dオブジェクト用: Collider から ClickableObject を探す
                ClickableObject clickable = hitInfo.collider.GetComponentInParent<ClickableObject>();
                if (clickable != null)
                {
                    clickable.NotifyClicked();
                    return;
                }
            }

            // 2D物理を使う場合はこちらも試す
            Ray ray2D = targetCamera.ScreenPointToRay(screenPosition);
            RaycastHit2D hit2D = Physics2D.GetRayIntersection(ray2D, float.MaxValue, clickableLayerMask);
            if (hit2D.collider != null)
            {
                ClickableObject clickable2D = hit2D.collider.GetComponentInParent<ClickableObject>();
                if (clickable2D != null)
                {
                    clickable2D.NotifyClicked();
                }
            }
        }
    }

    /// <summary>
    /// ClickableObject の使い方例:
    /// クリックされたら色を変えるだけのシンプルなコンポーネント。
    /// </summary>
    [RequireComponent(typeof(Renderer))]
    public class ClickColorChanger : MonoBehaviour
    {
        [SerializeField]
        private ClickableObject clickableObject;

        [SerializeField]
        private Color clickedColor = Color.yellow;

        private Renderer _renderer;
        private Color _originalColor;

        private void Awake()
        {
            _renderer = GetComponent<Renderer>();
            _originalColor = _renderer.material.color;

            // もしインスペクターで未設定なら、自分の階層から探す
            if (clickableObject == null)
            {
                clickableObject = GetComponentInParent<ClickableObject>();
            }

            if (clickableObject != null)
            {
                // C#イベントを購読してクリック時の処理を登録
                clickableObject.Clicked += OnClicked;
            }
            else
            {
                Debug.LogWarning("[ClickColorChanger] ClickableObject が見つかりません。クリックイベントを受け取れません。", this);
            }
        }

        private void OnDestroy()
        {
            if (clickableObject != null)
            {
                clickableObject.Clicked -= OnClicked;
            }
        }

        private void OnClicked(ClickableObject source)
        {
            // クリックされたら色をトグルする
            if (_renderer.material.color == _originalColor)
            {
                _renderer.material.color = clickedColor;
            }
            else
            {
                _renderer.material.color = _originalColor;
            }
        }
    }
}

使い方の手順

ここからは、実際にシーンに配置して動かす手順を見ていきましょう。

手順①:クリック入力マネージャをシーンに置く

  1. 空のGameObjectを作成し、名前を ClickInputManager などにします。
  2. 上記コードの ClickInputManager コンポーネントをアタッチします。
  3. Target Camera にクリック判定に使いたいカメラ(通常はMain Camera)をドラッグ&ドロップします。
    未設定でも Camera.main を自動で使いますが、複数カメラがある場合は明示的に指定した方が安全です。
  4. Clickable Layer Mask で、クリック対象にしたいレイヤーを選びます(デフォルトは全レイヤー)。

手順②:クリックされるオブジェクトに ClickableObject を付ける

例として「プレイヤーキャラ」をクリック可能にする場合:

  1. プレイヤーのプレハブ(またはシーン上のGameObject)を選択します。
  2. 3Dなら Collider(BoxCollider など)、2Dなら Collider2D(BoxCollider2D など)が付いていることを確認します。
    ※レイキャストは Collider/Collider2D を頼りにヒット判定を行います。
  3. プレイヤーに ClickableObject コンポーネントを追加します。
  4. デバッグでログを見たい場合は Log On Click にチェックを入れておくと、クリック時にコンソールにログが出ます。

手順③:シグナル(イベント)を受け取るコンポーネントを用意する

クリックされたときに何をするかは、別コンポーネントに切り出すのがポイントです。例として:

  • プレイヤー:クリックされたら選択状態にする、マーカーを出す
  • :クリックされたらターゲットに設定する、HPバーを表示する
  • 動く床:クリックされたら動き始める

ここでは、プレイヤーがクリックされたら選択状態をトグルする簡単な例を示します。

using UnityEngine;
using ClickSample;

public class PlayerSelection : MonoBehaviour
{
    [SerializeField]
    private ClickableObject clickableObject;

    [SerializeField]
    private GameObject selectionMarker; // プレイヤーの頭上に表示するマーカーなど

    private bool isSelected;

    private void Awake()
    {
        if (clickableObject == null)
        {
            clickableObject = GetComponentInParent<ClickableObject>();
        }

        if (selectionMarker != null)
        {
            selectionMarker.SetActive(false);
        }

        if (clickableObject != null)
        {
            clickableObject.Clicked += OnClicked;
        }
        else
        {
            Debug.LogWarning("[PlayerSelection] ClickableObject が見つかりません。", this);
        }
    }

    private void OnDestroy()
    {
        if (clickableObject != null)
        {
            clickableObject.Clicked -= OnClicked;
        }
    }

    private void OnClicked(ClickableObject source)
    {
        isSelected = !isSelected;

        if (selectionMarker != null)
        {
            selectionMarker.SetActive(isSelected);
        }
    }
}

このように、ClickableObject は「クリックされた」という事実だけを発火し、実際の挙動は PlayerSelection 側で完結させる構成にします。

手順④:UnityEventを使ってインスペクターだけで完結させる

コードを書かずに、インスペクターでシグナルをつなぐこともできます。

  1. クリック対象オブジェクトに ClickableObject を追加する。
  2. インスペクターで On Clicked(UnityEvent)にイベントを追加する。
  3. 例えば「動く床」の場合:
    • 床の GameObject に「MovePlatform」などのスクリプトを付ける(後述の改造案を参照)。
    • On Clicked のイベントリストに床の GameObject をドラッグ。
    • ドロップダウンから MovePlatform.ToggleMove のようなメソッドを選択。

これで、クリック検出 → シグナル発火 → 挙動 までを、きれいに分離することができます。

メリットと応用

ClickableObject を導入することで、プレハブ管理やレベルデザインがかなり楽になります。

  • クリック判定の責務を分離できる
    クリックの検出は ClickInputManager、クリックされたという事実は ClickableObject、実際の挙動は個別コンポーネント――というように、役割ごとにクラスを分割できます。
  • プレハブが再利用しやすくなる
    例えば「クリックすると色が変わる箱」「クリックすると動く床」「クリックするとUIパネルが開くボタン」など、どれも ClickableObject を共通で利用できます。
    クリックされたあとに何をするかだけを、それぞれのプレハブで差し替えればOKです。
  • レベルデザイン時に「クリックアクション」を後から付け足せる
    まずは「クリックされるオブジェクト」を並べるだけ並べておき、後から「どのオブジェクトがクリックされたら何が起きるか」をインスペクターで設定していく、というワークフローが取りやすくなります。
  • Godクラスを避けられる
    すべてのクリック処理を1つの巨大な GameManager に書く必要がなくなり、オブジェクト単位の小さなコンポーネントに分割できます。

改造案:クリックで動き出す「動く床」コンポーネント

最後に、ClickableObject と組み合わせて使える簡単な改造案を紹介します。
「クリックされたら動き出し、もう一度クリックされたら止まる」動く床コンポーネントです。

using UnityEngine;
using ClickSample;

public class MovePlatform : MonoBehaviour
{
    [SerializeField]
    private ClickableObject clickableObject;

    [SerializeField]
    private Vector3 moveDirection = Vector3.right;

    [SerializeField]
    private float moveSpeed = 2f;

    private bool isMoving;

    private void Awake()
    {
        if (clickableObject == null)
        {
            clickableObject = GetComponentInParent<ClickableObject>();
        }

        if (clickableObject != null)
        {
            clickableObject.Clicked += OnClicked;
        }
    }

    private void OnDestroy()
    {
        if (clickableObject != null)
        {
            clickableObject.Clicked -= OnClicked;
        }
    }

    private void Update()
    {
        if (isMoving)
        {
            transform.position += moveDirection.normalized * moveSpeed * Time.deltaTime;
        }
    }

    // ClickableObject から呼ばれる
    private void OnClicked(ClickableObject source)
    {
        isMoving = !isMoving;
    }
}

このように、クリックの検出ロジックは共通化しつつ、クリック後の挙動は小さなコンポーネントに分けていくことで、保守しやすく拡張しやすいプロジェクト構成になっていきます。
ぜひ自分のプロジェクトでも、クリック系の処理を ClickableObject でコンポーネント化してみてください。