【Cocos Creator】アタッチするだけ!CoinMagnet (コイン磁石)の実装方法【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】CoinMagnet(コイン磁石)の実装:アタッチするだけで「指定タグのコインだけを強力に吸い寄せる」汎用スクリプト

本記事では、プレイヤーなどのノードにアタッチするだけで、周囲の「コイン」だけを自動的に吸い寄せる CoinMagnet コンポーネントを実装します。
「コインだけを対象にしたい」「他のアイテムは動かしたくない」「既存のプロジェクトに簡単に組み込みたい」といった場面でそのまま使える、完全に独立した汎用スクリプトとして設計します。


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

機能要件の整理

  • このコンポーネントをアタッチしたノード(例: プレイヤー)を中心に、一定の半径内にある「コイン」ノードを検出する。
  • 検出されたコインは、プレイヤーに向かって滑らかに移動させる。
  • コインだけを対象にするため、ノード名・グループ名ではなく「タグ(文字列)」で判定できるようにする。
  • 他のカスタムスクリプト(GameManager 等)には一切依存せず、この1ファイルだけで完結させる。
  • コイン側にも特別なスクリプトは不要。
    ただし、「コインである」ことを判別するためのタグ文字列をどこかに設定しておく必要がある。

外部依存をなくすためのアプローチ

「コインだけを吸い寄せる」には、通常なら「Coin」コンポーネントや共通のManagerを使いたくなりますが、本記事の前提では外部スクリプトへの依存は禁止です。

そこで、以下のルールで設計します。

  1. コインノードの name または 自前の識別用プロパティ(例: Node の layergroup を使うと、既存プロジェクトの設計と衝突しやすい。
  2. 代わりに、「コイン判定は Node の name を使う」か「特定の親ノード配下をコイン置き場とみなす」という2通りを用意し、インスペクタから選べるようにする。

その結果、以下の2つの方法で「コインだけを対象にする」ことができます。

  • 名前一致方式:
    • コインノードの name を「Coin」などに統一しておき、CoinMagnet 側で targetName を「Coin」に設定する。
  • 親ノード指定方式:
    • シーン上に「CoinsRoot」などのノードを作り、その子ノードをすべてコインとして扱う。
    • CoinMagnet 側で coinsRoot にそのノードを指定する。

どちらか一方だけを使ってもよいですし、両方指定した場合は「親ノード配下のうち、名前が一致するノードだけ」をコインとして扱うようにします。

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

以下のようなプロパティを用意します。

  • enabledMagnet: boolean
    • 磁石機能の ON / OFF。
    • チェックを外すと一時的に吸引を止めたい時に使える。
  • radius: number
    • 吸引を開始する半径(ワールド座標距離)。
    • この距離以内に入ったコインだけが対象になる。
    • 例: 300〜600 くらいから調整開始。
  • moveSpeed: number
    • コインがプレイヤーに向かって移動する最大速度(単位: 単位距離/秒)。
    • 大きいほど強力に吸い寄せる。
  • acceleration: number
    • コインの速度が時間とともに増加する加速度。
    • 0 にすると一定速度で移動、正の値にすると近づくほど加速して気持ちよく吸い込まれる演出が可能。
  • minDistanceToStop: number
    • プレイヤー中心からこの距離より近づいたら「到達」とみなす距離。
    • 到達した後の処理(コインの削除など)はプロジェクト側に任せるため、本コンポーネントでは「それ以上近づけない」だけを行う。
  • scanInterval: number
    • 周囲のコインをスキャンする間隔(秒)。
    • 0 にすると毎フレーム find し続けて負荷が高くなる可能性があるため、0.1〜0.3 秒程度を推奨。
  • coinsRoot: Node | null
    • (任意)コインをまとめて配置している親ノード。
    • 指定した場合、その子孫ノードだけをスキャン対象にする。
    • 未指定の場合、シーン全体から検索する。
  • targetName: string
    • (任意)コインノードの name と一致させるための文字列。
    • 空文字の場合は「名前でのフィルタリングを行わない」。
  • debugDrawRadius: boolean
    • 吸引半径を簡易的にデバッグ表示するかどうか。
    • エディタまたはプレイ中に、コンソールログで半径情報を出力する簡易デバッグ用。

また、防御的実装として:

  • 自分自身の Node は必ず存在するため、そのまま使用。
  • coinsRoot が未設定の場合は シーン全体から find で検索する。
  • 想定外の状況(null チェックなど)は console.warn でログを出す。

TypeScriptコードの実装


import { _decorator, Component, Node, Vec3, math, director, Scene } from 'cc';
const { ccclass, property } = _decorator;

/**
 * CoinMagnet
 * - このコンポーネントをアタッチしたノードを中心に、
 *   指定された条件に合う「コイン」ノードだけを吸い寄せる汎用スクリプト。
 *
 * 前提:
 * - コインノードは name を targetName と一致させるか、
 *   coinsRoot の子孫として配置しておく。
 * - コイン側には特別なスクリプトは不要。
 */
@ccclass('CoinMagnet')
export class CoinMagnet extends Component {

    @property({
        tooltip: '磁石機能の ON / OFF。false にすると一時的に吸引を停止します。',
    })
    public enabledMagnet: boolean = true;

    @property({
        tooltip: 'コインを吸引し始める半径(ワールド座標距離)。この距離以内のコインだけが対象になります。',
        min: 0,
    })
    public radius: number = 400;

    @property({
        tooltip: 'コインがプレイヤーに向かって移動する基本速度(単位: 距離/秒)。',
        min: 0,
    })
    public moveSpeed: number = 600;

    @property({
        tooltip: 'コインの速度を時間とともに増加させる加速度。0 で一定速度、正の値で徐々に加速します。',
        min: 0,
    })
    public acceleration: number = 800;

    @property({
        tooltip: 'プレイヤー中心からこの距離より近づいたら到達とみなし、それ以上は近づけません。',
        min: 0,
    })
    public minDistanceToStop: number = 20;

    @property({
        tooltip: '周囲のコインをスキャンする間隔(秒)。0.1〜0.3 くらいが目安です。',
        min: 0,
        max: 5,
    })
    public scanInterval: number = 0.2;

    @property({
        tooltip: 'コインをまとめて配置している親ノード。未設定の場合はシーン全体から検索します。',
    })
    public coinsRoot: Node | null = null;

    @property({
        tooltip: 'コインノードの name と一致させる文字列。空文字の場合は name でのフィルタリングを行いません。',
    })
    public targetName: string = 'Coin';

    @property({
        tooltip: 'true にすると、起動時に現在の半径などをログ出力してデバッグしやすくします。',
    })
    public debugDrawRadius: boolean = false;

    // 内部用: スキャン用タイマー
    private _scanTimer: number = 0;

    // 内部用: 現在吸引対象として認識しているコインノード一覧
    private _candidateCoins: Node[] = [];

    // 内部用: コインごとの現在速度(加速度を掛けるため)
    private _coinSpeeds: Map<Node, number> = new Map<Node, number>();

    onLoad() {
        if (this.debugDrawRadius) {
            console.log(
                `[CoinMagnet] onLoad: radius=${this.radius}, moveSpeed=${this.moveSpeed}, ` +
                `acceleration=${this.acceleration}, targetName="${this.targetName}", coinsRoot=${this.coinsRoot ? this.coinsRoot.name : 'null'}`
            );
        }

        if (!this.node) {
            console.error('[CoinMagnet] このコンポーネントは Node にアタッチされている必要があります。');
        }
    }

    start() {
        // 初回に一度スキャンしておく
        this._scanCoins();
    }

    update(deltaTime: number) {
        if (!this.enabledMagnet) {
            return;
        }

        // 一定間隔でコイン候補をスキャン
        this._scanTimer += deltaTime;
        if (this._scanTimer >= this.scanInterval) {
            this._scanTimer = 0;
            this._scanCoins();
        }

        // 吸引処理
        this._updateCoinMovement(deltaTime);
    }

    /**
     * シーンまたは指定親ノードからコイン候補を収集します。
     */
    private _scanCoins() {
        const scene: Scene | null = director.getScene();
        if (!scene) {
            console.warn('[CoinMagnet] シーンがまだロードされていません。');
            return;
        }

        let root: Node;
        if (this.coinsRoot) {
            root = this.coinsRoot;
        } else {
            // coinsRoot が未指定の場合はシーンのルートノードを対象にする
            root = scene;
        }

        const foundCoins: Node[] = [];
        this._collectCoinsRecursive(root, foundCoins);

        this._candidateCoins = foundCoins;

        // 速度マップを整理(存在しないコインを削除)
        const newMap = new Map<Node, number>();
        for (const coin of this._candidateCoins) {
            const prevSpeed = this._coinSpeeds.get(coin) ?? 0;
            newMap.set(coin, prevSpeed);
        }
        this._coinSpeeds = newMap;
    }

    /**
     * 再帰的に子孫ノードから「コイン」とみなせるノードを収集します。
     */
    private _collectCoinsRecursive(node: Node, outArray: Node[]) {
        // 自分自身がコイン条件に合うか判定
        if (this._isCoinNode(node)) {
            outArray.push(node);
        }

        // 子ノードもチェック
        const children = node.children;
        for (let i = 0; i < children.length; i++) {
            this._collectCoinsRecursive(children[i], outArray);
        }
    }

    /**
     * 与えられたノードが「コイン」とみなせるかどうかを判定します。
     */
    private _isCoinNode(node: Node): boolean {
        // 自分自身(プレイヤーなど)を誤ってコイン扱いしないようにする
        if (node === this.node) {
            return false;
        }

        // targetName が空文字の場合は「名前でのフィルタリングなし」で常に候補とする
        if (!this.targetName || this.targetName.length === 0) {
            return true;
        }

        // name が targetName と一致するノードのみコインとみなす
        return node.name === this.targetName;
    }

    /**
     * 吸引対象コインの移動処理。
     */
    private _updateCoinMovement(deltaTime: number) {
        if (!this.node) {
            return;
        }

        const selfWorldPos = this.node.worldPosition;

        const tmpCoinPos = new Vec3();
        const dir = new Vec3();

        for (let i = this._candidateCoins.length - 1; i >= 0; i--) {
            const coin = this._candidateCoins[i];

            // すでにシーンから削除されたコインはリストから外す
            if (!coin || !coin.isValid) {
                this._candidateCoins.splice(i, 1);
                this._coinSpeeds.delete(coin);
                continue;
            }

            coin.getWorldPosition(tmpCoinPos);

            // 距離チェック
            const dist = Vec3.distance(selfWorldPos, tmpCoinPos);

            // 吸引半径外なら無視
            if (dist > this.radius) {
                continue;
            }

            // 最小距離以内ならそれ以上近づけない(位置をそのまま維持)
            if (dist <= this.minDistanceToStop) {
                // 到達状態だが、ここではコインの削除やスコア加算は行わない。
                // プロジェクト側で衝突判定などにより処理する想定。
                continue;
            }

            // 方向ベクトル = プレイヤー - コイン
            Vec3.subtract(dir, selfWorldPos, tmpCoinPos);
            dir.normalize();

            // 現在速度を取得し、加速度を適用
            const currentSpeed = this._coinSpeeds.get(coin) ?? this.moveSpeed;
            const newSpeed = currentSpeed + this.acceleration * deltaTime;
            this._coinSpeeds.set(coin, newSpeed);

            // 実際に移動させる距離 = 速度 * dt
            const moveDist = newSpeed * deltaTime;

            // 過剰に突き抜けないよう、残り距離を超えないように clamp
            const clampedMove = Math.min(moveDist, dist - this.minDistanceToStop);

            // 移動ベクトル = 方向 * 移動距離
            const moveVec = new Vec3(
                dir.x * clampedMove,
                dir.y * clampedMove,
                dir.z * clampedMove
            );

            // ワールド座標を更新
            const newWorldPos = new Vec3(
                tmpCoinPos.x + moveVec.x,
                tmpCoinPos.y + moveVec.y,
                tmpCoinPos.z + moveVec.z
            );
            coin.setWorldPosition(newWorldPos);
        }
    }
}

コードの要点解説

  • onLoad
    • debugDrawRadius が true の場合に、現在の設定値をコンソールに出力してデバッグしやすくしています。
    • 自ノードが存在しない異常ケースに備えて簡易チェックを入れています。
  • start
    • ゲーム開始時に一度だけ _scanCoins() を呼び出し、初期のコイン候補リストを作成します。
  • update(deltaTime)
    • enabledMagnet が false の場合は何もしません(処理負荷も抑えられます)。
    • scanInterval ごとに _scanCoins() を実行し、周囲のコイン候補を更新します。
    • _updateCoinMovement(deltaTime) で実際の吸引移動を行います。
  • _scanCoins()
    • coinsRoot が指定されていればその配下を、未指定ならシーン全体を再帰的に探索します。
    • _collectCoinsRecursive で「コインとみなせるノード」だけを _candidateCoins に登録します。
    • 同時に _coinSpeeds マップも整理し、不要なエントリを削除します。
  • _isCoinNode(node)
    • 自分自身(プレイヤー)を誤ってコイン扱いしないように node === this.node を除外しています。
    • targetName が空文字なら「すべてのノードを候補」とし、
      そうでなければ node.name === targetName のノードだけをコインとみなします。
  • _updateCoinMovement(deltaTime)
    • 各コインについて:
      • 距離が radius より大きければ無視。
      • 距離が minDistanceToStop 以下ならそれ以上近づけない。
      • それ以外なら、方向ベクトルを求めて moveSpeedacceleration に基づいて移動させます。
      • コインごとに _coinSpeeds で現在速度を保持し、時間とともに加速させています。
    • ワールド座標で移動しているため、プレイヤーやコインがどの親ノード配下にあっても正しく吸引されます。

使用手順と動作確認

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

  1. エディタ左下の Assets パネルで、任意のフォルダ(例: assets/scripts)を選択します。
  2. 右クリック → CreateTypeScript を選択し、ファイル名を CoinMagnet.ts にします。
  3. 作成された CoinMagnet.ts をダブルクリックして開き、
    中身をすべて削除して、上記の CoinMagnet コードを貼り付けて保存します。

2. テスト用シーンとノードの準備

  1. プレイヤーノードの作成
    1. Hierarchy パネルで右クリック → Create3D ObjectNode(または 2D 用に UISprite など)を作成します。
    2. 名前を分かりやすく Player に変更します。
  2. コイン親ノードの作成(任意だが推奨)
    1. Hierarchy パネルで右クリック → CreateNode を作成し、名前を CoinsRoot にします。
    2. このノードの子として、複数のコインノードを作成します。
      1. CoinsRoot を右クリック → CreateNode(または Sprite 等)を作成。
      2. 名前を Coin に変更します。
      3. 同様の手順で、複数の Coin ノードを作成し、
        プレイヤーの周囲に散らばるように Position を調整します。

3. CoinMagnet コンポーネントをアタッチ

  1. Hierarchy で Player ノードを選択します。
  2. Inspector パネル下部の Add Component ボタンをクリック。
  3. CustomCoinMagnet を選択して追加します。

4. プロパティの設定

Player ノードにアタッチされた CoinMagnet の Inspector を確認し、以下のように設定してみましょう。

  • Enabled Magnet: チェック ON(デフォルト)
  • Radius: 500
    • プレイヤーの周囲 500 ユニット以内のコインを吸い寄せます。
  • Move Speed: 600
  • Acceleration: 800
    • 吸い寄せ開始時はそこそこ速く、近づくほどさらに加速する気持ちよい挙動になります。
  • Min Distance To Stop: 20
  • Scan Interval: 0.2
    • 0.2 秒ごとに周囲のコインをスキャンします。
  • Coins Root:
    • Hierarchy から CoinsRoot ノードをドラッグ&ドロップして設定。
    • これにより、CoinsRoot 以下のノードだけがコイン候補になります。
  • Target Name: Coin
    • name が「Coin」のノードだけをコインとして扱うようになります。
  • Debug Draw Radius: 必要に応じて ON
    • ON にすると、ゲーム開始時にコンソールへ設定値が出力されます。

5. 動作確認

  1. シーンを保存します(Ctrl+S または Command+S)。
  2. 上部ツールバーの Play ボタンを押してゲームを実行します。
  3. プレイヤーが中央付近に、コインがその周囲に配置されている状態であれば、
    • プレイヤーに近いコインから順に、スーッと吸い寄せられるように移動してくるはずです。
    • 半径 radius より外側にあるコインはそのまま静止しています。
    • プレイヤーにかなり近づくと minDistanceToStop で止まり、それ以上は近づきません。
  4. もしコインが動かない場合は、次の点を確認してください。
    • CoinsRoot の子ノードの nameCoin になっているか。
    • CoinsRootCoins Root プロパティに正しく設定しているか。
    • Enabled Magnet が ON になっているか。
    • プレイヤーとコインの距離が radius より小さいか。

6. 応用的な使い方

  • ゲーム中に磁石を ON/OFF したい
    • 別の UI ボタンやイベントから、CoinMagnet コンポーネントの enabledMagnet を切り替えることで、
      「一定時間だけ磁石が有効になるアイテム」などを実現できます。
  • 通常アイテムとコインを分けたい
    • 通常アイテムには name = "Item"、コインには name = "Coin" を付けておき、
    • CoinMagnet の targetNameCoin にすることで、コインだけを吸い寄せることができます。
  • 複数種類のコインを扱いたい
    • 例: CoinSmall, CoinBig など。
      • この場合は targetName を空文字にして、「CoinsRoot 以下のノードはすべてコイン」とみなす設計にすると簡単です。

まとめ

本記事では、Cocos Creator 3.8 / TypeScript で、他のスクリプトに一切依存せず、アタッチするだけでコインだけを強力に吸い寄せる汎用コンポーネント CoinMagnet を実装しました。

  • インスペクタから
    • 吸引半径(radius
    • 移動速度・加速度(moveSpeed, acceleration
    • スキャン頻度(scanInterval
    • 対象コインの範囲(coinsRoot, targetName

    を柔軟に調整できるため、ゲームごとの気持ちよい吸い込み感を素早くチューニングできます。

  • コイン側には特別なコンポーネントを要求せず、name と配置だけで識別するシンプルな仕組みなので、既存プロジェクトにも組み込みやすいです。
  • このスクリプト単体で完結しているため、Prefab のプレイヤーに最初から仕込んでおけば、
    • 「磁石アイテムを取ったら enabledMagnet = true にする」
    • 「レベルに応じて radius を拡大する」

    といった拡張も簡単に行えます。

同じ設計パターンで ItemMagnetEnemyMagnet などにも応用できるので、
ゲーム内のさまざまな「吸い寄せ」演出を共通化したい場合のベースとしても活用してみてください。

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