Godot 4でアクションゲームやツインスティック系の操作を作っていると、

  • 「プレイヤーは画面中央に固定されすぎて、前方が見えない」
  • 「マウスの方向をちょっとだけ覗き込みたいけど、毎回カメラ用のスクリプトを継承して書き直すのがダルい」

……みたいな悩みが出てきますよね。

普通にやると、Camera2D を継承した「PlayerCamera」「EnemyCamera」「BossCamera」みたいなシーンを量産して、

  • それぞれに似たようなコードをコピペ
  • ちょっと仕様が変わるたびに全部修正
  • カメラ用ノード階層がどんどん深くなっていく

という「継承&スパゲッティカメラ地獄」に入りがちです。

そこで今回は、どんなカメラにも「ポン付け」できるコンポーネントとして、特定キーを押している間だけマウスの方向にカメラをずらす MousePeek コンポーネントを用意しました。
カメラ本体は素の Camera2D のまま、合成(Composition) で「マウス視点の覗き込み機能」だけを後付けしていきましょう。

【Godot 4】マウスの先をチラ見!「MousePeek」コンポーネント

フルコード(GDScript / Godot 4)


extends Node
class_name MousePeek
## マウス方向にカメラを「覗き込み」させるコンポーネント
## 親ノードの Camera2D を動かすことを前提にしています。

@export var enabled: bool = true:
	set(value):
		enabled = value
		if not enabled:
			# 無効化されたら即座にオフセットをリセット
			_reset_camera_offset()

## 覗き込みを有効にするキー(デフォルトは右マウスボタン)
## InputMap で "mouse_peek" などを作ってもOK
@export var peek_action_name: StringName = &"mouse_peek"

## 覗き込み距離(ピクセル)
## 値を大きくすると、より遠くまで画面をずらします。
@export_range(0.0, 2000.0, 1.0, "or_greater") var max_distance: float = 160.0

## 覗き込みのスムージング速度(大きいほど素早く追従)
@export_range(0.1, 30.0, 0.1, "or_greater") var follow_speed: float = 8.0

## 覗き込みを開始・終了するときの補間にかける時間(秒)
@export_range(0.0, 1.0, 0.01, "or_greater") var blend_time: float = 0.12

## カメラのズームを覗き込み計算に反映するかどうか
## true の場合、ズームインしているときは覗き込み距離が短く感じます。
@export var respect_zoom: bool = true

## デバッグ用に、現在の覗き込みオフセットを可視化するか
@export var draw_debug_gizmo: bool = false

## 内部状態
var _camera: Camera2D
var _current_offset: Vector2 = Vector2.ZERO
var _target_offset: Vector2 = Vector2.ZERO
var _blend_t: float = 0.0
var _is_peeking: bool = false

func _ready() -> void:
	# 親ノードから Camera2D を探す
	_camera = _find_camera()
	if _camera == null:
		push_warning("[MousePeek] 親に Camera2D が見つかりません。このコンポーネントは Camera2D の子に配置してください。")

	# 初期状態ではオフセットをリセット
	_reset_camera_offset()
	set_process(true)

func _process(delta: float) -> void:
	if not enabled or _camera == null:
		return

	# 覗き込み入力の状態を取得
	var want_peek := Input.is_action_pressed(peek_action_name)

	if want_peek != _is_peeking:
		# 状態が変わったらブレンドをリセット
		_is_peeking = want_peek
		_blend_t = 0.0

	# 覗き込み中ならターゲットオフセットを更新
	if _is_peeking:
		_target_offset = _calculate_peek_offset()
	else:
		_target_offset = Vector2.ZERO

	# blend_time が 0 の場合は即座に切り替え
	if blend_time <= 0.0:
		_current_offset = _target_offset
	else:
		# 0.0 ~ 1.0 で補間係数を進める
		_blend_t = clamp(_blend_t + delta / blend_time, 0.0, 1.0)
		_current_offset = _current_offset.lerp(_target_offset, min(follow_speed * delta, 1.0))

	_apply_camera_offset()

	# デバッグ表示
	if draw_debug_gizmo:
		queue_redraw()

func _unhandled_input(event: InputEvent) -> void:
	# InputMap を使わず、特定キーだけで動かしたい場合はここに直接書いてもOK
	# デフォルト実装では何もしません。
	pass

func _draw() -> void:
	if not draw_debug_gizmo or _camera == null:
		return

	# コンポーネント自身のローカル座標系に、覗き込みオフセットを描画
	var color := Color(0.2, 0.8, 1.0, 0.6)
	draw_line(Vector2.ZERO, _current_offset, color, 2.0)
	draw_circle(_current_offset, 4.0, color)

# --- 内部処理 -------------------------------------------------------------

## 親階層から Camera2D を探す。
func _find_camera() -> Camera2D:
	var node: Node = get_parent()
	while node != null:
		if node is Camera2D:
			return node
		node = node.get_parent()
	return null

## 現在のマウス位置から、覗き込みオフセットを計算する。
func _calculate_peek_offset() -> Vector2:
	# ビューポート座標系のマウス位置(スクリーン座標)
	var viewport := get_viewport()
	if viewport == null:
		return Vector2.ZERO

	var mouse_pos: Vector2 = viewport.get_mouse_position()

	# カメラ中心のスクリーン座標を求める
	# Camera2D は基本的に画面中央を映すので、単純にビューポートサイズの半分。
	var viewport_size: Vector2 = viewport.get_visible_rect().size
	var screen_center: Vector2 = viewport_size * 0.5

	# 中心からマウスへの方向ベクトル
	var dir: Vector2 = mouse_pos - screen_center
	if dir.length() == 0.0:
		return Vector2.ZERO

	dir = dir.normalized()

	var distance := max_distance
	if respect_zoom:
		# ズームが大きいほど(拡大)実際のオフセットは小さく感じるので、補正をかける
		# ここでは X のズーム値だけを使用(通常は X=Y なので問題なし)
		if _camera.zoom.x != 0.0:
			distance /= _camera.zoom.x

	return dir * distance

## Camera2D の offset を更新する。
func _apply_camera_offset() -> void:
	if _camera == null:
		return

	# Camera2D には既存の offset があるかもしれないので、それに加算する形にします。
	# こうしておくと、他のコンポーネントで「ベースのオフセット」を決めていても共存できます。
	var base_offset: Vector2 = _camera.offset
	var new_offset: Vector2 = base_offset + _current_offset

	# ただし、複数の MousePeek が付いていると二重加算になるので注意。
	# 通常は 1 カメラに 1 コンポーネントだけにしてください。
	_camera.offset = new_offset

## 覗き込みオフセットをリセットする。
func _reset_camera_offset() -> void:
	_current_offset = Vector2.ZERO
	_target_offset = Vector2.ZERO
	_blend_t = 0.0
	# Camera2D.offset 自体は他のコンポーネントが触っているかもしれないので、
	# ここでは直接リセットしません。_apply_camera_offset() の中だけで扱います。

使い方の手順

ここでは、2Dアクションゲームのプレイヤー用カメラに MousePeek を付ける例で説明します。

手順①:InputMap にアクションを追加

  1. Project > Project Settings > Input Map を開く。
  2. mouse_peek というアクションを追加。
  3. 右側の「+」ボタンから、例えば「Mouse Button > Right Button」を割り当てる。
    (右クリックしている間だけ覗き込み、というイメージです)

手順②:カメラシーンにコンポーネントを追加

プレイヤーシーンの構成例はこんな感じにします。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Camera2D
 │    └── MousePeek (Node)
 └── ・・・(他のコンポーネントなど)
  1. Player シーンを開く。
  2. Camera2D をプレイヤーの子として配置(すでにある場合はそのまま)。
  3. Camera2D の子として Node を追加し、名前を MousePeek に変更。
  4. その MousePeek ノードに、上記の GDScript(MousePeek.gd)をアタッチ。

これで、右クリック(mouse_peek アクション)を押している間、カメラがマウス方向にスッとずれるようになります。

手順③:パラメータを調整する

MousePeek ノードを選択すると、インスペクタに以下のパラメータが出てきます。

  • enabled … 一時的にオフにしたいとき用。
  • peek_action_name … デフォルトは mouse_peek。ゲームによって aim などに変えてもOK。
  • max_distance … 覗き込み距離(例: 160~320 あたりから調整)。
  • follow_speed … 覗き込みの追従スピード。大きいほどキビキビ動きます。
  • blend_time … 覗き込み開始・終了の「なめらかさ」。ゼロにするとカクッと切り替え。
  • respect_zoom … カメラズームを覗き込み距離に反映するかどうか。
  • draw_debug_gizmo … オフセットのベクトルを画面上に描いて確認したいときにオン。

トップダウンシューターなら max_distance = 300 くらい、横スクロールアクションなら max_distance = 160 前後から試してみるとよいですね。

手順④:別のカメラにもそのまま使い回す

このコンポーネントは「親が Camera2D なら動く」だけのシンプルな仕様なので、

  • ボス専用のカメラ
  • ステージギミック用の追従カメラ
  • デバッグ用フリーフライカメラ

などにも、そのままペタッと貼り付けて使えます。

例えば、ステージ全体を見渡せる「観戦カメラ」を作る場合:

SpectatorCamera (Camera2D)
 ├── MousePeek (Node)
 └── WASDController (Node)  ← これは別コンポーネントで自由移動を実装

こんな構成にしておけば、WASDController でカメラの基本移動、MousePeek でマウス方向の覗き込み、というふうに「合成」で機能を足していけます。

メリットと応用

MousePeek コンポーネントを使う最大のメリットは、

  • Camera2D を一切継承しなくていい(=Godot のシーンツリーを汚さない)
  • カメラ機能を「小さい部品」に分割できる(覗き込み・シェイク・ズームなどを別コンポーネント化)
  • どのカメラにも同じコンポーネントを貼るだけで再利用できる

たとえば、

  • CameraShake コンポーネント:ダメージ時にカメラを揺らす
  • CameraBounds コンポーネント:ステージ外に出ないように制限する
  • CameraZoomArea コンポーネント:特定エリアに入ったらズームを変える

といったものをそれぞれ独立コンポーネントにしておけば、

Camera2D
 ├── MousePeek
 ├── CameraShake
 └── CameraBounds

のように「カメラ機能をレゴブロック感覚で組み立て」られるようになります。
継承ベースで全部を 1 クラスに押し込むより、変更やデバッグが圧倒的に楽になりますね。

改造案:ゲームパッド右スティックでも覗き込み

「マウスだけじゃなく、右スティックでも覗き込みたい」というケースはよくあります。
その場合、_calculate_peek_offset() を少し改造して、ゲームパッド入力を優先するようにすると便利です。


func _calculate_peek_offset() -> Vector2:
	# まずはゲームパッド右スティックをチェック
	var stick_dir := Vector2(
		Input.get_action_strength("look_right") - Input.get_action_strength("look_left"),
		Input.get_action_strength("look_down") - Input.get_action_strength("look_up")
	)

	if stick_dir.length() > 0.1:
		stick_dir = stick_dir.normalized()
		var distance := max_distance
		if respect_zoom and _camera.zoom.x != 0.0:
			distance /= _camera.zoom.x
		return stick_dir * distance

	# 右スティックがニュートラルなら、マウスベースの計算にフォールバック
	return _calculate_peek_offset_from_mouse()

このように「入力の解釈部分だけを差し替える」改造がしやすいのも、コンポーネント指向で小さく分けているおかげですね。
カメラ本体は一切いじらず、MousePeek だけを差し替える・付け替える、という運用をしていきましょう。