【Unity】FPSゲームのリコイルの実装

# 概要
こんな感じのリコイルを作ります。
![Recoil.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3686460/577d50ed-c6de-bcd4-79f1-6ece7967028b.gif)
どんなリコイルパターンになるか自由に設定でき、滑らかな視点移動にしました。

:::note info
前提として以前に書いた記事の[方法2の視点移動Script]を使っています。
:::
[方法2の視点移動Script]: https://qiita.com/koinuma/items/538094f60c1eb6652f6f#%E6%96%B9%E6%B3%952script%E5%88%B6%E5%BE%A1
# 仕組み
### 回転ベクトルについて
以前のコードに新しく変数を追加します。
```POVController.cs
[SerializeField] private float _returnSpeed = 1;
[SerializeField] private float _snappiness = 6;
private Vector3 _recoilTargetRotation;
private Vector3 _currentRecoilRotation;
private Vector3 _returnTarget;
```
リコイルが入力されたとき`_recoilTargetRotation`の値を変え、それに伴って`_currentRecoilRotation`が変動します。プレイヤーの視点に反映されるのは`_currentRecoilRotation`です。
`_returnTarget`はリコイルによって跳ね上がった視点が戻ってくる場所です。
この３つのベクトルによりリコイルの入力で瞬間的に`_recoilTargetRotation`の値が変わるが、`_snappiness`の速度で跳ね上がり、`_returnSpeed`のスピードで元の視点に戻ります。
```mermaid
graph RL;
    A([_currentRecoilRotation])-->|_snappiness| B([_recoilTargetRotation])
    D([リコイルの入力])--> B
    B-->|_returnSpeed| C([_returnTarget])
```
```POVController.cs
    /// <summary>指定したリコイルを設定する</summary>
    public void Recoil(Vector2 recoil)
    {
        _recoilTargetRotation += new Vector3(-recoil.y, recoil.x, 0);
    }

    /// <summary>リコイルを反映させる</summary>
    void ReflectsRecoil()
    {
        _recoilTargetRotation = Vector3.Slerp(_recoilTargetRotation, _returnTarget,
            _returnSpeed * Time.fixedDeltaTime);
        _currentRecoilRotation = Vector3.Slerp(_currentRecoilRotation, _recoilTargetRotation,
            _snappiness * Time.fixedDeltaTime);
    }
```
`Recoil`メソッドでリコイルの入力を受け付けています。入力時に直感的にわかりやすいので引数をVector2、XY反転でやっていますが好みで。
`ReflectsRecoil`で各ベクトルの動きを計算しています。`Vectr3.Slerp`を使うことで2つのベクトルの差が大きいほど速い視点移動となります。つまり一定の縦リコイルを入力し続けても、次第にリコイルは小さくなるということです。
加減速の計算について詳しくないので`ReflectsRecoil`はFixedUpdateで実行しています。
### ベクトルを反映させる
以前の[記事のコードのLookの一部](https://qiita.com/koinuma/items/538094f60c1eb6652f6f#%E6%96%B9%E6%B3%952script%E5%88%B6%E5%BE%A1:~:text=//%20%E9%A0%AD%E3%80%81%E4%BD%93%E3%81%AE%E5%90%91%E3%81%8D%E3%81%AE%E9%81%A9%E7%94%A8%0A%20%20%20%20%20%20%20%20_head.transform.localRotation%20%3D%20Quaternion.Euler(_xRotation%2C%200%2C%200)%3B%0A%20%20%20%20%20%20%20%20_body.transform.localRotation%20%3D%20Quaternion.Euler(0%2C%20_yRotation%2C%200)%3B)を書き換えます
```diff_c_sharp
        // 頭、体の向きの適用
-        _head.transform.localRotation = Quaternion.Euler(_xRotation, 0, 0);
-        _body.transform.localRotation = Quaternion.Euler(0, _yRotation, 0);
+        _head.transform.localRotation = 
+            Quaternion.Euler(_xRotation + _currentRecoilRotation.x, 0, 0);
+        _body.transform.localRotation = 
+            Quaternion.Euler(0, _yRotation + _currentRecoilRotation.y, 0);
```
ベクトルの入力にリコイルのベクトルを追加しているだけです。
次は`_returnTarget`について
このままだとリコイル制御でマウスを動かしたとき、その分だけリコイルが終わったときに視点が動いてしまいます。なので、リコイル制御したときに戻ってくる場所を更新する必要がある。
```diff_c_sharp
+    if (mouseInput.magnitude != 0)
+    {
+        _returnTarget = _currentRecoilRotation;
+    }
```
### リコイルの入力
一定間隔で射撃でき、最初は一定のリコイルパターンがあるが最後のほうはランダムなリコイルになる、よくあるようなリコイルを再現します。射撃をやめればリコイルパターンはリセットされます。
<details><summary>リコイルテストコード</summary>

```RecoilTest.cs
using UnityEngine;

public class RecoilTest : MonoBehaviour
{
    [SerializeField] private POVController _povController;
    [SerializeField] private GameObject _bulletMarkObject;
    [SerializeField] private float _fireRate = 0.12f;
    [SerializeField] private Vector2[] _recoilPattern;
    [SerializeField] private Vector2 _randomRecoil;

    private float _fireTimer;
    private int _recoilPatternIndex;

    private void Update()
    {
        if (_fireTimer < _fireRate)
        {
            _fireTimer += Time.deltaTime;
        }

        if (Input.GetButton("Fire1"))
        {
            if (_fireTimer >= _fireRate)
            {
                _fireTimer = 0;

                if (_recoilPatternIndex < _recoilPattern.Length) // パターンが設定されていればそれに従う
                {
                    _povController.Recoil(_recoilPattern[_recoilPatternIndex]);
                    _recoilPatternIndex++;
                }
                else // 一定以降はランダムとか
                {
                    _povController.Recoil(new Vector2(Random.Range(-_randomRecoil.x, _randomRecoil.x), _randomRecoil.y));
                }
                
                // 着弾地点が分かるようにオブジェクト生成
                Physics.Raycast(Camera.main.transform.position, Camera.main.transform.forward, out RaycastHit hit);
                Destroy(Instantiate(_bulletMarkObject, hit.point, Quaternion.identity), 5);
            }
        }
        else
        {
            _recoilPatternIndex = 0;
        }
    }
}

```
</details>
<details><summary>Inspector</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3686460/b8a37ee4-bf7d-2243-21b7-91e2378bdf70.png)
</details>
もっとこだわれば既存のゲームを再現できるでしょう。