FPSやTPSのカメラ制御って、ついプレイヤーのスクリプトにベタ書きしてしまいがちですよね。
最初はそれでも動くのですが、
- 敵AIにも同じような視点制御を入れたくなった
- タレットや監視カメラにも「マウスっぽい視点操作」を流用したい
- プレイヤーのロジック(移動・ステート管理)とカメラ処理がごちゃ混ぜになる
…といったタイミングで、「あれ、このカメラ処理だけ分離したいな」と感じることが多いはずです。
Godot 4 ではノード継承でガッツリ作り込むより、「視点制御はコンポーネントとして Camera やキャラにアタッチする」方が、後々の拡張がかなり楽になります。
そこで今回は、マウスの移動量をカメラの回転に変換するだけに責務を絞った、
コンポーネント指向の MouseLook コンポーネントを作ってみましょう。
【Godot 4】マウスだけでヌルヌル視点操作!「MouseLook」コンポーネント
この MouseLook は、
- FPS/TPS 用の視点回転(Yaw / Pitch)
- 上下回転の角度制限
- 感度調整
- 一時的な有効/無効切り替え(UI表示中など)
といった機能を、どのノードにも後付けできるコンポーネントとして実装します。
「カメラの回転をどう適用するか」は親ノード構造に合わせて柔軟に指定できるようにしておきます。
フルコード:MouseLook.gd
extends Node
class_name MouseLook
## マウス移動量をYaw/Pitch回転に変換して、指定したノードを回転させるコンポーネント。
## - 典型的なFPS/TPS構成を想定(親がキャラ、子がカメラなど)
## - Yaw(左右回転)とPitch(上下回転)を別ノードに分離して扱えるようにしている
@export_group("Targets")
## 左右回転(Yaw)を適用するノード(通常はプレイヤーのルートなど)
@export var yaw_target: Node3D
## 上下回転(Pitch)を適用するノード(通常はカメラの親ノードなど)
@export var pitch_target: Node3D
@export_group("Sensitivity")
## マウス感度(左右)。度/ピクセル的なイメージ。
@export var sensitivity_x: float = 0.1
## マウス感度(上下)。
@export var sensitivity_y: float = 0.1
@export_group("Pitch Limits")
## 上方向の最大ピッチ角(度)。例: 80度なら真上までは向かないようにする。
@export_range(0.0, 89.9, 0.1)
var max_pitch_up_deg: float = 80.0
## 下方向の最大ピッチ角(度)。例: 80度なら真下までは向かないようにする。
@export_range(0.0, 89.9, 0.1)
var max_pitch_down_deg: float = 80.0
@export_group("Options")
## マウス反転(上下)。true にすると「下に動かすと上を見る」操作になる。
@export var invert_y: bool = false
## コンポーネントを一時的に無効化するフラグ(メニュー中など)
@export var enabled: bool = true
## マウスキャプチャをこのコンポーネントで管理するかどうか
## false にすると、外部(GameManagerなど)で Input.set_mouse_mode を制御する前提になる。
@export var manage_mouse_capture: bool = true
## 初期状態でマウスをキャプチャするかどうか
@export var capture_on_ready: bool = true
## 内部状態:現在のピッチ角(ラジアン)
var _pitch_rad: float = 0.0
func _ready() -> void:
# yaw_target / pitch_target が未設定の場合、ある程度自動で補完する。
# - yaw_target: 親が Node3D ならそれを使う
# - pitch_target: 自分の子ノードのうち最初の Node3D を使う
if yaw_target == null:
var parent_3d := get_parent()
if parent_3d is Node3D:
yaw_target = parent_3d as Node3D
if pitch_target == null:
for child in get_children():
if child is Node3D:
pitch_target = child as Node3D
break
# どちらかが設定されていないと動作できないので、警告を出す
if yaw_target == null:
push_warning("MouseLook: yaw_target が設定されていません。インスペクタで設定してください。")
if pitch_target == null:
push_warning("MouseLook: pitch_target が設定されていません。インスペクタで設定してください。")
# 現在のピッチ角を初期値として覚えておく
if pitch_target:
# X軸回りの回転がPitch。rotation.x はラジアン。
_pitch_rad = pitch_target.rotation.x
# マウスキャプチャ設定
if manage_mouse_capture and capture_on_ready:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
func _unhandled_input(event: InputEvent) -> void:
if not enabled:
return
# マウスキャプチャを管理する場合、Esc で解放、クリックで再キャプチャなどをここでやってもよい。
if manage_mouse_capture:
if event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
return
if event is InputEventMouseButton and event.pressed:
# 左クリックで再キャプチャする例
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
# マウスがキャプチャされていない場合は視点を動かさない
if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
return
if event is InputEventMouseMotion:
_process_mouse_motion(event)
func _process_mouse_motion(event: InputEventMouseMotion) -> void:
if yaw_target == null or pitch_target == null:
return
# マウスの相対移動量
var delta := event.relative
# 左右回転(Yaw): マウスのX移動でY軸回転
# 画面右に動かしたら右を向くように、符号を反転している。
var yaw_delta_rad := -deg_to_rad(delta.x * sensitivity_x)
# 上下回転(Pitch): マウスのY移動でX軸回転
var sign_y := invert_y ? 1.0 : -1.0
var pitch_delta_rad := sign_y * deg_to_rad(delta.y * sensitivity_y)
# Yaw を適用
yaw_target.rotate_y(yaw_delta_rad)
# Pitch を適用(角度制限付き)
_pitch_rad += pitch_delta_rad
var max_up_rad := deg_to_rad(max_pitch_up_deg)
var max_down_rad := deg_to_rad(max_pitch_down_deg)
# Godotの右手座標系では、上を向くとピッチは負方向、下を向くと正方向になる。
# ここでは「上方向: -max_up_rad ~ 下方向: +max_down_rad」にクランプする。
_pitch_rad = clamp(_pitch_rad, -max_up_rad, max_down_rad)
var rot := pitch_target.rotation
rot.x = _pitch_rad
pitch_target.rotation = rot
# --- 公開API(ゲーム側から制御しやすくする) ---
## コンポーネントを有効化/無効化するヘルパー
func set_enabled(value: bool) -> void:
enabled = value
## 現在のピッチ角(度)を取得
func get_pitch_degrees() -> float:
return rad_to_deg(_pitch_rad)
## ピッチ角を直接設定(度)
func set_pitch_degrees(deg: float) -> void:
var max_up_rad := deg_to_rad(max_pitch_up_deg)
var max_down_rad := deg_to_rad(max_pitch_down_deg)
var r := deg_to_rad(deg)
_pitch_rad = clamp(r, -max_up_rad, max_down_rad)
if pitch_target:
var rot := pitch_target.rotation
rot.x = _pitch_rad
pitch_target.rotation = rot
使い方の手順
ここでは典型的な FPS プレイヤー構成を例に、MouseLook を組み込んでみます。
シーン構成例(FPS プレイヤー)
Player (CharacterBody3D)
├── CollisionShape3D
├── MeshInstance3D
└── CameraPivot (Node3D) ← 上下回転を担当
└── Camera3D
└── MouseLook (Node) ← このコンポーネントをアタッチ
手順①:スクリプトファイルを用意する
res://components/MouseLook.gdなど好きな場所に、上記フルコードをコピペして保存します。- Godot エディタが
class_name MouseLookを認識すると、ノード追加ダイアログの「スクリプトクラス」にMouseLookが出てくるようになります。
手順②:ノード構成を作る
プレイヤーのシーンを 3D で作っている前提で、こんな構成にします。
Player (CharacterBody3D)
├── CollisionShape3D
├── MeshInstance3D
└── CameraPivot (Node3D)
└── Camera3D
└── MouseLook (Node) ← ここに追加
- Player: 左右回転(Yaw)を担当。移動ロジックもここに。
- CameraPivot: 上下回転(Pitch)専用の中間ノード。
- Camera3D: 実際に描画するカメラ。
- MouseLook: カメラの視点制御だけを担当するコンポーネント。
手順③:MouseLook のインスペクタ設定
MouseLook ノードを選択し、インスペクタで以下のように設定します。
- yaw_target:
Player(CharacterBody3D)をドラッグ&ドロップ - pitch_target:
CameraPivot(Node3D)をドラッグ&ドロップ - sensitivity_x / sensitivity_y: 0.1〜0.3 あたりから調整
- max_pitch_up_deg / max_pitch_down_deg: 70〜85 度くらいが自然
- invert_y: 好みに応じてチェック
- manage_mouse_capture: とりあえず
trueのままでOK - capture_on_ready: ゲーム開始時にマウスキャプチャしたいなら
true
これで、ゲームを再生してマウスを動かすと、
- 左右移動 →
Playerが Y 軸回転 - 上下移動 →
CameraPivotが X 軸回転(角度制限付き)
という挙動になります。
手順④:TPS やタレットにもそのまま流用
このコンポーネントは「どのノードを回すか」を外から指定するだけなので、TPS やタレットにもそのまま使えます。
TPS プレイヤー例
TPSPlayer (CharacterBody3D) ├── CollisionShape3D ├── MeshInstance3D ├── CameraRoot (Node3D) ← キャラの後ろにオフセット │ └── Camera3D └── MouseLook (Node)
- yaw_target:
TPSPlayer - pitch_target:
CameraRoot
監視カメラ・タレット例
Turret (Node3D) ├── Base (Node3D) ← 左右回転 │ └── Barrel (Node3D)← 上下回転 │ └── Camera3D └── MouseLook (Node)
- yaw_target:
Base - pitch_target:
Barrel
プレイヤー用に作ったコンポーネントが、そのまま AI タレットにも流用できるのが「継承より合成」の強みですね。
メリットと応用
MouseLook をコンポーネントとして切り出すことで、いろいろ嬉しいポイントがあります。
- プレイヤーのスクリプトがスリムになる
移動・ジャンプ・ステート管理と、カメラ制御の責務が分離されるので、読みやすさが段違いです。 - シーン構造がシンプル
「カメラ付きキャラ」の巨大な継承ツリーを作らずに、CharacterBody3D+CameraPivot+MouseLookの組み合わせで完結します。 - 再利用性が高い
プレイヤー・敵・タレット・フリーカメラなど、どこにでも同じMouseLookをポン付けできます。 - テストしやすい
「視点だけ動かすテストシーン」を作って、マウス感度や角度制限を調整しやすくなります。
さらに、MouseLook 側にちょっとした API を足してあげると、演出やカットシーンとも連携しやすくなります。
改造案:一時的に視点をアニメーションで奪う
例えば、「イベント中だけカメラを所定の方向に向けて、その後元の操作に戻す」ような演出をしたい場合、
MouseLook にこんな関数を追加しておくと便利です。
## 指定した方向を向く(ワールド空間の方向ベクトル)。
## duration > 0 の場合は Tween でスムーズに補間する。
func look_at_direction(direction: Vector3, duration: float = 0.0) -> void:
if yaw_target == null or pitch_target == null:
return
var dir := direction.normalized()
# Yaw: 水平方向の角度
var flat := Vector3(dir.x, 0.0, dir.z).normalized()
if flat.length() > 0.0:
var target_yaw := atan2(-flat.x, -flat.z) # GodotのZ前方に合わせた計算
if duration <= 0.0:
yaw_target.rotation.y = target_yaw
else:
var tween := create_tween()
tween.tween_property(yaw_target, "rotation:y", target_yaw, duration)
# Pitch: 上下方向の角度
var target_pitch := asin(clamp(dir.y, -1.0, 1.0))
# 角度制限を反映
var max_up_rad := deg_to_rad(max_pitch_up_deg)
var max_down_rad := deg_to_rad(max_pitch_down_deg)
target_pitch = clamp(target_pitch, -max_up_rad, max_down_rad)
if duration <= 0.0:
_pitch_rad = target_pitch
var rot := pitch_target.rotation
rot.x = _pitch_rad
pitch_target.rotation = rot
else:
var tween2 := create_tween()
tween2.tween_method(func(value):
_pitch_rad = value
var r := pitch_target.rotation
r.x = _pitch_rad
pitch_target.rotation = r
, _pitch_rad, target_pitch, duration)
イベント開始時に
$CameraPivot/MouseLook.set_enabled(false)
$CameraPivot/MouseLook.look_at_direction(target_dir, 0.5)
といった感じで視点を奪い、イベント終了時に set_enabled(true) で操作を返してあげる、という使い方ができます。
こんなふうに、「視点制御」という 1 つの責務をコンポーネントに閉じ込めておくと、
プレイヤーのロジックと演出ロジックの両方から、きれいに再利用できるようになりますね。
ぜひ自分のプロジェクト用にカスタマイズしてみてください。
