左右移動時にオブジェクトを傾ける方法

# 概要
本記事では、Unityで`Mathf.Clamp()`を用いて、特定の角度でオブジェクトを傾ける実装を解説します。

# 目的
本来Unityでは、オブジェクトの角度を`Transform`の`Rotation`で管理することが一般的ですが、それでは値を比較することによる範囲制御ができません。

例えば、以下のような実装を想像してください。
```c#:C#
if (transform.eulerAngle <= 30)
{
    //処理の内容を記述
}
```
上記の実装では、`30度以下`という条件を用いています。しかし、`50度`という角度は、`-310度`とも表すことができるので、`30度以下`のような条件を満たすと捉えることができるのです。

以上のように、角度制御の実装を行うためには、`Rotation`以外を用いた実装が必要です。本記事では、`Mathf.Clamp()`を用いて、この問題を解決します。

# Mathf.Clamp()とは？
`Mathf.Clamp()`とは、主に**値の範囲を制御するための関数**です。ここでは、範囲を制御するという説明に留めますが、より細かい説明を参照したい方は、[公式のリファレンス](https://docs.unity3d.com/ja/560/ScriptReference/Mathf.Clamp.html)等も参考にしてみてください。

さて、続いては引数として用いる値について、紹介していきます。`Mathf.Clamp()`を記述する際には、以下のように引数を用います。
```c#:C#
Mathf.Clamp(float value, float max, float min)
```
基本的に、この関数が出力する値は`value`の値そのものです。ただし、`min`から`max`の範囲内に収まっているかを判断し、範囲外の場合には最寄りの値に変化させます。

### 使用例
最後に、以下のような実装の場合どのようになるか考えましょう。
```c#:C#
using UnityEngine;

public class ClampSide : MonoBehaviour
{
    // 最小値
    public float min = -1;
    // 最大値
    public float max = 1;

    // 判定する値
    private float _value = 3;

    private void Start()
    {
        // x軸方向の移動範囲制限
        _value = Mathf.Clamp(_value, min, max);

        Debug.Log(_value);
    }
}
```

初め、　変数`_value`の数値は`3`で定義されています。`Clamp()`関数の引数に注目すると、最小値は変数`min`で`-1`、最大値は変数`max`で`1`と指定されています。この時、変数`_value`の値は指定した範囲を超えるので、関数の出力としては`1`が出力されることとなります。

したがって、コンソールビューには`1`が表示されることとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942780/01833707-ecfa-0c2d-e073-4228b0bf2d64.png)

# 実装の概要
ここからは、実装について順を追ってみていきましょう。実装の手順は、主に以下の**３点**
- **現在の角度を取得する変数を用意**
- **変数をClamp関数で制御しながら更新**
- **Transformへ変数を適用**

順を追って説明していきますね

### 現在の角度を取得する変数`_currentAngle`を用意
以下のように、角度を数値として保存するための変数を用意。中途半端な角度に備え、`float`で宣言。
```c#:C#
private float _currentAngle;
```

### `_currantAngle`を`Clamp()`関数で制御しながら更新
`Update()`関数内で、フレームごとに`_currentAngle`の値を更新。ここでは、フレームごとに`rotateSpeed * Time.deltaTime`増えるように実装。

制限をするために、`maxAngle`を設定。これによって、`_currentAngle`は`maxAngle`**以上**にも`-maxAngle`**以下**にもならない。
```c#:C#
_currentAngle = Mathf.Clamp(_currentAngle + rotateSpeed * Time.deltaTime, -maxAngle, maxAngle);
```
### `Transform`へ`_currentAngle`の値を適用
最後に、更新した`_currentAngle`を`Transform`に適用すれば処理は完了。
```c#:C#
transform.rotation = Quaternion.Euler(0, 0, _currentAngle);
```

# 実際の実装
### 挙動
![Screen Recording 2025-01-08 at 17.12.39.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942780/e822bb4f-47d7-907d-55ff-83eaacfeeba9.gif)

### スクリプト
Playerに対してスクリプトを作成し、以下のようなコードで実装した。
```c#:C#
using UnityEngine;

public class player : MonoBehaviour
{
    public float moveSpeed = 2.0f;
    public float rotateSpeed = 200.0f; // 回転スピード
    public float maxAngle = 20.0f; // 最大傾き角度

    private float _currentAngle = 0.0f; //現在の角度を取得する変数

    // Update is called once per frame
    void Update()
    {
        // 左キー（A）を押している間
        if (Input.GetKey(KeyCode.A))
        {
            // 左に移動
            transform.position += new Vector3(-moveSpeed, 0, 0);

            // 傾きを計算して制限
            _currentAngle = Mathf.Clamp(_currentAngle + rotateSpeed * Time.deltaTime, -maxAngle, maxAngle);
            //実際に傾きを適用
            transform.rotation = Quaternion.Euler(0, 0, _currentAngle);
        }else
        {
            // 傾きを緩やかに元に戻す
            _currentAngle = Mathf.Lerp(_currentAngle, 0, Time.deltaTime * 5);
            //実際に傾きを適用
            transform.rotation = Quaternion.Euler(0, 0, _currentAngle);
        }

        // 右キー（D）を押している間
        if (Input.GetKey(KeyCode.D))
        {
            // 右に移動
            transform.position += new Vector3(moveSpeed, 0, 0);

            // 傾きを計算して制限
            _currentAngle = Mathf.Clamp(_currentAngle - rotateSpeed * Time.deltaTime, -maxAngle, maxAngle);
            //実際に傾きを適用
            transform.rotation = Quaternion.Euler(0, 0, _currentAngle);
        }
        else
        {
            // 傾きを緩やかに元に戻す
            _currentAngle = Mathf.Lerp(_currentAngle, 0, Time.deltaTime * 5);
            //実際に傾きを適用
            transform.rotation = Quaternion.Euler(0, 0, _currentAngle);
        }
    }
}
```