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) ← このノードに上記スクリプトをアタッチ
- Player シーンを開く
- Player(ルートの CharacterBody2D)に子ノードとして
Nodeを追加 - その Node の名前を
ZoomTriggerに変更 - 作成した
ZoomTrigger.gdをこのノードにアタッチ
target_camera を未設定のままにしておけば、親(Player)の子孫から自動で Camera2D を探してくれます。
手順②:ズーム用の Area2D をステージに配置
次に、ステージシーン(例: Level1.tscn)にズーム用の Area2D を置きます。
Level1 (Node2D) ├── TileMap ├── Player (インスタンス) ├── ZoomArea (Area2D) │ ├── CollisionShape2D │ └── (見た目用の Sprite2D など任意) └── その他いろいろ
Area2Dを追加して名前をZoomAreaに変更CollisionShape2Dを子に追加し、プレイヤーが入ってほしい範囲を設定
この ZoomArea が trigger_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_areaをVector2(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 をカスタマイズして、カメラ演出をどんどんリッチにしていきましょう。
