【Cocos Creator】アタッチするだけ!BlackHole (ブラックホール)の実装方法【TypeScript】

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

【Cocos Creator 3.8】BlackHole の実装:アタッチするだけで周囲の RigidBody を中心に吸い込む汎用スクリプト

このガイドでは、任意のノードにアタッチするだけで「ブラックホール」のように周囲の物体(RigidBody2D)を中心へ吸い込む BlackHole コンポーネントを実装します。
外部の GameManager やシングルトンに一切依存せず、インスペクタの設定だけで挙動を調整できる「独立コンポーネント」として設計します。


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

1. 機能要件の整理

  • このコンポーネントをアタッチしたノードを「ブラックホール」とみなす。
  • ブラックホールの一定半径内に存在する 2D 物理ボディ(RigidBody2D)を検出し、中心に向かう力を加える。
  • 力の強さは「距離に応じて変化」させる(近いほど強く、遠いほど弱く)ようにできる。
  • 2D 物理のみを対象とし、3D 物理 (RigidBody) には影響しない。
  • 外部スクリプトには依存せず、物理ワールド(PhysicsSystem2D)とインスペクタ設定だけで完結させる。

2. 実装アプローチ

  • PhysicsSystem2DtestAABB で、ブラックホールの影響範囲にあるコライダーを取得する。
  • 取得したコライダーから RigidBody2D を取り出し、applyForceToCenter で吸引力を加える。
  • 吸引力は「基本強度」「距離減衰」「最大速度制限」などをインスペクタから調整できるようにする。
  • ブラックホールの「影響半径」をインスペクタで設定し、シーン上では debugDraw などに頼らずとも数値ベースで調整できるようにする。
  • 防御的実装として、対象ノードに RigidBody2D がない場合はスキップし、エラーにはしない。

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

以下のプロパティを @property で公開します。

  • enabledBlackHole: boolean
    ブラックホールの ON/OFF。チェックを外すと一時的に吸引を止める。
  • radius: number
    影響半径(ワールド単位 / 2D 物理単位)。この円内の RigidBody2D に力を加える。
    例: 5〜20 程度で調整。
  • forceStrength: number
    基本の吸引力の強さ。大きいほど強く引き寄せる。
    例: 50〜500 くらいから調整。
  • distanceFalloff: number
    距離に応じた減衰係数。0 に近いと距離による差が小さく、1 以上にすると遠くはかなり弱くなる。
    実装では 1 / (1 + distance * distanceFalloff) のような形で使用。
  • minDistance: number
    ブラックホール中心との最小距離。これより近い場合はゼロ距離扱いを避けるため、距離を minDistance として計算する。
    例: 0.1〜0.5 程度。
  • maxForcePerBody: number
    1 フレームあたり 1 つの RigidBody2D に加える最大力。異常な加速を防ぐ安全装置。
    例: 1000〜5000。
  • maxSpeed: number
    吸い込まれるオブジェクトの最大速度。これを超えている場合はそれ以上力を加えない。
    例: 10〜30 程度。
  • affectKinematic: boolean
    Kinematic タイプの RigidBody2D も対象にするかどうか。通常は false 推奨。
  • affectStatic: boolean
    Static タイプの RigidBody2D も対象にするかどうか。通常は false(ステージの床などに力をかけないため)。
  • debugDrawGizmo: boolean
    エディタ実行時に、radius の範囲を簡易的にログ表示するかどうか(実際の Gizmo 描画は Editor 拡張が必要なため、本コンポーネントではログのみ)。

TypeScriptコードの実装

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


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

/**
 * BlackHole
 * 2D 物理ワールド内の RigidBody2D を中心に向かって吸い込むコンポーネント
 *
 * このコンポーネントをアタッチしたノードの位置を「ブラックホール中心」とみなし、
 * 半径 radius 内の RigidBody2D に対して、毎フレーム中心方向の力を加えます。
 */
@ccclass('BlackHole')
export class BlackHole extends Component {

    @property({
        tooltip: 'ブラックホールの吸引を有効にするかどうか。\n一時的に止めたいときはチェックを外します。',
    })
    public enabledBlackHole: boolean = true;

    @property({
        tooltip: 'ブラックホールの影響半径(ワールド単位 / 2D 物理単位)。\nこの円の中にある RigidBody2D に力を加えます。',
        min: 0.1,
    })
    public radius: number = 10;

    @property({
        tooltip: '基本の吸引力の強さ。\n値を大きくするほど強く引き寄せます。',
        min: 0,
    })
    public forceStrength: number = 200;

    @property({
        tooltip: '距離による減衰係数。\n0 に近いほど距離による差が小さく、値を大きくすると遠いほど弱くなります。',
        min: 0,
    })
    public distanceFalloff: number = 0.2;

    @property({
        tooltip: '中心との最小距離(ゼロ距離による暴走防止)。\nこれより近い場合は、この距離として計算します。',
        min: 0.01,
    })
    public minDistance: number = 0.2;

    @property({
        tooltip: '1 フレームあたり 1 つの RigidBody2D に加える最大力。\n異常な加速を防ぐための上限値です。',
        min: 0,
    })
    public maxForcePerBody: number = 2000;

    @property({
        tooltip: 'この速度を超えている RigidBody2D には、これ以上力を加えません。\n吸い込み速度の上限として機能します。',
        min: 0,
    })
    public maxSpeed: number = 20;

    @property({
        tooltip: 'Kinematic タイプの RigidBody2D も吸い込み対象にするかどうか。',
    })
    public affectKinematic: boolean = false;

    @property({
        tooltip: 'Static タイプの RigidBody2D も吸い込み対象にするかどうか。\n通常は false 推奨(床や壁などに力を加えないため)。',
    })
    public affectStatic: boolean = false;

    @property({
        tooltip: '実行時にブラックホールの半径などをログ出力します。\n簡易デバッグ用です。',
    })
    public debugDrawGizmo: boolean = false;

    // ワーク用ベクトル(毎フレームの new を減らすため)
    private _centerWorld: Vec2 = new Vec2();
    private _tmpWorld: Vec2 = new Vec2();
    private _tmpForce: Vec2 = new Vec2();
    private _aabbRect: Rect = new Rect();

    onLoad() {
        // 2D 物理システムが有効化されているかチェック
        const physics2D = PhysicsSystem2D.instance;
        if (!physics2D) {
            console.error('[BlackHole] PhysicsSystem2D が利用できません。このコンポーネントは 2D 物理が有効なプロジェクトでのみ動作します。');
            return;
        }

        if (this.debugDrawGizmo) {
            console.log(`[BlackHole] 有効化されました。radius=${this.radius}, forceStrength=${this.forceStrength}`);
        }
    }

    start() {
        // 特に必須コンポーネントはないため、ここでは何もしません。
        // BlackHole 自身は「力を与えるだけ」であり、RigidBody2D は周囲のノード側に付与されている前提です。
    }

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

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

        // ブラックホール中心のワールド座標を取得(Vec3 -> Vec2)
        const worldPos3 = this.node.worldPosition;
        this._centerWorld.set(worldPos3.x, worldPos3.y);

        // 影響範囲の AABB を計算(円を外接する正方形)
        const r = this.radius;
        this._aabbRect.x = this._centerWorld.x - r;
        this._aabbRect.y = this._centerWorld.y - r;
        this._aabbRect.width = r * 2;
        this._aabbRect.height = r * 2;

        // AABB 内の Collider2D を取得
        const results: Collider2D[] = [];
        physics2D.testAABB(this._aabbRect, results);

        // 各コライダーに対して処理
        for (let i = 0; i < results.length; i++) {
            const col = results[i];
            if (!col) continue;

            const body = col.getComponent(RigidBody2D);
            if (!body) {
                // 対象ノードに RigidBody2D がない場合はスキップ
                continue;
            }

            // 自身のノードに付いた RigidBody2D も吸い込み対象にしたくない場合は、ここで弾くこともできる
            // if (body.node === this.node) continue;

            // ボディのタイプによるフィルタリング
            const type = body.type;
            if (type === ERigidBody2DType.Static && !this.affectStatic) {
                continue;
            }
            if (type === ERigidBody2DType.Kinematic && !this.affectKinematic) {
                continue;
            }

            // ボディのワールド位置を Vec2 で取得
            const bodyPos3 = body.node.worldPosition;
            this._tmpWorld.set(bodyPos3.x, bodyPos3.y);

            // 中心への方向ベクトル = center - bodyPos
            const dirX = this._centerWorld.x - this._tmpWorld.x;
            const dirY = this._centerWorld.y - this._tmpWorld.y;
            let dist = Math.sqrt(dirX * dirX + dirY * dirY);

            // 影響半径の外側ならスキップ
            if (dist > this.radius) {
                continue;
            }

            // 最小距離の補正(ゼロ除算や極端な力を避ける)
            if (dist < this.minDistance) {
                dist = this.minDistance;
            }

            // 正規化方向ベクトル
            const invDist = 1 / dist;
            const ndirX = dirX * invDist;
            const ndirY = dirY * invDist;

            // 距離減衰係数(例: 1 / (1 + d * falloff))
            const falloff = 1 / (1 + dist * this.distanceFalloff);

            // 基本力 * 減衰
            let forceMag = this.forceStrength * falloff;

            // 上限クランプ
            if (forceMag > this.maxForcePerBody) {
                forceMag = this.maxForcePerBody;
            }

            // 速度制限:現在速度が maxSpeed を超えている場合は力を加えない
            const lv = body.linearVelocity;
            const speed = lv.length();
            if (speed >= this.maxSpeed && this.maxSpeed > 0) {
                continue;
            }

            // 最終的な力ベクトル
            this._tmpForce.x = ndirX * forceMag;
            this._tmpForce.y = ndirY * forceMag;

            // 中心に向かう力を加える(連続的な吸引なので deltaTime で割らず、毎フレーム一定量加える)
            body.applyForceToCenter(this._tmpForce, true);
        }
    }
}

主要メソッドの解説

onLoad()

  • PhysicsSystem2D.instance が存在するかチェックし、2D 物理が無効なプロジェクトでの誤用を検出します。
  • debugDrawGizmo が true の場合、基本パラメータをログに出して簡易的な確認を行います。

update(deltaTime)

  1. 有効フラグと物理システムの確認
    enabledBlackHole が false の場合や、PhysicsSystem2D が無効な場合は即 return します。
  2. ブラックホール中心のワールド座標取得
    this.node.worldPosition から Vec2 へ変換し、_centerWorld に格納します。
  3. AABB(影響範囲)の計算
    半径 radius の円を外接する正方形を Rect として計算し、testAABB に渡します。
  4. PhysicsSystem2D.testAABB
    AABB 内にある Collider2Dresults に収集します。円ではなく四角ですが、後段で距離チェックを行うため問題ありません。
  5. 各 Collider2D に対して吸引処理

各 Collider2D ごとの処理内容:

  • RigidBody2D を取得し、なければスキップ。
  • RigidBody2D のタイプ(Static / Kinematic / Dynamic)を見て、affectStatic / affectKinematic に応じて対象外ならスキップ。
  • ボディのワールド座標を取得し、中心との距離と方向を計算。
  • 距離が radius を超えていればスキップ(AABB に入っていても円の外側は無視)。
  • 距離が minDistance 未満の場合は minDistance として扱い、極端な力を防ぎます。
  • 距離減衰係数 falloff = 1 / (1 + dist * distanceFalloff) を計算。
  • 基本力 forceStrength に減衰を掛けた力の大きさ forceMag を求め、maxForcePerBody でクランプ。
  • 現在の速度 linearVelocity.length()maxSpeed を超えている場合は、それ以上加速させないためにスキップ。
  • 最終的な力ベクトルを求め、applyForceToCenter で中心方向に力を加えます。

使用手順と動作確認

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

  1. エディタ上部メニュー、または Assets パネルで任意のフォルダを選択します。
  2. 右クリック → CreateTypeScript を選択します。
  3. ファイル名を BlackHole.ts に変更します。
  4. 作成された BlackHole.ts をダブルクリックし、エディタ(VSCode 等)で開き、上記コードをすべて貼り付けて保存します。

2. テスト用シーンの準備(2D 物理の有効化)

  1. メインメニューから ProjectProject Settings を開きます。
  2. 左側メニューから Physics2D を選択し、Enable(有効)になっていることを確認します。
  3. 必要であれば Gravity(重力)を調整します。ブラックホールだけを見たい場合は (0, 0) にしても構いません。

3. ブラックホール用ノードの作成とアタッチ

  1. Hierarchy パネルで右クリック → CreateEmpty Node を選択し、ノード名を BlackHoleNode に変更します。
  2. BlackHoleNode を選択し、Inspector パネルの Add Component ボタンをクリックします。
  3. Custom カテゴリ内から BlackHole を選択して追加します。
  4. Inspector 上で BlackHole コンポーネントのプロパティを以下のように設定してみます(例):
    • Enabled Black Hole: チェック ON
    • Radius: 10
    • Force Strength: 300
    • Distance Falloff: 0.2
    • Min Distance: 0.2
    • Max Force Per Body: 2000
    • Max Speed: 20
    • Affect Kinematic: OFF
    • Affect Static: OFF
    • Debug Draw Gizmo: 必要なら ON(ログで確認したい場合)
  5. BlackHoleNode の位置をシーン中央あたり(例: (0, 0, 0))に配置します。

4. 吸い込まれるオブジェクトの作成

ブラックホールの効果を確認するため、いくつかの 2D 物体を用意します。

  1. Hierarchy パネルで右クリック → Create2D ObjectSprite を選択し、ノード名を FallingObject1 にします。
  2. FallingObject1 を選択し、InspectorAdd Component から:
    • Physics 2DRigidBody2D
    • Physics 2DCircleCollider2D(または BoxCollider2D)
  3. RigidBody2D の設定例:
    • Type: Dynamic
    • Gravity Scale: 1(重力あり)または 0(重力なしでブラックホールだけを見る)
    • その他はデフォルトで OK
  4. FallingObject1 の位置をブラックホールから少し離れた場所に移動(例: (10, 0, 0)(-8, 5, 0) など)。
  5. 同様の手順で FallingObject2, FallingObject3 など複数作成し、ブラックホールを囲むように配置します。

5. 再生して動作確認

  1. エディタ右上の Play ボタンを押してゲームを再生します。
  2. Dynamic な RigidBody2D を持つオブジェクトが、徐々にブラックホール中心へ吸い寄せられるのを確認します。
  3. 吸い込まれる速度が遅すぎる場合:
    • Force Strength を増やす(例: 500〜1000)
    • Distance Falloff を小さくする(例: 0.1)
  4. 吸い込みが激しすぎて暴れる場合:
    • Force Strength を小さくする
    • Max Force Per Body を下げる(例: 800〜1200)
    • Min Distance を少し大きくする(例: 0.5)
  5. ブラックホールの範囲を広げたい / 狭めたい場合:
    • Radius を変更し、再生して挙動を確認します。

6. よくある調整パターン

  • 見た目重視のゆっくり吸い込み
    • Radius: 12〜20
    • Force Strength: 150〜300
    • Distance Falloff: 0.1〜0.2
    • Max Speed: 10〜15
  • アクションゲーム向けの強力なブラックホール
    • Radius: 8〜12
    • Force Strength: 600〜1200
    • Distance Falloff: 0.3〜0.5
    • Max Speed: 25〜40
    • Max Force Per Body: 3000〜5000

まとめ

この BlackHole コンポーネントは、

  • 任意のノードにアタッチするだけで「ブラックホール的な吸引効果」を付与できる。
  • 外部の GameManager やシングルトンに依存せず、完全に独立して動作する。
  • 影響半径、吸引力、距離減衰、最大速度などをインスペクタから直感的に調整できる。
  • 対象とする RigidBody2D のタイプ(Static / Kinematic / Dynamic)を細かく制御できる。

ゲームへの応用例としては、

  • 敵の特殊攻撃としてプレイヤーや弾を吸い込むギミック
  • ステージ上のアイテムを中心に集める「マグネット」的な効果
  • 画面外へオブジェクトを掃除する「ゴミ箱」ゾーンの実装

など、さまざまな場面で再利用が可能です。
パラメータだけで挙動を変えられるため、「弱い重力場」「強烈なブラックホール」「ふわっと集まるマグネット」など、1 つのコンポーネントで多彩な表現ができます。

このままプロジェクトに組み込んで、まずはテストシーンで挙動を確認し、ゲームの演出やギミックに合わせてパラメータをチューニングしてみてください。

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

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

脱・初心者!Godot 4 ゲーム開発の「2歩目」

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

Godot4ローグライク入門 ~ダンジョン自動生成~

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

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

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

URLをコピーしました!