ゲームパッドやスマホでFPS/TPSっぽい操作を作ろうとすると、スティックだけだと「あとちょっと右!」が合わせづらい問題、ありますよね。
Godot 4 でも Input マップや Joypad の生値を触れば実装できますが、
- 毎回
Input.get_joy_axis()やInputEventJoypadMotionを自前で処理する - スマホ用には
InputEventScreenDragと加速度センサーを別途見る - プレイヤー・敵・タレットなど、照準を持つノードごとにロジックをコピペしがち
…と、「入力処理の継承クラス」がどんどん増えていくパターンにハマりやすいです。
そこで今回は、ノードにアタッチするだけで「ジャイロ照準による微調整」が手に入るコンポーネントとして、GyroAim を用意しました。
プレイヤーでもタレットでも、「向き」を持つノードにポン付けするだけで、
スティック+ジャイロのハイブリッド照準を実現していきましょう。
【Godot 4】スティック+ジャイロでヌルヌル照準!「GyroAim」コンポーネント
このコンポーネントは、
- ゲームパッドのジャイロ(傾き)
- モバイル端末の加速度センサー / ジャイロ
から「回転の微調整量」を計算して、親ノード(または指定ノード)の向きに加算する仕組みになっています。
本体の移動や大まかな狙いはスティックで行い、最後の微調整だけジャイロに任せるイメージですね。
フルコード(GDScript / Godot 4)
extends Node
class_name GyroAim
## GyroAim (ジャイロ照準) コンポーネント
## - コントローラーやスマホの傾きセンサーから「回転の微調整量」を取得し、
## ターゲットノードの rotation / rotation_degrees に加算する。
## - 2D を想定した実装。3D 用に拡張したい場合は rotation.y などに適用してください。
@export_group("基本設定")
## ジャイロの回転を適用する対象ノード。
## 未設定の場合は、このコンポーネントの親ノードをターゲットにします。
@export var target_node: Node2D
## ジャイロ照準を有効にするかどうかのフラグ。
@export var enabled: bool = true
## 1 秒あたりの最大回転速度(度)。感度のベース値になります。
@export_range(0.0, 720.0, 1.0, "or_greater") var base_sensitivity_deg: float = 180.0
## 実際のジャイロ入力値に乗算する係数。
## プラットフォームやコントローラーによってスケールが違うので、ここで調整します。
@export_range(0.0, 10.0, 0.01, "or_greater") var gyro_scale: float = 1.0
## スティックとジャイロを併用する場合の重み。
## 0.0 = スティックだけ、1.0 = ジャイロだけ。
@export_range(0.0, 1.0, 0.01) var gyro_weight: float = 1.0
@export_group("入力デバイス設定")
## どの Joypad ID を使うか。0 がデフォルトコントローラー。
@export_range(0, 7, 1, "or_greater") var joypad_id: int = 0
## ジャイロ対応コントローラーを使う場合に true。
## 例: Switch Pro Controller, DualShock 4/5 など(プラットフォーム依存)
@export var use_joypad_gyro: bool = true
## モバイル端末のセンサーを使う場合に true。
@export var use_mobile_sensors: bool = true
@export_group("スティック連携 (任意)")
## スティックの X 軸(右スティックの横方向など)の入力アクション名。
## 空文字の場合はスティック連携を無効にします。
@export var stick_action_x: StringName = &"aim_right_stick_x"
## スティックの Y 軸(右スティックの縦方向など)の入力アクション名。
@export var stick_action_y: StringName = &"aim_right_stick_y"
## スティック入力から回転を計算する際の感度(度/秒)。
@export_range(0.0, 720.0, 1.0, "or_greater") var stick_sensitivity_deg: float = 240.0
@export_group("フィルタリング")
## ジャイロ値のノイズを軽減するための簡易ローパスフィルタ係数。
## 0.0 = フィルタなし(生値)、1.0 = 変化なし(完全固定)。
@export_range(0.0, 1.0, 0.01) var gyro_smooth_factor: float = 0.2
## ジャイロのデッドゾーン(絶対値がこの値未満なら 0 とみなす)。
@export_range(0.0, 1.0, 0.001) var gyro_deadzone: float = 0.02
## スティックのデッドゾーン。
@export_range(0.0, 1.0, 0.001) var stick_deadzone: float = 0.15
# 内部状態
var _filtered_gyro_x: float = 0.0
var _filtered_gyro_y: float = 0.0
func _ready() -> void:
# target_node が未設定なら親ノードを自動ターゲットにする
if target_node == null:
var parent := get_parent()
if parent is Node2D:
target_node = parent
else:
push_warning("GyroAim: 親ノードが Node2D ではないため、自動設定に失敗しました。target_node を明示的に設定してください。")
# モバイル用センサーを有効化(対応プラットフォームのみ)
if use_mobile_sensors:
_enable_mobile_sensors()
func _physics_process(delta: float) -> void:
if not enabled:
return
if target_node == null:
return
var gyro_rot_delta := _get_gyro_rotation_delta(delta)
var stick_rot_delta := _get_stick_rotation_delta(delta)
# スティックとジャイロをブレンド
var final_rot_delta := lerp(stick_rot_delta, gyro_rot_delta, gyro_weight)
# 2D の場合、回転は Z 軸(rotation_degrees)とみなす
target_node.rotation_degrees += final_rot_delta
# --- ジャイロ処理 ---------------------------------------------------------
func _get_gyro_rotation_delta(delta: float) -> float:
var gyro_value_x := 0.0
var gyro_value_y := 0.0
if use_joypad_gyro:
var joy_gyro := _get_joypad_gyro()
gyro_value_x += joy_gyro.x
gyro_value_y += joy_gyro.y
if use_mobile_sensors:
var mobile_gyro := _get_mobile_gyro()
gyro_value_x += mobile_gyro.x
gyro_value_y += mobile_gyro.y
# ローパスフィルタでノイズを軽減
_filtered_gyro_x = lerp(_filtered_gyro_x, gyro_value_x, 1.0 - gyro_smooth_factor)
_filtered_gyro_y = lerp(_filtered_gyro_y, gyro_value_y, 1.0 - gyro_smooth_factor)
# デッドゾーン適用
var gx := _apply_deadzone(_filtered_gyro_x, gyro_deadzone)
var gy := _apply_deadzone(_filtered_gyro_y, gyro_deadzone)
# ここでは「横回転」を Z 軸回転にマッピングする想定。
# コントローラーや端末の向きによっては、X/Y を入れ替えたり符号を反転させてください。
var tilt := gx # 例: コントローラーを右に傾けると正方向
# 角速度(度/秒)にスケーリング
var degrees_per_sec := tilt * base_sensitivity_deg * gyro_scale
return degrees_per_sec * delta
func _get_joypad_gyro() -> Vector2:
# Godot 4 では現状、汎用的な「ジャイロ API」は薄く、
# プラットフォームやドライバ依存になるケースがあります。
# ここではサンプルとして Joypad の「回転系」軸を読む例を示します。
#
# 実プロジェクトでは、使用するプラットフォームに合わせて
# 適切な軸番号やプラグインを利用してください。
if not Input.is_joy_known(joypad_id):
return Vector2.ZERO
var gyro_x := 0.0
var gyro_y := 0.0
# 例: 右スティックの軸を「簡易ジャイロ」として使う(デバッグ用途)
# 実機ジャイロが取れる環境では、対応する軸に差し替えてください。
gyro_x = Input.get_joy_axis(joypad_id, JOY_AXIS_RIGHT_X)
gyro_y = Input.get_joy_axis(joypad_id, JOY_AXIS_RIGHT_Y)
return Vector2(gyro_x, gyro_y)
func _enable_mobile_sensors() -> void:
# Godot 4 のモバイルセンサーは Project Settings で有効化が必要な場合があります。
# ここでは、利用可能であれば有効化を試みるだけにとどめます。
if Engine.has_singleton("GodotAndroid"):
# Android プラグイン経由での制御を行う場合はここで呼び出す
# (プロジェクト固有の実装になるため、サンプルではコメントアウト)
# var android = Engine.get_singleton("GodotAndroid")
# android.enable_sensor("gyroscope")
pass
# iOS など他プラットフォームも同様にプラグイン側で制御してください。
func _get_mobile_gyro() -> Vector2:
# シンプルな例として、加速度センサーを「傾き」として扱います。
# 実際には専用のジャイロセンサーを使う方が精度が高いです。
var accel := Input.get_accelerometer() # Vector3
# 端末を手前に倒したり左右に傾けたときの変化を 2D に落とし込む。
# 端末の想定向きに合わせて軸や符号を調整してください。
var gx := accel.x
var gy := accel.y
return Vector2(gx, gy)
# --- スティック処理 -------------------------------------------------------
func _get_stick_rotation_delta(delta: float) -> float:
if stick_action_x == StringName("") and stick_action_y == StringName(""):
return 0.0
var sx := 0.0
var sy := 0.0
if stick_action_x != StringName(""):
sx = Input.get_action_strength(stick_action_x)
# 左方向を負値として扱いたい場合は、別アクションやカスタム処理が必要です。
# ここでは「右方向のみ」を想定した簡易実装としておきます。
if stick_action_y != StringName(""):
sy = Input.get_action_strength(stick_action_y)
# デッドゾーン
sx = _apply_deadzone(sx, stick_deadzone)
sy = _apply_deadzone(sy, stick_deadzone)
# ここでは「水平方向の入力だけで回転させる」簡易パターン。
# 2D Twin-stick シューティングなら、右スティックの角度から
# 直接 target_node の向きを決める実装に差し替えても OK です。
var horizontal := sx
var degrees_per_sec := horizontal * stick_sensitivity_deg
return degrees_per_sec * delta
# --- ユーティリティ -------------------------------------------------------
func _apply_deadzone(value: float, deadzone: float) -> float:
return 0.0 if absf(value) < deadzone else value
## 外部から一時的にジャイロ照準を有効/無効にするためのヘルパー。
func set_gyro_enabled(on: bool) -> void:
enabled = on
## 実行時に感度を変更するためのヘルパー。
func set_sensitivity_degrees_per_sec(deg: float) -> void:
base_sensitivity_deg = maxf(deg, 0.0)
使い方の手順
ここでは 2D アクションゲームのプレイヤーに「スティック+ジャイロ照準」を付ける例で説明します。
手順①: Input アクションの設定
- Project > Project Settings > Input Map を開く。
aim_right_stick_x,aim_right_stick_yを追加。- それぞれにコントローラーの右スティック軸を割り当てる(Axis など)。
※とりあえずデバッグ用途ならJOY_AXIS_RIGHT_Xをaim_right_stick_xに割り当てれば OK です。
手順②: プレイヤーシーンにコンポーネントを追加
プレイヤーのシーン構成例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── GyroAim (Node)
- Player シーンを開く。
- Player の子として
Nodeを追加し、スクリプトに上記GyroAim.gdをアタッチ。 - インスペクタで以下のように設定:
- target_node: 空のまま(自動で親の
Playerを参照) - enabled: チェック ON
- base_sensitivity_deg: 180 ~ 360 あたりから調整
- gyro_scale: 0.5 ~ 2.0 あたりから調整
- gyro_weight: 0.5(スティック+ジャイロ半々)
- use_joypad_gyro: PC+コントローラーなら ON
- use_mobile_sensors: モバイルビルドを想定するなら ON
- target_node: 空のまま(自動で親の
手順③: プレイヤーの回転を「向き」として利用する
例えば、プレイヤーの向きに合わせて弾を撃つ場合:
# Player.gd (例)
extends CharacterBody2D
@export var bullet_scene: PackedScene
func _process(delta: float) -> void:
if Input.is_action_just_pressed("shoot"):
_shoot()
func _shoot() -> void:
if bullet_scene == null:
return
var bullet := bullet_scene.instantiate()
get_tree().current_scene.add_child(bullet)
# プレイヤーの位置と向きを引き継ぐ
bullet.global_position = global_position
bullet.rotation = rotation
これだけで、スティックで大まかに向きを決めつつ、ジャイロで微調整した向きで弾が飛ぶようになります。
手順④: 敵タレットや動く床にも再利用する
コンポーネント指向の良さは、「向き」を持つノードなら何にでもポン付けできることです。
例えば、敵タレット:
EnemyTurret (Node2D) ├── Sprite2D ├── CollisionShape2D └── GyroAim (Node)
あるいは、プレイヤーが乗る「動く砲台付きの床」:
MovingPlatform (Node2D) ├── Sprite2D ├── CollisionShape2D ├── GyroAim (Node) # プレイヤーが乗っている間だけ有効にする └── TurretSprite (Sprite2D)
どちらも GyroAim の target_node を EnemyTurret や MovingPlatform に向けてやるだけで、
同じ照準ロジックを完全に使い回せるようになります。
メリットと応用
GyroAim をコンポーネントとして切り出すことで、
- プレイヤーの移動ロジックと照準ロジックが分離され、スクリプトがスッキリする
- 敵タレットやギミックにも 同じジャイロ照準を再利用できる
- 「ジャイロをオフにしたい」「感度を変えたい」といった要望に、1 箇所の修正で対応できる
- シーンツリー上も
Player ├── Sprite2D ├── CollisionShape2D └── GyroAimというフラットな構造で済み、継承ツリーが肥大化しない
つまり、「プレイヤー用のサブクラス」「タレット用のサブクラス」などを増やす代わりに、
「照準」という関心事を 1 個のコンポーネントに閉じ込めて合成(Composition)する形ですね。
改造案: ボタン長押し中だけジャイロ照準を有効にする
「常にジャイロだと酔う」「狙いをつけるときだけジャイロをオンにしたい」という場合、
以下のように _physics_process を少し拡張して、Input アクションから有効/無効を切り替えるのもアリです。
func _physics_process(delta: float) -> void:
# 例: "gyro_aim" アクションが押されている間だけジャイロを使う
enabled = Input.is_action_pressed("gyro_aim")
if not enabled:
return
if target_node == null:
return
var gyro_rot_delta := _get_gyro_rotation_delta(delta)
var stick_rot_delta := _get_stick_rotation_delta(delta)
var final_rot_delta := lerp(stick_rot_delta, gyro_rot_delta, gyro_weight)
target_node.rotation_degrees += final_rot_delta
このように、GyroAim 自体を 1 つの「照準モジュール」としておいておけば、
・「ADS(構え)中だけジャイロ ON」
・「スナイパーモード中は感度を半分に」
といったゲームデザイン上のアイデアも、プレイヤー本体のスクリプトを汚さずに実験できます。
継承でガチガチに固める前に、まずはこうしたコンポーネントを組み合わせてみると、
Godot 4 の開発体験がかなり快適になりますよ。
