【Cocos Creator】アタッチするだけ!DragDropSlot (スロット機能)の実装方法【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】DragDropSlot の実装:アタッチするだけで「アイテムのドラッグ&ドロップを受け入れるスロット機能」を実現する汎用スクリプト

本記事では、任意のノードにアタッチするだけで「ドラッグ中のアイテムをドロップして受け取れるスロット」になる汎用コンポーネント DragDropSlot を実装します。
インベントリUI・装備スロット・スキルスロットなど、「ここにアイテムを置ける場所」を簡単に作りたいときに役立ちます。

このコンポーネントは完全に単体で完結しており、他の GameManager やシングルトンに依存しません。
インスペクタで設定するだけで、ドラッグ中アイテムを検出し、ドロップを許可/拒否し、受け入れたアイテムを自動的にスロットの子ノードとして配置できます。


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

1. 機能要件の整理

  • このコンポーネントをアタッチしたノードを「スロット」とみなす。
  • スロットは、ドラッグ中のアイテム(ノード)が自分の上でマウス/タッチを離したときに、それを受け入れる。
  • スロットは「現在入っているアイテムノード」を保持し、インスペクタからも参照できるようにする。
  • スロットごとに、どの種類のアイテムを受け入れるかを文字列タグで制御できるようにする(例:weapon だけ、potion だけ、またはすべて許可)。
  • 受け入れ時/拒否時に イベントコールバック(EventHandler を発行できるようにする。
  • 他のスクリプトに依存せず、「ドラッグ中のアイテム」情報もこのコンポーネント内の静的変数で管理する。
  • ドラッグ中アイテムの判定は、画面座標をワールド座標に変換し、スロットの矩形領域(UI の RectTransform もしくはノードのバウンディングボックス)と重なっているかで行う。

2. 想定するドラッグ&ドロップの流れ

  1. ドラッグ可能なアイテムノード側で、ドラッグ開始/ドラッグ中/ドラッグ終了のタイミングで DragDropSlot の静的メソッドを呼び出す。
  2. スロット側は常に「今ドラッグ中のアイテムがあるか」を静的変数から参照できる。
  3. ドラッグ終了時に、スロットは「ドラッグ終了位置が自分の上かどうか」を判定し、条件を満たす場合はそのアイテムを受け入れる。

本記事では、スロット側のロジックを汎用化することが目的なので、
「ドラッグ可能アイテム側から呼ぶべき静的メソッド」も含めて DragDropSlot に内包します。
(ドラッグ可能アイテム側の最小実装例も後半で触れます。)

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

DragDropSlot に用意する主な @property は以下の通りです。

  • acceptAllItems: boolean
    すべてのアイテムを受け入れるかどうか。
    • true: アイテムタグに関係なく受け入れる。
    • false: acceptedItemTags に含まれるタグのみ受け入れる。
  • acceptedItemTags: string[]
    受け入れを許可するアイテムタグの配列。
    例:["weapon", "sword"] のように設定。
  • allowOverride: boolean
    すでにスロットにアイテムが入っているとき、新しいアイテムで上書きできるかどうか。
    • true: 既存アイテムは外され、新しいアイテムで置き換え。
    • false: 既にアイテムがある場合はドロップを拒否。
  • snapToCenter: boolean
    受け入れたアイテムをスロットの中心に自動配置するかどうか。
  • useUITransform: boolean
    このスロットノードが UI(Canvas 配下)で UITransform を使っているかどうか。
    • true: UITransform の矩形を使ってヒット判定。
    • false: 通常の Node のワールド AABB を使ってヒット判定。
  • currentItemNode: Node | null (readonly)
    現在スロットに入っているアイテムノード。
    コードから参照したい場合に利用(インスペクタでは参照のみ)。
  • onItemAccepted: EventHandler[]
    アイテムを受け入れたときに呼び出されるイベント。
    引数:(slotNode: Node, itemNode: Node)
  • onItemRejected: EventHandler[]
    アイテムを拒否したときに呼び出されるイベント。
    引数:(slotNode: Node, itemNode: Node)
  • onItemRemoved: EventHandler[]
    スロットからアイテムが取り出されたとき(上書き・明示的な remove)のイベント。
    引数:(slotNode: Node, removedItemNode: Node)

アイテムの「タグ」情報は、アイテムノード側の userDatadragTag というキーで文字列を入れておく、という設計にします。
(例:itemNode["dragTag"] = "weapon"; など)
他のカスタムスクリプトに依存しないための最小限の方法です。


TypeScriptコードの実装

以下が完成した DragDropSlot.ts の全コードです。


import { _decorator, Component, Node, UITransform, EventHandler, Vec2, Vec3, Camera, view, Canvas, math, sys } from 'cc';
const { ccclass, property, executeInEditMode, menu } = _decorator;

/**
 * DragDropSlot
 * 
 * 任意のノードを「ドラッグ&ドロップでアイテムを受け入れるスロット」にする汎用コンポーネント。
 * 
 * - 他のカスタムスクリプトに依存しません。
 * - ドラッグ中アイテム情報は、このクラスの static フィールドで管理します。
 * - スロットは、ドラッグ終了位置が自分の領域内であればアイテムを受け入れます。
 * - 受け入れ可否は、タグ(dragTag)と allowOverride などで制御します。
 * 
 * 【前提】
 * - ドラッグ可能なアイテムノード側で、ドラッグ開始/更新/終了時に
 *   DragDropSlot.startGlobalDrag / updateGlobalDrag / endGlobalDrag を呼び出してください。
 * - アイテムノードには string 型のタグ情報を userData などで持たせてください。
 *   例: (itemNode as any).dragTag = "weapon";
 */
@ccclass('DragDropSlot')
@executeInEditMode(true)
@menu('Custom/DragDropSlot')
export class DragDropSlot extends Component {

    // =========================
    // 静的フィールド (グローバルなドラッグ状態管理)
    // =========================

    /** 現在ドラッグ中のアイテムノード(なければ null) */
    private static _currentDraggedItem: Node | null = null;

    /** 現在ドラッグ中アイテムのタグ(なければ null) */
    private static _currentDraggedTag: string | null = null;

    /** 現在のドラッグ位置(スクリーン座標) */
    private static _currentDragScreenPos: Vec2 | null = null;

    /** ドラッグが有効かどうか */
    private static _isDragging: boolean = false;

    // =========================
    // インスペクタプロパティ
    // =========================

    @property({
        tooltip: 'すべてのアイテムを受け入れる場合は true。\nfalse の場合、acceptedItemTags に含まれるタグのみ受け入れます。'
    })
    public acceptAllItems: boolean = true;

    @property({
        tooltip: 'acceptAllItems が false のときに、受け入れを許可するアイテムタグ一覧。\n例: ["weapon", "potion"]',
        type: [String]
    })
    public acceptedItemTags: string[] = [];

    @property({
        tooltip: '既にスロットにアイテムが入っているとき、新しいアイテムで上書きするかどうか。'
    })
    public allowOverride: boolean = true;

    @property({
        tooltip: 'アイテム受け入れ時に、スロットの中心に自動で移動させるかどうか。'
    })
    public snapToCenter: boolean = true;

    @property({
        tooltip: 'このスロットが UI (Canvas 配下) のノードで、UITransform の矩形を使ってヒット判定する場合は true。\n3D や通常ノードで AABB を使う場合は false。'
    })
    public useUITransform: boolean = true;

    @property({
        tooltip: '現在スロットに入っているアイテムノード(読み取り専用)。\nコードから参照したいときに使用します。',
        readonly: true
    })
    public currentItemNode: Node | null = null;

    @property({
        type: [EventHandler],
        tooltip: 'アイテムを受け入れたときに呼び出されるイベント。\n引数: (slotNode: Node, itemNode: Node)'
    })
    public onItemAccepted: EventHandler[] = [];

    @property({
        type: [EventHandler],
        tooltip: 'アイテムを拒否したときに呼び出されるイベント。\n引数: (slotNode: Node, itemNode: Node)'
    })
    public onItemRejected: EventHandler[] = [];

    @property({
        type: [EventHandler],
        tooltip: 'スロットからアイテムが取り出されたときに呼び出されるイベント。\n引数: (slotNode: Node, removedItemNode: Node)'
    })
    public onItemRemoved: EventHandler[] = [];

    // 内部キャッシュ
    private _uiTransform: UITransform | null = null;
    private _cachedCanvas: Canvas | null = null;

    // =========================
    // ライフサイクル
    // =========================

    onLoad() {
        if (this.useUITransform) {
            this._uiTransform = this.getComponent(UITransform);
            if (!this._uiTransform) {
                console.error(
                    '[DragDropSlot] useUITransform が true ですが、UITransform コンポーネントが見つかりません。',
                    'ノードに UITransform が付いているか確認してください。',
                    this.node
                );
            }
        }

        // Canvas のキャッシュ (UI の場合にスクリーン座標変換で利用)
        this._cachedCanvas = this.node.scene?.getComponentInChildren(Canvas) || null;
        if (this.useUITransform && !this._cachedCanvas) {
            console.warn(
                '[DragDropSlot] useUITransform が true ですが、シーン内に Canvas が見つかりません。',
                'スクリーン座標からの変換に問題が出る可能性があります。'
            );
        }
    }

    start() {
        // 実行時に特別な初期化は不要だが、デバッグログを残しておくと便利
        if (!sys.isEditor) {
            // console.log('[DragDropSlot] Slot initialized on node:', this.node.name);
        }
    }

    update(deltaTime: number) {
        // このスロット自身がドラッグ終了イベントを直接受け取るわけではないため、
        // update では特に処理しません。
        // ドラッグ終了タイミングで static endGlobalDrag が呼ばれ、
        // その中で全スロットに対してヒット判定を行うのが基本設計です。
    }

    // =========================
    // パブリック API(インスタンス)
    // =========================

    /**
     * このスロットが、指定されたアイテムを受け入れ可能かどうかを判定します。
     * @param item アイテムノード
     * @param tag アイテムタグ
     */
    public canAcceptItem(item: Node, tag: string | null): boolean {
        // 既にアイテムがある & 上書き不可
        if (this.currentItemNode && !this.allowOverride) {
            return false;
        }

        // タグチェック
        if (!this.acceptAllItems) {
            if (!tag) {
                return false;
            }
            if (this.acceptedItemTags.length > 0) {
                if (!this.acceptedItemTags.includes(tag)) {
                    return false;
                }
            } else {
                // acceptedItemTags が空で acceptAllItems が false の場合は何も受け入れない
                return false;
            }
        }

        return true;
    }

    /**
     * このスロットにアイテムをセットします(強制的に)。
     * 条件チェックなどは行わず、既存アイテムがあれば削除イベントを発行します。
     */
    public setItem(item: Node | null): void {
        if (item === this.currentItemNode) {
            return;
        }

        // 既存アイテムがあれば削除イベント
        if (this.currentItemNode && this.currentItemNode.isValid) {
            const removed = this.currentItemNode;
            this.currentItemNode = null;
            this._emitItemRemoved(removed);
        }

        if (item) {
            this.currentItemNode = item;

            // 親子関係をスロットの子にする
            item.setParent(this.node);

            // スロット中心にスナップ
            if (this.snapToCenter) {
                item.setPosition(Vec3.ZERO);
            }

            this._emitItemAccepted(item);
        }
    }

    /**
     * このスロットから現在のアイテムを取り出します。
     * スロットからは外されますが、親ノードは変更しません(必要なら呼び出し側で変更)。
     */
    public removeItem(): Node | null {
        if (!this.currentItemNode) {
            return null;
        }
        const removed = this.currentItemNode;
        this.currentItemNode = null;
        this._emitItemRemoved(removed);
        return removed;
    }

    // =========================
    // 静的 API(ドラッグ管理)
    // =========================

    /**
     * グローバルなドラッグ開始を通知します。
     * ドラッグ可能アイテム側から呼び出してください。
     * 
     * @param item ドラッグするアイテムノード
     * @param screenPos ドラッグ開始位置(スクリーン座標)
     * @param tag アイテムタグ(例: "weapon")
     */
    public static startGlobalDrag(item: Node, screenPos: Vec2, tag: string | null = null): void {
        DragDropSlot._currentDraggedItem = item;
        DragDropSlot._currentDraggedTag = tag ?? DragDropSlot._getTagFromNode(item);
        DragDropSlot._currentDragScreenPos = screenPos.clone();
        DragDropSlot._isDragging = true;
    }

    /**
     * グローバルなドラッグ位置更新を通知します。
     * ドラッグ中に、ポインタ位置が変わるたびに呼び出してください。
     * 
     * @param screenPos 現在のポインタ位置(スクリーン座標)
     */
    public static updateGlobalDrag(screenPos: Vec2): void {
        if (!DragDropSlot._isDragging) {
            return;
        }
        if (!DragDropSlot._currentDragScreenPos) {
            DragDropSlot._currentDragScreenPos = new Vec2();
        }
        DragDropSlot._currentDragScreenPos.set(screenPos);
    }

    /**
     * グローバルなドラッグ終了を通知します。
     * このタイミングで、すべての DragDropSlot を走査して、
     * ドロップ位置にあるスロットがあればアイテムを渡そうとします。
     * 
     * @param screenPos ドラッグ終了位置(スクリーン座標)
     */
    public static endGlobalDrag(screenPos: Vec2): void {
        if (!DragDropSlot._isDragging || !DragDropSlot._currentDraggedItem) {
            DragDropSlot._resetGlobalDrag();
            return;
        }

        // 位置を更新
        DragDropSlot.updateGlobalDrag(screenPos);

        const item = DragDropSlot._currentDraggedItem;
        const tag = DragDropSlot._currentDraggedTag;

        // シーン内のすべての DragDropSlot を取得
        const slots: DragDropSlot[] = DragDropSlot._getAllSlotsInScene(item.scene);

        let acceptedBySlot: DragDropSlot | null = null;

        for (const slot of slots) {
            if (!slot.enabledInHierarchy) {
                continue;
            }
            if (!slot._isPointerOverSlot(screenPos)) {
                continue;
            }

            // このスロットがアイテムを受け入れ可能かチェック
            if (!slot.canAcceptItem(item, tag)) {
                slot._emitItemRejected(item);
                continue;
            }

            // 受け入れ可能なスロットが見つかった
            acceptedBySlot = slot;
            break;
        }

        if (acceptedBySlot) {
            // スロットにアイテムをセット
            acceptedBySlot.setItem(item);
        } else {
            // どのスロットにも受け入れられなかった
            // (必要に応じて、ここで「どこにもドロップされなかった」処理を追加してもよい)
        }

        // ドラッグ状態をリセット
        DragDropSlot._resetGlobalDrag();
    }

    // =========================
    // 内部ヘルパー (静的)
    // =========================

    private static _resetGlobalDrag(): void {
        DragDropSlot._currentDraggedItem = null;
        DragDropSlot._currentDraggedTag = null;
        DragDropSlot._currentDragScreenPos = null;
        DragDropSlot._isDragging = false;
    }

    /**
     * シーン内の全 DragDropSlot を取得します。
     */
    private static _getAllSlotsInScene(scene: any): DragDropSlot[] {
        if (!scene) {
            return [];
        }
        const result: DragDropSlot[] = [];
        const rootNodes = scene.children as Node[];
        for (const root of rootNodes) {
            root.getComponentsInChildren(DragDropSlot).forEach(slot => result.push(slot));
        }
        return result;
    }

    /**
     * アイテムノードから dragTag を取得します。
     * 他スクリプトに依存しないため、Node の任意プロパティとして参照します。
     */
    private static _getTagFromNode(node: Node): string | null {
        const anyNode = node as any;
        if (typeof anyNode.dragTag === 'string') {
            return anyNode.dragTag as string;
        }
        return null;
    }

    // =========================
    // 内部ヘルパー (インスタンス)
    // =========================

    /**
     * スクリーン座標上のポインタが、このスロットの領域の上にあるかどうかを判定します。
     */
    private _isPointerOverSlot(screenPos: Vec2): boolean {
        if (this.useUITransform) {
            if (!this._uiTransform) {
                return false;
            }

            // Canvas があれば、それを使ってスクリーン座標 → ワールド座標に変換
            let worldPos = new Vec3();
            if (this._cachedCanvas) {
                const uiCamera = this._cachedCanvas.camera;
                if (!uiCamera) {
                    console.warn('[DragDropSlot] Canvas に Camera が設定されていません。', this._cachedCanvas.node);
                    return false;
                }
                // スクリーン座標をワールド座標に変換
                uiCamera.screenToWorld(new Vec3(screenPos.x, screenPos.y, 0), worldPos);
            } else {
                // Canvas が無い場合、view のポートを使って近似的に変換
                const visibleSize = view.getVisibleSize();
                // スクリーン座標を正規化して -0.5 ~ 0.5 の範囲にマッピングしてから、
                // ノードの親の空間に変換するなど、厳密には難しいため、
                // ここでは簡易的に UITransform の世界 AABB で判定します。
                worldPos.set(screenPos.x, screenPos.y, 0);
            }

            const localPos = new Vec3();
            this.node.inverseTransformPoint(localPos, worldPos);

            const rect = this._uiTransform.contentSize;
            const anchor = this._uiTransform.anchorPoint;

            const minX = -rect.width * anchor.x;
            const maxX = rect.width * (1 - anchor.x);
            const minY = -rect.height * anchor.y;
            const maxY = rect.height * (1 - anchor.y);

            return (localPos.x >= minX && localPos.x <= maxX && localPos.y >= minY && localPos.y <= maxY);
        } else {
            // UITransform を使わない場合は、ノードのワールド AABB で判定
            const worldPos = new Vec3(screenPos.x, screenPos.y, 0);
            // AABB 判定のために、カメラが必要だが、ここでは簡易的な実装とします。
            // 実際には 3D カメラの screenToWorld などを利用してください。

            // ノードのワールドバウンディングボックスを取得
            const aabb = this.node.getComponent(UITransform)?.getBoundingBoxToWorld();
            if (!aabb) {
                // UITransform が無い場合は判定できない
                return false;
            }

            return aabb.contains(new Vec2(worldPos.x, worldPos.y));
        }
    }

    private _emitItemAccepted(item: Node): void {
        if (!this.onItemAccepted || this.onItemAccepted.length === 0) {
            return;
        }
        EventHandler.emitEvents(this.onItemAccepted, this.node, item);
    }

    private _emitItemRejected(item: Node): void {
        if (!this.onItemRejected || this.onItemRejected.length === 0) {
            return;
        }
        EventHandler.emitEvents(this.onItemRejected, this.node, item);
    }

    private _emitItemRemoved(item: Node): void {
        if (!this.onItemRemoved || this.onItemRemoved.length === 0) {
            return;
        }
        EventHandler.emitEvents(this.onItemRemoved, this.node, item);
    }
}

コードのポイント解説

  • 静的フィールドによるドラッグ状態管理
    _currentDraggedItem, _currentDraggedTag, _currentDragScreenPos, _isDragging で、
    「今ドラッグされているアイテムは何か」「どこにあるか」DragDropSlot クラス全体で共有しています。
  • ドラッグ開始/更新/終了の静的メソッド
    • startGlobalDrag(item, screenPos, tag?)
    • updateGlobalDrag(screenPos)
    • endGlobalDrag(screenPos)

    ドラッグ可能アイテム側からこれらを呼び出すことで、スロット側は一切アイテム側の実装を知らなくてよい設計になっています。
    endGlobalDrag では、シーン内の全 DragDropSlot を走査し、
    _isPointerOverSlot でヒット判定 → canAcceptItem で受け入れ可否判定 → setItem で配置、という流れです。

  • タグによる受け入れ制御
    acceptAllItemsfalse のとき、acceptedItemTags に含まれるタグだけ受け入れます。
    タグは startGlobalDrag の第3引数で渡すか、itemNode の任意プロパティ dragTag から自動取得します。
  • UITransform を利用したヒット判定
    useUITransformtrue の場合、UITransformcontentSizeanchorPoint を使って、
    スクリーン座標 → ワールド座標 → ローカル座標変換し、矩形内に入っているかどうかをチェックします。
  • EventHandler によるコールバック
    onItemAccepted, onItemRejected, onItemRemoved はすべて EventHandler[] で、
    インスペクタから任意のコンポーネントメソッドを紐づけられます。
    EventHandler.emitEvents(handlers, this.node, itemNode); のように、
    第一引数: slotNode, 第二引数: itemNode でコールされます。

使用手順と動作確認

1. DragDropSlot.ts の作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を DragDropSlot.ts に変更します。
  3. 作成された DragDropSlot.ts をダブルクリックして開き、
    先ほどの 実装コード全文 を貼り付けて保存します。

2. テスト用 UI シーンの準備

  1. Hierarchy パネルで右クリック → Create → UI → Canvas を作成します(既にあれば不要)。
  2. Canvas 配下に 背景用の UI ノード を作成します。
    例:Canvas を右クリック → Create → UI → SpriteBG ノードを作成し、適当なスプライトを設定します。

3. スロットノードの作成と DragDropSlot のアタッチ

  1. Canvas を右クリック → Create → UI → Sprite でスロット用ノードを作成します。
    ノード名を Slot1 などにしておきます。
  2. Slot1 ノードを選択し、Inspector の Add Component ボタンをクリックします。
  3. Custom → DragDropSlot を選択してアタッチします。
  4. Inspector で DragDropSlot のプロパティを設定します:
    • Accept All Items: とりあえず true(すべて受け入れ)
    • Allow Override: true(上書き許可)
    • Snap To Center: true(中心にスナップ)
    • Use UI Transform: true(UI スロットの場合)

これで Slot1 ノードは「ドラッグ&ドロップを受け入れられるスロット」として準備完了です。
次に、ドラッグ可能なアイテムノードを簡単に実装して動作確認します。

4. 簡易ドラッグアイテムコンポーネントの実装(テスト用)

この部分は「汎用スロット」の動作確認用であり、DragDropSlot 自体はこのスクリプトに依存しません
あなたのプロジェクトに合わせて自由にドラッグ処理を実装して構いません。

テスト用に、非常にシンプルなドラッグコンポーネント SimpleDraggableItem.ts を作ってみます。

  1. Assets パネルで右クリック → Create → TypeScript を選択し、
    ファイル名を SimpleDraggableItem.ts にします。
  2. 以下のコードを貼り付けます:

import { _decorator, Component, Node, EventTouch, Vec2, Vec3, UITransform } from 'cc';
import { DragDropSlot } from './DragDropSlot';
const { ccclass, property } = _decorator;

/**
 * テスト用の非常にシンプルなドラッグコンポーネント。
 * - タッチ開始でドラッグ開始
 * - タッチ移動でノードを追従
 * - タッチ終了で DragDropSlot.endGlobalDrag を呼ぶ
 */
@ccclass('SimpleDraggableItem')
export class SimpleDraggableItem extends Component {

    @property({
        tooltip: 'このアイテムのタグ。DragDropSlot 側の acceptedItemTags とマッチさせます。'
    })
    public dragTag: string = 'item';

    private _isDragging: boolean = false;

    onEnable() {
        this.node.on(Node.EventType.TOUCH_START, this._onTouchStart, this);
        this.node.on(Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
        this.node.on(Node.EventType.TOUCH_END, this._onTouchEnd, this);
        this.node.on(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
    }

    onDisable() {
        this.node.off(Node.EventType.TOUCH_START, this._onTouchStart, this);
        this.node.off(Node.EventType.TOUCH_MOVE, this._onTouchMove, this);
        this.node.off(Node.EventType.TOUCH_END, this._onTouchEnd, this);
        this.node.off(Node.EventType.TOUCH_CANCEL, this._onTouchEnd, this);
    }

    private _onTouchStart(event: EventTouch) {
        this._isDragging = true;

        const loc = event.getLocation();
        // DragDropSlot にドラッグ開始を通知
        DragDropSlot.startGlobalDrag(this.node, new Vec2(loc.x, loc.y), this.dragTag);

        // タッチ位置にノードを移動
        const uiTrans = this.getComponent(UITransform);
        if (uiTrans) {
            const worldPos = new Vec3(loc.x, loc.y, 0);
            this.node.setWorldPosition(worldPos);
        }
    }

    private _onTouchMove(event: EventTouch) {
        if (!this._isDragging) {
            return;
        }
        const loc = event.getLocation();
        // DragDropSlot にドラッグ位置更新を通知
        DragDropSlot.updateGlobalDrag(new Vec2(loc.x, loc.y));

        // ノードをタッチ位置に追従
        const worldPos = new Vec3(loc.x, loc.y, 0);
        this.node.setWorldPosition(worldPos);
    }

    private _onTouchEnd(event: EventTouch) {
        if (!this._isDragging) {
            return;
        }
        this._isDragging = false;

        const loc = event.getLocation();
        // DragDropSlot にドラッグ終了を通知
        DragDropSlot.endGlobalDrag(new Vec2(loc.x, loc.y));
    }
}

エディタでの設定

  1. Canvas を右クリック → Create → UI → Sprite でアイテム用ノードを作成します。
    ノード名を Item1 などにします。
  2. Item1 ノードに適当なスプライト画像を設定します。
  3. Item1 ノードを選択し、Inspector の Add ComponentCustom → SimpleDraggableItem を追加します。
  4. SimpleDraggableItemDrag Tagitem のままにするか、
    例として weapon に変更しておきます。

5. スロットのタグ制限を試す

DragDropSlot を使って、特定タグだけを受け入れるスロット を作ってみます。

  1. Slot1 ノード(DragDropSlot が付いている)を選択します。
  2. Inspector の DragDropSlot で以下のように設定します:
    • Accept All Items: false
    • Accepted Item Tags: 要素数を 1 にして、"weapon" と入力
    • Allow Override: true
    • Snap To Center: true
    • Use UI Transform: true
  3. Item1 の SimpleDraggableItem.dragTag"weapon" にしておきます。

この状態でゲームを再生し、Item1 をドラッグして Slot1 の上で離すと、スロットに吸い込まれて中心に配置されます
もし dragTag"potion" などに変えて同じ操作をすると、
acceptedItemTags = ["weapon"] のため、スロットはアイテムを拒否します。

6. イベントコールバックでログを出す(動作確認)

受け入れ/拒否/削除イベントが正しく動いているか確認するため、簡単なログ出力コンポーネントを作成します。

  1. Assets パネルで右クリック → Create → TypeScriptSlotLogger.ts を作成します。
  2. 以下のコードを貼り付けます:

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

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

    // DragDropSlot の EventHandler から呼ばれる想定
    public onItemAccepted(slotNode: Node, itemNode: Node) {
        console.log(`[SlotLogger] Accepted item "${itemNode.name}" into slot "${slotNode.name}"`);
    }

    public onItemRejected(slotNode: Node, itemNode: Node) {
        console.log(`[SlotLogger] Rejected item "${itemNode.name}" from slot "${slotNode.name}"`);
    }

    public onItemRemoved(slotNode: Node, itemNode: Node) {
        console.log(`[SlotLogger] Removed item "${itemNode.name}" from slot "${slotNode.name}"`);
    }
}

エディタでの接続手順

  1. Slot1 ノードを選択し、Add Component → Custom → SlotLogger を追加します。
  2. Inspector の DragDropSlot コンポーネント内で:
    • On Item Accepted の右側の「+」ボタンをクリック。
    • 追加された EventHandler の Target に Slot1 ノードをドラッグ&ドロップ。
    • Component ドロップダウンから SlotLogger を選択。
    • Handler ドロップダウンから onItemAccepted を選択。
  3. 同様に:
    • On Item RejectedSlotLogger.onItemRejected
    • On Item RemovedSlotLogger.onItemRemoved

ゲームを再生してドラッグ&ドロップを行うと、Console に受け入れ/拒否/削除のログが出力されます。
これでスロットのイベント連携も確認できます。


まとめ

本記事では、Cocos Creator 3.8 向けに、

  • DragDropSlot:任意のノードを「アイテムを受け入れるスロット」に変える汎用コンポーネント

を実装しました。

ポイントを整理すると:

  • 完全な独立性:他の GameManager やシングルトンに依存せず、
    ドラッグ中アイテムの情報も DragDropSlot 自身の静的フィールドで完結。
  • インスペクタで柔軟に制御
    • 受け入れタグ(acceptedItemTags
    • 上書き可否(allowOverride
    • 自動スナップ(snapToCenter
    • UITransform を使うかどうか(useUITransform
  • EventHandler による拡張性
    • アイテム受け入れ/拒否/削除のタイミングで、
      任意のコンポーネントメソッドを呼び出せる。
  • 用途
    • インベントリのアイテムスロット
    • 装備スロット(武器・防具・アクセサリなど)
    • スキルショートカットスロット
    • クラフトレシピの材料スロット

一度この DragDropSlot をプロジェクトに組み込んでおけば、
新しい UI 画面を作るときも 「ノードにアタッチして、タグとイベントを設定するだけ」 でドラッグ&ドロップ対応のスロットを量産できます。
結果として、UI 実装の再利用性と開発スピードが大きく向上します。

あとは、あなたのゲームに合わせて:

  • アイテムデータ(ID, スタック数, アイコンなど)をノードに持たせる
  • スロットが受け入れたときに、インベントリデータを更新する
  • スロット間でアイテムを入れ替えるロジックを追加する

といった形で拡張していけば、本格的なドラッグ&ドロップUI を構築できます。

必要であれば、次のステップとして「スロット間のアイテムスワップ」「スタック数の統合」など、さらに一段進んだ汎用コンポーネントも設計できます。

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をコピーしました!