Godot 4 で 2D ゲームを作っていると、シーンごとにカメラのズーム設定を変えたい場面ってけっこうありますよね。
たとえば「ボス部屋に入ったらグッと引きで見せたい」「狭い通路に入ったら少しズームインしたい」みたいなやつです。

素直にやると、こんな感じになりがちです:

  • プレイヤーシーンに Camera2D を継承してカスタムスクリプトを書く
  • エリアごとにスクリプトをコピペして、Camera2D を直接操作する
  • ボス部屋ごとに「ボス用カメラ」「通常カメラ」を作って切り替える

でもこのやり方だと、

  • プレイヤーのスクリプトがどんどん肥大化する
  • カメラ制御ロジックがあちこちに散らばる
  • 「このシーンのズームはどこで変えてるんだっけ?」と迷子になる

そこで登場するのが、コンポーネントとしてアタッチするだけでズーム制御が完結する ZoomTrigger コンポーネントです。
継承ではなく「合成(Composition)」で、どのシーンにも簡単に後付けできるズームトリガーを用意しておきましょう。

【Godot 4】エリアに入るだけでカメラがいい感じに!「ZoomTrigger」コンポーネント

ZoomTrigger は、自分の親ノードが Area2D に入ったときに、指定した Camera2D の zoom を変更するコンポーネントです。
使い方としては、例えばこんなイメージです:

  • プレイヤーが「ズームエリア」に入る → カメラがズームイン
  • 敵が「警戒エリア」に入る → 敵専用カメラがズームアウト
  • 動く足場が特定のポイントに来たら → そこだけカメラを引きで見せる

どのケースでも、「ズームしたいオブジェクトの子に ZoomTrigger を付ける」だけで実現できるようにしてあります。


フルコード:ZoomTrigger.gd


extends Node
class_name ZoomTrigger
"""
ZoomTrigger コンポーネント

親ノードが指定した Area2D に入った/出たタイミングで、
ターゲットの Camera2D の zoom を変更するコンポーネントです。

・プレイヤーの子に付けて「特定エリアに入ったらズーム変更」
・敵キャラの子に付けて「ボス部屋に入ったらズームアウト」
・動く足場の子に付けて「足場がゾーンに入ったらカメラを引く」

など、どのノードでも「合成」で簡単に使えるように設計しています。
"""

@export_group("基本設定")
## ズームを制御する Camera2D。
## null の場合、親ノードの子孫から自動で Camera2D を探します。
@export var target_camera: Camera2D

## どの Area2D に入ったらズームを変更するか。
## 通常はシーン上に置いた Area2D をドラッグ&ドロップで指定します。
@export var trigger_area: Area2D

## 親が trigger_area に入ったときに適用するズーム値。
## 例: Vector2(0.5, 0.5) でズームイン、Vector2(2.0, 2.0) でズームアウト。
@export var zoom_in_area: Vector2 = Vector2(0.7, 0.7)

## エリアから出たときに戻すズーム値。
## null の場合、ゲーム開始時のカメラ zoom を自動で記録しておき、そこに戻します。
@export var zoom_outside_area: Vector2

@export_group("ふるまい")
## エリアから出たときにズームを元に戻すかどうか。
@export var restore_on_exit: bool = true

## ズーム変更を補間する時間(秒)。
## 0 の場合は即時変更。
@export_range(0.0, 5.0, 0.05)
@export var tween_duration: float = 0.3

## 補間に使う Tween のトランジションタイプ。
@export var tween_transition: Tween.TransitionType = Tween.TRANS_SINE

## 補間に使う Tween のイーズタイプ。
@export var tween_ease: Tween.EaseType = Tween.EASE_OUT

@export_group("デバッグ")
## 実行中にコンソールへログを出すかどうか。
@export var debug_log: bool = false


var _default_zoom: Vector2
var _tween: Tween
var _is_inside: bool = false


func _ready() -> void:
    # Camera2D が未指定なら、親ノード以下から自動で探す
    if target_camera == null:
        target_camera = _find_camera_in_parent()
        if debug_log:
            if target_camera:
                print("[ZoomTrigger] Auto-detected Camera2D: ", target_camera)
            else:
                push_warning("[ZoomTrigger] Camera2D not found. Please assign target_camera.")

    # デフォルトズーム値を記録
    if target_camera:
        _default_zoom = target_camera.zoom
        if zoom_outside_area == Vector2.ZERO:
            # ユーザーが未設定なら、開始時の値を「外側ズーム」として使う
            zoom_outside_area = _default_zoom

    # Area2D のシグナル接続
    if trigger_area:
        # 親が Area2D 内に入った/出たかを判定するため、
        # trigger_area の body_entered / body_exited を監視します。
        trigger_area.body_entered.connect(_on_trigger_body_entered)
        trigger_area.body_exited.connect(_on_trigger_body_exited)
    else:
        push_warning("[ZoomTrigger] trigger_area is not assigned. ZoomTrigger will do nothing.")


func _find_camera_in_parent() -> Camera2D:
    """
    親ノード以下から最初に見つかった Camera2D を返します。
    見つからなければ null。
    """
    var root := get_parent()
    if root == null:
        return null

    # 深いノード階層を意識せず、
    # 「とりあえずこの親のどこかに Camera2D があれば使う」くらいのゆるい設計。
    var cameras: Array = root.get_tree().get_nodes_in_group("__zoomtrigger_temp__")
    # 念のためグループを使わない安全な方法で探索
    for child in root.get_children():
        var cam := _find_camera_recursive(child)
        if cam:
            return cam
    return null


func _find_camera_recursive(node: Node) -> Camera2D:
    if node is Camera2D:
        return node
    for child in node.get_children():
        var cam := _find_camera_recursive(child)
        if cam:
            return cam
    return null


func _on_trigger_body_entered(body: Node) -> void:
    # 「親ノードが Area2D に入ったか?」だけを見たいので、
    # body がこのコンポーネントの親であるかをチェックします。
    if body != get_parent():
        return

    _is_inside = true
    if debug_log:
        print("[ZoomTrigger] Entered area: ", trigger_area.name, " by ", body.name)

    _apply_zoom(zoom_in_area)


func _on_trigger_body_exited(body: Node) -> void:
    if body != get_parent():
        return

    _is_inside = false
    if debug_log:
        print("[ZoomTrigger] Exited area: ", trigger_area.name, " by ", body.name)

    if restore_on_exit:
        _apply_zoom(zoom_outside_area)


func _apply_zoom(target_zoom: Vector2) -> void:
    if target_camera == null:
        push_warning("[ZoomTrigger] target_camera is null. Cannot apply zoom.")
        return

    # すでに Tween が動いていたら一旦止める
    if _tween and _tween.is_running():
        _tween.kill()

    if tween_duration <= 0.0:
        # 即時変更
        target_camera.zoom = target_zoom
        return

    # Tween でなめらかに補間
    _tween = create_tween()
    _tween.set_trans(tween_transition)
    _tween.set_ease(tween_ease)
    _tween.tween_property(target_camera, "zoom", target_zoom, tween_duration)

使い方の手順

ここでは代表的な例として、プレイヤーが特定エリアに入ったらカメラをズームインするケースを見ていきます。

手順①:プレイヤーシーンに ZoomTrigger をアタッチ

まず、プレイヤーシーンはこんな構成を想定します:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Camera2D
 └── ZoomTrigger (Node)  ← このノードに上記スクリプトをアタッチ
  1. Player シーンを開く
  2. Player(ルートの CharacterBody2D)に子ノードとして Node を追加
  3. その Node の名前を ZoomTrigger に変更
  4. 作成した ZoomTrigger.gd をこのノードにアタッチ

target_camera を未設定のままにしておけば、親(Player)の子孫から自動で Camera2D を探してくれます。

手順②:ズーム用の Area2D をステージに配置

次に、ステージシーン(例: Level1.tscn)にズーム用の Area2D を置きます。

Level1 (Node2D)
 ├── TileMap
 ├── Player (インスタンス)
 ├── ZoomArea (Area2D)
 │    ├── CollisionShape2D
 │    └── (見た目用の Sprite2D など任意)
 └── その他いろいろ
  1. Area2D を追加して名前を ZoomArea に変更
  2. CollisionShape2D を子に追加し、プレイヤーが入ってほしい範囲を設定

この ZoomAreatrigger_area になります。

手順③:ZoomTrigger のパラメータを設定

Level1 シーンで、Player インスタンスの子にある ZoomTrigger ノードを選択し、インスペクタで次のように設定します。

  • target_camera: 未設定のままで OK(自動検出)
  • trigger_area: シーンツリーから ZoomArea (Area2D) をドラッグ&ドロップ
  • zoom_in_area: Vector2(0.7, 0.7)(少しズームイン)
  • zoom_outside_area: 空のままで OK(開始時の zoom に戻る)
  • restore_on_exit: ON(エリアから出たら元のズームに戻す)
  • tween_duration: 0.3 ~ 0.5 あたり(お好みで)

これだけで、

  • Player が ZoomArea に入る → カメラが 0.7 倍にズームイン
  • Player が ZoomArea から出る → 元のズームにスムーズに戻る

という挙動になります。

手順④:敵や動く床にもそのまま再利用

このコンポーネントの良いところは、プレイヤー専用ではないところです。
たとえば、ボス専用のカメラを持つ敵シーンを作りたい場合:

Boss (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── BossCamera (Camera2D)
 └── ZoomTrigger (Node)
  • Boss シーンの ZoomTrigger に、ボス部屋の BossRoomArea (Area2D)trigger_area として指定
  • zoom_in_areaVector2(1.5, 1.5)(少し引き気味)に設定

これで、

  • ボスが部屋に入ったら BossCamera のズームが変わる
  • プレイヤーのカメラとは完全に独立して制御できる

という構成になります。
「プレイヤー用のスクリプトをボスにも流用する」のではなく、「ZoomTrigger コンポーネントをボスにもアタッチする」という考え方ですね。


メリットと応用

ZoomTrigger をコンポーネントとして切り出しておくと、いろいろと嬉しいことがあります。

  • シーン構造がスッキリ
    カメラ制御ロジックを Player や Boss の巨大スクリプトから切り離せるので、
    「移動」「攻撃」「アニメーション」と「ズーム制御」が綺麗に分離できます。
  • 再利用性が高い
    どんなノードにも子として ZoomTrigger を付けるだけで同じ仕組みが使えるので、
    新しいギミックを作るときも「またズーム処理書かなきゃ…」から解放されます。
  • レベルデザインが楽
    ズームの調整は Area2D の形と zoom_in_area の値をいじるだけ。
    スクリプトを触らず、レベルデザイナー視点でパラメータ調整ができます。
  • ノード階層に依存しない
    Camera2D を自動検出する仕様にしてあるので、「Camera2D のパスが変わったから全部修正…」という事故を減らせます。

そして何より、「継承より合成」のスタイルに自然と寄せられるのがポイントです。
「ズームするプレイヤー」クラスを増やすのではなく、「プレイヤー + ZoomTrigger コンポーネント」という組み合わせで表現する、という発想ですね。

改造案:エリアごとに複数のズームプリセットを持たせる

応用として、「エリアに入る向きによってズームを変える」みたいなギミックを作りたい場合は、
ZoomTrigger に「プリセットを切り替える関数」を足してあげると便利です。


## 例: 外部からズームプリセットを切り替える API を追加
func set_zoom_preset(inside: Vector2, outside: Vector2, duration: float = -1.0) -> void:
    zoom_in_area = inside
    zoom_outside_area = outside
    if duration >= 0.0:
        tween_duration = duration

    # すでにエリア内にいる場合は、即座に新しいプリセットを適用
    if _is_inside:
        _apply_zoom(zoom_in_area)
    else:
        if restore_on_exit:
            _apply_zoom(zoom_outside_area)

この関数を使えば、例えば「ボタンを押したらズームの強さが変わる」「ゲーム進行に合わせてズーム演出を変える」といったことも、
コンポーネントを差し替えずに実現できます。

こんな感じで、一つの責務に絞ったコンポーネントを積み重ねていくと、Godot プロジェクトの見通しがかなり良くなります。
ぜひ自分のゲーム用に ZoomTrigger をカスタマイズして、カメラ演出をどんどんリッチにしていきましょう。