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 パターンを例にします。
- プレイヤーが地雷を設置
- 敵が一定間隔で地雷をばらまく
- 動く床が通り道に地雷を残していく
手順①:コンポーネントスクリプトを用意する
res://components/MineLayer.gdなど、好きな場所に上記コードを保存します。- Godot エディタでスクリプトを開き、エラーが出ていないことを確認します。
手順②:プレイヤーに MineLayer をアタッチする
例として、シーン構成はこんな感じにします:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── MineLayer (Node)
Playerシーンを開く。- 子ノードとして
Nodeを追加し、名前をMineLayerに変更。 - その
MineLayerノードに、先ほどのMineLayer.gdをアタッチ。 - インスペクタで
mine_radiusやcooldownなどを好みに調整。
手順③:入力から地雷設置を呼び出す
次に、プレイヤーのスクリプトから 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()
これだけで、プレイヤーの足元にポンポン地雷を置けるようになります。
爆発対象のグループを変えたい場合は、MineLayer の trigger_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 コンポーネントで一括してダメージ処理まで面倒を見てもらえます。
「継承より合成」で、どんどん機能を小さなコンポーネントに分割していきましょう。




