Godotで「隠し通路」や「隠し部屋」を作ろうとすると、けっこう面倒ですよね。典型的なやり方だと、

  • プレイヤーと当たったらアニメーション再生するために Area2D を仕込む
  • 壁のスプライト用ノードとコリジョン用ノードを別々に管理する
  • シーンごとに似たようなスクリプトをコピペして書き換える

…という感じで、「また同じようなの書いてるな?」となりがちです。

さらに、壁シーンを継承して「隠し壁用シーン」を作りはじめると、

  • ベースの壁シーンを変えると全部の派生シーンが巻き込まれる
  • シーンツリーが「壁系シーン」で埋まっていく
  • ちょっと挙動を変えたいだけなのに、新しい派生シーンを作る羽目になる

といった「継承地獄」にもハマりがちです。

そこで今回は、「どんな壁ノードにもポン付けできる」コンポーネントとして、HiddenWall を用意しました。
透明度の制御やトリガー判定はこのコンポーネントに任せて、壁本体はただの StaticBody2DTileMapLayer としてシンプルに保つ、という方針ですね。

【Godot 4】触るとスッと透ける隠し通路!「HiddenWall」コンポーネント

このコンポーネントをアタッチしておけば、

  • プレイヤーが触れたときにだけ壁が半透明になる
  • 離れたら自動で元の不透明度に戻る
  • 透明度の変化時間や最終アルファ値をインスペクタから調整できる
  • Sprite2D でも TileMapLayer でも、CanvasItem なら何でも対応

という「隠し通路」挙動を、どこにでも再利用できるようになります。


フルコード:HiddenWall.gd


extends Area2D
class_name HiddenWall
##
## HiddenWall (隠し通路) コンポーネント
##
## - プレイヤーなど、指定したボディが触れている間だけ
##   対象ノードの透明度を下げて向こう側を見えるようにする。
## - 継承ではなく「アタッチして使う」コンポーネント前提。
##

@export_category("HiddenWall Settings")

## どのノードの透明度を変えるか。
## 通常は同じシーン内の Sprite2D / TileMapLayer などを指定します。
## 未指定の場合は、親ノード (get_parent()) を対象にします。
@export var target_canvas_item: CanvasItem

## 触れている間に下げる透明度(アルファ値)。
## 0.0 = 完全に透明, 1.0 = 不透明。
@export_range(0.0, 1.0, 0.01)
var faded_alpha: float = 0.3

## 元の透明度に戻るまでの時間(秒)。
@export_range(0.0, 5.0, 0.01)
var fade_in_time: float = 0.3

## 透明にするまでの時間(秒)。
@export_range(0.0, 5.0, 0.01)
var fade_out_time: float = 0.15

## どのレイヤーのボディに反応するか。
## プレイヤー用の Physics Layer を指定しておくと便利です。
@export_flags_2d_physics var trigger_layers: int = 1

## 「プレイヤーだけに反応したい」など、名前でフィルタしたい場合に使用。
## 空文字列のときは無視します。
@export var required_body_group: StringName = &""

@export_category("Debug")

## デバッグ用: 現在何体のボディが中にいるかをインスペクタで確認したいとき用。
@export var overlapping_count: int = 0:
    set(value):
        overlapping_count = value
    get:
        return _overlapping_bodies.size()

## 内部状態
var _original_alpha: float = 1.0
var _tween: Tween
var _overlapping_bodies: Array[Node] = []

func _ready() -> void:
    # 対象の CanvasItem を決定
    if target_canvas_item == null:
        var parent := get_parent()
        if parent is CanvasItem:
            target_canvas_item = parent
        else:
            push_warning("HiddenWall: target_canvas_item が未設定で、親も CanvasItem ではありません。透明度を変更できません。")

    # 初期アルファ値を記録しておく
    if target_canvas_item:
        _original_alpha = target_canvas_item.modulate.a

    # シグナル接続
    body_entered.connect(_on_body_entered)
    body_exited.connect(_on_body_exited)

    # Area2D のコリジョンレイヤー/マスク設定は
    # シーン側で行ってもよいですが、ここでマスクだけ上書きしておくのもアリです
    collision_mask = trigger_layers

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

    if not _overlapping_bodies.has(body):
        _overlapping_bodies.append(body)

    _update_fade_state()

func _on_body_exited(body: Node) -> void:
    if not _overlapping_bodies.has(body):
        return

    _overlapping_bodies.erase(body)
    _update_fade_state()

func _should_trigger_for_body(body: Node) -> bool:
    # PhysicsBody2D 以外には反応しない
    if not (body is PhysicsBody2D):
        return false

    # グループ指定があればチェック
    if required_body_group != &"" and not body.is_in_group(required_body_group):
        return false

    return true

func _update_fade_state() -> void:
    # ターゲットが無い場合は何もしない
    if target_canvas_item == null:
        return

    var should_be_faded := _overlapping_bodies.size() > 0
    var target_alpha := faded_alpha if should_be_faded else _original_alpha
    var duration := fade_out_time if should_be_faded else fade_in_time

    # すでに近いアルファ値なら Tween を作らない
    if is_equal_approx(target_canvas_item.modulate.a, target_alpha):
        return

    # 既存の Tween があれば止める
    if _tween and _tween.is_running():
        _tween.kill()

    _tween = create_tween()
    _tween.tween_property(
        target_canvas_item,
        "modulate:a",
        target_alpha,
        duration
    ).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)

    # 重なっている状態であれば、Tween 完了後も
    # まだボディが残っている可能性があるので再チェック
    _tween.finished.connect(_on_tween_finished)

func _on_tween_finished() -> void:
    # 途中でボディの出入りがあった場合に備えて再評価
    _update_fade_state()

## --- おまけ: 手動で「強制的に透過/復帰」させたい場合のAPI ---

func force_fade_out() -> void:
    _overlapping_bodies.clear()
    _overlapping_bodies.append(self) # ダミーで1つ入れて「常に重なっている」扱いに
    _update_fade_state()

func force_restore() -> void:
    _overlapping_bodies.clear()
    _update_fade_state()

使い方の手順

ここでは 2D を例にしますが、CanvasItem であれば基本は同じです。

手順①:壁シーンを用意する

まずは普通の壁シーンを作ります。例えばこんな感じ:

HiddenWallTile (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D      # 壁の当たり判定
 └── HiddenWall (Area2D)   # ← 今回のコンポーネント
  • HiddenWallTileStaticBody2D など、壁として使いたいノード
  • Sprite2D は壁の見た目
  • CollisionShape2D はプレイヤーがぶつかるコリジョン
  • HiddenWall は上の GDScript をアタッチした Area2D ノード

ポイント:
HiddenWallCollisionShape2D(または CollisionPolygon2D)は、
壁とほぼ同じ形・位置にしておくと自然です。
Area2D の子として CollisionShape2D を追加してください)

手順②:HiddenWall のパラメータを設定する

インスペクタで HiddenWall ノードを選択し、以下を設定します。

  • target_canvas_item
    壁の見た目を持つ Sprite2D をドラッグ&ドロップ。
    未設定の場合は「親ノード」が CanvasItem ならそれを対象にします。
  • faded_alpha
    触れている間の透明度。0.2〜0.4 くらいが「うっすら見える」感じでおすすめです。
  • fade_out_time / fade_in_time
    透明になる/戻るまでの時間。
    隠し通路っぽくしたいなら、fade_out_time = 0.15fade_in_time = 0.3 くらいが自然ですね。
  • trigger_layers
    プレイヤーの Physics Layer にチェックを入れておきます。
    (プレイヤーの collision_layer と合わせてください)
  • required_body_group(任意)
    例えばプレイヤーに "player" グループを付けているなら、ここに player と指定すると、
    敵や動く床には反応しなくなります。

手順③:プレイヤー側の設定

プレイヤー側は通常の CharacterBody2D でOKです。例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── Camera2D
  • Playercollision_layer が、HiddenWall.trigger_layers と一致するように設定
  • グループを使う場合は、Player"player" グループを追加しておく

これで、プレイヤーが壁に重なった瞬間に壁がスッと透けて、離れると元に戻るようになります。

手順④:他の用途への展開例

このコンポーネントは「壁」専用ではありません。例えば:

  • 動く床の足場がプレイヤーに重なったときだけ透ける
MovingPlatform (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── HiddenWall (Area2D)  # 透ける足場コンポーネントとして再利用
  • 敵の背後にある茂みが、プレイヤーが近づくと半透明になる
Bush (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── HiddenWall (Area2D)

どれも「見た目を透過させたい CanvasItem に対して HiddenWall をアタッチする」というパターンで、
同じコンポーネントをそのまま流用できます。


メリットと応用

この HiddenWall コンポーネントの良いところは、「隠し通路用の特別な壁シーン」を作らなくていい点です。

  • 普通の壁シーンに後から HiddenWall をくっつけるだけで「隠し壁」化できる
  • プレイヤーの仕様が変わっても、コンポーネント側の判定だけ直せば全ての隠し壁に反映される
  • 「透ける茂み」「透ける柱」など、別オブジェクトにも再利用しやすい
  • シーンツリーが「壁A」「隠し壁B」「半透明壁C」みたいに増殖しない

つまり、継承ではなく合成(Composition) で機能を足していけるので、
レベルデザイン中に「ここも隠し通路にしよう」と思ったら、対象ノードに HiddenWall をペタッと付けるだけで済みます。

改造案:音やエフェクトを追加する

例えば、プレイヤーが触れた瞬間に「スッ」という音やパーティクルを出したい場合、
HiddenWall にこんな関数を追加して、シグナルやインスペクタから呼び出すのもアリです。


## 触れた瞬間に一度だけ SE を鳴らしたい場合の簡易実装例
@export var reveal_sound: AudioStreamPlayer2D

func _play_reveal_effect() -> void:
    if reveal_sound and not reveal_sound.playing:
        reveal_sound.play()

_on_body_entered() の中で、


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

    var was_empty := _overlapping_bodies.is_empty()

    if not _overlapping_bodies.has(body):
        _overlapping_bodies.append(body)

    if was_empty:
        _play_reveal_effect()

    _update_fade_state()

のように「最初の1体が入ったときだけ」エフェクトを出す、という拡張もできます。
このように、HiddenWall をベースにして自分のゲーム専用の「隠し通路コンポーネント」を育てていくと、
プロジェクトが進むほど楽になっていきますね。

ぜひ、自分のプロジェクト用にカスタマイズしつつ、「継承より合成」スタイルでコンポーネントを増やしていきましょう。