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)     ← このコンポーネントをアタッチ

手順①:スクリプトファイルを用意する

  1. res://components/MouseLook.gd など好きな場所に、上記フルコードをコピペして保存します。
  2. 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 つの責務をコンポーネントに閉じ込めておくと、
プレイヤーのロジックと演出ロジックの両方から、きれいに再利用できるようになりますね。
ぜひ自分のプロジェクト用にカスタマイズしてみてください。