【Unity】角度を0~360度や-180~180度の範囲に正規化する(循環させる)

こじゃら
こじゃら

キャラクターを回転させるプログラムを作っていて、1週したときに角度を360度から0度に戻すようにしたいの…

このは
このは

360度を超える角度、例えば370度は10度にして循環させたいということね。
簡単に実現できる方法があるわ!

本記事では、角度を0~360度の範囲に正規化する方法をご紹介します。1厳密な範囲は0度以上360度未満とし、最大値の360度は含まないものとします。
また、角度を-180~180度の範囲に正規化する方法もあわせて紹介します。2厳密な範囲は-180度以上180度未満とし、最大値の180度は含まないものとします。

ループのような重たい計算無しに実現できます。3計算量は入力値によらず定数オーダーとなります。

角度を0~360度の範囲に正規化する

Unity APIが使える環境においては、角度の循環はUnityのMathf.Repeat()メソッドを使うと簡単です。

Unity環境に依存しない、Mathf.Repeat()メソッドを使わない方法も紹介します。

Mathf.Repeat()メソッドを使う

次のコードで0~360度の範囲で正規化された角度が得られます。

var normalizedAngle = Mathf.Repeat(angle, 360);

Mathf.Repeat()メソッドは、次のように定義されています。

public static float Repeat(float t, float length);

tを0~lengthの間でループさせた値が返されます。
tは負数にも対応していて、例えばtが-10、lengthが360の場合、戻り値は350となります。

なお、Mathf.Repeat()メソッドは内部的にはMathf.Clamp()とMathf.Floor()メソッドを使用して循環処理を実現しているようです。

自前で計算する

Mathf.Repeat()メソッドを使わない場合、次のコードで0~360度に正規化された角度が求められます。

public static float GetNormalizedAngle(float angle)
{
    angle %= 360;

    if (angle < 0)
        angle += 360;

    return angle;
}
var normalizedAngle = GetNormalizedAngle(angle);

こちらも先の方法と同様に、angleが負の場合でも有効です。
angleが負数の場合は、剰余演算によって-360~0の値となるので、360を足して補正しています。

角度を-180~180度の範囲に正規化する

角度の1週を-180~180度の範囲で表現したい場合、例えば190度は-170度としたい場合、0~360度に丸める方法に一工夫加えることで実現できます。

こちらもMathf.Repeat()メソッドを使う方法と、自前で実装する方法を紹介します。

Mathf.Repeat()メソッドを使う

次のコードで-180~180度の範囲で正規化された角度が得られます。

var normalizedAngle180 = Mathf.Repeat(angle + 180, 360) - 180;

角度を180度で折り返しながら360度周期で循環させるため、Mathf.Repeat()メソッドに渡す角度に180度足して、得られた結果から180度引いて元の角度に戻しています。

自前で計算する

次のコードで-180~180度の範囲に正規化された角度が求められます。

public static float GetNormalizedAngle180(float angle)
{
    angle = (angle + 180) % 360 - 180;

    if (angle < -180)
        angle += 360;

    return angle;
}
var normalizedAngle180 = GetNormalizedAngle180(angle);

こちらも同じ要領で360度で循環させる前に角度に180度足して、循環させた後に180度引いています。

このは
このは

循環計算の前後で値を足し引きすることで循環範囲を自由にずらせるわ。
角度以外でも使えるテクニックだから、覚えておいて損は無いわ!

角度の正規化範囲の一般化

ここまで角度を0~360度、-180~180度に丸める(正規化する)方法を解説しました。

これら範囲の下限と上限は一般化可能です。
一般化できれば、共通のコードを使いまわすことが可能になります。

Mathf.Repeat()メソッドを使う

次のような実装になります。

public static float GetNormalizedAngle(float angle, float min, float max)
{
    return Mathf.Repeat(angle - min, max - min) + min;
}

第1引数に正規化前の角度、第2引数に循環範囲の下限、第3引数に循環範囲の上限を指定するメソッドを作りました。

使用例は次のようになります。

// 0° ≦ angle < 360°
var normalizedAngle = GetNormalizedAngle(angle, 0, 360);

// -180° ≦ angle < 180°
var normalizedAngle180 = GetNormalizedAngle(angle, -180, 180);

自前で計算する

Mathf.Repeat()メソッドを使わず自前で実装する場合も、同じ要領です。

public static float GetNormalizedAngle(float angle, float min, float max)
{
    var cycle = max - min;
    angle = (angle - min) % cycle + min;

    if (angle < min)
        angle += cycle;

    return angle;
}

使用例は先と同じため、割愛させていただきます。

ラジアン表記への対応

ここまで度数法表記での角度の正規化について解説してきました。
ラジアン表記4弧度法表記とも。1週を2πで表現します。の角度も度数法表記と同じ要領で正規化できます。

ラジアン表記の場合、範囲0~360度は0~2πに-180~180度は-π~πに対応します。

角度の正規化範囲の一般化ができたので、先のサンプルソースで上げたメソッドがそのまま使えます。

使用例は次のようになります。

// 0 ≦ angle < 2π
var normalizedAngle = GetNormalizedAngle(angle, 0, 2 * Mathf.PI);

// -π ≦ angle < π
var normalizedAnglePi = GetNormalizedAngle(angle, -Mathf.PI, Mathf.PI);

さいごに

今回はゲーム中で扱う角度を指定の範囲で正規化する方法について解説しました。
値の循環を駆使することが鍵となります。

制作するゲームに合わせて、適宜使いやすい形にメソッド化して活用するのが良いでしょう。

参考サイト