【Unity】回転範囲を制限したLookAt関数を作る

# はじめに
UnityでGameObjectに特定の方向を向かせたいときには，LookAtメソッドを用いることで簡単に実現できますが，回転範囲に制限がないため，アバターの顔の向きを変える場合等に首が180度曲がってこっちを向いたりしてきます(不自然で怖いですw)．
他にも，戦艦の主砲やこちらを向く監視カメラを実装する場合においても，可動域を制限してこちらを向いてほしい実装箇所はいくつもあると思います．
以上の理由から，回転範囲をオイラー角で制限できるLookAt関数を実装しました．
# 仕様
回転の為に指定する要素は次の4つです．
・Rotator: 回転してほしい対象(Transform)
・LookingSpeed: Euler角の軸毎の回転速度(Vector3)
・Target: 向いてほしい対象(Transform)
・LocalRotRangeX,Y,Z: Euler角の可動範囲を指定 (Vector2×3)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/620824/fef09852-cacb-b803-96ff-8ff4d22704c3.png)

上記を指定して，オブジェクトにアタッチすることで，回転範囲内でターゲットを向いてくれます．



# ソースコード
```c#
/// <summary>
/// localRotationの可動域指定可能 LookAt
/// </summary>
public class ExtendLookAt : MonoBehaviour
{
    //回転させたい対象
    [SerializeField] Transform rotator;
    //回転速度
    [SerializeField] Vector3 lookingSpeed;
    //向いてほしいターゲット
    [SerializeField] Transform target;
    //Quaternion defaultLocalRotation;

    //回転可動域の制限
    [SerializeField] Vector2 localRotRangeX;
    [SerializeField] Vector2 localRotRangeY;
    [SerializeField] Vector2 localRotRangeZ;
    // Start is called before the first frame update
    void Start()
    {
        //defaultLocalRotation = transform.localRotation;
    }

    // Update is called once per frame
    void Update()
    {
        //Updateで常に呼び出すことで徐々にターゲットの方へ向いていく
        LookingTarget(target.position, lookingSpeed,rotator);
    }
    /// <summary>
    /// <para>拡張LookAt</para> 
    /// <para>第1引数: 向いて欲しい座標</para> 
    /// <para>第2引数: 回転速度</para> 
    /// <para>第3引数: 回転してほしい対象</para> 
    /// </summary>
    /// <param name="targetPos"></param>
    /// <param name="rotSpeed"></param>
    /// <param name="rotator"></param>
    void LookingTarget(Vector3 targetPos, Vector3 rotSpeed, Transform rotator)
    {

        //方策
        //一度回転させて超えたら戻すを,x,y,z軸ごとにやる

        //ターゲット
        Vector3 deviation = targetPos - rotator.position;
        Quaternion rot = Quaternion.LookRotation(deviation);
        
        //回転前のオイラー角を取得
        Vector3 defaultRotation = rotator.rotation.eulerAngles;

        float slerpX = Quaternion.RotateTowards(rotator.rotation, rot, rotSpeed.x * Time.deltaTime).eulerAngles.x;
        rotator.rotation = Quaternion.Euler(slerpX, defaultRotation.y, defaultRotation.z);
        //回転後の角度が範囲を超えてないかチェック
        if (!InAnAngleCheck(rotator.localRotation.eulerAngles.x, localRotRangeX))
        {
            //問題があればデフォルトに戻す
            rotator.rotation = Quaternion.Euler(defaultRotation.x, defaultRotation.y, defaultRotation.z);
        }
        defaultRotation = rotator.rotation.eulerAngles;//問題がなければデフォルトを更新

        float slerpY = Quaternion.RotateTowards(rotator.rotation, rot, rotSpeed.y * Time.deltaTime).eulerAngles.y;
        rotator.rotation = Quaternion.Euler(defaultRotation.x, slerpY, defaultRotation.z);
        //回転後の角度が範囲を超えてないかチェック
        if (!InAnAngleCheck(rotator.localRotation.eulerAngles.y, localRotRangeY))
        {
            //問題があればデフォルトに戻す
            rotator.rotation = Quaternion.Euler(defaultRotation.x, defaultRotation.y, defaultRotation.z);
        }
        defaultRotation = rotator.rotation.eulerAngles;//問題がなければデフォルトを更新



        float slerpZ = Quaternion.RotateTowards(rotator.rotation, rot, rotSpeed.z * Time.deltaTime).eulerAngles.z;
        rotator.rotation = Quaternion.Euler(defaultRotation.x, defaultRotation.y, slerpZ);
        //回転後の角度が範囲を超えてないかチェック
        if (!InAnAngleCheck(rotator.localRotation.eulerAngles.z, localRotRangeZ))
        {
            //問題があればデフォルトに戻す
            rotator.rotation = Quaternion.Euler(defaultRotation.x, defaultRotation.y, defaultRotation.z);
        }

        //方策
        //一度回転させて超えたら戻すを,x,y,z軸ごとにやる
    }
    /// <summary>
    /// 0～360の角度を-180～180に変換する
    /// </summary>
    /// <param name="angle"></param>
    /// <returns></returns>
    float ConvertTo180Minus180(float angle)
    {
        return Mathf.Repeat(angle + 180, 360) - 180;
    }
    bool RangeCheck(float value, Vector2 range)
    {
        if (value <= range.x) return false;
        else if (value >= range.y) return false;
        else return true;
    }
    /// <summary>
    /// 0～360度の角度と -180～180の角度範囲指定(Vector2)を入力した場合，第一引数が,角度範囲に入っていたらTrueを返す
    /// </summary>
    /// <param name="value"></param>
    /// <param name="range"></param>
    /// <returns></returns>
    bool InAnAngleCheck(float value, Vector2 range)
    {
        return RangeCheck(ConvertTo180Minus180(value), range);
    }

}

```

# 工夫点
・ワールド座標のrotationでなく，localRotationを基準としたこと
親オブジェクトの回転状態が変わっても，追従先で可動範囲を維持できる． (親オブジェクトがY軸で90度回転した場合，このオブジェクトの可動範囲も追従してY90度回転してくれる)
・制限角度を-180度～180度で指定できるようにしたこと (UnityのlocalRotationは，0～360でありそのままでは使いづらい，以下の参考資料に感謝！)

# 参考
https://nekojara.city/unity-normalize-angle