【Godot 4】LaserTrap (レーザー罠) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

Godot で「レーザー罠」を作ろうとすると、ついこんな構成にしがちですよね。

  • レーザーノードを継承した LaserTrapA, LaserTrapB, LaserTrapBoss … を量産
  • それぞれに「点滅ロジック」「ダメージ判定」「エフェクト制御」をベタ書き
  • 敵やギミックのシーンの中に、さらに深い子ノード階層でレーザーを埋め込む

結果として、

  • ちょっとだけ挙動を変えたいだけなのに、継承ツリーがどんどん増える
  • 別シーンのレーザー挙動を直したいとき、どのスクリプトを直せばいいか分からない
  • シーンツリーが深くなりすぎて、エディタ上で追いづらい

こういう「継承・深いノード階層」地獄から抜け出すには、やっぱりコンポーネント化が効きます。
今回紹介する 「LaserTrap」コンポーネント は、

  • 任意のノードにアタッチするだけで「周期的に点滅するレーザー罠」になる
  • ダメージ判定もコンポーネント内で完結
  • ON/OFF の周期やダメージ量をインスペクタから簡単に調整できる

という、「置くだけレーザー罠」を目指したコンポーネントです。
プレイヤー用シーンにも、ステージギミック用シーンにも、同じコンポーネントをポンポン再利用していきましょう。

【Godot 4】置くだけ即死ゾーン!「LaserTrap」コンポーネント

今回の LaserTrap は以下の特徴を持ちます。

  • 一定周期で ON/OFF を繰り返す点滅レーザー
  • ON のときだけ Area2D の監視を有効化してダメージ判定
  • ダメージ対象は「特定のグループ」に属するノード(例: "damageable"
  • 対象側は「apply_damage(amount, source)」というメソッドを持っていれば OK(ゆるい依存)
  • レーザーの見た目(Sprite2D / Line2D / MeshInstance2D)は、親シーン側で自由に作る

つまり、「見た目」と「挙動」を分離して、挙動だけをコンポーネントとしてアタッチするスタイルですね。


フルコード: LaserTrap.gd


extends Node2D
class_name LaserTrap
## 周期的に点滅し、ONの間だけダメージ判定を行うレーザー罠コンポーネント。
##
## 想定構成:
##  - このノードの子に Area2D (+ CollisionShape2D) を置く
##  - 見た目用に Sprite2D / Line2D / MeshInstance2D などを親シーン側で自由に配置
##
## ダメージ対象:
##  - target_group に属しているノード
##  - かつ apply_damage(amount: float, source: Node) メソッドを持っているノード

@export_category("Laser Timing")
@export var start_enabled: bool = true:
	set(value):
		start_enabled = value
		# エディタ上でのプレビュー用: 値変更時に状態を更新
		if Engine.is_editor_hint():
			_is_on = value
			_update_visual_state()

## レーザーがONになっている時間(秒)
@export_range(0.1, 60.0, 0.1)
@export var on_duration: float = 1.5

## レーザーがOFFになっている時間(秒)
@export_range(0.1, 60.0, 0.1)
@export var off_duration: float = 1.0

## 最初にONになるまでのディレイ(秒)
@export_range(0.0, 60.0, 0.1)
@export var initial_delay: float = 0.0

@export_category("Damage")
## 1回触れた時に与えるダメージ量
@export_range(0.0, 9999.0, 0.5)
@export var damage_amount: float = 10.0

## ダメージ対象となるノードが属するグループ名
## 例: "player", "enemy", "damageable" など
@export var target_group: StringName = &"damageable"

## 同じ対象に対するダメージの連射間隔(秒)
## 0 にすると、接触中は毎フレームダメージになるので注意
@export_range(0.0, 10.0, 0.05)
@export var damage_cooldown: float = 0.2

@export_category("Visual")
## ON/OFFで切り替えたいノードのグループ名
## 例: レーザーのSprite2DやGlowを "laser_visual" グループに入れておく
@export var visual_group: StringName = &"laser_visual"

## ON/OFFで切り替えたいサウンドプレイヤー(任意)
@export var sfx_on: AudioStreamPlayer2D
@export var sfx_off: AudioStreamPlayer2D
@export var sfx_hit: AudioStreamPlayer2D

## 内部状態
var _is_on: bool = false
var _area: Area2D
var _timer: Timer

## 対象ごとのダメージクールダウン管理用: { Node: next_allowed_time }
var _next_damage_time: Dictionary = {}

func _ready() -> void:
	# 子ノードから Area2D を探す(明示的に名前を決めたくないので自動検出)
	_area = _find_area2d()
	if _area == null:
		push_warning("[LaserTrap] 子ノードに Area2D が見つかりません。ダメージ判定は行われません。")
	else:
		# Area2D のシグナルを接続
		_area.body_entered.connect(_on_body_entered)
		_area.body_exited.connect(_on_body_exited)
		_area.area_entered.connect(_on_area_entered)
		_area.area_exited.connect(_on_area_exited)
	
	# タイマーを内部で生成(外部ノードに依存しないようにする)
	_timer = Timer.new()
	_timer.one_shot = true
	add_child(_timer)
	_timer.timeout.connect(_on_timer_timeout)

	# 初期状態を設定
	_is_on = start_enabled
	_update_visual_state()
	_update_area_monitoring()

	# 初回の切り替えスケジュール
	if initial_delay > 0.0:
		_timer.start(initial_delay)
	else:
		# すぐに次の状態切り替えをスケジュール
		_schedule_next_toggle()

func _process(delta: float) -> void:
	# ON状態のときのみ、接触中の対象に対してクールダウンをチェック
	if not _is_on:
		return
	
	if damage_cooldown <= 0.0:
		# クールダウンなしの場合は、Area2Dのentered系シグナルでのみダメージを与える運用にする
		return
	
	# 現在接触中のボディ/エリアを走査してクールダウン経過を確認
	if _area == null:
		return
	
	var now := Time.get_ticks_msec() / 1000.0
	for body in _area.get_overlapping_bodies():
		_try_apply_damage(body, now)
	for area in _area.get_overlapping_areas():
		_try_apply_damage(area, now)

# --- 公開API ---

## レーザーを強制的にONにする
func turn_on() -> void:
	_is_on = true
	_update_visual_state()
	_update_area_monitoring()
	_schedule_next_toggle()

## レーザーを強制的にOFFにする
func turn_off() -> void:
	_is_on = false
	_update_visual_state()
	_update_area_monitoring()
	_schedule_next_toggle()

## 現在のON/OFF状態を取得
func is_on() -> bool:
	return _is_on

# --- 内部処理 ---

func _find_area2d() -> Area2D:
	# 自分の直下の子から Area2D を1つ見つけるだけのシンプル実装
	for child in get_children():
		if child is Area2D:
			return child
	return null

func _on_timer_timeout() -> void:
	# ON/OFFをトグルして、次の切り替えをスケジュール
	_is_on = not _is_on
	_update_visual_state()
	_update_area_monitoring()
	_schedule_next_toggle()

func _schedule_next_toggle() -> void:
	var wait_time := (_is_on) ? on_duration : off_duration
	if wait_time <= 0.0:
		return
	_timer.start(wait_time)

func _update_visual_state() -> void:
	# visual_group に属するノードの visible / modulate などを切り替える
	var nodes := get_tree().get_nodes_in_group(visual_group)
	for n in nodes:
		# このコンポーネント配下にあるものだけに限定(安全のため)
		if not is_ancestor_of(n):
			continue
		if "visible" in n:
			n.visible = _is_on
		# もし Emitting を持つパーティクルならON/OFF
		if "emitting" in n:
			n.emitting = _is_on
	
	# サウンド
	if _is_on and sfx_on:
		sfx_on.play()
	elif (not _is_on) and sfx_off:
		sfx_off.play()

func _update_area_monitoring() -> void:
	if _area == null:
		return
	# ONのときだけ監視を有効化
	_area.monitoring = _is_on
	_area.monitorable = _is_on

	# OFFになったらクールダウンテーブルをクリア
	if not _is_on:
		_next_damage_time.clear()

func _on_body_entered(body: Node) -> void:
	if not _is_on:
		return
	_try_apply_damage(body)

func _on_body_exited(body: Node) -> void:
	# 接触が終わったのでクールダウン情報を削除
	_next_damage_time.erase(body)

func _on_area_entered(area: Area2D) -> void:
	if not _is_on:
		return
	_try_apply_damage(area)

func _on_area_exited(area: Area2D) -> void:
	_next_damage_time.erase(area)

func _try_apply_damage(target: Node, now: float = -1.0) -> void:
	if not is_instance_valid(target):
		return
	if not target.is_in_group(target_group):
		return
	if not target.has_method("apply_damage"):
		push_warning("[LaserTrap] ターゲット %s は apply_damage() を持っていません。" % target.name)
		return

	if now < 0.0:
		now = Time.get_ticks_msec() / 1000.0
	
	if damage_cooldown > 0.0:
		var next_time := _next_damage_time.get(target, 0.0)
		if now < next_time:
			return
		_next_damage_time[target] = now + damage_cooldown
	
	# 実際にダメージを与える
	target.apply_damage(damage_amount, self)
	
	if sfx_hit:
		sfx_hit.play()

使い方の手順

ここでは 2D のステージギミックとして使う例で説明します。

① スクリプトを用意する

  1. res://components/LaserTrap.gd などのパスで、上記コードを保存します。
  2. Godot を再読み込みすると、ノード追加ダイアログから LaserTrap を直接追加できるようになります。

② レーザー罠シーンを作る

レーザー自体を 1 つのシーンとして作っておくと、ステージにポンポン配置できて便利です。

LaserTrapVertical (Node2D)  ← シーンのルート
 ├── LaserTrap (Node2D)     ← コンポーネント
 │    └── Area2D
 │         └── CollisionShape2D(RectangleShape2D など、レーザーの長さ・太さ)
 └── Sprite2D(レーザー本体の見た目。Line2D でもOK)
  • LaserTrap (Node2D) に上記スクリプト LaserTrap.gd をアタッチ
  • Area2D のコリジョンをレーザーの形状に合わせて伸ばす
  • Sprite2D をレーザーの見た目に合わせて配置
  • Sprite2D を "laser_visual" グループに追加(右クリック > グループ)

こうしておくと、LaserTrap コンポーネントが ON/OFF に応じて "laser_visual" グループのノードを自動で表示・非表示してくれます。

③ ダメージを受ける側(プレイヤーなど)を用意する

プレイヤー側は、「damageable グループ」に入り、apply_damage メソッドを持っていればOKです。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── PlayerHealth (Node / Script)

PlayerHealth.gd の例:


extends Node

@export var max_hp: float = 100.0
var current_hp: float

func _ready() -> void:
	current_hp = max_hp
	# 親ノード(Player)を damageable グループに入れる
	get_parent().add_to_group("damageable")

## LaserTrap などから呼ばれる想定のメソッド
func apply_damage(amount: float, source: Node) -> void:
	current_hp -= amount
	print("Player took %s damage from %s. HP = %s" % [amount, source.name, current_hp])
	if current_hp <= 0.0:
		_die()

func _die() -> void:
	print("Player died")
	# ここでリスポーンやゲームオーバー処理など

このようにしておけば、LaserTrap は「damageable グループに属していて、apply_damage を持っているノード」にだけダメージを与えます。
インターフェイスとしての依存だけに留めることで、プレイヤー以外の敵や動く床などにも簡単に流用できます。

④ ステージに配置してパラメータを調整する

最後に、ステージシーンにレーザー罠シーンを配置します。

Stage01 (Node2D)
 ├── TileMap
 ├── Player (CharacterBody2D)
 ├── LaserTrapVertical (PackedScene Instance)
 └── LaserTrapHorizontal (PackedScene Instance)
  • LaserTrap コンポーネントのインスペクタで、以下の値を調整します:
    • start_enabled: ステージ開始時に ON にしておくか
    • on_duration: ON の時間(例: 2.0 秒)
    • off_duration: OFF の時間(例: 1.0 秒)
    • initial_delay: 最初の ON までのディレイ(複数レーザーのタイミングをずらすのに便利)
    • damage_amount: ダメージ量(例: 25.0)
    • damage_cooldown: 接触し続けたときの連射間隔(例: 0.5 秒)
    • target_group: ダメージ対象のグループ名(デフォルト: damageable
    • visual_group: 見た目を切り替えるグループ名(デフォルト: laser_visual

この時点で、もう「周期的に点滅するレーザー罠」が完成しています。
レーザーの形状や見た目を変えたいときは、シーン側の Sprite2D / Area2D を編集するだけでOK。コンポーネントのコードは触らなくて済みます。


メリットと応用

この LaserTrap コンポーネントを使うことで、以下のようなメリットがあります。

  • シーン構造がシンプル
    レーザーの挙動は LaserTrap に完全に閉じているので、ステージ側は「どこに置くか」「どんな見た目にするか」だけ考えればOKです。
  • 継承ツリーが増えない
    「ボス用レーザー」「チュートリアル用レーザー」なども、すべて同じコンポーネントを使い回し、パラメータだけ変えれば済みます。
  • 見た目とロジックの分離
    アーティストがレーザーの見た目をいじっても、コンポーネント側にはノータッチで済みます。逆に、ロジックを改善しても全シーンに自動で反映されます。
  • ゆるい依存で他オブジェクトとも連携しやすい
    damageable グループ + apply_damage メソッド」というゆるい契約だけなので、プレイヤーでも敵でも、動く箱でも、同じレーザー罠でダメージ処理を共有できます。

こういう「汎用ギミック」は、1個のコンポーネントに閉じ込めるのが本当にラクです。
ステージ側は「インスタンスを置く」「パラメータをちょっと変える」だけでレベルデザインに集中できますね。

改造案: 一撃死モードを一時的に有効化する

例えば「ボス戦だけ、レーザーが即死になる」みたいなギミックを作りたい場合、
コンポーネントにこんなヘルパー関数を追加しておくと便利です。


## 一定時間だけ一撃死モードにする(ダメージ量を一時的に上書き)
func enable_one_shot_kill(duration: float, one_shot_damage: float = 9999.0) -> void:
	var original_damage := damage_amount
	damage_amount = one_shot_damage
	
	var t := get_tree().create_timer(duration)
	t.timeout.connect(func ():
		# 時間が経ったら元のダメージに戻す
		damage_amount = original_damage
	)

ゲーム側からは、例えばボスのフェーズ移行時に:


# ボスのスクリプトなどから
for laser in get_tree().get_nodes_in_group("boss_lasers"):
	if laser is LaserTrap:
		laser.enable_one_shot_kill(5.0)  # 5秒間だけ即死レーザー

のように呼び出せます。
このように、「コンポーネントに小さなAPIを生やしておく」と、ゲーム側からの制御もきれいに書けますね。


継承ベースでレーザー専用の巨大クラスを作るより、小さなコンポーネントとして LaserTrap を作っておくと、
「いろんなシーンで再利用」「パラメータだけ変えてバリエーション量産」が本当にやりやすくなります。
ぜひ、自分のプロジェクトでも「罠」「ギミック」「エフェクト制御」などをどんどんコンポーネント化していきましょう。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!