【Cocos Creator】アタッチするだけ!ClickableObject (クリック判定)の実装方法【TypeScript】

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】ClickableObject の実装:アタッチするだけで「クリック判定とイベント通知」を実現する汎用スクリプト

このコンポーネントは、任意のノードにアタッチするだけで「そのノードがクリックされたこと」を検知し、コールバックやイベント的な仕組みで他のノードに通知できる汎用クリック判定コンポーネントです。UI ボタンだけでなく、2D スプライトや 3D オブジェクトなど、あらゆるノードを「クリック可能オブジェクト」に変える用途で役立ちます。

コンポーネントの設計方針

1. 要件整理

  • このコンポーネントをアタッチしたノードがクリックされたことを検知する。
  • クリック検知は「UIノード(Widget/Control 系)」でも「通常ノード(Sprite など)」でも使えるようにする。
  • クリックされた際に「シグナル(通知)」を出す。
  • 外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結する。
  • 通知方法は、インスペクタで設定したコールバックを呼び出す形で実現する(Cocos Creator では Godot の Signal に近い仕組みとして EventHandler がある)。
  • クリック判定のために、UI 系には UITransform + Button / Widget があるが、ここではより汎用的に「マウス座標とノードの矩形(2D)」によるヒット判定を行う。

2. 設計アプローチ

完全な独立性を保つため、以下のようなアプローチをとります。

  • 内部でマウスイベントを購読し、クリック位置が自身のノードの当たり判定領域内かどうかを計算する。
  • 当たり判定は 2D を基本とし、UITransform があればその矩形を基準にする。
  • UITransform がない場合は警告を出し、クリック判定を無効化する(防御的実装)。
  • クリックされたときに、インスペクタから設定できる EventHandler[] を順に呼び出して「シグナル相当」の通知を行う。
  • イベントの種類(左クリックのみ / 右クリックも許可など)をプロパティで切り替えられるようにする。
  • クリック時に簡易的なデバッグログを出すかどうかを設定できるようにする。

3. インスペクタで設定可能なプロパティ

ClickableObject コンポーネントに用意するプロパティと役割は以下の通りです。

  • enabledClick(boolean)
    「クリック判定を有効にするかどうか」
    true: クリック判定を行う。
    false: 一時的にクリック判定を無効にする(イベントリスナは残るが、内部で判定をスキップ)。
  • acceptRightButton(boolean)
    「右クリックも有効にするかどうか」
    false: 左クリックのみ有効。
    true: 左クリックと右クリックの両方で反応する。
  • clickMaxMoveDistance(number)
    「クリックとみなす最大移動距離(ピクセル)」
    マウス押下位置と離した位置の距離がこの値以下ならクリックとみなす。
    ドラッグ操作とクリックを区別したい場合に使用。0 を指定すると距離チェックを無効化。
  • debugLog(boolean)
    「クリック時にコンソールへログを出すかどうか」
    true: クリック検知時に console.log を出力。
    false: ログ出力なし。
  • clickEvents(EventHandler[])
    「クリックされたときに呼び出すコールバック一覧」
    Cocos Creator の標準的な UI イベント設定と同じ形式。
    インスペクタから、「ターゲットノード」「コンポーネント」「メソッド名」を指定することで、クリック時に任意のメソッドを呼び出せる。

これにより、ClickableObject は「アタッチするだけでクリック検知ができ、外部へは EventHandler で通知する」という汎用的なコンポーネントになります。

TypeScriptコードの実装


import {
    _decorator,
    Component,
    Node,
    EventMouse,
    input,
    Input,
    UITransform,
    Vec2,
    Vec3,
    Camera,
    find,
    EventHandler,
} from 'cc';
const { ccclass, property } = _decorator;

/**
 * ClickableObject
 * 任意のノードを「クリック可能」にする汎用コンポーネント。
 * - 2D UITransform を利用して矩形ヒット判定を行う。
 * - クリック時に EventHandler 経由で外部へ通知する。
 */
@ccclass('ClickableObject')
export class ClickableObject extends Component {

    @property({
        tooltip: 'クリック判定を有効にするかどうか。\nfalse にすると一時的にクリックを無効化します。',
    })
    public enabledClick: boolean = true;

    @property({
        tooltip: '右クリックもクリックとして扱うかどうか。\nON: 左右どちらのボタンでも反応します。',
    })
    public acceptRightButton: boolean = false;

    @property({
        tooltip: 'クリックとみなす最大移動距離(ピクセル)。\n0 以下で距離チェックを無効化します。',
    })
    public clickMaxMoveDistance: number = 5;

    @property({
        tooltip: 'クリック検知時にデバッグログを出力するかどうか。',
    })
    public debugLog: boolean = false;

    @property({
        type: [EventHandler],
        tooltip: 'クリックされたときに呼び出すイベントハンドラ一覧。\nUI Button と同様の形式で設定できます。',
    })
    public clickEvents: EventHandler[] = [];

    // 内部状態:マウス押下位置
    private _pressPos: Vec2 | null = null;
    // 内部状態:クリック判定用フラグ
    private _isPressing: boolean = false;
    // キャッシュ:UITransform
    private _uiTransform: UITransform | null = null;
    // 使用するカメラ(2D 想定)。null の場合は Canvas 下の Camera を自動検出
    private _camera2D: Camera | null = null;

    onLoad() {
        // 必要なコンポーネントの取得
        this._uiTransform = this.getComponent(UITransform);
        if (!this._uiTransform) {
            console.warn(
                `[ClickableObject] Node "${this.node.name}" に UITransform コンポーネントがありません。` +
                'クリック判定は UITransform の矩形を利用するため、2D ノードとして使用する場合は ' +
                '「Add Component → UI → UITransform」を追加してください。'
            );
        }

        // カメラの自動検出(Canvas 配下の Camera を想定)
        this._camera2D = this._find2DCamera();
        if (!this._camera2D) {
            console.warn(
                '[ClickableObject] 2D カメラが見つかりませんでした。' +
                'クリック位置のワールド座標変換が正しく行えない可能性があります。' +
                'Canvas 配下に Camera コンポーネントを持つノードを配置してください。'
            );
        }
    }

    start() {
        // グローバル入力イベントへ登録
        input.on(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
        input.on(Input.EventType.MOUSE_UP, this._onMouseUp, this);
    }

    onDestroy() {
        // イベント登録解除(メモリリーク防止)
        input.off(Input.EventType.MOUSE_DOWN, this._onMouseDown, this);
        input.off(Input.EventType.MOUSE_UP, this._onMouseUp, this);
    }

    /**
     * 2D 用の Camera をシーンから自動検出する。
     * ここでは簡易的に Canvas/Camera を探す。
     */
    private _find2DCamera(): Camera | null {
        // よくある構成: Canvas/Camera
        const canvas = find('Canvas');
        if (!canvas) {
            return null;
        }
        let camNode = canvas.getChildByName('Camera');
        if (!camNode) {
            // 子孫から Camera を探す
            const cameras: Camera[] = canvas.getComponentsInChildren(Camera);
            return cameras[0] || null;
        }
        const cam = camNode.getComponent(Camera);
        return cam || null;
    }

    private _onMouseDown(event: EventMouse) {
        if (!this.enabledClick) {
            return;
        }

        // 対象ボタンチェック(左/右)
        const button = event.getButton();
        if (button !== EventMouse.BUTTON_LEFT && button !== EventMouse.BUTTON_RIGHT) {
            return;
        }
        if (!this.acceptRightButton && button === EventMouse.BUTTON_RIGHT) {
            return;
        }

        // マウス押下位置を保存
        const loc = event.getLocation(); // 画面座標 (px)
        this._pressPos = new Vec2(loc.x, loc.y);
        this._isPressing = true;
    }

    private _onMouseUp(event: EventMouse) {
        if (!this.enabledClick) {
            return;
        }
        if (!this._isPressing) {
            return;
        }

        // 対象ボタンチェック(左/右)
        const button = event.getButton();
        if (button !== EventMouse.BUTTON_LEFT && button !== EventMouse.BUTTON_RIGHT) {
            return;
        }
        if (!this.acceptRightButton && button === EventMouse.BUTTON_RIGHT) {
            return;
        }

        const releaseLoc = event.getLocation();
        const releasePos = new Vec2(releaseLoc.x, releaseLoc.y);

        // 距離チェック(ドラッグ判定)
        if (this._pressPos && this.clickMaxMoveDistance > 0) {
            const dist = Vec2.distance(this._pressPos, releasePos);
            if (dist > this.clickMaxMoveDistance) {
                // クリックではなくドラッグとみなす
                this._resetPressState();
                return;
            }
        }

        // マウスアップ位置がこのノードの矩形内かどうかをチェック
        const isHit = this._isPointInsideNode(releasePos);
        if (isHit) {
            this._onClicked(event);
        }

        this._resetPressState();
    }

    private _resetPressState() {
        this._isPressing = false;
        this._pressPos = null;
    }

    /**
     * 画面座標 (px) 上の点が、このノードの UITransform 矩形内かどうかを判定する。
     */
    private _isPointInsideNode(screenPos: Vec2): boolean {
        if (!this._uiTransform || !this._camera2D) {
            return false;
        }

        // 画面座標 → ワールド座標へ変換
        const worldPos = this._camera2D.screenToWorld(new Vec3(screenPos.x, screenPos.y, 0));

        // ワールド座標 → ローカル座標へ変換
        const localPos = this.node.inverseTransformPoint(new Vec3(), worldPos);

        // UITransform のローカル矩形で判定
        const uiTrans = this._uiTransform;
        const width = uiTrans.width;
        const height = uiTrans.height;

        const halfW = width * 0.5;
        const halfH = height * 0.5;

        const x = localPos.x;
        const y = localPos.y;

        const inside =
            x >= -halfW && x <= halfW &&
            y >= -halfH && y <= halfH;

        return inside;
    }

    /**
     * クリックが確定したときに呼ばれる内部メソッド。
     * EventHandler 経由で外部へ通知する。
     */
    private _onClicked(event: EventMouse) {
        if (this.debugLog) {
            console.log(
                `[ClickableObject] Node "${this.node.name}" がクリックされました。` +
                `button=${event.getButton()}`
            );
        }

        // 設定された EventHandler を順に実行
        if (this.clickEvents && this.clickEvents.length > 0) {
            EventHandler.emitEvents(this.clickEvents, this.node);
        }
    }
}

コードのポイント解説

  • onLoad
    • UITransform を取得し、なければ警告を出します(クリック判定は UITransform の矩形に依存)。
    • Canvas 配下の Camera を自動的に探し、クリック位置の座標変換に使用します。
  • start
    • input.on でグローバルなマウスダウン / マウスアップイベントを購読します。
  • onDestroy
    • イベント購読を解除し、シーンから削除された後もイベントが飛んでこないようにします。
  • _onMouseDown / _onMouseUp
    • 左クリック / 右クリックのフィルタリング。
    • 押下位置と離した位置の距離チェック(ドラッグ除外)。
    • マウスアップ位置が自ノードの矩形内かどうかを判定し、ヒットしていれば _onClicked を呼び出します。
  • _isPointInsideNode
    • 画面座標 → カメラでワールド座標 → ノードのローカル座標と変換。
    • UITransform.width/height から矩形領域(中央原点)を求め、内外判定します。
  • _onClicked
    • debugLog が有効ならログを出力。
    • EventHandler.emitEvents(this.clickEvents, this.node) で、インスペクタから設定されたすべてのコールバックを呼び出します。第二引数 this.node は、受け側メソッドの引数として渡せます。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. ファイル名を ClickableObject.ts に変更します。
  4. 作成した ClickableObject.ts をダブルクリックしてエディタで開き、上記の TypeScript コードをまるごと貼り付けて保存します。

2. テスト用ノードの作成(2D UI の例)

  1. Hierarchy パネルで右クリック → Create → UI → Canvas を作成(既にある場合は不要)。
  2. Canvas を選択し、右クリック → Create → UI → Sprite で子ノードとして Sprite を作成します。
  3. Sprite ノードを選択し、Inspector で以下を確認します。
    • UITransform コンポーネントが付いていること(UI Sprite なら自動で付いています)。
    • 幅・高さをクリックしやすいサイズに設定(例: Width=200, Height=100)。

3. ClickableObject コンポーネントのアタッチ

  1. Hierarchy で先ほどの Sprite ノードを選択します。
  2. Inspector 下部の Add Component ボタンをクリックします。
  3. Custom カテゴリから ClickableObject を選択して追加します。

Inspector 上には、次のようなプロパティが表示されます。

  • Enabled Click(enabledClick)
  • Accept Right Button(acceptRightButton)
  • Click Max Move Distance(clickMaxMoveDistance)
  • Debug Log(debugLog)
  • Click Events(clickEvents)

4. クリック時の処理を受け取るスクリプトの用意

ClickableObject は「シグナル相当」の仕組みとして EventHandler を使うため、受け側のコンポーネントで任意のメソッドを用意します。

  1. Assets パネルで右クリック → Create → TypeScript を選択し、ClickReceiver.ts という名前で作成します。
  2. 以下のような簡単なコードを記述します。

import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;

@ccclass('ClickReceiver')
export class ClickReceiver extends Component {

    // ClickableObject の EventHandler から呼び出される想定のメソッド
    public onClickedFromObject(sender: Node) {
        console.log(`[ClickReceiver] クリック通知を受信: from="${sender.name}"`);
    }
}
  1. Hierarchy で任意のノード(例: Canvas ノード)を選択し、Add Component → Custom → ClickReceiver を追加します。

5. Click Events の設定(シグナルの接続)

  1. 再び、ClickableObject をアタッチした Sprite ノードを選択します。
  2. Inspector の ClickableObject → Click Events の右側にある「+」ボタンをクリックして、新しいイベント要素を追加します。
  3. 追加された要素の各項目を次のように設定します。
    • Target: ClickReceiver コンポーネントをアタッチしたノード(例: Canvas)をドラッグ&ドロップ。
    • Component: ドロップダウンから ClickReceiver を選択。
    • Handler: ドロップダウンから onClickedFromObject を選択。
    • CustomEventData: 今回は空のままでOK。

これで、Sprite がクリックされると ClickReceiver の onClickedFromObject メソッドが呼ばれるようになります。

6. プロパティの調整と動作確認

  1. ClickableObject のプロパティを次のように設定してみます。
    • enabledClick: チェック ON(true)。
    • acceptRightButton: チェック OFF(false)※左クリックのみ有効。
    • clickMaxMoveDistance: 10(ドラッグ判定を少しゆるく)。
    • debugLog: チェック ON(true)。
  2. エディタ右上の Play ボタンでシーンをプレビューします。
  3. ゲームビュー上で Sprite を左クリックします。
    • コンソールに [ClickableObject] Node "Sprite" がクリックされました。 のようなログが表示されます。
    • 同時に、[ClickReceiver] クリック通知を受信: from="Sprite" というログも表示されます。
  4. 右クリックしても反応しないことを確認します(acceptRightButton=false のため)。
  5. 次に acceptRightButton を ON にして再生し、右クリックでも同様にイベントが飛ぶことを確認します。

7. よくあるつまずきポイント

  • UITransform がない
    ノードに UITransform が付いていないと、クリック判定が機能しません。
    Sprite(UI)や Label(UI)など UI ノードを使うか、「Add Component → UI → UITransform」を追加してください。
  • Canvas / Camera がない
    Canvas 配下に Camera を持つノードがないと、座標変換が正しく行えません。
    デフォルトの 2D テンプレートを使うか、Canvas の子として Camera を追加し、2D 用に設定してください。
  • Click Events の設定漏れ
    ClickableObject 自体はクリックを検知しますが、clickEvents に何も設定していないと外部へ通知されません。
    必ず「Target / Component / Handler」を設定してください。

まとめ

本記事では、Cocos Creator 3.8 向けに、任意のノードを「クリック可能オブジェクト」に変える汎用コンポーネント ClickableObject を実装しました。

  • UITransform を持つ 2D ノードにアタッチするだけでクリック判定が可能。
  • クリック時には EventHandler[] を通じて外部コンポーネントへ通知でき、Godot の Signal に近い使い勝手で利用できる。
  • 左クリック / 右クリック対応、ドラッグとの判別、デバッグログ出力など、実運用で必要になる調整項目をインスペクタから設定可能。
  • GameManager などの外部スクリプトに一切依存せず、このコンポーネント単体で完結する設計。

この ClickableObject をベースに、例えば「ホバー時のハイライト表示」「クリック時のアニメーション再生」「3D オブジェクトへのレイキャスト対応」などを追加していけば、より強力なインタラクションフレームワークを構築できます。まずは本記事のコードをコピーしてプロジェクトに組み込み、シーン内のさまざまなノードにアタッチして挙動を確認してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!