ベクトルの内積によるサイコロの出目判定

[PONOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ponos)の19日目の記事です。
昨日は@loveRiceさんの「[[Unity] DOTweenを使ってみよう](https://qiita.com/loveRice/items/d2dcc1cd67ee1a6998f3)」でした。

###はじめに

今日はUnityでサイコロを作ったお話をさせていただきます。
![スクリーンショット.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/9af02914-6373-b44d-3606-fc7c8512a8e7.png)
上の画像が完成形ですが、物理演算で転がり終えたサイコロの出目をテキストで表示しています。

とあるゲーム企画でサイコロを出してみたいという要望があり試作したものです。

###要件
サイコロの要件は以下の通り。
① 3D空間上にサイコロを出現させたい
② 物理演算でリアルに転がしたい
③ 転がり終わったときの出目を判定したい

技術的に可能かどうか聞かれ、もちろん実装できますよ！と即答したものの...
Unityなら①と②はお茶の子さいさいなのですが、さて③の出目判定はどうしたものか...？

ここはエンジニアの腕の見せ所！と、ちょっぴり本気出して考えてみました。

###運や偶然
いきなりですが、ちょっと脱線します。

サイコロやルーレットなど、運や偶然の要素は楽しさを演出しプレイヤーをワクワクさせてくれます。

ただ、そういったゲームの殆どは、実は表示される前から結果が決まっているもので、
私が知る限り、大半は「予め結果を確定させた上でビジュアルをその結果に合わせる」処理をしています。

いかにも今この場でリアルタイムに確定したように見えても、実はそう思わされているだけなのです。

例えば、サイコロをふるボタンを押して3秒後に出目が確定するゲームの場合
　1.サイコロをふるボタンが押された
　2.乱数により「5」が選ばれる（※この時点で確定）
　3.サイコロのアニメーション再生開始（再生開始ポイントは5が出る3秒前に設定）
　4.出目が5の状態でサイコロのアニメーションが停止する
　5.プレイヤーが結果が5であることを認識する

こういう話をすると、そんなのインチキだと暴れ出す人もいるかもしれませんが、これはユーザーの当選権利を守ったり、確率を適正にコントロールするための仕様だったりします。
（少なくとも自分がこれまで関わったゲームについてはインチキしていませんので。^^;）

理由はそれぞれとしても、大概のゲームはルーレットが回り始める前に結果を確定させています。

なので、今回やろうとしている出目判定というのは、サイコロが登場するゲームの中でも特殊と言えます。

###いくつかの案
さて、どうやってサイコロの出目をリアルタイムに判定するか？に戻しますが、
軽く考えを巡らせてみただけでもアイデアはいくつか出てきます。

・6面それぞれにCollider（当たり判定）を配置し、地面と衝突している面の対面を出目とする。
・6面それぞれの面の中心座標を比較し、最も高い位置にある面を出目とする。
・立方体モデルの角の8頂点のうち高さの上位4頂点の組み合わせから出目を求める。
・立方体モデルの向き（回転角度）から出目を求める。
などなど

今回は「立方体モデルの向きから出目を求める」方法をチョイスしました。

###回転角度から出目を求める
オブジェクトの回転角度ですが、UnityのGameObjectならtransform.rotationで取得できます。
ただ、角度をオイラー角で判定するというのは経験上ろくなことがなかったので(0度や360度を跨いだ時の処理とか、ジンバルロックとか...)、迷わず「ベクトルの内積」を使って判定することにしました。

測定したことはないですが、おそらく計算処理も軽いはずです。

######ベクトルの内積
ベクトルの内積を使うと以下のことがわかります。
・２つのベクトルの射影
・２つのベクトルが平行かどうかの判定
・２つのベクトルが垂直かどうかの判定
・２つのベクトルの角度差

Unityには[Vector3.Dot()](https://docs.unity3d.com/jp/460/ScriptReference/Vector3.Dot.html)という便利なものが用意されています。

###具合案
最初に考えた案は、サイコロの6面の法線ベクトル（面に垂直なベクトル）の中で、ワールド空間上の上向きベクトル（アップベクトル）に最も近いものを探す、というものでした。

でも面と裏面の法線ベクトルは符号の違いでしかないので、6面分の法線ベクトルでなくても3面分で良いことに途中で気付きました。更に突き詰めると法線も必要なく、transformの3つの回転ベクトル成分で十分ということになりました。

整理すると...
ワールド空間のアップベクトルとサイコロの回転ベクトルXYZを比較して、最もアップベクトルに近いベクトルをみつけ、且つそのベクトルがプラス方向かマイナス方向なのかで出目を判定します。


###実装してみた

UnityエディタのGizmoでは、Xが赤、Yが緑、Zが青で色付けされています。


まず、基準となるワールド空間の座標は以下のようになっています。
![スクリーンショット 2019-12-12 13.37.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/322328fe-c2e7-7e72-a037-fbd86fb60125.png)
グレーの原点から上方向に伸びる緑色の線をアップベクトル（=Vector3.up）とし、基準にします。


一方、サイコロが転がり終えた後のベクトルはこんな状態です。
![スクリーンショット 2019-12-12 13.32.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/538709/dcbcef56-3004-be4e-d9e2-816cacb29a78.png)

サイコロの赤青黄の線のうち、上記ワールド空間のアップベクトルと方向が一致しているのは青い線ですね。青い線はZ方向ベクトル（.foward）、且つ正方向なので、出目は1だと判定できます。


スクリプトは以下の通り。

```
(Unity c# MonoBehaviour)

    // 出目チェック
    int GetNumber (Transform diceTransform)
    {
        int result = 0;

        float innerProductX = Vector3.Dot (diceTransform.right, Vector3.up);
        float innerProductY = Vector3.Dot (diceTransform.up, Vector3.up);
        float innerProductZ = Vector3.Dot (diceTransform.forward, Vector3.up);

        if ((Mathf.Abs (innerProductX) > Mathf.Abs (innerProductY)) && (Mathf.Abs (innerProductX) > Mathf.Abs (innerProductZ))) {
            // X軸が一番近い
            if (innerProductX > 0f) {
                result = 4;
            } else {
                result = 3;
            }
        } else if ((Mathf.Abs (innerProductY) > Mathf.Abs (innerProductX)) && (Mathf.Abs (innerProductY) > Mathf.Abs (innerProductZ))) {
            // Y軸が一番近い
            if (innerProductY > 0f) {
                result = 5;
            } else {
                result = 2;
            }
        } else {
            // Z軸が一番近い
            if (innerProductZ > 0f) {
                result = 1;
            } else {
                result = 6;
            }
        }

        return result;
    }
```

少しだけ解説すると、以下がベクトルの内積を求めている部分であり、サイコロのX方向ベクトル（.right）とワールド空間のアップベクトル(.up)の内積、つまり角度差を求めています。

```
float innerProductX = Vector3.Dot (diceTransform.right, Vector3.up);
```

XYZのベクトルのうち最もワールド空間のアップベクトルと角度差が小さいものを判定し、更に正方向か否か場合分けして出てきた答えをresultとして返します。

実際動かしてみると思った通りの判定結果を得ることができました！

###補足
サイコロの目の配置は世界標準で決まっているそうです。
裏表の目を足して7になるというルールは有名かと思いますが、それ以外の配置も決まっています。
「天一地六東五西二南三北四」
1は天の方向、6は地の方向、4は北の方向...という意味で、その配置が世界基準です。

しかしながらあまり知られていないので、そもそもサイコロ3Dモデルがそのルールに法って作られているか確認も必要です。上記サンプルのサイコロ3DモデルはZ方向が「1」となっていたので、スクリプトもそれに合わせています。

###おわりに

今回の判定方法はあくまでもひとつの手段に過ぎません。
もっとスマートな方法もあるでしょうし、どの方法が最適かは要件によっても変わってくるでしょう。

結果ではなく、自分が試行錯誤した過程をみていただければと思い記事にしました。
そもそもサイコロ作らなきゃいけない人ってそんなに居ないでしょうしｗ


明日は@e73ryoさんですー！