Unityを触り始めると、つい「プレイヤーの入力処理」「カメラ制御」「UI更新」などを全部ひとつの Update() に書いてしまいがちですよね。
動き始めるまでは良いのですが、あとから「ジャイロで照準を微調整したい」「パッドでもスマホでも動くようにしたい」といった要望が出てくると、一気に地獄化します。

そこでこの記事では、「ジャイロの傾きだけ」を責務とする小さなコンポーネントとして、GyroAim(ジャイロ照準) を用意します。
照準ロジックをこのコンポーネントに切り出すことで、プレイヤー本体のスクリプトはシンプルなまま、「ジャイロ対応の有無」 をコンポーネントの付け外しで切り替えられるようにしていきましょう。

【Unity】ジャイロでヌルっと照準微調整!「GyroAim」コンポーネント

このコンポーネントは、スマホやコントローラーの傾き(ジャイロ)を読み取り、ターゲットTransformの回転に「上乗せ」する形で照準を微調整します。
・スマホなら端末そのものの傾き
・ゲームパッドなら、対応しているプラットフォームのモーションセンサー
を使う想定です。

Unity6 では新しい Input System が標準なので、今回は Unity Input System を使った実装にします。

フルコード:GyroAim.cs


using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.LowLevel;

/// <summary>
/// GyroAim
/// ジャイロ/モーションセンサーの傾きをもとに、
/// ターゲットTransformの回転に「照準微調整」を加えるコンポーネント。
/// スマホ端末や対応コントローラーで利用可能。
/// </summary>
public class GyroAim : MonoBehaviour
{
    // --- 設定項目 ---

    [Header("照準対象")]
    [SerializeField]
    private Transform target; 
    // 照準を回転させたい対象(例:カメラ、銃口、プレイヤーの上半身)

    [Header("感度設定")]
    [SerializeField, Tooltip("ジャイロの回転速度をどれだけ照準に反映するか(度/秒スケール)")]
    private float sensitivity = 2.0f;

    [SerializeField, Tooltip("Yaw(水平)方向の回転を有効にするか")]
    private bool useYaw = true;

    [SerializeField, Tooltip("Pitch(垂直)方向の回転を有効にするか")]
    private bool usePitch = true;

    [Header("制限角度")]
    [SerializeField, Tooltip("Pitch(上下)角度の最小値(度)")]
    private float minPitch = -80f;

    [SerializeField, Tooltip("Pitch(上下)角度の最大値(度)")]
    private float maxPitch = 80f;

    [Header("スムージング")]
    [SerializeField, Tooltip("補間の速さ。0で補間なし、1以上で急激、0.1前後でなめらか")]
    private float rotationLerpSpeed = 15f;

    [Header("入力デバイス優先度")]
    [SerializeField, Tooltip("スマホのジャイロを優先的に使うか(true:スマホ優先 / false:ゲームパッド優先)")]
    private bool preferMobileGyro = true;

    // --- 内部状態 ---

    // 現在使用しているモーションセンサー
    private IGyroProvider gyroProvider;

    // 回転の元となる「基準角度」
    private float baseYaw;
    private float basePitch;

    // 現在の「微調整後」角度
    private float currentYaw;
    private float currentPitch;

    // 初期化済みフラグ
    private bool initialized = false;

    #region Unity Lifecycle

    private void Awake()
    {
        // target が未指定なら、自分自身をターゲットにする
        if (target == null)
        {
            target = transform;
        }

        // 初期回転を基準として保存
        Vector3 euler = target.rotation.eulerAngles;
        baseYaw = euler.y;
        basePitch = NormalizePitch(euler.x);

        currentYaw = baseYaw;
        currentPitch = basePitch;

        // 利用可能なジャイロ入力を探索
        gyroProvider = CreateGyroProvider();
        if (gyroProvider != null)
        {
            gyroProvider.Enable();
            initialized = true;
        }
        else
        {
            Debug.LogWarning("[GyroAim] 利用可能なジャイロ/モーションセンサーが見つかりませんでした。ジャイロ照準は無効です。");
        }
    }

    private void OnDestroy()
    {
        if (gyroProvider != null)
        {
            gyroProvider.Disable();
        }
    }

    private void Update()
    {
        if (!initialized || gyroProvider == null)
        {
            return;
        }

        // デバイスの角速度(rad/s)を取得
        Vector3 angularVelocity = gyroProvider.GetAngularVelocity();

        // Unityの右手座標系に合わせて、Yaw/Pitch を取り出す
        // ここでは「ワールド空間での回転速度」ではなく、
        // デバイスのローカル空間のうち、Yaw=Y軸、Pitch=X軸を使う前提にしています。
        float deltaYaw = 0f;
        float deltaPitch = 0f;

        if (useYaw)
        {
            // Y軸回転をYawとして扱う(左右のひねり)
            deltaYaw = -angularVelocity.y * Mathf.Rad2Deg * sensitivity * Time.deltaTime;
        }

        if (usePitch)
        {
            // X軸回転をPitchとして扱う(上下の傾き)
            deltaPitch = angularVelocity.x * Mathf.Rad2Deg * sensitivity * Time.deltaTime;
        }

        // 角度を積算
        currentYaw += deltaYaw;
        currentPitch += deltaPitch;

        // Pitch に制限をかける
        currentPitch = Mathf.Clamp(currentPitch, minPitch, maxPitch);

        // 目標回転
        Quaternion targetRotation = Quaternion.Euler(currentPitch, currentYaw, 0f);

        if (rotationLerpSpeed > 0f)
        {
            // スムーズに補間
            target.rotation = Quaternion.Slerp(
                target.rotation,
                targetRotation,
                rotationLerpSpeed * Time.deltaTime
            );
        }
        else
        {
            // 即時反映
            target.rotation = targetRotation;
        }
    }

    #endregion

    #region Public API

    /// <summary>
    /// 現在のジャイロ照準を「0」とみなし、そこを基準角度として再キャリブレーションします。
    /// 例:プレイヤーが端末を持ち直した時などに呼ぶと良いです。
    /// </summary>
    public void Recenter()
    {
        if (target == null) return;

        Vector3 euler = target.rotation.eulerAngles;
        baseYaw = euler.y;
        basePitch = NormalizePitch(euler.x);

        currentYaw = baseYaw;
        currentPitch = basePitch;
    }

    /// <summary>
    /// ジャイロ照準を一時的に有効/無効にする。
    /// </summary>
    public void SetEnabled(bool enable)
    {
        if (enable)
        {
            if (!initialized)
            {
                gyroProvider = CreateGyroProvider();
                if (gyroProvider != null)
                {
                    gyroProvider.Enable();
                    initialized = true;
                }
            }
        }
        else
        {
            if (gyroProvider != null)
            {
                gyroProvider.Disable();
            }
            initialized = false;
        }
    }

    #endregion

    #region Gyro Provider Factory

    /// <summary>
    /// 利用可能なジャイロ入力ソースを探して、IGyroProvider を生成する。
    /// スマホ > ゲームパッド のような優先度で選択します。
    /// </summary>
    private IGyroProvider CreateGyroProvider()
    {
        IGyroProvider mobile = new MobileGyroProvider();
        IGyroProvider gamepad = new GamepadGyroProvider();

        bool mobileAvailable = mobile.IsAvailable();
        bool gamepadAvailable = gamepad.IsAvailable();

        if (!mobileAvailable && !gamepadAvailable)
        {
            return null;
        }

        if (preferMobileGyro)
        {
            if (mobileAvailable) return mobile;
            if (gamepadAvailable) return gamepad;
        }
        else
        {
            if (gamepadAvailable) return gamepad;
            if (mobileAvailable) return mobile;
        }

        return null;
    }

    #endregion

    #region Utility

    /// <summary>
    /// UnityのEuler Xは0~360度になるので、-180~180に正規化する。
    /// Pitchのクランプに使いやすくするため。
    /// </summary>
    private float NormalizePitch(float rawX)
    {
        float x = rawX;
        if (x > 180f)
        {
            x -= 360f;
        }
        return x;
    }

    #endregion

    #region Gyro Provider Interface & Implementations

    /// <summary>
    /// ジャイロ/モーションセンサーの抽象インターフェース。
    /// 実装を差し替えることで、スマホ・ゲームパッドなどを共通の扱いにします。
    /// </summary>
    private interface IGyroProvider
    {
        /// <summary>利用可能かどうか</summary>
        bool IsAvailable();

        /// <summary>有効化(必要ならセンサーの有効化など)</summary>
        void Enable();

        /// <summary>無効化</summary>
        void Disable();

        /// <summary>角速度(rad/s)を返す</summary>
        Vector3 GetAngularVelocity();
    }

    /// <summary>
    /// スマホ端末のジャイロを使う実装。
    /// UnityEngine.InputSystem.Gyroscope を利用します。
    /// </summary>
    private class MobileGyroProvider : IGyroProvider
    {
        private Gyroscope gyro;

        public bool IsAvailable()
        {
            // InputSystem の Gyroscope デバイスが存在するか確認
            gyro = Gyroscope.current;
            return gyro != null;
        }

        public void Enable()
        {
            if (gyro == null)
            {
                gyro = Gyroscope.current;
            }

            if (gyro != null)
            {
                InputSystem.EnableDevice(gyro);
            }
        }

        public void Disable()
        {
            if (gyro != null)
            {
                InputSystem.DisableDevice(gyro);
            }
        }

        public Vector3 GetAngularVelocity()
        {
            if (gyro == null)
            {
                gyro = Gyroscope.current;
                if (gyro == null) return Vector3.zero;
            }

            // Gyroscope の angularVelocity は Vector3 (rad/s)
            // 端末ローカル座標なので、ここではそのまま返します。
            return gyro.angularVelocity.ReadValue();
        }
    }

    /// <summary>
    /// ゲームパッドのモーションセンサーを使う実装。
    /// 対応しているコントローラーが接続されていれば利用可能です。
    /// </summary>
    private class GamepadGyroProvider : IGyroProvider
    {
        private Gamepad gamepad;

        public bool IsAvailable()
        {
            // 何かしらのゲームパッドが接続されているか
            gamepad = Gamepad.current;
            if (gamepad == null) return false;

            // Motion センサーを持っているかチェックする(プラットフォーム依存)
            // ここでは簡易的に「ジャイロステートが取得できるか」で判定します。
            try
            {
                // 低レベルイベントを読むわけではなく、
                // Gamepad の state を GyroscopeState にキャストできるかを
                // 確認するような形にしておく。
                // 実際の対応状況はプラットフォームとデバイスに依存します。
                var state = gamepad.GetState();
                return state is GyroscopeState;
            }
            catch
            {
                return false;
            }
        }

        public void Enable()
        {
            if (gamepad == null)
            {
                gamepad = Gamepad.current;
            }

            if (gamepad != null)
            {
                InputSystem.EnableDevice(gamepad);
            }
        }

        public void Disable()
        {
            if (gamepad != null)
            {
                InputSystem.DisableDevice(gamepad);
            }
        }

        public Vector3 GetAngularVelocity()
        {
            if (gamepad == null)
            {
                gamepad = Gamepad.current;
                if (gamepad == null) return Vector3.zero;
            }

            // ここでは「GyroscopeState を持つゲームパッド」という前提で、
            // 角速度を取得する例を示します。
            // 実際には各プラットフォームの拡張Gamepad実装を参照して、
            // 適切なプロパティから値を取得してください。
            var state = gamepad.GetState();

            if (state is GyroscopeState gyroState)
            {
                // GyroscopeState.angularVelocity は Vector3 (rad/s)
                return gyroState.angularVelocity;
            }

            // モーションセンサーが使えない場合は0を返す
            return Vector3.zero;
        }
    }

    #endregion
}

注意: Gamepad.GetState()GyroscopeState 部分は、実際にはプラットフォームやデバイス固有の拡張クラスになる場合があります。
「スマホでのジャイロ照準」が主目的であれば、MobileGyroProvider だけ使う形に簡略化しても構いません。

使い方の手順

  1. ① 新しい C# スクリプトを作成
    Unity の Project ビューで右クリック → Create → C# Script → 名前を GyroAim にして、上記コードをまるごとコピペして保存します。

  2. ② Input System を有効化
    Unity メニューから
    Edit > Project Settings... > Player > Other Settings
    Active Input HandlingInput System Package (New) もしくは Both に設定しておきます。
    まだ Input System パッケージを入れていない場合は、Package Manager から「Input System」をインストールしておきましょう。

  3. ③ コンポーネントをアタッチ
    具体例として:

    • FPS視点のプレイヤー:カメラオブジェクトに GyroAim を追加し、target にそのカメラの Transform を指定。
    • TPSの肩越し照準:銃口オブジェクト(GunPivot など)に GyroAim を付けて、キャラクターの基本エイムは右スティック、微調整だけジャイロに任せる。
    • タレットや固定砲台:砲台の上部(回転部分)に GyroAim を付けて、スマホを傾けて砲身を動かすミニゲームにする。

    target を未設定にすると、自分自身の Transform に対して回転をかけるので、基本は「そのまま」でも動きます。

  4. ④ キャリブレーションとチューニング
    実機で試しながら、以下を調整しましょう。

    • Sensitivity:感度。大きいほど小さな傾きで大きく動きます。
    • Min/Max Pitch:上下の可動範囲。上下に向きすぎて酔わないように制限。
    • Rotation Lerp Speed:スムーズさ。0だと生のジャイロ値、10〜20くらいでヌルっとした動き。
    • Prefer Mobile Gyro:スマホ優先かゲームパッド優先か。

    プレイヤーが「持ち方を変えた」「向きを変えた」タイミングで、
    例えば UI ボタンやキー入力から GyroAim.Recenter() を呼び出すと、その姿勢を新しい基準としてリセットできます。

メリットと応用

GyroAim をコンポーネントとして切り出すことで:

  • プレイヤー制御スクリプトが細くなる
    入力の種類(スティック・マウス・ジャイロ)を全部ひとつのクラスで面倒を見る必要がなくなります。
    「ジャイロの有無」は GyroAim コンポーネントを付けるかどうかで切り替えられます。
  • プレハブの再利用性が高まる
    例えば「ジャイロ対応のFPSカメラプレハブ」「通常のマウスエイムカメラプレハブ」を作っておいて、シーンごとに使い分ける、という構成が取りやすくなります。
  • レベルデザインが楽になる
    特定のステージだけ「ジャイロで狙うギミック」を入れたい時は、対象のオブジェクトに GyroAim を足すだけ。
    スクリプトを書き換えず、レベルデザイナーがインスペクタ操作だけで調整できるようになります。
  • モーションセンサー対応の実験がしやすい
    IGyroProvider インターフェースで抽象化してあるので、あとから「Switch向けの特定コントローラー用実装」「VRコントローラー用実装」などを追加しても、GyroAim 本体はほとんど触らずに済みます。

応用として、例えば「ジャイロの影響をだんだん弱める」「ADS(構え中)だけジャイロを強くする」といった改造も簡単です。

以下は、右クリックでエイムダウンサイト(ADS)中だけジャイロ感度を上げる簡単な改造案の例です。
GyroAim に追加するメソッドとして使えます。


/// <summary>
/// ADS(構え)中だけジャイロ感度を変える例。
/// Input System のボタンや、他コンポーネントから呼び出して使う想定です。
/// </summary>
public void SetAdsState(bool isAds)
{
    // 通常時とADS時の感度を決め打ち
    float normalSensitivity = 2.0f;
    float adsSensitivity = 4.0f;

    sensitivity = isAds ? adsSensitivity : normalSensitivity;
}

このように、ジャイロ照準をひとつのコンポーネントとして独立させておくと、
・プレイヤーの入力処理
・カメラ制御
・武器の挙動
といった他の責務ときれいに分離できて、後からの改造・実験がとてもやりやすくなります。
ぜひ、自分のプロジェクト用にカスタマイズしてみてください。