FPS視点で武器モデルを動かすとき、つい「カメラに子ノードで武器をぶら下げて、そのまま一緒に動かす」実装をしがちですよね。
でもそれだと、武器がカメラに完全に固定されてしまって、マウスを動かしても一切「揺れ」や「追従の遅れ」が出ないので、どうしても安っぽく見えてしまいます。
さらに悪いパターンだと、
- プレイヤーシーンの中に「武器用のアニメーション」や「マウス処理」をべったり書いてしまう
- 武器ごとに別クラスを継承して、似たようなコードを何度もコピペする
- カメラと武器のノード階層がどんどん深くなって、あとから変更しづらい
こうなると、「別の武器モデルに差し替えたい」「敵の武器にも同じ揺れを使いたい」といったときに、継承ツリーとノード階層が足かせになってきます。
そこで今回は、どんな武器ノードにもポン付けできるコンポーネントとして、
「WeaponSway」コンポーネントを用意してみました。
マウス移動に応じて、武器モデルが少し遅れて追従することで、FPSらしい「スウェイ(揺れ)」を簡単に実現します。
【Godot 4】マウス操作に合わせて武器がヌルっと追従!「WeaponSway」コンポーネント
以下は、Godot 4 用の WeaponSway コンポーネントのフルコードです。
3D FPS を想定して Node3D ベースで作っていますが、カメラにぶら下がった武器モデルなら何でもOKです。
extends Node3D
class_name WeaponSway
##
## WeaponSway.gd
## マウス移動に応じて、武器モデルを少し遅れて追従させるコンポーネント。
## - FPS視点のカメラの子として配置された武器モデルにアタッチして使う想定。
## - どの武器モデルにも再利用できるよう、「武器側には一切ロジックを書かない」方針。
##
## --- 基本設定 ---
@export_category("Input")
## マウスの相対移動をどこから取得するか。
## 通常は "ui_look" のようなカスタムアクションを使わず、
## Input.get_last_mouse_velocity() を使うので、このままでもOK。
@export var use_raw_mouse_velocity: bool = true
## マウス感度。大きいほど、少しのマウス移動で大きく揺れる。
@export_range(0.0, 5.0, 0.01, "or_greater")
var sensitivity: float = 1.0
@export_category("Sway Amount")
## X軸(左右)回転の揺れの強さ(度数)。マウスの横移動に反応。
@export_range(0.0, 30.0, 0.1, "or_greater")
var max_yaw_degrees: float = 8.0
## Y軸(上下)回転の揺れの強さ(度数)。マウスの縦移動に反応。
@export_range(0.0, 30.0, 0.1, "or_greater")
var max_pitch_degrees: float = 5.0
## 位置の揺れの強さ。値が大きいほど、武器が画面端方向へ大きくスライドする。
@export_range(0.0, 0.5, 0.001, "or_greater")
var max_position_offset: float = 0.08
@export_category("Smoothing")
## 回転の追従速度。大きいほど、すばやく目標角度に追いつく(カクッとしやすい)。
@export_range(0.1, 30.0, 0.1, "or_greater")
var rotation_lerp_speed: float = 10.0
## 位置の追従速度。大きいほど、すばやく目標位置に追いつく。
@export_range(0.1, 30.0, 0.1, "or_greater")
var position_lerp_speed: float = 10.0
@export_category("Reset")
## マウスが止まったとき、どれくらいの速さで「元の姿勢」に戻すか。
@export_range(0.1, 20.0, 0.1, "or_greater")
var return_speed: float = 4.0
## 入力がほぼゼロとみなされる閾値。この値より小さい速度なら「止まっている」と判断。
@export_range(0.0, 50.0, 1.0, "or_greater")
var deadzone: float = 3.0
@export_category("Axes")
## Y反転。FPSによってはマウスの上下を反転させたい場合がある。
@export var invert_y: bool = false
## X反転。武器を逆側に揺らしたいときなどに。
@export var invert_x: bool = false
## --- 内部状態 ---
## 初期ローカル位置と回転を保持しておき、揺れはそこからの相対変化として扱う。
var _base_position: Vector3
var _base_rotation: Vector3
## 現在のターゲット(目標)オフセット
var _target_rot_offset: Vector3 = Vector3.ZERO
var _current_rot_offset: Vector3 = Vector3.ZERO
var _target_pos_offset: Vector3 = Vector3.ZERO
var _current_pos_offset: Vector3 = Vector3.ZERO
func _ready() -> void:
## ベースとなるローカル位置と回転を記録。
_base_position = position
_base_rotation = rotation_degrees
func _process(delta: float) -> void:
var mouse_vel: Vector2 = _get_mouse_velocity()
_update_target_offsets(mouse_vel, delta)
_apply_sway(delta)
## マウスの相対移動(速度)を取得する。
## - use_raw_mouse_velocity = true の場合は、Godot 4 の Input.get_last_mouse_velocity() を利用。
## - それ以外の場合は、InputEventMouseMotion を自前で拾う実装に差し替えてもOK。
func _get_mouse_velocity() -> Vector2:
if use_raw_mouse_velocity:
return Input.get_last_mouse_velocity()
# カスタム入力系に差し替えたい場合はここを改造する
return Input.get_last_mouse_velocity()
## マウス速度から、目標オフセット(回転・位置)を計算する。
func _update_target_offsets(mouse_vel: Vector2, delta: float) -> void:
var speed: float = mouse_vel.length()
if speed < deadzone:
## 入力がほぼゼロなら、ゆっくりと元の姿勢へ戻す
_target_rot_offset = _target_rot_offset.lerp(Vector3.ZERO, return_speed * delta)
_target_pos_offset = _target_pos_offset.lerp(Vector3.ZERO, return_speed * delta)
return
## 反転オプションを反映
var dx := mouse_vel.x * (invert_x ? -1.0 : 1.0)
var dy := mouse_vel.y * (invert_y ? -1.0 : 1.0)
## マウスの動きに応じて、武器を「逆方向」に傾けるイメージ
## - マウスを右に動かす → 画面上では武器が少し左へ流れる
var normalized_x := clamp(dx / 400.0, -1.0, 1.0)
var normalized_y := clamp(dy / 400.0, -1.0, 1.0)
## 回転オフセット(度数)
var target_yaw := -normalized_x * max_yaw_degrees * sensitivity
var target_pitch := normalized_y * max_pitch_degrees * sensitivity
_target_rot_offset.y = target_yaw
_target_rot_offset.x = target_pitch
## 位置オフセット(ローカル座標)
## 横方向の動きに応じて、武器を左右にスライドさせる。
## Z方向に少し引いたり押し出したりしても面白い。
_target_pos_offset.x = -normalized_x * max_position_offset * sensitivity
_target_pos_offset.y = -normalized_y * (max_position_offset * 0.6) * sensitivity
_target_pos_offset.z = abs(normalized_x) * (max_position_offset * 0.3) * sensitivity
## 現在のオフセットを目標オフセットにスムーズに近づけ、実際のTransformに反映する。
func _apply_sway(delta: float) -> void:
_current_rot_offset = _current_rot_offset.lerp(
_target_rot_offset,
rotation_lerp_speed * delta
)
_current_pos_offset = _current_pos_offset.lerp(
_target_pos_offset,
position_lerp_speed * delta
)
## ローカル回転・位置にオフセットを適用
rotation_degrees = _base_rotation + _current_rot_offset
position = _base_position + _current_pos_offset
## 外部から「即座に元の姿勢に戻したい」ときに呼ぶユーティリティ。
## 例: 武器を持ち替えた瞬間、ADSに入った瞬間など。
func reset_sway_immediately() -> void:
_target_rot_offset = Vector3.ZERO
_current_rot_offset = Vector3.ZERO
_target_pos_offset = Vector3.ZERO
_current_pos_offset = Vector3.ZERO
rotation_degrees = _base_rotation
position = _base_position
使い方の手順
ここからは、実際に FPS プレイヤーシーンに組み込む手順を見ていきましょう。
例として、以下のような 3D プレイヤー構成を想定します。
Player (CharacterBody3D) ├── Camera3D │ └── WeaponRoot (Node3D) ← 武器の基準位置 │ └── RifleMesh (Node3D or MeshInstance3D) │ └── WeaponSway (Node3D, this component) └── その他(コリジョン、移動ロジックなど)
手順①: スクリプトを用意する
WeaponSway.gdという名前で、上記のコードをそのまま保存します。- Godot エディタの「プロジェクト設定 > スクリプトクラス」から自動登録されるので、
ノードにアタッチするときはクラス名WeaponSwayとして選択できます。
手順②: シーン構成を整える
FPS プレイヤーシーンの一例はこんな感じです。
Player (CharacterBody3D) ├── CollisionShape3D ├── Camera3D │ └── WeaponRoot (Node3D) │ └── Rifle (Node3D) │ ├── MeshInstance3D │ └── WeaponSway (Node3D) ← コンポーネント └── その他のノード...
Camera3Dの子にWeaponRootを作成し、そこに武器モデルをまとめてぶら下げます。- 武器モデル(ここでは
Rifle)の子としてWeaponSwayノードを追加し、
そのノードにWeaponSway.gdをアタッチします。 - ポイント:武器の見た目(MeshInstance3D)と、揺れロジック(WeaponSway)を分けることで、
モデルを差し替えてもコンポーネントはそのまま使い回せます。
手順③: マウスロックと感度を設定する
プレイヤー側のスクリプト(例: Player.gd)では、通常通り Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) を設定しておきます。
func _ready() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
マウス感度は、視点回転のスクリプトと WeaponSway の sensitivity を
バランスを見ながら調整しましょう。
- 視点回転が速い場合 →
WeaponSway.sensitivityはやや小さめ(0.5〜1.0) - 視点回転が控えめ →
WeaponSway.sensitivityを少し大きめ(1.0〜2.0)
手順④: 他の武器・敵にも再利用する
このコンポーネントは「マウスの動き → ローカル位置&回転のオフセット」という単純な構造なので、
- ショットガン、ハンドガン、スナイパーライフルなど、別シーンの武器
- プレイヤーではなく、敵の視点カメラにぶら下がった武器(デバッグ用途など)
- 腕モデルだけを表示する「アームズモデル」に対しても
同じように WeaponSway ノードを追加するだけで、全て同じ揺れロジックを共有できます。
ShotgunScene (Node3D) ├── MeshInstance3D └── WeaponSway (Node3D) SniperScene (Node3D) ├── MeshInstance3D └── WeaponSway (Node3D)
武器ごとに「揺れ方」を変えたい場合は、max_yaw_degrees や max_position_offset を個別に調整するだけでOKです。
メリットと応用
この WeaponSway コンポーネントを使うことで、次のようなメリットがあります。
- プレイヤーや武器クラスにロジックを書かないので、クラス継承がスッキリする
- どの武器シーンにも同じコンポーネントをポン付けできるので、使い回しが非常に楽
- 武器モデルの差し替えや、武器の追加をしても、ノード階層やスクリプトをほとんどいじらずに済む
- 視点回転ロジックと「見た目の揺れ」が完全に分離されるので、デバッグやチューニングがしやすい
特に、Godot の典型的な「深いノード階層+プレイヤー巨大スクリプト」構成を避けられるのが大きいですね。
「視点回転はプレイヤー」「揺れはWeaponSway」「アニメーションは別コンポーネント」といった形で、機能ごとにスクリプトを分割すると、あとからの改造が圧倒的に楽になります。
改造案:ADS(構え)時は揺れを弱くする
例えば「右クリックで構え(ADS)している間は、武器揺れを弱くする」といった演出を加えたい場合、
次のようなメソッドを WeaponSway に追加するだけで対応できます。
## WeaponSway.gd に追記する例
@export_category("ADS")
@export_range(0.0, 1.0, 0.01)
var ads_sway_scale: float = 0.3
var _ads_factor: float = 1.0
func set_ads(active: bool) -> void:
## 外部(プレイヤー側)から「ADS中かどうか」を教えてもらう
_ads_factor = ads_sway_scale if active else 1.0
func _update_target_offsets(mouse_vel: Vector2, delta: float) -> void:
# 既存処理の中で、sensitivity を _ads_factor でスケーリングする
var speed: float = mouse_vel.length()
if speed < deadzone:
_target_rot_offset = _target_rot_offset.lerp(Vector3.ZERO, return_speed * delta)
_target_pos_offset = _target_pos_offset.lerp(Vector3.ZERO, return_speed * delta)
return
var dx := mouse_vel.x * (invert_x ? -1.0 : 1.0)
var dy := mouse_vel.y * (invert_y ? -1.0 : 1.0)
var normalized_x := clamp(dx / 400.0, -1.0, 1.0)
var normalized_y := clamp(dy / 400.0, -1.0, 1.0)
var sway_sens := sensitivity * _ads_factor
var target_yaw := -normalized_x * max_yaw_degrees * sway_sens
var target_pitch := normalized_y * max_pitch_degrees * sway_sens
_target_rot_offset.y = target_yaw
_target_rot_offset.x = target_pitch
_target_pos_offset.x = -normalized_x * max_position_offset * sway_sens
_target_pos_offset.y = -normalized_y * (max_position_offset * 0.6) * sway_sens
_target_pos_offset.z = abs(normalized_x) * (max_position_offset * 0.3) * sway_sens
プレイヤー側では、
# Player.gd の例
@onready var weapon_sway: WeaponSway = $Camera3D/WeaponRoot/Rifle/WeaponSway
func _process(delta: float) -> void:
var ads := Input.is_action_pressed("aim") # 右クリックなど
weapon_sway.set_ads(ads)
といった感じで、「ADS の状態」をコンポーネントに伝えてあげるだけです。
プレイヤーの巨大スクリプトを汚さずに、WeaponSway 側だけを改造して演出を強化できるのが、コンポーネント指向の気持ちいいところですね。
このスタイルで、「リコイル」「ブレス(呼吸)による上下揺れ」「ダッシュ中の揺れ強化」なども、
それぞれ独立したコンポーネントとして積み上げていくと、かなり気持ちよく拡張できるようになります。ぜひ遊んでみてください。
