【Cocos Creator】アタッチするだけ!MagnetAttractor (磁石化)の実装方法【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】MagnetAttractor の実装:アタッチするだけで周囲のアイテムを吸い寄せる汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「周囲の Rigidbody(物理アイテム)を中心ノードに向かって引き寄せる」磁石コンポーネント MagnetAttractor を実装します。
プレイヤーが取得した「マグネットアイテム」や、特定エリアにアイテムを集約する仕組みなどにそのまま使える、外部依存なしの汎用スクリプトです。


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

1. 機能要件の整理

  • このコンポーネントをアタッチしたノードを「磁石の中心」とみなす。
  • 指定した半径内にある対象 Rigidbody に対して、「中心ノードに向かう力」を毎フレーム加える。
  • 対象は 2D 物理(RigidBody2D)を想定する。
  • どのレイヤーのオブジェクトを吸い寄せるかを Mask で指定できるようにする。
  • 吸引力の強さ・最大速度・有効距離などをインスペクタから調整できるようにする。
  • 外部の GameManager やシングルトンなどには一切依存しない。

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

  • 磁石の中心は「このコンポーネントがアタッチされたノード自身」。
  • 物理ワールド(PhysicsSystem2D)から、毎フレーム「一定半径の AABB をオーバーラップ」して周囲の Collider2D を取得する。
  • 取得した Collider2D から RigidBody2D を取り出し、中心に向かう力(または速度変更)を加える。
  • レイヤーマスクで対象をフィルタリングすることで、「アイテムだけを吸う」「敵だけを吸う」など柔軟に利用可能。

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

以下のような @property を用意します。

  • enabledMagnet: boolean
    – 磁石効果の ON/OFF。
    true のときのみ吸引処理を行う。
  • radius: number
    – 磁石の有効半径(ワールド座標系での距離)。
    – この半径以内にいる Rigidbody のみが吸い寄せられる。
    – 例: 3.0 ~ 8.0 くらいから調整。
  • force: number
    – 吸引力の強さ(力の大きさ)。
    – 値が大きいほど、素早く中心へ引き寄せられる。
    – 物理挙動なので、対象の質量やドラッグによって体感が変わる。
  • maxSpeed: number
    – 吸引によって与える速度の上限。
    – あまり速くしすぎると物理的に不自然になるので、制限を設ける。
  • useImpulse: boolean
    true: applyLinearImpulse で瞬間的なインパルスを与える方式。
    false: applyForceToCenter で継続的な力を与える方式。
  • attractLayerMask: number
    – 吸引対象とする Collider2D のレイヤーマスク。
    Collider2D.group と AND を取り、0 でなければ対象とみなす。
  • debugDrawGizmo: boolean
    true: エディタのシーンビューで有効半径を円で表示し、範囲を視覚的に確認できる。
  • updateInterval: number
    – 周囲のオブジェクト探索を行う間隔(秒)。
    – 毎フレーム PhysicsSystem2D でオーバーラップを行うと重くなるため、0.1 秒など少し間引く。

これらをすべてインスペクタから設定できるようにし、他のスクリプトから直接参照しなくても使える完全独立コンポーネントにします。


TypeScriptコードの実装


import {
    _decorator,
    Component,
    Node,
    Vec2,
    Vec3,
    PhysicsSystem2D,
    EPhysics2DDrawFlags,
    Collider2D,
    RigidBody2D,
    director,
    math
} from 'cc';
const { ccclass, property, executeInEditMode, menu } = _decorator;

/**
 * MagnetAttractor
 * このコンポーネントをアタッチしたノードを中心として、
 * 周囲の Collider2D / RigidBody2D を吸い寄せる磁石コンポーネント。
 */
@ccclass('MagnetAttractor')
@executeInEditMode(true)
@menu('Custom/MagnetAttractor')
export class MagnetAttractor extends Component {

    @property({
        tooltip: '磁石効果のON/OFF。falseにすると一切の吸引処理を行いません。'
    })
    public enabledMagnet: boolean = true;

    @property({
        tooltip: '磁石の有効半径(ワールド座標系)。この距離以内のRigidBody2Dを吸い寄せます。'
    })
    public radius: number = 5.0;

    @property({
        tooltip: '吸引力の強さ。値が大きいほど強く中心に引き寄せます。'
    })
    public force: number = 10.0;

    @property({
        tooltip: '吸引によって与えられる移動速度の上限(m/s)。0以下で無制限。'
    })
    public maxSpeed: number = 10.0;

    @property({
        tooltip: 'true: インパルス(瞬間的な力)で引き寄せる / false: 継続的な力で引き寄せる'
    })
    public useImpulse: boolean = false;

    @property({
        tooltip: '吸引対象とするCollider2Dのレイヤーマスク。Collider2D.groupとのANDが0でないものだけ対象になります。'
    })
    public attractLayerMask: number = 0xffffffff; // デフォルト: 全レイヤー対象

    @property({
        tooltip: 'デバッグ用に有効半径をシーンビューに表示するかどうか(エディタのみ)。'
    })
    public debugDrawGizmo: boolean = true;

    @property({
        tooltip: '周囲のオブジェクト探索を行う間隔(秒)。小さくするほど正確だが処理が重くなります。'
    })
    public updateInterval: number = 0.1;

    // 内部用ワーク
    private _timeAccumulator: number = 0;
    private _tmpCenter: Vec2 = new Vec2();
    private _tmpMin: Vec2 = new Vec2();
    private _tmpMax: Vec2 = new Vec2();
    private _tmpDir: Vec2 = new Vec2();

    onLoad() {
        // 2D物理が有効かどうかをチェック(無効ならログを出す)
        const phys2D = PhysicsSystem2D.instance;
        if (!phys2D) {
            console.error('[MagnetAttractor] PhysicsSystem2D が利用できません。Project Settings で 2D Physics を有効にしてください。');
        }

        // エディタ上でのギズモ描画用に、drawFlags を設定(ゲーム中は不要)
        if (director.isPaused() && phys2D) {
            phys2D.debugDrawFlags = this.debugDrawGizmo
                ? (EPhysics2DDrawFlags.Aabb | EPhysics2DDrawFlags.Pair)
                : EPhysics2DDrawFlags.None;
        }
    }

    start() {
        // 特別な初期化は不要だが、防御的に値を補正しておく
        if (this.radius <= 0) {
            console.warn('[MagnetAttractor] radius が 0 以下だったため、1 に補正しました。');
            this.radius = 1;
        }
        if (this.updateInterval <= 0) {
            console.warn('[MagnetAttractor] updateInterval が 0 以下だったため、0.05 に補正しました。');
            this.updateInterval = 0.05;
        }
    }

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

        const phys2D = PhysicsSystem2D.instance;
        if (!phys2D || !phys2D.enabled) {
            // 2D物理が無効な場合は何もしない
            return;
        }

        this._timeAccumulator += deltaTime;
        if (this._timeAccumulator < this.updateInterval) {
            return;
        }
        this._timeAccumulator = 0;

        // 中心ノードのワールド座標を取得
        const worldPos3 = this.node.worldPosition;
        const center = this._tmpCenter;
        center.set(worldPos3.x, worldPos3.y);

        // 探索用のAABB(中心を囲む正方形)を計算
        const half = this.radius;
        this._tmpMin.set(center.x - half, center.y - half);
        this._tmpMax.set(center.x + half, center.y + half);

        const results: Collider2D[] = [];
        // AABBオーバーラップで周囲のCollider2Dを取得
        phys2D.testAABB(this._tmpMin, this._tmpMax, results);

        if (results.length === 0) {
            return;
        }

        for (let i = 0; i < results.length; i++) {
            const col = results[i];
            if (!col) continue;

            // 自分自身のCollider2Dは無視
            if (col.node === this.node) {
                continue;
            }

            // レイヤーマスクチェック
            if ((col.group & this.attractLayerMask) === 0) {
                continue;
            }

            const rb = col.getComponent(RigidBody2D);
            if (!rb) {
                // RigidBody2Dを持たないものは吸引対象外
                continue;
            }

            // 対象のワールド座標
            const bodyPos3 = col.node.worldPosition;
            const bodyPos = new Vec2(bodyPos3.x, bodyPos3.y);

            // 中心への方向ベクトルと距離
            const dir = this._tmpDir;
            dir.set(center.x - bodyPos.x, center.y - bodyPos.y);
            const dist = dir.length();

            if (dist <= 0.0001) {
                continue;
            }

            // 半径外は無視(AABBは正方形なので、対角の角は半径を超える可能性がある)
            if (dist > this.radius) {
                continue;
            }

            // 正規化
            dir.multiplyScalar(1 / dist);

            // 距離に応じた減衰(任意):近いほど強く、遠いほど弱くする
            // 0 <= t <= 1
            const t = 1.0 - (dist / this.radius);
            const strength = this.force * t;

            // 力(またはインパルス)ベクトル
            const forceVec = new Vec2(dir.x * strength, dir.y * strength);

            if (this.useImpulse) {
                // 質量を考慮したインパルスを適用(deltaTimeは内部で考慮される)
                rb.applyLinearImpulse(forceVec, rb.getWorldCenter(), true);
            } else {
                // 継続的な力を中心に適用
                rb.applyForceToCenter(forceVec, true);
            }

            // 最大速度制限(0以下なら無制限)
            if (this.maxSpeed > 0) {
                const vel = rb.linearVelocity;
                const speed = vel.length();
                if (speed > this.maxSpeed) {
                    vel.multiplyScalar(this.maxSpeed / speed);
                    rb.linearVelocity = vel;
                }
            }
        }
    }

    /**
     * エディタ上でのギズモ描画用。
     * Cocos Creator 3.x では専用のギズモクラスもあるが、
     * ここでは簡易的に物理デバッグ描画を利用する。
     */
    onEnable() {
        const phys2D = PhysicsSystem2D.instance;
        if (phys2D && director.isPaused()) {
            if (this.debugDrawGizmo) {
                phys2D.debugDrawFlags |= EPhysics2DDrawFlags.Aabb;
            }
        }
    }

    onDisable() {
        const phys2D = PhysicsSystem2D.instance;
        if (phys2D && director.isPaused()) {
            if (this.debugDrawGizmo) {
                // 他のコンポーネントもAabbを使っている可能性があるので、
                // ここでは安易にフラグを全消ししない(Aabbだけを落とす)。
                phys2D.debugDrawFlags &= ~EPhysics2DDrawFlags.Aabb;
            }
        }
    }
}

コードのポイント解説

  • onLoad
    PhysicsSystem2D が利用可能かチェックし、無効ならエラーログを出します。
    – エディタ停止中(director.isPaused())かつ debugDrawGizmo が true の場合、物理デバッグ描画を有効にして AABB を見やすくします。
  • start
    radiusupdateInterval が 0 以下になっていた場合に警告を出しつつ安全な値に補正します。
  • update
    enabledMagnet が false のときは何もしません。
    updateInterval に基づいて、一定間隔ごとにだけ物理クエリを行い、負荷を下げています。
    – 自身のワールド座標を中心に、半径 radius の正方形 AABB を作成し、PhysicsSystem2D.testAABB で周囲の Collider2D を取得します。
    – 取得した Collider2D について:
    • 自分自身のノードはスキップ。
    • attractLayerMaskCollider2D.group の AND が 0 のものはスキップ。
    • RigidBody2D を持たないものはスキップ。

    – 対象のワールド座標から中心への方向と距離を計算し、半径外のものは除外します。
    – 距離に応じて力を減衰させ(近いほど強く、遠いほど弱く)、applyForceToCenter または applyLinearImpulse で吸引します。
    maxSpeed が正なら、RigidBody2DlinearVelocity をクランプして速度の暴走を防ぎます。

  • onEnable / onDisable
    – エディタ停止中に限り、debugDrawGizmo が true のときに物理デバッグ描画フラグを切り替え、範囲の確認をしやすくします。

使用手順と動作確認

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

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

2. テスト用シーンの準備(2D物理設定)

  1. メインメニューから Project → Project Settings… を開きます。
  2. Module タブで 2D Physics が有効になっていることを確認します。無効ならチェックを入れて有効化します。
  3. シーンを 2D 用に作成し、Canvas または空のノードを用意します。

3. 磁石の中心ノードを作成してアタッチ

  1. Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を MagnetCenter などに変更します。
  2. MagnetCenter ノードを選択し、Inspector パネルの Add Component ボタンをクリックします。
  3. Custom → MagnetAttractor を選択してアタッチします。
  4. Inspector 上で MagnetAttractor の各プロパティを設定します(例):
    • Enabled Magnet: チェック ON
    • Radius: 6
    • Force: 20
    • Max Speed: 12
    • Use Impulse: OFF(まずは Force モードで確認)
    • Attract Layer Mask: とりあえず 0xffffffff のまま(全レイヤー対象)
    • Debug Draw Gizmo: ON
    • Update Interval: 0.1

4. 吸い寄せられるアイテムノードの作成

物理挙動を持つ「アイテム」をいくつか作成します。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、名前を Item1 に変更します。
  2. 同様に Item2, Item3 など複数作成し、MagnetCenter から少し離れた位置に配置します。
  3. 各 Item ノードを選択し、Inspector で次のコンポーネントを追加します:
    • Add Component → Physics 2D → RigidBody2D
    • Add Component → Physics 2D → CircleCollider2D(または BoxCollider2D)
  4. RigidBody2D の設定例:
    • Body Type: Dynamic
    • Gravity Scale: 0(重力で落ちないようにしたい場合)
    • Linear Damping: 0.5 くらい(減速しすぎる場合は下げる)
    • Angular Damping: 1 くらい(回転を抑えたい場合)
  5. CircleCollider2DGroup を、磁石で吸いたいレイヤーに設定します(デフォルトの Default であれば MagnetAttractor.attractLayerMask0xffffffff のままで OK)。

5. 磁石の動作確認

  1. シーンを保存します。
  2. エディタ右上の Play ボタンを押してゲームを再生します。
  3. 再生すると、MagnetCenter 周辺にある Item たちが、ゆっくりと中心に向かって吸い寄せられていくはずです。
  4. もし動かない場合は、次を確認します:
    • Project Settings で 2D Physics が有効か。
    • Item ノードに RigidBody2D + Collider2D が両方付いているか。
    • RigidBody2D.Body Type が Dynamic になっているか。
    • MagnetAttractor.enabledMagnet が ON になっているか。
    • radius が十分大きく、Item がその範囲内にいるか。
    • attractLayerMask と Item の Collider2D.group がマッチしているか。

6. パラメータ調整のヒント

  • 吸引が弱い/届かない
    force を上げる、radius を広げる、RigidBody2D.Linear Damping を下げる。
  • 吸引が速すぎて不自然
    force を下げる、maxSpeed を下げる。
  • 処理が重いと感じる
    updateInterval を 0.1 ~ 0.2 など少し大きくする(探索頻度を下げる)。
  • 一気に引き寄せたい(パチンと吸い付く感じ)
    useImpulse を ON にし、force をやや高めに設定。

まとめ

MagnetAttractor コンポーネントは、

  • 任意のノードにアタッチするだけで「周囲の Rigidbody2D を中心に吸い寄せる」動きを付与できる。
  • 外部スクリプトやシングルトンに一切依存せず、インスペクタのプロパティだけで完結する。
  • レイヤーマスク・半径・力の強さ・最大速度・更新間隔などを調整することで、さまざまなゲームに流用可能。

例えば、

  • プレイヤーがマグネットアイテムを取得したときに一時的に MagnetAttractor を有効化する。
  • ステージの中央に「回収ポイント」を置き、落ちたアイテムを自動的に集約する。
  • 敵が放った弾を、特定のオブジェクトに引き寄せてギミックにする。

といった用途に、そのまま再利用できます。
このコンポーネントをベースに、「距離に応じて色を変える」「吸い寄せ完了時に自動で破棄する」などの拡張も容易です。まずは本記事のコードをプロジェクトに組み込んで、実際にマグネット効果を体験してみてください。

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