【Godot 4】CoinMagnet (コイン磁石) コンポーネントの作り方

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時点)

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)。

手順①:コインシーンを用意してグループを設定

  1. コイン用のシーン(例:Coin.tscn)を作成します。
  2. ルートは Node3D または RigidBody3D / Area3D など、位置を持てる 3D ノードにします。
  3. インスペクタの「ノード > グループ」から 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)
  1. プレイヤーシーンを開き、Player の子として Node を追加し、名前を CoinMagnet にします。
  2. そのノードに、先ほどの CoinMagnet.gd をアタッチします。
  3. target_node を空のままにしておけば、自動的に親(= Player)がターゲットになります。
  4. coin_group"coins" に設定します(コイン側のグループと合わせる)。

これで、プレイヤーを中心に半径 radius 内のコインが自動的に吸い寄せられるようになります。

手順③:敵や動く床にも簡単に流用できる

敵が近づくとコインを吸い取る、動く床の上に近づいたコインがスーッと乗ってくる…といった挙動も、同じコンポーネントをペタっと貼るだけでOKです。

例えば、動く床のシーン:

MovingPlatform (Node3D)
 ├── MeshInstance3D
 ├── CollisionShape3D
 └── CoinMagnet (Node)
  1. MovingPlatform の子に Node を追加し、CoinMagnet.gd をアタッチ。
  2. target_node を空のままにしておけば、MovingPlatform 自身がターゲットになります。
  3. 磁石の強さを変えたいなら、プレイヤー用と別インスタンスにして radiuspull_speed を調整しましょう。

継承もコピペも不要で、「コインを吸い寄せる能力」を好きなノードに付け外しできるのがポイントですね。

手順④:レベル全体での動作確認とチューニング

  1. テスト用のステージシーンを作成し、プレイヤーと複数のコインを配置します。
  2. ゲームを実行し、プレイヤーがコインに近づいたときに「スーッ」と吸い寄せられるか確認します。
  3. 吸い寄せが弱ければ pull_speed を上げる、範囲を広げたいなら radius を上げるなどして調整します。
  4. ゲーム中に半径を可視化したい場合は、debug_draw_radius をオンにして挙動を確認しましょう。

メリットと応用

この CoinMagnet コンポーネントを使う一番のメリットは、

  • 「コインを吸い寄せる」というロジックがプレイヤーやコイン本体から完全に分離される
  • プレイヤー、敵、動く床など、複数のノードで同じロジックを簡単に再利用できる
  • シーン構造が「見て分かる」ようになる(どのノードが磁石を持っているか一目瞭然)

という点です。

特に大規模になってくると、「プレイヤーのスクリプトに全部書く」方式だと、後から仕様変更が入ったときに地獄を見ます。
「コイン磁石を一時的にオフにしたい」「特定のエリアでは磁石無効にしたい」「敵だけコイン吸い取りを強くしたい」などの要件が出てきたとき、コンポーネントとして分離してあれば:

  • 特定のシーンだけ CoinMagnet ノードを削除 / 無効化する
  • インスタンスごとに radiuspull_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)

のように、「磁石能力のオン/オフ」を簡単に制御できます。
ロジックをコンポーネントに閉じ込めておくことで、ゲーム全体の設計がかなりスッキリしてきますね。

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をコピーしました!