LoginSignup
3
2

More than 3 years have passed since last update.

Unity (C#) で小数点型の変数をintにキャストすると意図した値にならない(ように見える)ことがある

Last updated at Posted at 2020-07-09

座標値をint型にする処理

int x = (int)hit.collider.gameObject.transform.position.x;
Debug.Log("x1=" + hit.collider.gameObject.transform.position.x);
Debug.Log("x2=" + x);

Unityで書いてしまいそうな処理があります。
例えば、ボードゲーム系のゲームを作ろうとしたときに、クリックしたマスのボード上の座標を求めたいみたいなものがあると思います。

hit.collider.gameObject.transform.position.xは2.0になっているつもりです。

intにキャストしているため、小数点以下は切り捨てになるはずなので2.5なら2や3.0なら3になってほしいわけですが..

image.png

このような結果になりました。

これは(原因を知らないと)おかしいと思ってしまいますね。
こういうので初心者さんが挫折してしまう理由にもなりかねないんですよね。

ちなみに、キャストは四捨五入なんじゃないかとも言われそうですが
仕様としても切り捨てです。(厳密に言うと0に近い整数値)

原因を調べる (ToString)

初心者だと、なかなか発想が浮かびにくいですが、そもそもこの値は2.0じゃないのではないかという発想になりますが、これを検証するのは知らないとなかなか難しいです。

デバッガでステップ実行しても、デバッガの表示も ToString()での値になるので生の値がわかりません。

ちなみに、Unityのposition.x などは float型 つまり System.Single 型です。

ドキュメントを見てみると、ToString()は ToString("G")の省略形のようですね。

"G" を見てみると、7桁の精度しかないようです。

System.Single 型のドキュメントを見ると、

既定では、戻り値の精度は7桁のみですが、内部的には最大9桁が保持されます。

とあるので、9桁を出して見ると..

Debug.Log("x1=" + hit.collider.gameObject.transform.position.x.ToString("G9"));
Debug.Log("x2=" + x);

image.png

これで謎が少し解決しそうですね。

ということで表示上は2になってしまうが、実は1.99999967だったみたいです。

ちなみに

Debug.Log("x2=" + x);

というのも、実は ToString が勝手に補完されます。

Debug.Log("x2=" + x.ToString());

ところで、ToString()はなんで切り上げになったのか

精度指定子によって結果文字列内の小数部の桁数を制御する場合、結果文字列では無限に正確な結果に最も近い表現可能な結果に丸められた数値が反映されます。 同じように近い表現可能な結果が 2 つある場合は、次のようになります。
.NET Framework および .NET Core 2.0 までの .NET Core の場合、最下位の数字が大きい方の結果が選択されます (つまり、MidpointRounding.AwayFromZero が使用されます)。
.NET Core 2.1 以降の場合、ランタイムでは最下位の数字が同一である結果が選択されます (つまり、MidpointRounding.ToEven が使用されます)。

とあります、

            float f = 1.99999976f;
            Console.WriteLine(Math.Round(f, 6, MidpointRounding.AwayFromZero));

というような処理が行われていると想像できます。
つまり丸められて切り上げになったんですね。

つまり

   class Program
    {
        static void Main(string[] args)
        {
            float f = 1.99999976f;

            Console.WriteLine(f);
            Console.WriteLine((int)f);

            Console.WriteLine(f.ToString("G9"));

        }
    }

2
1
1.99999976

という結果になります。

そもそも、そんな細かい値を使うの?

と思うかもしれませんが、UnityでGameObjectを作ったら、座標がランダムっぽい値で生成されるときがあります。

image.png

これをとくに気にせずに親のオブジェクトとして

Instantiate(guide , new Vector3(2, 0, 0) , Quaternion.identity, guideList.transform);

のようなオブジェクトを作ると (2, 0, 0) はグローバル座標なので

image.png

生成後はこのようなローカル座標になります。

Unityはグローバル座標ではなく、どうもローカル座標から足し引きで求めているようで、本題のような微妙な座標値になることがあります。

なので上の座標値を (0,0,0) にするだけでも本題のバグが発生しない可能性が高いです。

ちなみに
transform.position はグローバル座標で
transform.localPosition は ローカル座標です。(インスペクタに表示されるのもこっち)

まとめ

(int) でキャストして切り捨てにすると意図しない挙動になる場合があるので

float f = 1.99999976f;
int x = (int) Math.Round(f);

のように四捨五入するのが安全かなと思います。

3
2
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
3
2