【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
–radiusやupdateIntervalが 0 以下になっていた場合に警告を出しつつ安全な値に補正します。update
–enabledMagnetが false のときは何もしません。
–updateIntervalに基づいて、一定間隔ごとにだけ物理クエリを行い、負荷を下げています。
– 自身のワールド座標を中心に、半径radiusの正方形 AABB を作成し、PhysicsSystem2D.testAABBで周囲のCollider2Dを取得します。
– 取得したCollider2Dについて:- 自分自身のノードはスキップ。
attractLayerMaskとCollider2D.groupの AND が 0 のものはスキップ。RigidBody2Dを持たないものはスキップ。
– 対象のワールド座標から中心への方向と距離を計算し、半径外のものは除外します。
– 距離に応じて力を減衰させ(近いほど強く、遠いほど弱く)、applyForceToCenterまたはapplyLinearImpulseで吸引します。
–maxSpeedが正なら、RigidBody2DのlinearVelocityをクランプして速度の暴走を防ぎます。onEnable / onDisable
– エディタ停止中に限り、debugDrawGizmoが true のときに物理デバッグ描画フラグを切り替え、範囲の確認をしやすくします。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
- Create → TypeScript を選択します。
- 作成されたファイル名を
MagnetAttractor.tsに変更します。 - ダブルクリックしてエディタで開き、先ほどのコード全文を貼り付けて保存します。
2. テスト用シーンの準備(2D物理設定)
- メインメニューから Project → Project Settings… を開きます。
- Module タブで 2D Physics が有効になっていることを確認します。無効ならチェックを入れて有効化します。
- シーンを 2D 用に作成し、Canvas または空のノードを用意します。
3. 磁石の中心ノードを作成してアタッチ
- Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を
MagnetCenterなどに変更します。 MagnetCenterノードを選択し、Inspector パネルの Add Component ボタンをクリックします。- Custom → MagnetAttractor を選択してアタッチします。
- 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. 吸い寄せられるアイテムノードの作成
物理挙動を持つ「アイテム」をいくつか作成します。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、名前を
Item1に変更します。 - 同様に
Item2,Item3など複数作成し、MagnetCenterから少し離れた位置に配置します。 - 各 Item ノードを選択し、Inspector で次のコンポーネントを追加します:
- Add Component → Physics 2D → RigidBody2D
- Add Component → Physics 2D → CircleCollider2D(または BoxCollider2D)
RigidBody2Dの設定例:- Body Type: Dynamic
- Gravity Scale: 0(重力で落ちないようにしたい場合)
- Linear Damping: 0.5 くらい(減速しすぎる場合は下げる)
- Angular Damping: 1 くらい(回転を抑えたい場合)
CircleCollider2Dの Group を、磁石で吸いたいレイヤーに設定します(デフォルトのDefaultであればMagnetAttractor.attractLayerMaskを0xffffffffのままで OK)。
5. 磁石の動作確認
- シーンを保存します。
- エディタ右上の Play ボタンを押してゲームを再生します。
- 再生すると、
MagnetCenter周辺にあるItemたちが、ゆっくりと中心に向かって吸い寄せられていくはずです。 - もし動かない場合は、次を確認します:
- 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を有効化する。 - ステージの中央に「回収ポイント」を置き、落ちたアイテムを自動的に集約する。
- 敵が放った弾を、特定のオブジェクトに引き寄せてギミックにする。
といった用途に、そのまま再利用できます。
このコンポーネントをベースに、「距離に応じて色を変える」「吸い寄せ完了時に自動で破棄する」などの拡張も容易です。まずは本記事のコードをプロジェクトに組み込んで、実際にマグネット効果を体験してみてください。




