【Cocos Creator 3.8】MagnetBlock の実装:アタッチするだけで「周囲の鉄製オブジェクトを引き寄せる/反発させる」汎用スクリプト
このガイドでは、任意のノードにアタッチするだけで「磁石ブロック」として振る舞い、周囲の鉄製オブジェクトを引き寄せたり、反発させたりできる MagnetBlock コンポーネントを実装します。
鉄製オブジェクト側には簡単なマーカー(タグ)コンポーネントを付けるだけでよく、ゲーム全体の特別なマネージャやシングルトンは一切不要です。シーン内のどのノードにも独立して追加できる、再利用性の高い設計を目指します。
コンポーネントの設計方針
1. 機能要件の整理
- このコンポーネントをアタッチしたノードを「磁石ブロック」とみなす。
- 磁石ブロックは、一定の半径内にある「鉄製オブジェクト」に力を加える。
- 力の向きは「引き寄せ(吸引)」または「反発(押し出し)」を切り替え可能。
- 磁力の強さ、最大距離、減衰カーブ(距離によって弱くなる)をインスペクタから調整可能。
- 磁石を ON/OFF できる(例えばスイッチやギミックと連動させる)。
- 外部のカスタムスクリプトには依存しない。必要なのは:
- 磁石ノード側: 2D 物理(
RigidBody2Dは任意)+Collider2D(トリガー領域に使う) - 鉄製オブジェクト側:
RigidBody2D+Collider2D+ マーカーコンポーネント
- 磁石ノード側: 2D 物理(
ここでは「周囲のオブジェクトを検出する」ために 2D 物理のトリガー領域 を使います。磁石ノードに Collider2D(例えば CircleCollider2D)を付与し、isTrigger = true にしておくことで、その領域に入ってきた「鉄製オブジェクト」を検出し、リスト管理します。
2. 鉄製オブジェクトの識別方法
外部のマネージャに頼らず、かつ柔軟に「これは磁石の影響を受ける鉄製オブジェクトだ」と識別するために、シンプルなマーカーコンポーネント を用意します。
このガイドでは、同一ファイル内に以下 2 コンポーネントを定義します。
MagneticObject:鉄製オブジェクトにアタッチする「マーカー」。Rigidbody2D 必須。MagnetBlock:磁石ブロック本体。MagneticObject を検出して力を加える。
どちらも同じ TypeScript ファイルに定義するため、この 1 ファイルだけで完結します。他ファイルへの依存はありません。
3. インスペクタで設定可能なプロパティ
MagneticObject(鉄製オブジェクト側)
magnetMultiplier: number- このオブジェクトが磁力をどれだけ受けやすいかの係数。
- 1 が標準。2 なら 2 倍の力、0.5 なら半分の力。
- 質量の違いなどをざっくり調整したいときに使う。
maxMagnetSpeed: number- 磁力によって加速される最高速度の目安(m/s)。
- あまりに高速になりすぎるのを防ぐためのクランプ値。
MagnetBlock(磁石ブロック側)
enabledMagnet: boolean- 磁石の ON/OFF。
- ゲーム中に他スクリプトから
node.getComponent(MagnetBlock)!.enabledMagnet = false;のように切り替え可能。
isRepulsive: booleanfalse:引き寄せ(吸引)。true:反発(押し出し)。
baseForce: number- 磁力の基礎強度(N に相当するイメージ)。
- 数値が大きいほど、鉄製オブジェクトに加わる力が強くなる。
maxDistance: number- 磁力が届く最大距離(ワールド座標上の距離)。
- Collider2D の半径(またはサイズ)とできるだけ合わせる。
distanceFalloff: number- 距離による減衰指数。
- 1:距離に反比例(1 / d)。
- 2:距離の 2 乗に反比例(1 / d²)で、近いほど急激に強くなる。
minEffectiveDistance: number- あまりに近すぎると計算が暴れるための「最小距離クランプ」。
- 0.1 などの小さな値を設定。
debugDrawRange: boolean- 有効にすると、
Gizmos的にシーンビューで磁力範囲を線で表示(簡易)。 - ゲームプレイには影響しない。
- 有効にすると、
debugColor: Color- 範囲のデバッグ表示色。
これらのプロパティを使って、磁石の強さ・範囲・挙動をインスペクタから完結に調整できるようにします。
TypeScriptコードの実装
以下が完成した MagnetBlock.ts(MagneticObject も同ファイル内)の全コードです。
import { _decorator, Component, Node, RigidBody2D, Collider2D, IPhysics2DContact, Vec2, Vec3, Color, Graphics, UITransform, director, math } from 'cc';
const { ccclass, property } = _decorator;
/**
* 鉄製オブジェクト側に付けるマーカーコンポーネント。
* RigidBody2D を持っている前提で、MagnetBlock から力を加えられる対象になります。
*/
@ccclass('MagneticObject')
export class MagneticObject extends Component {
@property({
tooltip: 'このオブジェクトが磁力をどれだけ受けやすいかの係数。\n1=標準, 2=2倍の力, 0.5=半分の力。'
})
public magnetMultiplier: number = 1.0;
@property({
tooltip: '磁力によって加速される最高速度の目安(m/s)。\n0以下にすると無制限になります。'
})
public maxMagnetSpeed: number = 10.0;
private _rigidBody: RigidBody2D | null = null;
onLoad() {
this._rigidBody = this.getComponent(RigidBody2D);
if (!this._rigidBody) {
console.error('[MagneticObject] RigidBody2D が見つかりません。このノードには RigidBody2D を追加してください。', this.node.name);
}
}
/**
* MagnetBlock から呼ばれる、力を加えるためのメソッド。
* @param force 加える力(ワールド座標系)
*/
public applyMagnetForce(force: Vec2) {
if (!this._rigidBody) {
return;
}
// 速度制限が有効な場合は、現在速度と比較してクランプ
if (this.maxMagnetSpeed > 0) {
const currentVel = this._rigidBody.linearVelocity;
const speed = currentVel.length();
if (speed > this.maxMagnetSpeed) {
// すでに上限を超えている場合、これ以上加速させない
return;
}
}
// 磁力の係数を適用
const scaledForce = new Vec2(force.x * this.magnetMultiplier, force.y * this.magnetMultiplier);
this._rigidBody.applyForceToCenter(scaledForce, true);
}
}
/**
* 磁石ブロック本体。
* このコンポーネントを持つノードに Collider2D(isTrigger=true) を付けることで、
* その範囲に入ってきた MagneticObject に対して磁力を加えます。
*/
@ccclass('MagnetBlock')
export class MagnetBlock extends Component {
@property({
tooltip: '磁石のON/OFF。falseにすると一切磁力を発生しません。'
})
public enabledMagnet: boolean = true;
@property({
tooltip: 'trueにすると反発(押し出し)、falseにすると引き寄せ(吸引)になります。'
})
public isRepulsive: boolean = false;
@property({
tooltip: '磁力の基礎強度。値が大きいほど対象に加わる力が強くなります。'
})
public baseForce: number = 30.0;
@property({
tooltip: '磁力が届く最大距離(ワールド座標上)。Collider2Dのサイズと揃えると分かりやすいです。'
})
public maxDistance: number = 5.0;
@property({
tooltip: '距離による減衰指数。\n1=距離に反比例(1/d)、2=距離の2乗に反比例(1/d^2)など。'
})
public distanceFalloff: number = 1.0;
@property({
tooltip: 'あまりに近すぎると計算が不安定になるための最小距離クランプ値。'
})
public minEffectiveDistance: number = 0.2;
@property({
tooltip: 'シーンビュー上に磁力範囲を簡易表示するかどうか。',
})
public debugDrawRange: boolean = false;
@property({
tooltip: '磁力範囲のデバッグ表示色。',
})
public debugColor: Color = new Color(0, 200, 255, 255);
private _collider: Collider2D | null = null;
private _graphics: Graphics | null = null;
// 現在磁力の影響下にある MagneticObject の集合
private _targets: Set = new Set();
onLoad() {
// Collider2D を取得(磁力範囲の検知に使用)
this._collider = this.getComponent(Collider2D);
if (!this._collider) {
console.error('[MagnetBlock] Collider2D が見つかりません。このノードに CircleCollider2D などの Collider2D を追加し、isTrigger=true に設定してください。', this.node.name);
} else {
// トリガーイベントを購読
this._collider.on('onBeginContact', this._onBeginContact, this);
this._collider.on('onEndContact', this._onEndContact, this);
}
// デバッグ表示用に Graphics(2D 描画コンポーネント)を用意(任意)
// UIノードでない場合は自前で Graphics 用ノードを作るか、ここでは簡易に同ノードに付与を試みる。
if (this.debugDrawRange) {
this._setupGraphicsForDebug();
}
}
onDestroy() {
if (this._collider) {
this._collider.off('onBeginContact', this._onBeginContact, this);
this._collider.off('onEndContact', this._onEndContact, this);
}
}
start() {
// maxDistance が 0 以下の場合、Collider2D から自動推定を試みる(円形前提の簡易実装)
if (this.maxDistance <= 0 && this._collider) {
const worldAabb = this._collider.worldAABB;
// おおよその半径として、幅と高さのうち大きい方の半分を採用
this.maxDistance = Math.max(worldAabb.width, worldAabb.height) * 0.5;
}
}
update(deltaTime: number) {
if (!this.enabledMagnet || this._targets.size === 0) {
return;
}
const magnetWorldPos = this.node.worldPosition;
// 登録されている全ターゲットに対して力を加える
this._targets.forEach((magneticObj) => {
const targetNode = magneticObj.node;
const targetWorldPos = targetNode.worldPosition;
// 距離ベクトルと距離を計算
const dir = new Vec3(
targetWorldPos.x - magnetWorldPos.x,
targetWorldPos.y - magnetWorldPos.y,
0
);
let distance = Math.sqrt(dir.x * dir.x + dir.y * dir.y);
if (distance <= 0.0001) {
// 同一座標に重なっている場合は何もしない
return;
}
// 範囲外は無視
if (this.maxDistance > 0 && distance > this.maxDistance) {
return;
}
// 最小距離クランプ
if (distance < this.minEffectiveDistance) {
distance = this.minEffectiveDistance;
}
// 単位ベクトルを計算
const dirNorm = new Vec2(dir.x / distance, dir.y / distance);
// 減衰を計算 (1 / distance^falloff)
let falloffFactor = 1.0;
if (this.distanceFalloff > 0) {
falloffFactor = 1.0 / Math.pow(distance, this.distanceFalloff);
}
// 基本の力の大きさ
const magnitude = this.baseForce * falloffFactor;
// 吸引 or 反発による方向反転
const directionSign = this.isRepulsive ? 1.0 : -1.0;
const force = new Vec2(
dirNorm.x * magnitude * directionSign,
dirNorm.y * magnitude * directionSign
);
// MagneticObject に力を適用
magneticObj.applyMagnetForce(force);
});
// デバッグ描画更新
if (this.debugDrawRange && this._graphics) {
this._drawDebugCircle();
}
}
/**
* Collider2D の onBeginContact コールバック。
* MagneticObject を検出してターゲットセットに追加します。
*/
private _onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
const magneticObj = otherCollider.getComponent(MagneticObject) || otherCollider.node.getComponent(MagneticObject);
if (magneticObj) {
this._targets.add(magneticObj);
}
}
/**
* Collider2D の onEndContact コールバック。
* 範囲から出た MagneticObject をターゲットセットから除外します。
*/
private _onEndContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) {
const magneticObj = otherCollider.getComponent(MagneticObject) || otherCollider.node.getComponent(MagneticObject);
if (magneticObj && this._targets.has(magneticObj)) {
this._targets.delete(magneticObj);
}
}
/**
* デバッグ表示用 Graphics コンポーネントをセットアップ。
* 失敗してもゲームロジックには影響しません。
*/
private _setupGraphicsForDebug() {
// すでに Graphics が付いていればそれを利用
this._graphics = this.getComponent(Graphics);
if (!this._graphics) {
// UITransform が必要なので、なければ追加を試みる
let uiTrans = this.getComponent(UITransform);
if (!uiTrans) {
uiTrans = this.node.addComponent(UITransform);
}
this._graphics = this.node.addComponent(Graphics);
}
if (!this._graphics) {
console.warn('[MagnetBlock] Graphics コンポーネントを追加できませんでした。debugDrawRange は機能しません。');
}
}
/**
* 磁力範囲のデバッグ用円を描画します。
*/
private _drawDebugCircle() {
if (!this._graphics) {
return;
}
const g = this._graphics;
g.clear();
g.strokeColor = this.debugColor;
g.lineWidth = 2;
// ローカル座標で円を描く。半径は maxDistance を使用。
const radius = this.maxDistance > 0 ? this.maxDistance : 1.0;
g.circle(0, 0, radius);
g.stroke();
}
}
主要な処理のポイント解説
- onLoad()
MagnetBlockは同ノードのCollider2Dを取得し、見つからない場合はエラーログを出します。- 見つかった場合、
onBeginContact/onEndContactを購読し、範囲に入った/出たMagneticObjectをセットで管理します。 - デバッグ描画が有効なら
Graphicsをセットアップします。
- start()
maxDistanceが 0 以下の場合、Collider2Dの AABB から半径を自動推定します(簡易)。
- update(deltaTime)
- 磁石が ON かつターゲットが存在する場合のみ処理。
- 各
MagneticObjectについて:- 磁石との距離と方向を計算。
maxDistanceを超えるものは無視。distanceFalloffに基づき1 / distance^falloffで減衰。isRepulsiveに応じて方向を反転(吸引 or 反発)。- 計算した力ベクトルを
MagneticObject.applyMagnetForceに渡す。
- デバッグ描画が有効なときは、
Graphicsで簡易な円を描画。
- MagneticObject.applyMagnetForce()
RigidBody2Dがない場合は何もしない(防御的)。maxMagnetSpeedが正の場合、現在速度が上限を超えていればこれ以上力を加えない。magnetMultiplierを掛けた力をapplyForceToCenterで適用。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリック → Create → TypeScript を選択します。
- ファイル名を MagnetBlock.ts にします。
- 作成された
MagnetBlock.tsをダブルクリックで開き、既存の中身をすべて削除して、上記のコードを丸ごと貼り付けて保存します。
2. 鉄製オブジェクト(MagneticObject)の準備
まずは磁石に引き寄せられる「鉄製オブジェクト」を作ります。
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などでテスト用ノードを作成します。
- 名前例:
IronBox
- 名前例:
IronBoxノードを選択し、Inspector の Add Component ボタンをクリックします。- Physics 2D → RigidBody2D を追加します。
- Body Type:
Dynamicに設定。 - 重力などはデフォルトのままで構いません。
- Body Type:
- 同じく Add Component → Physics 2D → BoxCollider2D(または CircleCollider2D)を追加します。
OffsetやSizeは Sprite の見た目に合わせて調整します。Is Triggerは OFF のままで構いません(物理衝突用)。
- さらに Add Component → Custom → MagneticObject を追加します。
Magnet Multiplier:1(標準)Max Magnet Speed:10など(あとで調整可能)
- この
IronBoxを複数配置したい場合は、Hierarchy 上で右クリック → Duplicate で複製し、位置をばらけさせておきます。
3. 磁石ブロック(MagnetBlock)の準備
- Hierarchy パネルで右クリック → Create → 2D Object → Sprite などで磁石用ノードを作成します。
- 名前例:
Magnet - 見た目として磁石アイコンのような画像を設定すると分かりやすいです(任意)。
- 名前例:
Magnetノードを選択し、Inspector の Add Component ボタンをクリック。- Physics 2D → CircleCollider2D を追加します。
Is Triggerに チェックを入れる(重要)。Radiusを例えば5に設定(シーンのスケールに応じて調整)。- この半径が「磁力が届く範囲の目安」になります。
- 同じく Add Component → Custom → MagnetBlock を追加します。
- Inspector 上で MagnetBlock のプロパティを設定します(例):
Enabled Magnet:trueIs Repulsive:false(引き寄せモード)Base Force:50Max Distance:5(CircleCollider2D の半径と合わせる)Distance Falloff:1(距離に反比例)Min Effective Distance:0.2Debug Draw Range:true(範囲を見ながら調整したい場合)Debug Color:お好みの色(デフォルトの水色でOK)
4. シーン配置と簡単なテスト
- シーンに
IronBox(MagneticObject 付き)を複数配置し、Magnetノードもシーン中央あたりに置きます。 - 物理がわかりやすいように、床となる静的コライダー(StaticBody2D + BoxCollider2D)を用意してもよいです。
- 上部メニューの Play(▶)ボタンを押してゲームを実行します。
- 引き寄せモード(Is Repulsive = false)の確認:
IronBoxを磁石の周囲に置いておくと、時間経過とともに Magnet に向かって移動し始めるはずです。- 勢いが弱い場合は
Base Forceを上げてみてください(例:100)。 - 強すぎて暴れる場合は
Base Forceを下げるか、Distance Falloffを 2 にして近距離でのみ強くするなど調整します。
- 反発モード(Is Repulsive = true)の確認:
- ゲーム停止後、Inspector で
Is Repulsiveをtrueに変更して再生します。 - 今度は IronBox が磁石から離れる方向に押し出されるように動くはずです。
- ゲーム停止後、Inspector で
- ON/OFF の確認:
- 再生中に Inspector で
Enabled Magnetをfalseにすると、その瞬間から磁力が止まり、IronBox は慣性と重力だけで動くようになります。
- 再生中に Inspector で
5. よくあるハマりポイントと確認事項
- MagnetBlock 側の Collider2D が Trigger になっていない
Is Triggerにチェックが入っていないとonBeginContact/onEndContactが期待通りに動作しません。
- MagneticObject に RigidBody2D が付いていない
- その場合、コンソールに
[MagneticObject] RigidBody2D が見つかりません...というエラーが出ます。 - 必ず
DynamicなRigidBody2Dを追加してください。
- その場合、コンソールに
- BaseForce が小さすぎて動きがわからない
- 距離減衰と質量の影響で、初期値ではあまり動かない場合があります。
- テスト時は
Base Force = 100〜300程度まで一度上げて、挙動を確認するとよいです。
- 物理が激しく暴れる/跳ねる
- BaseForce が大きすぎる、
Distance Falloffが小さすぎる(0 に近い)、Min Effective Distanceが小さすぎるといった場合に起こりやすいです。 - BaseForce を下げる、DistanceFalloff を 1.5〜2.0 に上げる、MinEffectiveDistance を 0.3〜0.5 にするなど、段階的に調整してください。
- BaseForce が大きすぎる、
まとめ
このガイドでは、Cocos Creator 3.8.7 と TypeScript を使って、
- 磁石ブロック本体コンポーネント
MagnetBlock - 鉄製オブジェクト用マーカーコンポーネント
MagneticObject
の 2 つを 1 つのスクリプトファイル内 に実装し、
- インスペクタから磁力の強さ・範囲・減衰・吸引/反発を自由に調整できる。
- 外部の GameManager やシングルトンに一切依存しない。
- 任意のノードにアタッチするだけで「磁石ギミック」を実現できる。
という、汎用性の高いコンポーネントを作成しました。
応用例としては:
- パズルゲームの「磁石ブロック」:鉄球を吸い寄せてスイッチに導く。
- アクションゲームの「反発フィールド」:プレイヤー弾を弾き返すバリア。
- ステージギミック:一定時間だけ ON になる磁石床や、プレイヤーが持ち運べる磁石アイテム。
など、さまざまなシーンで再利用できます。
すべての設定がインスペクタから完結しているため、プログラマ以外のメンバーでも値をいじりながらゲームバランスを調整でき、プロトタイピングのスピードも大きく向上します。
このコンポーネントをベースに、例えば「特定のレイヤーだけに作用する」「磁力を時間でフェードイン/アウトさせる」「3D 版の MagnetBlock」など、さらに発展させていくことも容易です。ぜひプロジェクトの共通ギミックとして組み込んでみてください。




