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 にアクションを追加
- Project > Project Settings > Input Map を開く。
mouse_peekというアクションを追加。- 右側の「+」ボタンから、例えば「Mouse Button > Right Button」を割り当てる。
(右クリックしている間だけ覗き込み、というイメージです)
手順②:カメラシーンにコンポーネントを追加
プレイヤーシーンの構成例はこんな感じにします。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── Camera2D │ └── MousePeek (Node) └── ・・・(他のコンポーネントなど)
- Player シーンを開く。
Camera2Dをプレイヤーの子として配置(すでにある場合はそのまま)。Camera2Dの子としてNodeを追加し、名前をMousePeekに変更。- その
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 だけを差し替える・付け替える、という運用をしていきましょう。
