Godotで「壊れる壁」を作ろうとすると、ついこういう構成にしがちですよね。

DestructibleWall (StaticBody2D を継承したカスタムシーン)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── GPUParticles2D(破片)
 └── AudioStreamPlayer2D(破壊音)

そしてプレイヤーや弾からのダメージ処理も、この DestructibleWall シーンの中に書いてしまう…。
最初はこれでも動きますが、だんだんと次のような問題が出てきます。

  • 壁のバリエーションごとにシーンを増やす → 修正が地獄
  • 「敵も壊れる壁も同じようなHP処理」をコピペ → バグの温床
  • ノード階層が深くなって、どこに何が書いてあるのか分かりづらい

そこで「壊れる」という振る舞いを 1 個のコンポーネントとして切り出し、
どんなノードにも後付けできるようにしたのが、今回紹介する DestructibleWall コンポーネントです。

【Godot 4】どんな壁もサクッと破壊可能に!「DestructibleWall」コンポーネント

このコンポーネントは、

  • HPの管理
  • ダメージ受付
  • 0 になったときの破壊演出(パーティクル・サウンド・コールバック)

をまとめて担当します。
壁そのものは StaticBody2D でも Node2D でも構いません。
「壊れる」というロジックはこのコンポーネントに全部寄せてしまいましょう。


GDScript フルコード


extends Node2D
class_name DestructibleWall
# Godot 4 用コンポーネント: 「壊れる」挙動をどのノードにも後付けできる

## --- 設定パラメータ(インスペクタで編集可能) ---

@export_category("Destructible Settings")

@export_range(1, 999, 1)
var max_hp: int = 3:
	set(value):
		max_hp = max(value, 1)
		hp = clamp(hp, 0, max_hp)

@export var start_hp_at_max: bool = true
## 壁の現在HP。_readyで初期化されるので、基本はインスペクタでいじらない想定
var hp: int = 3 : set = _set_hp

@export var destroy_on_zero_hp: bool = true
@export var queue_free_delay: float = 0.1
## true のとき、1 度壊れたら再利用しない(再生成したい場合は false + 再初期化処理)
@export var one_shot: bool = true

@export_category("Effects")

## 破壊時に再生するパーティクルシーン(任意)
@export var break_particles_scene: PackedScene
## 破壊時に再生するサウンド(任意)
@export var break_sound: AudioStream

## 破壊時に対象を非表示にするか
@export var hide_owner_on_break: bool = true
## HPが0になるときに、オーナーノードも一緒に削除するか
@export var free_owner_on_break: bool = true

@export_category("Debug")

## ダメージログをコンソールに出すか
@export var debug_log: bool = false


## --- シグナル ---

signal hp_changed(current_hp: int, max_hp: int)
signal broken(owner_node: Node)

## 内部フラグ
var _is_broken: bool = false


func _ready() -> void:
	# 所属ノードのHP初期化
	if start_hp_at_max:
		hp = max_hp
	else:
		hp = clamp(hp, 0, max_hp)

	# 破壊済みフラグ初期化
	_is_broken = false


func _set_hp(value: int) -> void:
	hp = clamp(value, 0, max_hp)
	emit_signal("hp_changed", hp, max_hp)

	if debug_log:
		print("[DestructibleWall] HP = %d / %d" % [hp, max_hp])

	# HPが0になったら破壊処理へ
	if hp == 0 and destroy_on_zero_hp and not _is_broken:
		_break()


## 外部から呼ぶ、基本のダメージ処理
func apply_damage(amount: int) -> void:
	if amount <= 0:
		return
	if _is_broken and one_shot:
		# すでに壊れている場合は無視
		return

	if debug_log:
		print("[DestructibleWall] apply_damage: ", amount)

	hp -= amount


## HPを全回復させる(再利用したい場合などに使用)
func restore_full() -> void:
	_is_broken = false
	hp = max_hp
	if hide_owner_on_break and owner:
		owner.visible = true


## 内部用:破壊処理本体
func _break() -> void:
	_is_broken = true

	if debug_log:
		print("[DestructibleWall] BROKEN!")

	# 破壊時のビジュアル/サウンド演出
	_spawn_break_particles()
	_play_break_sound()

	# 見た目を消す
	if hide_owner_on_break and owner and owner is CanvasItem:
		(owner as CanvasItem).visible = false

	# シグナルで外部に通知(スコア加算やドアの開閉などに利用)
	emit_signal("broken", owner)

	# オーナーごと削除する場合
	if free_owner_on_break and owner:
		if queue_free_delay > 0.0:
			owner.call_deferred("queue_free")
		else:
			owner.queue_free()
	else:
		# 自分自身だけ削除したい場合(コンポーネントだけ剥がす)
		if queue_free_delay > 0.0:
			call_deferred("queue_free")
		else:
			queue_free()


func _spawn_break_particles() -> void:
	if break_particles_scene == null:
		return

	var particles := break_particles_scene.instantiate()
	if not (particles is Node2D):
		push_warning("break_particles_scene is not a Node2D. Skipping.")
		return

	# オーナーの位置にパーティクルを出す
	var owner_2d := owner as Node2D
	if owner_2d:
		(particles as Node2D).global_position = owner_2d.global_position
	else:
		(particles as Node2D).global_position = global_position

	# ルートまたは一番近い2Dワールドに追加
	var root := get_tree().current_scene
	root.add_child(particles)

	# ワンショット系パーティクルを想定して自動削除
	if "one_shot" in particles:
		particles.one_shot = true
	if "emitting" in particles:
		particles.emitting = true
	if "finished" in particles:
		particles.finished.connect(func():
			particles.queue_free()
		)


func _play_break_sound() -> void:
	if break_sound == null:
		return

	var player := AudioStreamPlayer2D.new()
	player.stream = break_sound

	# オーナー位置に配置
	var owner_2d := owner as Node2D
	if owner_2d:
		player.global_position = owner_2d.global_position
	else:
		player.global_position = global_position

	get_tree().current_scene.add_child(player)
	player.play()

	# 再生終了後に自動削除
	player.finished.connect(func():
		player.queue_free()
	)


## 便利メソッド:HPが残っているかどうか
func is_alive() -> bool:
	return hp > 0 and not _is_broken


## 便利メソッド:すぐに破壊したいとき用(演出あり)
func force_break() -> void:
	if not _is_broken:
		hp = 0  # setter 経由で _break が呼ばれる

使い方の手順

① コンポーネントをプロジェクトに追加

  1. 上記のコードを res://components/destructible_wall.gd などに保存します。
  2. Godot エディタでスクリプトを開き、class_name DestructibleWall が認識されていることを確認します。
    スクリプトを保存したあと、ノード追加画面の「スクリプト」タブから DestructibleWall が選べるようになります)

② 壁シーンにコンポーネントをアタッチ

例として、シンプルな壊れる壁シーンを作ってみます。

BreakableWall (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DestructibleWall (Node2D)  ← このコンポーネントを追加
  1. StaticBody2D をルートにした新しいシーンを作成し、名前を BreakableWall にします。
  2. 子として Sprite2DCollisionShape2D を追加し、見た目と当たり判定を設定します。
  3. さらに子として Node2D を追加し、名前を DestructibleWall に変更します。
  4. その Node2D に、先ほどの DestructibleWall.gd スクリプトをアタッチします。

このとき、インスペクタで以下を設定しておくと分かりやすいです。

  • max_hp = 5
  • break_particles_scene に、破片用の GPUParticles2D シーンを指定
  • break_sound に、石が砕ける SE を指定
  • hide_owner_on_break = true, free_owner_on_break = true

これで「壊れる壁としての振る舞い」は完成です。
あとは攻撃側から apply_damage() を呼んであげればOKですね。

③ プレイヤーや弾からダメージを与える

例として、「プレイヤーの近接攻撃」が壁に当たったらダメージを与えるケースを考えます。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Area2D (SwordHitbox)
 │    └── CollisionShape2D
 └── (攻撃スクリプト)

プレイヤー側の攻撃スクリプトの一部はこんな感じになります。


# PlayerAttack.gd(イメージ用の簡略コード)
extends Node

@export var damage: int = 1
@onready var hitbox: Area2D = $"../SwordHitbox"

func _ready() -> void:
	hitbox.body_entered.connect(_on_hitbox_body_entered)


func _on_hitbox_body_entered(body: Node) -> void:
	# 壁などにアタッチされた DestructibleWall コンポーネントを探す
	var destructible := body.get_node_or_null("DestructibleWall")
	if destructible and destructible is DestructibleWall:
		destructible.apply_damage(damage)

このように、プレイヤーは「HPを持っているかどうか」を意識する必要はなく、
「もし壊れるコンポーネントを持っていたらダメージを送る」というゆるい結合で済ませられます。

④ 動く床や敵にもそのまま流用

同じコンポーネントを、動く床や敵にも付けてみましょう。

動く床:

MovingPlatform (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── PlatformMover (スクリプト)
 └── DestructibleWall (Node2D)  ← 同じコンポーネント

敵キャラ:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── EnemyAI (スクリプト)
 └── DestructibleWall (Node2D)  ← 同じコンポーネント

どのノードでも、壊したいオブジェクトに DestructibleWall を 1 個ぶら下げるだけです。
攻撃側は「DestructibleWall を持っているかどうか」だけ見ればよく、
相手が壁なのか敵なのか、床なのかを特別扱いする必要がなくなります。


メリットと応用

このコンポーネント方式にすると、次のようなメリットがあります。

  • シーン構造がシンプル
    壁・床・敵など、それぞれの本質的なノード構造はそのままに、「壊れる」という機能だけを後付けできます。
  • 再利用性が高い
    HPバーのある宝箱、ダメージで壊れるスイッチ、ボスのパーツなど、
    「壊れる」ものは全部このコンポーネントをアタッチするだけで済みます。
  • 継承地獄からの脱却
    BaseDestructibleDestructibleWallDestructibleEnemy
    という継承ツリーを作らなくてよくなり、
    「壊れるかどうか」はコンポーネントで表現できるようになります。
  • レベルデザインが楽
    ステージを作りながら、「このオブジェクトも壊れるようにしたいな」と思ったら、
    その場で DestructibleWall ノードを 1 個足すだけでOKです。

さらに、シグナル broken を活用すれば、
「壁が壊れたら隠し通路を開ける」「スイッチのように使う」といった応用も簡単です。

改造案:壊れたときにスコアを加算する

例えば、「壊れたらスコアマネージャーに通知する」機能を追加してみましょう。
最低限の改造として、以下のメソッドをコンポーネントに足すだけでもOKです。


func _on_broken_add_score(score_amount: int = 100) -> void:
	# どこかにあるグローバルなスコアマネージャーを想定
	var score_manager := get_tree().get_first_node_in_group("ScoreManager")
	if score_manager and "add_score" in score_manager:
		score_manager.add_score(score_amount)

あとは、_ready() の中などで、


broken.connect(func(_owner):
	_on_broken_add_score(100)
)

のようにシグナルをつなげば、「壊れたら 100 点加算」という挙動になります。
このように、コンポーネントをベースに少しずつ機能を積み増していくと、
プロジェクト全体の見通しがかなり良くなりますね。

継承ではなくコンポーネントで「壊れる」という性質を表現することで、
壁も敵もギミックも、気持ちよく砕け散る世界を作っていきましょう。