##角度の扱いづらさ
角度を普通に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°の範囲で正規化します。
例えば、225°の角度は-180°<θ<180°の間には入っていないため、-135°に正規化されます。
Angle angle = Angle.FromDegree(225).Normalize(); //-135°
同様に、-450°は-90°に正規化されます。
Angle angle = Angle.FromDegree(-450).Normalize(); //-90°
####PositiveNormalize
メソッド
角度を0°<=θ<360°の範囲で正規化します。
例えば、-135°を0°<=θ<360°の範囲に正規化すると、225°となります。
Angle angle = Angle.FromDegree(-135).PositiveNormalize(); //225°
同様に、-450°は270°に正規化されます。
Angle angle = Angle.FromDegree(-450).PositiveNormalize(); //270°
####Reverse
メソッド
Reverse
メソッドは、次のように見た目上の角度を変更せずに、方向のみを反転させます。
Angle angle = Angle.FromDegree(90).Reverse(); //-270°
Angle angle = Angle.FromDegree(-450).Reverse(); //630°
####SignReverse
メソッド
SignReverse
メソッドは、角度の符号を単純に反転させます。
Angle angle = Angle.FromDegree(90).SignReverse(); //-90°
####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のデータヒントにも同様のフォーマットで表示されるので便利です。
####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.
これをオーバーライドしないと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° < degree <= 180°)を取得します。
/// </summary>
/// <returns></returns>
public Angle Normalize() => new Angle(NormalizedDegree);
/// <summary>
/// 正の値で正規化された角度(0° <= degree < 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 < angle <= 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>
/// 正規化された角度値をラジアン(-π < rad < π)で取得します。
/// </summary>
public float NormalizedRadian => DegToRad(NormalizedDegree);
/// <summary>
/// 正規化された角度値(0 <= angle < 360)を取得します。
/// </summary>
public float PositiveNormalizedDegree
{
get
{
var normalized = NormalizedDegree;
return normalized >= 0 ? normalized : normalized + 360;
}
}
/// <summary>
/// 正規化された角度値をラジアン(0 <= rad < 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));
}
}
}
##最後に
角度の扱いは簡単なようで地味に厄介でした。
その厄介な部分を全部隠蔽してきれいな部分だけを外部に公開したこのAngle
構造体、少し使ってみたのですが普通に便利です。(自画自賛)
みなさんも自己責任でよかったらどうぞ。
以下GitHubでも公開しています。
##2020/2/7追記:DOTweenでTweenできるようにしました
DOTweenにAngle構造体を投入してTweenできるようにするDOTweenのPluginを作成しました。
下記記事を参照ください。
Plugin本体は下記GitHubを参照ください。