ゲームパッドやスマホで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 アクションの設定

  1. Project > Project Settings > Input Map を開く。
  2. aim_right_stick_x, aim_right_stick_y を追加。
  3. それぞれにコントローラーの右スティック軸を割り当てる(Axis など)。
    ※とりあえずデバッグ用途なら JOY_AXIS_RIGHT_Xaim_right_stick_x に割り当てれば OK です。

手順②: プレイヤーシーンにコンポーネントを追加

プレイヤーのシーン構成例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── GyroAim (Node)
  1. Player シーンを開く。
  2. Player の子として Node を追加し、スクリプトに上記 GyroAim.gd をアタッチ。
  3. インスペクタで以下のように設定:
    • 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

手順③: プレイヤーの回転を「向き」として利用する

例えば、プレイヤーの向きに合わせて弾を撃つ場合:


# 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)

どちらも GyroAimtarget_nodeEnemyTurretMovingPlatform に向けてやるだけで、
同じ照準ロジックを完全に使い回せるようになります。


メリットと応用

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 の開発体験がかなり快適になりますよ。