角度の扱いづらさ
角度を普通にfloatやdoubleで扱おうとすると、次のような問題が発生して扱いづらいです。
- 度数法(degree)なのか弧度法(radian)なのかわからない
- 360°以上になると角度値は違うけど見た目は同じになってしまいややこしくなる
- 360°と0°では角度値は違えど見た目は変わらない
- 180°と-180°、45°と405°なども同様。見た目は同じだけど内部値が全く違うので意図しない動きになったりする
これらの問題を専用の変換メソッド等を用意して解決しても良いのですが、グローバルなメソッドにすれば角度以外のfloat値に対して適切ではないメソッドが呼べてしまうし、必要な部分だけにスコープを限定したメソッドとして定義すれば再利用性が失われてしまいます。
それに、「角度」のように何らかの意味のある数値として扱う場合は、プリミティブ型をそのまま代用するのではなく、専用のオブジェクトとして定義したほうが、専用の操作をオブジェクト内に隠蔽することができるため、可読性や保守性に富みます。
というわけで、角度を表すAngle構造体を作りました。
角度を表すAngle構造体
作成したAngle構造体の機能を紹介します。
今すぐコードが見たい方はこちら。
インスタンスの生成を行うファクトリ
度数法と弧度法を混同させないため、コンストラクタは隠蔽しています。
その代わりに各種ファクトリメソッドを用意しています。
Angle.FromDegreeファクトリメソッド
度数法の値からAngle構造体のインスタンスを取得します。
Angleangle=Angle.FromDegree(60);周回数を指定することもできます。
Angleangle=Angle.FromDegree(1,60);//360°+60°Angle.FromRadianファクトリメソッド
弧度法の値からAngle構造体のインスタンスを取得します。
Angleangle=Angle.FromRadian(UnityEngine.Mathf.PI);こちらも同様に、周回数を指定することもできます。
Angleangle=Angle.FromRadian(-2,UnityEngine.Mathf.PI);//-4π+πAngle.Zeroファクトリプロパティ
角度が0のAngle構造体のインスタンスを取得します。
Angleangle=Angle.Zero;//0°Angle.Roundファクトリプロパティ
角度が360°のAngle構造体のインスタンスを取得します。
Angleangle=Angle.Round;//360°各種変換を行うメソッド
各種変換メソッドを提供します。
イミュータブルな設計とするため、変換メソッドを実行しても元のインスタンスは変更されず、新しいインスタンスを返すようになっています。
Normalizeメソッド
角度を-180°<θ<=180°の範囲で正規化します。
例えば、225°の角度は-180°<θ<180°の間には入っていないため、-135°に正規化されます。
Angleangle=Angle.FromDegree(225).Normalize();//-135°同様に、-450°は-90°に正規化されます。
Angleangle=Angle.FromDegree(-450).Normalize();//-90°PositiveNormalizeメソッド
角度を0°<=θ<360°の範囲で正規化します。
例えば、-135°を0°<=θ<360°の範囲に正規化すると、225°となります。
Angleangle=Angle.FromDegree(-135).PositiveNormalize();//225°同様に、-450°は270°に正規化されます。
Angleangle=Angle.FromDegree(-450).PositiveNormalize();//270°Reverseメソッド
Reverseメソッドは、次のように見た目上の角度を変更せずに、方向のみを反転させます。
Angleangle=Angle.FromDegree(90).Reverse();//-270°Angleangle=Angle.FromDegree(-450).Reverse();//630°SignReverseメソッド
SignReverseメソッドは、角度の符号を単純に反転させます。
Angleangle=Angle.FromDegree(90).SignReverse();//-90°Absoluteメソッド
Absoluteメソッドは、SignReverseメソッドの正の方向への片道切符バージョンです。
Angleangle=Angle.FromDegree(90).Absolute();//90°Angleangle=Angle.FromDegree(-90).Absolute();//90°情報の取得を行うプロパティ
Angle構造体から角度情報を取得することができます。
TotalDegreeプロパティ
角度値を度数法で取得します。
floatdeg1=Angle.FromRadian(UnityEngine.Mathf.PI).TotalDegree;//180ffloatdeg2=Angle.FromDegree(-1,-90).TotalDegree;//-450fTotalRadianプロパティ
角度値を弧度法で取得します。
floatrad1=Angle.FromRadian(UnityEngine.Mathf.PI).TotalRadian;//πfloatrad2=Angle.FromDegree(-1,-90).TotalRadian;//-5π/4NormalizedDegreeプロパティ
Normalizeした角度値を度数法で取得します。
floatdeg1=Angle.FromRadian(UnityEngine.Mathf.PI).NormalizedDegree;//180ffloatdeg2=Angle.FromDegree(-1,-90).NormalizedDegree;//-90fNormalizedRadianプロパティ
NormalizedDegreeプロパティの弧度法バージョン。
PositiveNormalizedDegreeプロパティ
NormalizedDegreeプロパティのPositiveNormalizeしたバージョン。
PositiveNoramlizedRadianプロパティ
PositiveNormalizedDegreeプロパティの弧度法バージョン。
Lapプロパティ
角度が何周しているかを取得します。
intlap1=Angle.FromDegree(180).Lap;//0intlap2=Angle.FromDegree(360).Lap;//1intlap3=Angle.FromDegree(-730).Lap;//-2IsCircledプロパティ
角度が1周以上回っているかどうかを取得します。
boolcircled1=Angle.FromDegree(180).IsCircled;//falseboolcircled2=Angle.FromDegree(360).IsCircled;//trueboolcircled3=Angle.FromDegree(-730).IsCircled;//trueIsTrueCircleプロパティ
角度が360°の倍数かどうかを取得します。
booltrueCircle1=Angle.FromDegree(180).IsTrueCircled;//falsebooltrueCircle2=Angle.FromDegree(360).IsTrueCircled;//truebooltrueCircle3=Angle.FromDegree(-730).IsTrueCircled;//falseIsPositiveプロパティ
正の方向への角度かどうか取得します。
boolcircled1=Angle.FromDegree(180).IsPositive;//trueboolcircled2=Angle.FromDegree(360).IsPositive;//trueboolcircled3=Angle.FromDegree(-730).IsPositive;//false演算子
各種演算子をオーバーロードしており、プリミティブ型と同じように各種演算をすることができます。
+-演算子
角度を加算/減算します。
varplusAngle=Angle.FromDegree(120)+Angle.Round;//480°varminusAngle=Angle.FromDegree(45)-Angle.Round;//-315°*/演算子
角度を実数で乗算/除算します。
varmultiAngle=Angle.FromDegree(120)*-3;//-360°vardivideAngle=Angle.FromDegree(120)/4;//30°==,!=,<,<=,>,>=演算子
角度の大きさを比較します。
varb1=Angle.FromDegree(90)==Angle.FromDegree(90);//true varb2=Angle.FromDegree(90)!=Angle.FromDegree(450);//true varb3=Angle.FromDegree(90)<Angle.FromDegree(45);//falsevarb4=Angle.FromDegree(90)<=Angle.FromDegree(90);//true varb5=Angle.FromDegree(90)>Angle.FromDegree(45);//true varb6=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メソッド
周回数と残りの角度を返します。
stringstr=Angle.FromDegree(2,45).ToString();//2x + 45°ToStringをオーバーライドしていると、VisualStudioのデータヒントにも同様のフォーマットで表示されるので便利です。
Equals(object o)メソッド
IEquatable<Angle>インターフェイスのEqualsメソッドも実装したのですが、objectクラスのEqualsメソッドもオーバーライドしています。
理由としては、前述の通りEquals(object o)メソッドの既定の動作が全フィールドの等価性比較であること、しかもそれがリフレクションによる比較であるためです。Angle構造体は内部で持つ角度値を単純に比較するだけで良いので、リフレクションを使う既定の動作をオーバーライドして封印します。
ちなみに、VisualStudioでは==演算子をオーバーロードするとEqualsメソッドとGetHashCodeメソッドもオーバーライドしなさいと警告が出ます。
==演算子をオーバーロードしている→自前で等価性比較処理が書けている→既定のEqualsを使う必要がない→だったらオーバーライドしろ、ということですね。
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.
https://docs.microsoft.com/en-us/dotnet/api/system.object.gethashcode?view=net-5.0
これをオーバーライドしないと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
*/usingSystem;usingUnityEngine;namespaceUnityUtility{/// <summary>/// 角度/// </summary>publicreadonlystructAngle:IEquatable<Angle>,IComparable<Angle>{/// <summary>/// 正規化していない角度の累積値/// </summary>privatereadonlyfloatm_totalDegree;/// <summary>/// 角度を度数法で指定して、新規インスタンスを作成します。/// </summary>/// <param name="angle">度数法の角度</param>/// <exception cref="NotFiniteNumberException"/>privateAngle(floatangle)=>m_totalDegree=ArithmeticCheck(()=>angle);/// <summary>/// 周回数と角度を指定して、新規インスタンスを作成します。/// </summary>/// <param name="lap">周回数</param>/// <param name="angle">度数法の角度</param>/// <exception cref="NotFiniteNumberException"/>/// <exception cref="OverflowException"/>privateAngle(intlap,floatangle)=>m_totalDegree=ArithmeticCheck(()=>checked(360*lap+angle));/// <summary>/// 度数法の値を使用して新規インスタンスを取得します。/// </summary>/// <param name="degree">度数法の角度(°)</param>/// <returns></returns>/// <exception cref="NotFiniteNumberException"/>publicstaticAngleFromDegree(floatdegree)=>newAngle(degree);/// <summary>/// 周回数と角度を指定して、新規インスタンスを取得します。/// </summary>/// <param name="lap">周回数</param>/// <param name="degree">度数法の角度(°)</param>/// <returns></returns>/// <exception cref="NotFiniteNumberException"/>publicstaticAngleFromDegree(intlap,floatdegree)=>newAngle(lap,degree);/// <summary>/// 弧度法の値を使用して新規インスタンスを取得します。/// </summary>/// <param name="radian">弧度法の角度(rad)</param>/// <returns></returns>/// <exception cref="NotFiniteNumberException"/>publicstaticAngleFromRadian(floatradian)=>newAngle(RadToDeg(radian));/// <summary>/// 周回数と角度を指定して、新規インスタンスを取得します。/// </summary>/// <param name="lap">周回数</param>/// <param name="radian">弧度法の角度(rad)</param>/// <returns></returns>/// <exception cref="NotFiniteNumberException"/>publicstaticAngleFromRadian(intlap,floatradian)=>newAngle(lap,RadToDeg(radian));/// <summary>/// 角度0°の新規インスタンスを取得します。/// </summary>publicstaticAngleZero=>newAngle(0);/// <summary>/// 角度360°の新規インスタンスを取得します。/// </summary>publicstaticAngleRound=>newAngle(360);publicboolEquals(Angleother)=>m_totalDegree==other.m_totalDegree;publicoverrideintGetHashCode()=>-1748791360+m_totalDegree.GetHashCode();publicoverridestringToString()=>$"{Lap}x + {m_totalDegree-Lap*360}°";publicoverrideboolEquals(objectobj){if(objisAngleangle)returnEquals(angle);elsereturnfalse;}publicintCompareTo(Angleother)=>m_totalDegree.CompareTo(other.m_totalDegree);/// <summary>/// 正規化された角度(-180° < degree <= 180°)を取得します。/// </summary>/// <returns></returns>publicAngleNormalize()=>newAngle(NormalizedDegree);/// <summary>/// 正の値で正規化された角度(0° <= degree < 360°)を取得します。/// </summary>/// <returns></returns>publicAnglePositiveNormalize()=>newAngle(PositiveNormalizedDegree);/// <summary>/// 方向を反転させた角度を取得します。/// 例:90°→-270°, -450°→630°/// </summary>/// <returns></returns>publicAngleReverse(){//ゼロならゼロif(this==Zero)returnZero;//真円の場合は真逆にするif(IsTrueCircle)returnnewAngle(-Lap,0);if(IsCircled){//1周以上しているif(IsPositive){//360~returnnewAngle(-Lap,NormalizedDegree-360);}else{//~-360returnnewAngle(-Lap,NormalizedDegree+360);}}else{//1周していないif(IsPositive){//0~360returnnewAngle(m_totalDegree-360);}else{//-360~0returnnewAngle(m_totalDegree+360);}}}/// <summary>/// 符号を反転させた角度を取得します。/// </summary>/// <returns></returns>publicAngleSignReverse()=>newAngle(-m_totalDegree);/// <summary>/// 角度の絶対値を取得します。/// </summary>/// <returns></returns>publicAngleAbsolute()=>IsPositive?this:SignReverse();/// <summary>/// 正規化していない角度値を取得します。/// </summary>publicfloatTotalDegree=>m_totalDegree;/// <summary>/// 正規化していない角度値をラジアンで取得します。/// </summary>publicfloatTotalRadian=>DegToRad(TotalDegree);/// <summary>/// 正規化された角度値(-180 < angle <= 180)を取得します。/// </summary>publicfloatNormalizedDegree{get{floatlapExcludedDegree=m_totalDegree-(Lap*360);if(lapExcludedDegree>180)returnlapExcludedDegree-360;if(lapExcludedDegree<=-180)returnlapExcludedDegree+360;returnlapExcludedDegree;}}/// <summary>/// 正規化された角度値をラジアン(-π < rad < π)で取得します。/// </summary>publicfloatNormalizedRadian=>DegToRad(NormalizedDegree);/// <summary>/// 正規化された角度値(0 <= angle < 360)を取得します。/// </summary>publicfloatPositiveNormalizedDegree{get{varnormalized=NormalizedDegree;returnnormalized>=0?normalized:normalized+360;}}/// <summary>/// 正規化された角度値をラジアン(0 <= rad < 2π)で取得します。/// </summary>publicfloatPositiveNormalizedRadian=>DegToRad(PositiveNormalizedDegree);/// <summary>/// 角度が何周しているかを取得します。/// 例:370°→1周, -1085°→-3周/// </summary>publicintLap=>((int)m_totalDegree)/360;/// <summary>/// 1周以上しているかどうか(360°以上、もしくは-360°以下かどうか)を取得します。/// </summary>publicboolIsCircled=>Lap!=0;/// <summary>/// 360の倍数の角度であるかどうかを取得します。/// </summary>publicboolIsTrueCircle=>IsCircled&&m_totalDegree%360==0;/// <summary>/// 正の角度かどうかを取得します。/// </summary>publicboolIsPositive=>m_totalDegree>=0;/// <exception cref="NotFiniteNumberException"/>publicstaticAngleoperator+(Angleleft,Angleright)=>newAngle(ArithmeticCheck(()=>left.m_totalDegree+right.m_totalDegree));/// <exception cref="NotFiniteNumberException"/>publicstaticAngleoperator-(Angleleft,Angleright)=>newAngle(ArithmeticCheck(()=>left.m_totalDegree-right.m_totalDegree));/// <exception cref="NotFiniteNumberException"/>publicstaticAngleoperator*(Angleleft,floatright)=>newAngle(ArithmeticCheck(()=>left.m_totalDegree*right));/// <exception cref="NotFiniteNumberException"/>publicstaticAngleoperator/(Angleleft,floatright)=>newAngle(ArithmeticCheck(()=>left.m_totalDegree/right));publicstaticbooloperator==(Angleleft,Angleright)=>left.m_totalDegree==right.m_totalDegree;publicstaticbooloperator!=(Angleleft,Angleright)=>left.m_totalDegree!=right.m_totalDegree;publicstaticbooloperator>(Angleleft,Angleright)=>left.m_totalDegree>right.m_totalDegree;publicstaticbooloperator<(Angleleft,Angleright)=>left.m_totalDegree<right.m_totalDegree;publicstaticbooloperator>=(Angleleft,Angleright)=>left.m_totalDegree>=right.m_totalDegree;publicstaticbooloperator<=(Angleleft,Angleright)=>left.m_totalDegree<=right.m_totalDegree;/// <summary>/// 演算結果が数値であることを確かめる/// </summary>/// <param name="func"></param>/// <returns></returns>privatestaticfloatArithmeticCheck(Func<float>func){varans=func();if(float.IsInfinity(ans))thrownewNotFiniteNumberException("演算の結果、角度が正の無限大または負の無限大になりました");if(float.IsNaN(ans))thrownewNotFiniteNumberException("演算の結果、角度がNaNになりました");returnans;}privatestaticfloatRadToDeg(floatrad)=>rad*180/Mathf.PI;privatestaticfloatDegToRad(floatdeg)=>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の仕様に依存すると言えば言い訳になるかも。
usingSystem;usingUnityUtility;usingUnityEngine;usingXunit;usingSystem.Linq;usingSystem.Collections.Generic;namespaceAngleStructUnitTest{publicclassUnitTest1{[Fact]publicvoidCreateInstance(){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]publicvoidNormalize(){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]publicvoidPositiveNormalize(){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]publicvoidReverse(){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]publicvoidSignReverse(){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]publicvoidAbsolute(){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]publicvoidStandardMethods(){Angle.FromDegree(3,270).ToString().Is("3x + 270°");Angle.FromDegree(90).Equals(Angle.FromDegree(1,90).Normalize()).IsTrue();Angle.FromDegree(45).Equals(45).IsFalse();objecto=Angle.FromDegree(135);Angle.FromDegree(135).Equals(o).IsTrue();Angle.Round.Equals(null).IsFalse();}[Fact]publicvoidOperator(){(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]privatevoidGetter(){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]privatevoidCompare(){varcollection=newList<Angle>{Angle.FromRadian(MathF.PI/2),Angle.FromDegree(45),Angle.FromDegree(-90),Angle.Zero,Angle.Round};collection.OrderBy(a=>a).SequenceEqual(newList<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));}}}テスト結果
最後に
角度の扱いは簡単なようで地味に厄介でした。
その厄介な部分を全部隠蔽してきれいな部分だけを外部に公開したこのAngle構造体、少し使ってみたのですが普通に便利です。(自画自賛)
みなさんも自己責任でよかったらどうぞ。
以下GitHubでも公開しています。
https://github.com/yutorisan/UnityUtility/blob/develop/UnityUnility/Structs/Angle.cs









