Godot で「コインだけをプレイヤーに吸い寄せたい」と思ったとき、ありがちな実装としては:
- プレイヤーシーンを継承して「CoinMagnet付きプレイヤー」を別途作る
- プレイヤーのスクリプトに「磁石ロジック」を直接書き足す
- コイン側に「プレイヤーを探して近づく」処理を全部書く
…みたいな感じになりがちですよね。すると、
- プレイヤーのスクリプトがどんどん肥大化する
- 敵や動く床など「別のノードで同じ磁石ロジックを使いたい」ときにコピペ地獄
- 「コインだけ強く吸い寄せたい、他のアイテムは弱めに」みたいな調整がしづらい
といった問題が出てきます。
そこで今回は、「コイン専用」の磁石ロジックをひとつのコンポーネントに切り出した CoinMagnet を用意して、プレイヤーでも敵でも、好きなノードにペタっと貼るだけで「コインだけを強力に吸い寄せる」挙動を実現していきましょう。
【Godot 4】コインだけ強烈に吸い寄せろ!「CoinMagnet」コンポーネント
このコンポーネントは:
- 任意のノード(プレイヤー、敵、動く床など)にアタッチ可能
- 指定した半径内の「コイン」だけを自動的に探索
- コインをホーミングさせる(ターゲット=このコンポーネントが付いているノード)
- 「コインの判定」はグループ名やカスタムクラス名で柔軟に指定可能
という、いかにも「合成(Composition)向き」な小さな部品です。
フルコード:CoinMagnet.gd
extends Node
class_name CoinMagnet
## コイン専用の「磁石」コンポーネント。
## アタッチされたノード(多くはプレイヤー)を中心に、
## 指定半径内の「コイン」だけを自動で吸い寄せます。
@export_group("基本設定")
## コインを探索するワールド空間での半径。
@export var radius: float = 160.0
## コインがプレイヤーに向かって移動するスピード(ピクセル/秒)。
@export var pull_speed: float = 400.0
## コインとの距離がこの値より小さくなったら、
## 磁石の影響を止める(=通常の取得処理に任せる)しきい値。
@export var stop_distance: float = 12.0
@export_group("ターゲット設定")
## 磁石の「中心」となるノード。
## 空のままなら、親ノード(get_parent())を自動でターゲットにします。
@export var target_node: Node3D
@export_group("コイン判定")
## コインが所属しているグループ名。
## ここに指定したグループに属するノードだけを吸い寄せます。
## 例: "coins"
@export var coin_group: String = "coins"
## クラスベースでフィルタしたい場合に使用。
## 例: Coin クラスを作っているなら "Coin" と指定。
@export var coin_class_name: String = ""
@export_group("挙動調整")
## 1フレームごとに全コインを総当りするのは重いので、
## 一定フレームごとに探索するようにします。
@export var scan_interval_frames: int = 3
## コインの移動に補間をかける割合(0.0〜1.0)。
## 1.0 だとカクっと動き、0.1 だとヌルっと追従するイメージ。
@export_range(0.0, 1.0, 0.01)
@export var follow_lerp_factor: float = 0.3
## コインを「引き寄せる」際に使うイージング。
## 0.0 に近いほど開始時に弱く、終わりに強くなる。
@export_range(0.0, 1.0, 0.01)
@export var ease_out_factor: float = 0.2
## デバッグ用に「探索半径」を描画するかどうか。
@export var debug_draw_radius: bool = false
var _frame_counter: int = 0
var _cached_coins: Array[Node3D] = []
func _ready() -> void:
# ターゲット未指定なら、親ノードをターゲットにする。
if target_node == null:
var parent := get_parent()
if parent is Node3D:
target_node = parent
else:
push_warning(
"CoinMagnet: target_node が未設定で、親が Node3D ではありません。" +
"必ず Node3D を target_node に設定してください。"
)
if scan_interval_frames <= 0:
scan_interval_frames = 1
func _physics_process(delta: float) -> void:
if target_node == null:
return
_frame_counter += 1
# 一定フレームごとにコイン一覧を更新
if _frame_counter % scan_interval_frames == 0:
_scan_coins()
# キャッシュされたコインを順に引き寄せる
for coin in _cached_coins:
if not is_instance_valid(coin):
continue
var coin_pos: Vector3 = coin.global_transform.origin
var target_pos: Vector3 = target_node.global_transform.origin
var to_target: Vector3 = target_pos - coin_pos
var distance: float = to_target.length()
# 一定距離以内なら磁石の影響をやめる(=拾い処理に任せる)
if distance <= stop_distance:
continue
if distance > radius:
# 範囲外は無視
continue
# 正規化ベクトル
var dir: Vector3 = to_target.normalized()
# 距離に応じてイージングをかける(近づくほど強く引き寄せる)
var t: float = clamp(distance / radius, 0.0, 1.0)
var eased: float = _ease_out(1.0 - t, ease_out_factor)
# このフレームで進める距離
var step: float = pull_speed * eased * delta
var target_move: Vector3 = dir * step
# コインの新しい位置(補間してヌルっと追従させる)
var desired_pos: Vector3 = coin_pos + target_move
var new_pos: Vector3 = coin_pos.lerp(desired_pos, follow_lerp_factor)
var coin_transform := coin.global_transform
coin_transform.origin = new_pos
coin.global_transform = coin_transform
func _scan_coins() -> void:
## 現在のシーンツリーから、条件に合う「コイン候補」を集める。
_cached_coins.clear()
var tree := get_tree()
if tree == null:
return
# グループ名でフィルタ
if coin_group != "":
for node in tree.get_nodes_in_group(coin_group):
if node is Node3D and _is_coin(node):
_cached_coins.append(node)
else:
# グループ未指定なら、クラス名だけでフィルタ
for node in tree.get_nodes_in_group("root"): # 疑似的な全探索は自前でほぼしない
# 実際には全ノード総当りは重いので、運用では coin_group の指定を推奨
if node is Node3D and _is_coin(node):
_cached_coins.append(node)
func _is_coin(node: Node) -> bool:
## 「これはコインか?」を判定するヘルパー。
## グループ / クラス名の両方でフィルタできます。
# クラス名でのフィルタ(Coin クラスなど)
if coin_class_name != "":
if node.get_class() != coin_class_name and not node.is_class(coin_class_name):
return false
# グループ名でのフィルタ(coins など)
if coin_group != "" and not node.is_in_group(coin_group):
return false
return true
func _ease_out(x: float, k: float) -> float:
## シンプルな ease-out カーブ。
## x: 0.0〜1.0, k: 0.0〜1.0 (0に近いほど終盤に強くなる)
return pow(x, 1.0 - k)
func _process(_delta: float) -> void:
if debug_draw_radius and target_node != null:
_debug_draw()
func _debug_draw() -> void:
## 簡易デバッグ描画。Editor では見えないが、ゲーム中に半径を確認できます。
var debug_world := get_viewport().debug_draw
if debug_world == null:
return
var center: Vector3 = target_node.global_transform.origin
var color := Color(1.0, 0.9, 0.2, 0.3) # 黄っぽい半透明
debug_world.draw_sphere(center, radius, color)
使い方の手順
ここでは 3D の例で説明しますが、2D プロジェクトでも考え方は同じです(その場合は Node2D / CharacterBody2D 版に書き換えればOK)。
手順①:コインシーンを用意してグループを設定
- コイン用のシーン(例:
Coin.tscn)を作成します。 - ルートは
Node3DまたはRigidBody3D/Area3Dなど、位置を持てる 3D ノードにします。 - インスペクタの「ノード > グループ」から coins グループを追加します(コードの
coin_group = "coins"に合わせる)。
Coin (Node3D) ├── MeshInstance3D └── CollisionShape3D
もしクラス名ベースでフィルタしたい場合は、Coin.gd を作って class_name Coin をつけておき、CoinMagnet 側の coin_class_name に "Coin" を設定します。
手順②:プレイヤーシーンに CoinMagnet をアタッチ
プレイヤーシーン例:
Player (CharacterBody3D) ├── MeshInstance3D ├── CollisionShape3D └── CoinMagnet (Node)
- プレイヤーシーンを開き、
Playerの子としてNodeを追加し、名前を CoinMagnet にします。 - そのノードに、先ほどの
CoinMagnet.gdをアタッチします。 target_nodeを空のままにしておけば、自動的に親(= Player)がターゲットになります。coin_groupを"coins"に設定します(コイン側のグループと合わせる)。
これで、プレイヤーを中心に半径 radius 内のコインが自動的に吸い寄せられるようになります。
手順③:敵や動く床にも簡単に流用できる
敵が近づくとコインを吸い取る、動く床の上に近づいたコインがスーッと乗ってくる…といった挙動も、同じコンポーネントをペタっと貼るだけでOKです。
例えば、動く床のシーン:
MovingPlatform (Node3D) ├── MeshInstance3D ├── CollisionShape3D └── CoinMagnet (Node)
MovingPlatformの子にNodeを追加し、CoinMagnet.gdをアタッチ。target_nodeを空のままにしておけば、MovingPlatform自身がターゲットになります。- 磁石の強さを変えたいなら、プレイヤー用と別インスタンスにして
radiusやpull_speedを調整しましょう。
継承もコピペも不要で、「コインを吸い寄せる能力」を好きなノードに付け外しできるのがポイントですね。
手順④:レベル全体での動作確認とチューニング
- テスト用のステージシーンを作成し、プレイヤーと複数のコインを配置します。
- ゲームを実行し、プレイヤーがコインに近づいたときに「スーッ」と吸い寄せられるか確認します。
- 吸い寄せが弱ければ
pull_speedを上げる、範囲を広げたいならradiusを上げるなどして調整します。 - ゲーム中に半径を可視化したい場合は、
debug_draw_radiusをオンにして挙動を確認しましょう。
メリットと応用
この CoinMagnet コンポーネントを使う一番のメリットは、
- 「コインを吸い寄せる」というロジックがプレイヤーやコイン本体から完全に分離される
- プレイヤー、敵、動く床など、複数のノードで同じロジックを簡単に再利用できる
- シーン構造が「見て分かる」ようになる(どのノードが磁石を持っているか一目瞭然)
という点です。
特に大規模になってくると、「プレイヤーのスクリプトに全部書く」方式だと、後から仕様変更が入ったときに地獄を見ます。
「コイン磁石を一時的にオフにしたい」「特定のエリアでは磁石無効にしたい」「敵だけコイン吸い取りを強くしたい」などの要件が出てきたとき、コンポーネントとして分離してあれば:
- 特定のシーンだけ CoinMagnet ノードを削除 / 無効化する
- インスタンスごとに
radiusやpull_speedを変える - エリアに入ったら
set_process(false)で磁石停止、出たら再開
といった運用がとてもやりやすくなります。
改造案:一時的に磁石をオン/オフする API を追加
例えば、パワーアップアイテムを取ったときだけコイン磁石を有効にしたい場合、次のような簡単なメソッドを追加すると便利です。
## CoinMagnet.gd に追記
var _enabled: bool = true
func set_enabled(value: bool) -> void:
_enabled = value
set_physics_process(value)
set_process(value)
func is_enabled() -> bool:
return _enabled
func _physics_process(delta: float) -> void:
if not _enabled:
return
# 既存の処理はこの下に置く
if target_node == null:
return
_frame_counter += 1
if _frame_counter % scan_interval_frames == 0:
_scan_coins()
for coin in _cached_coins:
# ...(以下略)
こうしておけば、プレイヤー側のスクリプトから:
# 例: パワーアップアイテム取得時に呼ぶ
func _on_powerup_collected() -> void:
var magnet: CoinMagnet = $CoinMagnet
magnet.set_enabled(true)
のように、「磁石能力のオン/オフ」を簡単に制御できます。
ロジックをコンポーネントに閉じ込めておくことで、ゲーム全体の設計がかなりスッキリしてきますね。




