【Godot 4】MineLayer (地雷設置) コンポーネントの作り方

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 で「踏むと爆発する地雷」を実装しようとすると、ありがちなパターンはこうですよね。

  • プレイヤーシーンの中に「地雷設置用の子ノード」を作る
  • 専用の Mine シーンを作り、プレイヤーのスクリプトからインスタンスして配置する
  • さらに爆発エフェクト用シーンも作り、Mine 側から生成して…

もちろんこれでも動きますが、プレイヤーのスクリプトに「地雷設置ロジック」がべったりくっついてしまいがちです。
「敵も地雷を置きたい」「動く床も地雷をばらまきたい」となったときに、継承やコピペで対応するとだんだんカオスになっていきます。

そこで今回は、どんなノードにもアタッチできるコンポーネントとして、MineLayer を用意してみましょう。
プレイヤーでも敵でも、MineLayer を 1 個アタッチするだけで、「足元に踏むと爆発する地雷を設置できる」ようにするコンポーネントです。

【Godot 4】足元にポンポン地雷設置!「MineLayer」コンポーネント

このコンポーネントは、親ノードの足元に Area2D ベースの地雷をスポーンしてくれます。
地雷そのものもコード内で完結させているので、「とりあえず動く最小構成」としてコピペで試せます。

コンポーネントの仕様

  • 親ノードの global_position を基準に、少し下(足元)に地雷を配置
  • 地雷は Area2D として生成され、body_entered で踏んだ相手を検知
  • 踏まれたら爆発エフェクト(簡易版)を表示しつつ、自身を削除
  • レイヤー側では「クールダウン」「最大設置数」などを制御

フルコード:MineLayer.gd


extends Node
class_name MineLayer
"""
親ノードの足元に「踏むと爆発する地雷(Area2D)」を設置するコンポーネント。

- 親ノードは 2D 用(CharacterBody2D, Node2D, RigidBody2D など)を想定
- 地雷はコード内で動的に生成するため、専用シーンを作らなくても動く
"""

# --- 設定パラメータ -----------------------------

@export var mine_radius: float = 16.0
## 地雷の当たり判定の半径(ピクセル)
## 小さくすると踏み判定がシビアになり、大きくすると踏みやすくなる

@export var mine_offset: Vector2 = Vector2(0, 8)
## 親ノードの足元からどれだけオフセットするか
## Y をプラスにすると下方向に配置される

@export var cooldown: float = 0.5
## 地雷を連続で設置できるまでのクールダウン時間(秒)

@export var max_mines: int = 5
## このコンポーネントが設置できる地雷の最大数
## 0 以下を指定すると無制限に設置可能

@export var mine_lifetime: float = 0.0
## 地雷の寿命(秒)。0 の場合は無制限。
## 一定時間で自動消滅させたい場合に使う。

@export var trigger_on_bodies_in_group: StringName = &"player"
## どのグループに属する PhysicsBody2D が踏んだら爆発するか
## 空文字のときは「誰が踏んでも爆発」

@export var mine_color: Color = Color(1, 0.3, 0.3, 1.0)
## 地雷本体の色(デバッグ用の見た目)

@export var explosion_color: Color = Color(1, 0.8, 0.2, 1.0)
## 爆発エフェクトの色(デバッグ用の見た目)

@export var explosion_duration: float = 0.25
## 爆発エフェクトが表示される時間(秒)

@export var debug_draw: bool = true
## シンプルな Circle を使って見た目を描画するかどうか
## false にして、代わりに好きな見た目をアタッチしても良い

# 信号:外部に通知したいイベント
signal mine_placed(mine: Area2D)
signal mine_exploded(mine: Area2D, body: Node)

# --- 内部状態 -----------------------------

var _time_since_last_place: float = 0.0
var _active_mines: Array[Area2D] = []

func _ready() -> void:
    # 特に必須ではないが、親が 2D ノードであることを軽くチェック
    if not owner or not (owner is Node2D):
        push_warning("MineLayer は 2D ノードにアタッチすることを想定しています。owner: %s" % [owner])

func _process(delta: float) -> void:
    # クールダウン経過時間を更新
    _time_since_last_place += delta


# --- パブリック API -----------------------------

func can_place_mine() -> bool:
    """
    現在、地雷を設置可能かどうかを返す。
    - クールダウンが終わっている
    - 最大設置数を超えていない
    """
    if cooldown > 0.0 and _time_since_last_place < cooldown:
        return false
    if max_mines > 0 and _active_mines.size() >= max_mines:
        return false
    return true


func place_mine() -> Area2D:
    """
    地雷を 1 個設置する。
    設置に成功したら Area2D を返し、失敗したら null を返す。
    """
    if not can_place_mine():
        return null

    if not owner or not (owner is Node2D):
        push_error("MineLayer の owner が Node2D ではないため、地雷を配置できません。")
        return null

    var parent_2d := owner as Node2D

    # --- 地雷ノードの生成 ---
    var mine := Area2D.new()
    mine.name = "Mine"
    mine.global_position = parent_2d.global_position + mine_offset

    # 衝突レイヤー/マスクはプロジェクトに合わせて調整してください
    mine.collision_layer = 1
    mine.collision_mask = 1

    # 当たり判定
    var shape := CircleShape2D.new()
    shape.radius = mine_radius

    var collision := CollisionShape2D.new()
    collision.shape = shape
    mine.add_child(collision)

    # 見た目(デバッグ用)
    if debug_draw:
        var sprite := _create_debug_sprite(mine_radius, mine_color)
        mine.add_child(sprite)

    # 踏まれたときの処理を接続
    mine.body_entered.connect(_on_mine_body_entered.bind(mine))

    # 寿命タイマー(任意)
    if mine_lifetime > 0.0:
        var lifetime_timer := Timer.new()
        lifetime_timer.one_shot = true
        lifetime_timer.wait_time = mine_lifetime
        lifetime_timer.timeout.connect(_on_mine_lifetime_timeout.bind(mine))
        mine.add_child(lifetime_timer)
        lifetime_timer.start()

    # シーンツリーに追加
    # 通常はワールドのルート(例えば親の最上位 Node2D)に追加するのが安全
    var tree := get_tree()
    if tree and tree.current_scene:
        tree.current_scene.add_child(mine)
    else:
        # 念のため owner の親にぶら下げる
        owner.get_tree().root.add_child(mine)

    # 内部管理
    _active_mines.append(mine)
    _time_since_last_place = 0.0

    mine_placed.emit(mine)

    return mine


# --- コールバック / 内部処理 -----------------------------

func _on_mine_body_entered(body: Node, mine: Area2D) -> void:
    # グループ指定がある場合はフィルタリング
    if trigger_on_bodies_in_group != StringName("") and not body.is_in_group(trigger_on_bodies_in_group):
        return

    # すでにキューに入っている場合は二重処理を避ける
    if not is_instance_valid(mine):
        return

    # 爆発エフェクトを表示
    _spawn_explosion(mine.global_position)

    mine_exploded.emit(mine, body)

    # 管理リストから除外して削除
    _active_mines.erase(mine)
    mine.queue_free()


func _on_mine_lifetime_timeout(mine: Area2D) -> void:
    if not is_instance_valid(mine):
        return
    _active_mines.erase(mine)
    mine.queue_free()


func _spawn_explosion(position: Vector2) -> void:
    """
    簡易的な爆発エフェクトを生成する。
    実運用ではここを差し替えて、アニメーション付きシーンなどをインスタンスすると良い。
    """
    var explosion := Node2D.new()
    explosion.global_position = position

    if debug_draw:
        var sprite := _create_debug_sprite(mine_radius * 1.5, explosion_color)
        explosion.add_child(sprite)

    var timer := Timer.new()
    timer.one_shot = true
    timer.wait_time = explosion_duration
    timer.timeout.connect(func():
        if is_instance_valid(explosion):
            explosion.queue_free()
    )
    explosion.add_child(timer)
    timer.start()

    var tree := get_tree()
    if tree and tree.current_scene:
        tree.current_scene.add_child(explosion)
    else:
        owner.get_tree().root.add_child(explosion)


func _create_debug_sprite(radius: float, color: Color) -> Node2D:
    """
    シンプルな円を描画するだけの Node2D を返す。
    Sprite2D を使わず、_draw で直接描画している。
    """
    var drawer := Node2D.new()
    drawer.set_process(false)

    drawer.draw.connect(func():
        drawer.draw_circle(Vector2.ZERO, radius, color)
    )

    drawer.update()
    return drawer

使い方の手順

ここでは典型的な 3 パターンを例にします。

  • プレイヤーが地雷を設置
  • 敵が一定間隔で地雷をばらまく
  • 動く床が通り道に地雷を残していく

手順①:コンポーネントスクリプトを用意する

  1. res://components/MineLayer.gd など、好きな場所に上記コードを保存します。
  2. Godot エディタでスクリプトを開き、エラーが出ていないことを確認します。

手順②:プレイヤーに MineLayer をアタッチする

例として、シーン構成はこんな感じにします:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── MineLayer (Node)
  1. Player シーンを開く。
  2. 子ノードとして Node を追加し、名前を MineLayer に変更。
  3. その MineLayer ノードに、先ほどの MineLayer.gd をアタッチ。
  4. インスペクタで mine_radiuscooldown などを好みに調整。

手順③:入力から地雷設置を呼び出す

次に、プレイヤーのスクリプトから place_mine() を呼び出します。


# Player.gd (例)
extends CharacterBody2D

@onready var mine_layer: MineLayer = $MineLayer

func _physics_process(delta: float) -> void:
    # 移動ロジックなど...

    # 入力で地雷設置(例: "ui_accept")
    if Input.is_action_just_pressed("ui_accept"):
        if mine_layer.can_place_mine():
            mine_layer.place_mine()

これだけで、プレイヤーの足元にポンポン地雷を置けるようになります。
爆発対象のグループを変えたい場合は、MineLayertrigger_on_bodies_in_group"player" から "enemy" に変えるなどして調整しましょう。

手順④:敵や動く床にもそのまま流用する

敵キャラにも同じように MineLayer をアタッチすれば、プレイヤーとまったく同じ仕組みで地雷をばらまく敵が作れます。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── MineLayer (Node)

# Enemy.gd (例)
extends CharacterBody2D

@onready var mine_layer: MineLayer = $MineLayer
var _timer: float = 0.0
var drop_interval: float = 1.5

func _physics_process(delta: float) -> void:
    _timer += delta
    if _timer >= drop_interval:
        _timer = 0.0
        mine_layer.place_mine()

同じく、動く床にもアタッチできます:

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── MineLayer (Node)

# MovingPlatform.gd (例)
extends Node2D

@onready var mine_layer: MineLayer = $MineLayer

func _process(delta: float) -> void:
    # ここに移動ロジック...
    # 一定確率で地雷を落とすなど
    if randf() < 0.01:
        mine_layer.place_mine()

このように、「地雷を置く」という機能を継承ではなくコンポーネントとして切り出すことで、
プレイヤー・敵・ギミックなど、どんなノードにも簡単に「地雷設置能力」を付与できます。


メリットと応用

  • シーン構造がスッキリ
    プレイヤーや敵のスクリプトに「地雷ロジック」を書かずに済むので、本来の責務(移動や AI)に集中できます。
  • 使い回しが超ラク
    別のキャラ、別のギミックに「MineLayer ノードを 1 個コピペ」するだけで同じ機能を共有可能。
  • テストもしやすい
    MineLayer 単体のテストシーンを作って、「クリックした位置に place_mine()」などを試すだけで挙動確認ができます。
  • 後から差し替えやすい
    地雷の見た目や爆発エフェクトを変えたくなったら、_spawn_explosion() だけ差し替えれば OK。既存のプレイヤーや敵のコードをいじる必要はありません。

コンポーネント指向で「地雷設置」という機能を外出ししておくと、レベルデザインの段階で『このギミックにも地雷置かせたいな…』と思ったときにすぐ対応できるのが大きいですね。

改造案:ダメージ処理をフックする

最後に、ちょっとした改造案です。
爆発時に「踏んだ相手にダメージを与える」フックを追加してみましょう。


# MineLayer.gd の一部に追加する例

@export var damage: int = 10
## 爆発時に与えるダメージ量
## 踏んだ相手が `apply_damage(amount: int)` を持っていれば呼び出す

func _on_mine_body_entered(body: Node, mine: Area2D) -> void:
    if trigger_on_bodies_in_group != StringName("") and not body.is_in_group(trigger_on_bodies_in_group):
        return

    if not is_instance_valid(mine):
        return

    # ここでダメージを与える
    if "apply_damage" in body:
        body.apply_damage(damage)

    _spawn_explosion(mine.global_position)
    mine_exploded.emit(mine, body)
    _active_mines.erase(mine)
    mine.queue_free()

こうしておけば、apply_damage() を持つプレイヤー/敵/ギミックは、同じ MineLayer コンポーネントで一括してダメージ処理まで面倒を見てもらえます。
「継承より合成」で、どんどん機能を小さなコンポーネントに分割していきましょう。

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