21
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Unity】角度を扱うときはfloatじゃなくて専用のAngle構造体を用意すると捗る

Last updated at Posted at 2021-01-20

##角度の扱いづらさ
角度を普通にfloatやdoubleで扱おうとすると、次のような問題が発生して扱いづらいです。

  • __度数法(degree)__なのか__弧度法(radian)__なのかわからない
  • 360°以上になると__角度値は違うけど見た目は同じ__になってしまいややこしくなる
    • 360°と0°では角度値は違えど見た目は変わらない
    • 180°と-180°、45°と405°なども同様。見た目は同じだけど内部値が全く違うので意図しない動きになったりする

これらの問題を専用の変換メソッド等を用意して解決しても良いのですが、__グローバルなメソッドにすれば角度以外のfloat値に対して適切ではないメソッドが呼べてしまう__し、必要な部分だけにスコープを限定したメソッドとして定義すれば再利用性が失われてしまいます

それに、「角度」のように何らかの意味のある数値として扱う場合は、プリミティブ型をそのまま代用するのではなく、専用のオブジェクトとして定義したほうが、専用の操作をオブジェクト内に隠蔽することができるため、可読性や保守性に富みます。

というわけで、角度を表すAngle構造体を作りました。

##角度を表すAngle構造体

作成したAngle構造体の機能を紹介します。
今すぐコードが見たい方はこちら

###インスタンスの生成を行うファクトリ

度数法と弧度法を混同させないため、コンストラクタは隠蔽しています。
その代わりに各種ファクトリメソッドを用意しています。

####Angle.FromDegreeファクトリメソッド
度数法の値からAngle構造体のインスタンスを取得します。

Angle angle = Angle.FromDegree(60);

周回数を指定することもできます。

Angle angle = Angle.FromDegree(1, 60); //360°+60°

####Angle.FromRadianファクトリメソッド
弧度法の値からAngle構造体のインスタンスを取得します。

Angle angle = Angle.FromRadian(UnityEngine.Mathf.PI);

こちらも同様に、周回数を指定することもできます。

Angle angle = Angle.FromRadian(-2, UnityEngine.Mathf.PI); //-4π+π

####Angle.Zeroファクトリプロパティ
角度が0のAngle構造体のインスタンスを取得します。

Angle angle = Angle.Zero; //0°

####Angle.Roundファクトリプロパティ
角度が360°のAngle構造体のインスタンスを取得します。

Angle angle = Angle.Round; //360°

###各種変換を行うメソッド

各種変換メソッドを提供します。
イミュータブルな設計とするため、変換メソッドを実行しても元のインスタンスは変更されず、新しいインスタンスを返すようになっています。

####Normalizeメソッド
角度を-180°<θ<=180°の範囲で正規化します。

Normalize.png

例えば、225°の角度は-180°<θ<180°の間には入っていないため、-135°に正規化されます。

Angle angle = Angle.FromDegree(225).Normalize(); //-135°

Normalize1.png

同様に、-450°は-90°に正規化されます。

Angle angle = Angle.FromDegree(-450).Normalize(); //-90°

Normalize2.png

####PositiveNormalizeメソッド
角度を0°<=θ<360°の範囲で正規化します。

PositiveNormalize.png

例えば、-135°を0°<=θ<360°の範囲に正規化すると、225°となります。

Angle angle = Angle.FromDegree(-135).PositiveNormalize(); //225°

PositiveNormalize1.png

同様に、-450°は270°に正規化されます。

Angle angle = Angle.FromDegree(-450).PositiveNormalize(); //270°

PositiveNormalize2.png

####Reverseメソッド
Reverseメソッドは、次のように見た目上の角度を変更せずに、方向のみを反転させます。

Angle angle = Angle.FromDegree(90).Reverse(); //-270°

Reverse1.png

Angle angle = Angle.FromDegree(-450).Reverse(); //630°

Reverse2.png

####SignReverseメソッド
SignReverseメソッドは、角度の符号を単純に反転させます。

Angle angle = Angle.FromDegree(90).SignReverse(); //-90°

SignReverse.png

####Absoluteメソッド
Absoluteメソッドは、SignReverseメソッドの正の方向への片道切符バージョンです。

Angle angle = Angle.FromDegree(90).Absolute(); //90°
Angle angle = Angle.FromDegree(-90).Absolute(); //90°

###情報の取得を行うプロパティ
Angle構造体から角度情報を取得することができます。

####TotalDegreeプロパティ
角度値を度数法で取得します。

float deg1 = Angle.FromRadian(UnityEngine.Mathf.PI).TotalDegree; //180f
float deg2 = Angle.FromDegree(-1, -90).TotalDegree; //-450f

####TotalRadianプロパティ
角度値を弧度法で取得します。

float rad1 = Angle.FromRadian(UnityEngine.Mathf.PI).TotalRadian; //π
float rad2 = Angle.FromDegree(-1, -90).TotalRadian; //-5π/4

####NormalizedDegreeプロパティ
Normalizeした角度値を度数法で取得します。

float deg1 = Angle.FromRadian(UnityEngine.Mathf.PI).NormalizedDegree; //180f
float deg2 = Angle.FromDegree(-1, -90).NormalizedDegree; //-90f

####NormalizedRadianプロパティ
NormalizedDegreeプロパティの弧度法バージョン。

####PositiveNormalizedDegreeプロパティ
NormalizedDegreeプロパティのPositiveNormalizeしたバージョン。

####PositiveNoramlizedRadianプロパティ
PositiveNormalizedDegreeプロパティの弧度法バージョン。

####Lapプロパティ
角度が何周しているかを取得します。

int lap1 = Angle.FromDegree(180).Lap; //0
int lap2 = Angle.FromDegree(360).Lap; //1
int lap3 = Angle.FromDegree(-730).Lap; //-2

####IsCircledプロパティ
角度が1周以上回っているかどうかを取得します。

bool circled1 = Angle.FromDegree(180).IsCircled; //false
bool circled2 = Angle.FromDegree(360).IsCircled; //true
bool circled3 = Angle.FromDegree(-730).IsCircled; //true

####IsTrueCircleプロパティ
角度が360°の倍数かどうかを取得します。

bool trueCircle1 = Angle.FromDegree(180).IsTrueCircled; //false
bool trueCircle2 = Angle.FromDegree(360).IsTrueCircled; //true
bool trueCircle3 = Angle.FromDegree(-730).IsTrueCircled; //false

####IsPositiveプロパティ
正の方向への角度かどうか取得します。

bool circled1 = Angle.FromDegree(180).IsPositive; //true
bool circled2 = Angle.FromDegree(360).IsPositive; //true
bool circled3 = Angle.FromDegree(-730).IsPositive; //false

###演算子
各種演算子をオーバーロードしており、プリミティブ型と同じように各種演算をすることができます。

####+-演算子
角度を加算/減算します。

var plusAngle = Angle.FromDegree(120) + Angle.Round; //480°
var minusAngle = Angle.FromDegree(45) - Angle.Round; //-315°

####*/演算子
角度を実数で乗算/除算します。

var multiAngle = Angle.FromDegree(120) * -3; //-360°
var divideAngle = Angle.FromDegree(120) / 4; //30°

####==,!=,<,<=,>,>=演算子
角度の大きさを比較します。

var b1 = Angle.FromDegree(90) == Angle.FromDegree(90); //true 
var b2 = Angle.FromDegree(90) != Angle.FromDegree(450); //true 
var b3 = Angle.FromDegree(90) < Angle.FromDegree(45); //false
var b4 = Angle.FromDegree(90) <= Angle.FromDegree(90); //true 
var b5 = Angle.FromDegree(90) > Angle.FromDegree(45); //true 
var b6 = Angle.FromDegree(90) >= Angle.FromDegree(45); //true 

###インターフェイス実装
次の2つのインターフェイスを実装しています。

####IEquatable<Angle>インターフェイス

等価性比較のためにIEquatable<Angle>インターフェイスを実装しています。

==演算子があるのにわざわざIEquatable<Angle>インターフェイスを実装するメリットは__この記事__が参考になりますが、要約すると次のようになります。

  • IEquatable<Angle>がないとobject版のEquals(object obj)が呼ばれることになる。値型をobjectにキャストすると__ボックス化__が発生するので__オーバーヘッドがかかってしまう__。
  • 構造体のobject版のEquals(object obj)メソッドの既定の動作は、すべてのフィールドの等価性を比較すること。これが==演算子の比較内容と異なると、==演算子の結果とEqualsメソッドの結果に相違が生じ、混乱を招いてしまう
    • しかも、等価性比較はリフレクションを用いて行われる模様なので、速度が圧倒的に遅い

このような理由から、構造体の場合は基本的にIEquatable<T>インターフェイスを実装したほうが良いようです。

####IComparable<Angle>インターフェイス

LINQのOrderByメソッドやMax,Minメソッドを利用できるようにするためにIComparable<Angle>インターフェイスを実装しています。

###オーバーライド

object型に定義されている次のメソッドをオーバーライドしています。

####ToStringメソッド
周回数と残りの角度を返します。

string str = Angle.FromDegree(2, 45).ToString(); //2x + 45°

ToStringをオーバーライドしていると、VisualStudioのデータヒントにも同様のフォーマットで表示されるので便利です。
image.png

####Equals(object o)メソッド

IEquatable<Angle>インターフェイスのEqualsメソッドも実装したのですが、objectクラスのEqualsメソッドもオーバーライドしています。
理由としては、前述の通りEquals(object o)メソッドの既定の動作が全フィールドの等価性比較であること、しかもそれがリフレクションによる比較であるためです。
Angle構造体は内部で持つ角度値を単純に比較するだけで良いので、リフレクションを使う既定の動作をオーバーライドして封印します。

ちなみに、VisualStudioでは==演算子をオーバーロードするとEqualsメソッドとGetHashCodeメソッドもオーバーライドしなさいと警告が出ます。
==演算子をオーバーロードしている→自前で等価性比較処理が書けている→既定のEqualsを使う必要がない→だったらオーバーライドしろ、ということですね。
image.png

####GetHashCodeメソッド
GetHashCodeメソッドはDictionaryのキーとして使うときに使われるようです。

A hash code is a numeric value that is used to insert and identify an object in a hash-based collection such as the Dictionary class, the Hashtable class, or a type derived from the DictionaryBase class. The GetHashCode method provides this hash code for algorithms that need quick checks of object equality.

これをオーバーライドしないとDictionary のキーとして使ったときに正しく動作しない可能性がある模様。

VisualStudioがオーバーライドをおすすめしてくれたのでオーバーライドします。
しかも有能VisualStudioが中身も自動で実装してくれます。

ぶっちゃけあまり詳しくない。

##ソースコード本体

上記機能を備えたAngle構造体のソースコードです。
そのままコピペで使えます。

[追記]
編集リクエスト頂きライセンスを明記しました。
改変等自由ですので是非ご利用ください。


/*
Angle.cs

Copyright (c) 2021 yutorisan

This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
*/

using System;
using UnityEngine;

namespace UnityUtility
{
    /// <summary>
    /// 角度
    /// </summary>
    public readonly struct Angle : IEquatable<Angle>, IComparable<Angle>
    {
        /// <summary>
        /// 正規化していない角度の累積値
        /// </summary>
        private readonly float m_totalDegree;

        /// <summary>
        /// 角度を度数法で指定して、新規インスタンスを作成します。
        /// </summary>
        /// <param name="angle">度数法の角度</param>
        /// <exception cref="NotFiniteNumberException"/>
        private Angle(float angle) => m_totalDegree = ArithmeticCheck(() => angle);
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを作成します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="angle">度数法の角度</param>
        /// <exception cref="NotFiniteNumberException"/>
        /// <exception cref="OverflowException"/>
        private Angle(int lap, float angle) => m_totalDegree = ArithmeticCheck(() => checked(360 * lap + angle));

        /// <summary>
        /// 度数法の値を使用して新規インスタンスを取得します。
        /// </summary>
        /// <param name="degree">度数法の角度(°)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromDegree(float degree) => new Angle(degree);
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを取得します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="degree">度数法の角度(°)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromDegree(int lap, float degree) => new Angle(lap, degree);
        /// <summary>
        /// 弧度法の値を使用して新規インスタンスを取得します。
        /// </summary>
        /// <param name="radian">弧度法の角度(rad)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromRadian(float radian) => new Angle(RadToDeg(radian));
        /// <summary>
        /// 周回数と角度を指定して、新規インスタンスを取得します。
        /// </summary>
        /// <param name="lap">周回数</param>
        /// <param name="radian">弧度法の角度(rad)</param>
        /// <returns></returns>
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle FromRadian(int lap, float radian) => new Angle(lap, RadToDeg(radian));
        /// <summary>
        /// 角度0°の新規インスタンスを取得します。
        /// </summary>
        public static Angle Zero => new Angle(0);
        /// <summary>
        /// 角度360°の新規インスタンスを取得します。
        /// </summary>
        public static Angle Round => new Angle(360);

        public bool Equals(Angle other) => m_totalDegree == other.m_totalDegree;

        public override int GetHashCode() => -1748791360 + m_totalDegree.GetHashCode();

        public override string ToString() => $"{Lap}x + {m_totalDegree - Lap * 360}°";

        public override bool Equals(object obj)
        {
            if (obj is Angle angle) return Equals(angle);
            else return false;
        }

        public int CompareTo(Angle other) => m_totalDegree.CompareTo(other.m_totalDegree);

        /// <summary>
        /// 正規化された角度(-180° &lt; degree &lt;= 180°)を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle Normalize() => new Angle(NormalizedDegree);

        /// <summary>
        /// 正の値で正規化された角度(0° &lt;= degree &lt; 360°)を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle PositiveNormalize() => new Angle(PositiveNormalizedDegree);

        /// <summary>
        /// 方向を反転させた角度を取得します。
        /// 例:90°→-270°, -450°→630°
        /// </summary>
        /// <returns></returns>
        public Angle Reverse()
        {
            //ゼロならゼロ
            if (this == Zero) return Zero;
            //真円の場合は真逆にする
            if (IsTrueCircle) return new Angle(-Lap, 0);
            if (IsCircled)
            { //1周以上している
                if (IsPositive)
                { //360~
                    return new Angle(-Lap, NormalizedDegree - 360);
                }
                else
                { //~-360
                    return new Angle(-Lap, NormalizedDegree + 360);
                }
            }
            else
            { //1周していない
                if (IsPositive)
                { //0~360
                    return new Angle(m_totalDegree - 360);
                }
                else
                { //-360~0
                    return new Angle(m_totalDegree + 360);
                }
            }
        }
        /// <summary>
        /// 符号を反転させた角度を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle SignReverse() => new Angle(-m_totalDegree);
        /// <summary>
        /// 角度の絶対値を取得します。
        /// </summary>
        /// <returns></returns>
        public Angle Absolute() => IsPositive ? this : SignReverse();

        /// <summary>
        /// 正規化していない角度値を取得します。
        /// </summary>
        public float TotalDegree => m_totalDegree;
        /// <summary>
        /// 正規化していない角度値をラジアンで取得します。
        /// </summary>
        public float TotalRadian => DegToRad(TotalDegree);
        /// <summary>
        /// 正規化された角度値(-180 &lt; angle &lt;= 180)を取得します。
        /// </summary>
        public float NormalizedDegree
        {
            get
            {
                float lapExcludedDegree = m_totalDegree - (Lap * 360);
                if (lapExcludedDegree > 180) return lapExcludedDegree - 360;
                if (lapExcludedDegree <= -180) return lapExcludedDegree + 360;
                return lapExcludedDegree;
            }
        }
        /// <summary>
        /// 正規化された角度値をラジアン(-π &lt; rad &lt; π)で取得します。
        /// </summary>
        public float NormalizedRadian => DegToRad(NormalizedDegree);
        /// <summary>
        /// 正規化された角度値(0 &lt;= angle &lt; 360)を取得します。
        /// </summary>
        public float PositiveNormalizedDegree
        {
            get
            {
                var normalized = NormalizedDegree;
                return normalized >= 0 ? normalized : normalized + 360;
            }
        }

        /// <summary>
        /// 正規化された角度値をラジアン(0 &lt;= rad &lt; 2π)で取得します。
        /// </summary>
        public float PositiveNormalizedRadian => DegToRad(PositiveNormalizedDegree);
        /// <summary>
        /// 角度が何周しているかを取得します。
        /// 例:370°→1周, -1085°→-3周
        /// </summary>
        public int Lap => ((int)m_totalDegree) / 360;
        /// <summary>
        /// 1周以上しているかどうか(360°以上、もしくは-360°以下かどうか)を取得します。
        /// </summary>
        public bool IsCircled => Lap != 0;
        /// <summary>
        /// 360の倍数の角度であるかどうかを取得します。
        /// </summary>
        public bool IsTrueCircle => IsCircled && m_totalDegree % 360 == 0;
        /// <summary>
        /// 正の角度かどうかを取得します。
        /// </summary>
        public bool IsPositive => m_totalDegree >= 0;

        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator +(Angle left, Angle right) => new Angle(ArithmeticCheck(() => left.m_totalDegree + right.m_totalDegree));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator -(Angle left, Angle right) => new Angle(ArithmeticCheck(() => left.m_totalDegree - right.m_totalDegree));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator *(Angle left, float right) => new Angle(ArithmeticCheck(() => left.m_totalDegree * right));
        /// <exception cref="NotFiniteNumberException"/>
        public static Angle operator /(Angle left, float right) => new Angle(ArithmeticCheck(() => left.m_totalDegree / right));
        public static bool operator ==(Angle left, Angle right) => left.m_totalDegree == right.m_totalDegree;
        public static bool operator !=(Angle left, Angle right) => left.m_totalDegree != right.m_totalDegree;
        public static bool operator >(Angle left, Angle right) => left.m_totalDegree > right.m_totalDegree;
        public static bool operator <(Angle left, Angle right) => left.m_totalDegree < right.m_totalDegree;
        public static bool operator >=(Angle left, Angle right) => left.m_totalDegree >= right.m_totalDegree;
        public static bool operator <=(Angle left, Angle right) => left.m_totalDegree <= right.m_totalDegree;

        /// <summary>
        /// 演算結果が数値であることを確かめる
        /// </summary>
        /// <param name="func"></param>
        /// <returns></returns>
        private static float ArithmeticCheck(Func<float> func)
        {
            var ans = func();
            if (float.IsInfinity(ans)) throw new NotFiniteNumberException("演算の結果、角度が正の無限大または負の無限大になりました");
            if (float.IsNaN(ans)) throw new NotFiniteNumberException("演算の結果、角度がNaNになりました");
            return ans;
        }
        private static float RadToDeg(float rad) => rad * 180 / Mathf.PI;
        private static float DegToRad(float deg) => deg * (Mathf.PI / 180);
    }
}


##単体テスト

Angle構造体はこれから長く使い続けられそうなので、きちんと単体テストを行いました。
次の記事を参考にしながら、xUnit と __Chainning Assertion__を使ってテストコードを記述しました。
xUnit.net でユニットテストを始める

以下がテストコードです。
IComparable<Angle>インターフェイスを実装したことにより、OrderByによる並べ替えも成功しています。

例外処理も可能な限りチェックしたかったのですが、Angle構造体は内部に単精度浮動小数点型(float)を使っているので、どうしても最大値・最小値の扱いが難しいです(例えばfloat.MaxValue+1000などとしても、floatの有効数字は7桁なので1000が丸め込まれてしまう)。
そのため明確にNaNもしくはInfinityまたはNegativeInfinityになった場合のみ例外の発生をチェックしました。
実際、3.40282347×10^38°なんて角度を使うことはほぼありえないため目をつむります。__floatの仕様に依存する__と言えば言い訳になるかも。


using System;
using UnityUtility;
using UnityEngine;
using Xunit;
using System.Linq;
using System.Collections.Generic;

namespace AngleStructUnitTest
{
    public class UnitTest1
    {
        [Fact]
        public void CreateInstance()
        {
            Angle.FromDegree(180).Is(Angle.FromRadian(Mathf.PI));
            Angle.FromRadian(-4 * Mathf.PI).Is(Angle.FromDegree(-720));
            Angle.FromDegree(-1, -180).Is(Angle.FromDegree(-360 + -180));
            Angle.FromRadian(-10, Mathf.PI).Is(Angle.FromDegree(-3600 + 180));
            Angle.Zero.Is(Angle.FromRadian(0));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.NaN));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.NegativeInfinity));
            Assert.ThrowsAny<ArithmeticException>(() => Angle.FromDegree(float.PositiveInfinity));
        }

        [Fact]
        public void Normalize()
        {
            Angle.Zero.Normalize().Is(Angle.Zero);
            Angle.FromDegree(180).Normalize().Is(Angle.FromDegree(180));
            Angle.FromDegree(270).Normalize().Is(Angle.FromDegree(-90));
            Angle.FromDegree(360).Normalize().Is(Angle.FromDegree(0));
            Angle.FromDegree(360 * 4 + 20).Normalize().Is(Angle.FromDegree(20));
            Angle.FromDegree(-360 * 80 + 20).Normalize().Is(Angle.FromDegree(20));
        }

        [Fact]
        public void PositiveNormalize()
        {
            Angle.FromDegree(0).PositiveNormalize().Is(Angle.Zero);
            Angle.FromDegree(180).PositiveNormalize().Is(Angle.FromDegree(180));
            Angle.FromDegree(270).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(360).PositiveNormalize().Is(Angle.FromDegree(0));
            Angle.FromDegree(380).PositiveNormalize().Is(Angle.FromDegree(20));
            Angle.FromDegree(-90).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(-360 - 90).PositiveNormalize().Is(Angle.FromDegree(270));
            Angle.FromDegree(-360 * 5 + 90).PositiveNormalize().Is(Angle.FromDegree(90));
        }

        [Fact]
        public void Reverse()
        {
            Angle.Zero.Reverse().Is(Angle.Zero);
            Angle.FromDegree(45).Reverse().Is(Angle.FromDegree(-315));
            Angle.FromDegree(-90).Reverse().Is(Angle.FromDegree(270));
            Angle.FromDegree(180).Reverse().Is(Angle.FromDegree(-180));
            Angle.FromDegree(360).Reverse().Is(Angle.FromDegree(-360));
            Angle.FromDegree(359).Reverse().Is(Angle.FromDegree(-1));
            Angle.FromDegree(361).Reverse().Is(Angle.FromDegree(-1, -359));
            Angle.FromDegree(-450).Reverse().Is(Angle.FromDegree(360 + 270));
            Angle.FromDegree(2, 90).Reverse().Is(Angle.FromDegree(-2, -270));
        }

        [Fact]
        public void SignReverse()
        {
            Angle.Zero.SignReverse().Is(Angle.Zero);
            Angle.FromDegree(60).SignReverse().Is(Angle.FromDegree(-60));
            Angle.FromDegree(-120).SignReverse().Is(Angle.FromDegree(120));
            Angle.FromDegree(-2, 60).SignReverse().Is(Angle.FromDegree(2, -60));
        }

        [Fact]
        public void Absolute()
        {
            Angle.Zero.Absolute().Is(Angle.Zero);
            Angle.FromDegree(60).Absolute().Is(Angle.FromDegree(60));
            Angle.FromDegree(-120).Absolute().Is(Angle.FromDegree(120));
            Angle.FromDegree(-4, 60).Absolute().Is(Angle.FromDegree(4, -60));
            Angle.FromDegree(4, -60).Absolute().Is(Angle.FromDegree(4, -60));
        }

        [Fact]
        public void StandardMethods()
        {
            Angle.FromDegree(3, 270).ToString().Is("3x + 270°");
            Angle.FromDegree(90).Equals(Angle.FromDegree(1, 90).Normalize()).IsTrue();
            Angle.FromDegree(45).Equals(45).IsFalse();

            object o = Angle.FromDegree(135);
            Angle.FromDegree(135).Equals(o).IsTrue();
            Angle.Round.Equals(null).IsFalse();
        }

        [Fact]
        public void Operator()
        {
            (Angle.FromDegree(45) + Angle.FromDegree(90)).Is(Angle.FromDegree(135));
            (Angle.FromDegree(30) - Angle.FromDegree(90)).Is(Angle.FromDegree(-60));
            (Angle.FromDegree(90) * 4.5f).Is(Angle.FromDegree(90 * 4.5f));
            (Angle.FromDegree(45) * -1).Reverse().Is(Angle.FromDegree(315));
            (Angle.FromDegree(90) * 0).Is(Angle.Zero);
            (Angle.FromDegree(4, 90) / 2).Is(Angle.FromDegree(2, 45));
            (Angle.FromDegree(5, 10).Normalize() == Angle.FromDegree(10)).IsTrue();
            (Angle.FromDegree(-5, 10).PositiveNormalize() == Angle.FromDegree(10)).IsTrue();
            (Angle.FromDegree(90).Reverse() != Angle.FromDegree(-90)).IsTrue();
            (Angle.FromDegree(45) > Angle.FromDegree(90)).IsFalse();
            (Angle.FromDegree(-1, 0) > Angle.FromDegree(-360)).IsFalse();
            (Angle.FromDegree(-1, 0) >= Angle.FromDegree(-360)).IsTrue();
            (Angle.FromDegree(-1, 20) < Angle.FromDegree(-360)).IsFalse();
            (Angle.FromDegree(1, 45).Normalize() <= Angle.FromDegree(45)).IsTrue();
            (Angle.FromDegree(1, 45).Normalize() <= Angle.FromDegree(90)).IsTrue();

            Assert.Throws<NotFiniteNumberException>(() => Angle.FromDegree(float.MaxValue) + Angle.FromDegree(float.MaxValue));
            Assert.Throws<NotFiniteNumberException>(() => Angle.Zero - Angle.FromDegree(float.MaxValue) * 2);
            Assert.Throws<NotFiniteNumberException>(() => Angle.Round / 0);

        }

        [Fact]
        private void Getter()
        {
            Angle.FromDegree(2, 90).TotalDegree.Is(810);
            Angle.FromDegree(2, 90).Normalize().TotalRadian.Is(Mathf.PI / 2);
            Angle.Zero.NormalizedDegree.Is(0);
            Angle.FromDegree(2, 90).NormalizedDegree.Is(90);
            Angle.FromDegree(-1, -90).NormalizedRadian.Is(-1 * Mathf.PI / 2);
            Angle.Zero.PositiveNormalizedDegree.Is(0);
            Angle.FromDegree(1, 90).Reverse().PositiveNormalizedDegree.Is(90);
            Angle.FromDegree(-2, 90).PositiveNormalizedRadian.Is(Mathf.PI / 2);
            Angle.FromDegree(3, 90).Lap.Is(3);
            Angle.FromDegree(360).Lap.Is(1);
            Angle.FromDegree(-180).Lap.Is(0);
            Angle.FromDegree(-750).Lap.Is(-2);
            Angle.FromDegree(-360).IsCircled.IsTrue();
            Angle.FromDegree(1, -1).IsCircled.IsFalse();
            Angle.FromDegree(1).IsPositive.IsTrue();
            (Angle.FromDegree(1) * -1).IsPositive.IsFalse();
            Angle.Round.IsTrueCircle.IsTrue();
            Angle.Zero.IsTrueCircle.IsFalse();
            Angle.FromDegree(180).IsTrueCircle.IsFalse();
            Angle.FromDegree(-720).IsTrueCircle.IsTrue();
        }

        [Fact]
        private void Compare()
        {
            var collection = new List<Angle>
            {
                Angle.FromRadian(MathF.PI / 2),
                Angle.FromDegree(45),
                Angle.FromDegree(-90),
                Angle.Zero,
                Angle.Round
            };

            collection.OrderBy(a => a).SequenceEqual(new List<Angle>
            {
                Angle.FromDegree(-90),
                Angle.Zero,
                Angle.FromDegree(45),
                Angle.FromRadian(MathF.PI / 2),
                Angle.Round
            }).IsTrue();
            collection.Max().Is(Angle.Round);
            collection.Min().Is(Angle.FromDegree(-90));
        }
    }
}

###テスト結果
すべて合格。
image.png

##最後に
角度の扱いは簡単なようで地味に厄介でした。
その厄介な部分を全部隠蔽してきれいな部分だけを外部に公開したこのAngle構造体、少し使ってみたのですが普通に便利です。(自画自賛)
みなさんも自己責任でよかったらどうぞ。

以下GitHubでも公開しています。

##2020/2/7追記:DOTweenでTweenできるようにしました
DOTweenにAngle構造体を投入してTweenできるようにするDOTweenのPluginを作成しました。
下記記事を参照ください。

Plugin本体は下記GitHubを参照ください。

21
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?