【Cocos Creator 3.8】BlackHole の実装:アタッチするだけで周囲の RigidBody を中心に吸い込む汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「ブラックホール」のように周囲の物体(RigidBody2D)を中心へ吸い込む BlackHole コンポーネントを実装します。
外部の GameManager やシングルトンに一切依存せず、インスペクタの設定だけで挙動を調整できる「独立コンポーネント」として設計します。
コンポーネントの設計方針
1. 機能要件の整理
- このコンポーネントをアタッチしたノードを「ブラックホール」とみなす。
- ブラックホールの一定半径内に存在する 2D 物理ボディ(
RigidBody2D)を検出し、中心に向かう力を加える。 - 力の強さは「距離に応じて変化」させる(近いほど強く、遠いほど弱く)ようにできる。
- 2D 物理のみを対象とし、3D 物理 (
RigidBody) には影響しない。 - 外部スクリプトには依存せず、物理ワールド(
PhysicsSystem2D)とインスペクタ設定だけで完結させる。
2. 実装アプローチ
- PhysicsSystem2D の
testAABBで、ブラックホールの影響範囲にあるコライダーを取得する。 - 取得したコライダーから
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)
- 有効フラグと物理システムの確認
enabledBlackHoleが false の場合や、PhysicsSystem2Dが無効な場合は即 return します。 - ブラックホール中心のワールド座標取得
this.node.worldPositionからVec2へ変換し、_centerWorldに格納します。 - AABB(影響範囲)の計算
半径radiusの円を外接する正方形をRectとして計算し、testAABBに渡します。 - PhysicsSystem2D.testAABB
AABB 内にあるCollider2Dをresultsに収集します。円ではなく四角ですが、後段で距離チェックを行うため問題ありません。 - 各 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. スクリプトファイルの作成
- エディタ上部メニュー、または Assets パネルで任意のフォルダを選択します。
- 右クリック → Create → TypeScript を選択します。
- ファイル名を
BlackHole.tsに変更します。 - 作成された
BlackHole.tsをダブルクリックし、エディタ(VSCode 等)で開き、上記コードをすべて貼り付けて保存します。
2. テスト用シーンの準備(2D 物理の有効化)
- メインメニューから Project → Project Settings を開きます。
- 左側メニューから Physics → 2D を選択し、Enable(有効)になっていることを確認します。
- 必要であれば Gravity(重力)を調整します。ブラックホールだけを見たい場合は
(0, 0)にしても構いません。
3. ブラックホール用ノードの作成とアタッチ
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を
BlackHoleNodeに変更します。 BlackHoleNodeを選択し、Inspector パネルの Add Component ボタンをクリックします。- Custom カテゴリ内から BlackHole を選択して追加します。
- 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(ログで確認したい場合)
BlackHoleNodeの位置をシーン中央あたり(例:(0, 0, 0))に配置します。
4. 吸い込まれるオブジェクトの作成
ブラックホールの効果を確認するため、いくつかの 2D 物体を用意します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、ノード名を
FallingObject1にします。 FallingObject1を選択し、Inspector の Add Component から:- Physics 2D → RigidBody2D
- Physics 2D → CircleCollider2D(または BoxCollider2D)
RigidBody2Dの設定例:- Type: Dynamic
- Gravity Scale:
1(重力あり)または0(重力なしでブラックホールだけを見る) - その他はデフォルトで OK
FallingObject1の位置をブラックホールから少し離れた場所に移動(例:(10, 0, 0)や(-8, 5, 0)など)。- 同様の手順で
FallingObject2,FallingObject3など複数作成し、ブラックホールを囲むように配置します。
5. 再生して動作確認
- エディタ右上の Play ボタンを押してゲームを再生します。
- Dynamic な RigidBody2D を持つオブジェクトが、徐々にブラックホール中心へ吸い寄せられるのを確認します。
- 吸い込まれる速度が遅すぎる場合:
Force Strengthを増やす(例: 500〜1000)Distance Falloffを小さくする(例: 0.1)
- 吸い込みが激しすぎて暴れる場合:
Force Strengthを小さくするMax Force Per Bodyを下げる(例: 800〜1200)Min Distanceを少し大きくする(例: 0.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 つのコンポーネントで多彩な表現ができます。
このままプロジェクトに組み込んで、まずはテストシーンで挙動を確認し、ゲームの演出やギミックに合わせてパラメータをチューニングしてみてください。




