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;
}
}
}
}
使い方の手順
ここからは、実際にシーンに配置して動かす手順を見ていきましょう。
手順①:クリック入力マネージャをシーンに置く
- 空のGameObjectを作成し、名前を
ClickInputManagerなどにします。 - 上記コードの
ClickInputManagerコンポーネントをアタッチします。 Target Cameraにクリック判定に使いたいカメラ(通常はMain Camera)をドラッグ&ドロップします。
未設定でもCamera.mainを自動で使いますが、複数カメラがある場合は明示的に指定した方が安全です。Clickable Layer Maskで、クリック対象にしたいレイヤーを選びます(デフォルトは全レイヤー)。
手順②:クリックされるオブジェクトに ClickableObject を付ける
例として「プレイヤーキャラ」をクリック可能にする場合:
- プレイヤーのプレハブ(またはシーン上のGameObject)を選択します。
- 3Dなら
Collider(BoxCollider など)、2DならCollider2D(BoxCollider2D など)が付いていることを確認します。
※レイキャストは Collider/Collider2D を頼りにヒット判定を行います。 - プレイヤーに
ClickableObjectコンポーネントを追加します。 - デバッグでログを見たい場合は
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を使ってインスペクターだけで完結させる
コードを書かずに、インスペクターでシグナルをつなぐこともできます。
- クリック対象オブジェクトに
ClickableObjectを追加する。 - インスペクターで
On Clicked(UnityEvent)にイベントを追加する。 - 例えば「動く床」の場合:
- 床の 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 でコンポーネント化してみてください。
