【S2お茶会】Unity で並べたい(5/22)

s2お茶会
緊急事態宣言が解除されました。
なぜレインボーブリッジが赤くなるのか意味がわかりませんがそうやって日々は続いていきます。

今回は社員のひとりが「ゲームエンジンUnity で最近作り始めた趣味のアプリと、それにまつわる試行錯誤について」デモを交えつつ話しました。

Unity で並べたい



自粛でどこにも行けないゴールデンウィーク。
ただ家に篭っているだけでは気が滅入ってしまいそうなので、何か綺麗なものをつくろうと思い、思いつきでUnityを使って万華鏡をつくることにしました。


Unity

ユニティ・テクノロジーズが開発しているゲームエンジン
3Dゲーム開発の手軽さとその物理エンジンが有名だが、2Dゲーム開発にも対応している。
他のゲームエンジンとは異なり、Unityの場合はゲーム自体の実行をする環境があらかじめ含まれている。
『ポケモンGO』もUnityを使って作られた。


  • iPhoneやWindowsなど、いろんな環境に移植しやすい
  • 去年Unityの2Dシューティングのチュートリアルをやって完全に理解した
  • Unityに詳しい人と同居しているので質問し放題

というのがUnityを選んだ理由です。

割と簡単にできるのでは。と思って始めたのですが、そううまくはいきませんでした…。

Unityの基本的な使い方


これはUnityの画面です。
それぞれ赤枠で囲んで番号が振ってありますが、簡単に説明します。


シーンビュー

作成しているゲーム世界に相互作用できるビューです。シーンビューを使って風景、キャラクター、カメラ、ライト、その他のすべての種類の ゲームオブジェクト を選択し配置します。

ヒエラルキーウィンドウ

現在のシーンにおける各ゲームオブジェクトのリストが表示されます。オブジェクトをドラッグして上下に順番を替えたり、「子」や「親」のオブジェクトにしたりできます。

プロジェクトウィンドウ

ゲームで使う画像やスクリプト、マテリアルなどの制作に必要な各種素材データがここに表示され、管理することができます。

インスペクターウィンドウ

現在選択されているゲームオブジェクトに関する詳細な情報を、ゲームオブジェクトにアタッチされたコンポーネントやそのプロパティーの情報も合わせて表示します。また、シーンのゲームオブジェクトの機能を変更することができます。


これらを操作して万華鏡をつくっていきます。

Unityの詳しい使い方についてはこちらのマニュアルをご参照ください。

万華鏡をつくる


これは万華鏡のオブジェクトです。
どうすればいいか同居人に相談したところ、さくっとつくってくれました。

万華鏡のシリンダーの底の画像をレンダリングしたレンダーテクスチャを
貼ったオブジェクト
をしきつめてつくったそうです。

なので私は早速、万華鏡の動きをつくっていくことに。

仕組みとしては、三角形や四角形のランダムに生成されたオブジェクトを選択して、回転するシリンダーの中に落とし、それを表示させるといった具合です。

そうしてできたのがこちら。


シーンビューで見ると下のように、選択されたオブジェクトがシリンダーの中に入っているのがわかります。


全体の背景が一定時間ごとに明るくなったり暗くなったりするように設定してあり、さらに動画左下にある矢印アイコンをクリックすると万華鏡が回転します。

これだけでも十分綺麗なのですが、他にも色々と機能を追加しました。

今日の本題は、実はその機能についてです。

かけらをボックスの中に並べる


これだけでは少し物足りない。と思ったので、機能を追加することにしました。
どのような機能かというと、


画像左上に箱形のアイコンがありますが、これをクリックすると、シリンダーに入れられたオブジェクト(以下かけら)の一覧が表示され、その中から選択されたかけらを読み出して、シリンダーの中に追加することができるようにしようと思いました。

失敗の連続


これがなかなかうまくいきません。

初め、Grid Layout Groupという、要素を自動的にタイリングしてくれるコンポーネントを使ってUI上に並べられるようにしようと思ったのですが、そのままではUI上に3Dのオブジェクトを並べることはできないというUnityの性質によりうまくいきませんでした。

どうやらUnityのUIと3Dオブジェクトは別の座標系を持っており、別々にレンダリングされ合成されるそうです。

それなら3Dオブジェクトの座標系の中でかけらの一覧をつくろうと思い調べたところ、object.transform.positionというクラスを使えばオブジェクトの位置が取得できることがわかったのですが、これもなんだか様子がおかしい。表示がずれてしまう。
3Dの座標系にオブジェクトを入れても、カメラによってレンダリング結果が変わってしまうのが原因でした…。

成功


どうしたものかと調べていたら、UIと3Dオブジェクト以外にもう一つ座標系があることがわかりました。
スクリーンの左下端を(0, 0)、右上端を (1, 1) とした座標系で、ビューポート座標といいます。

ViewportToWorldPoint()というメソッドを使えば、指定したカメラ視点のビューポート座標を3D 座標に変換できるようです(z座標は奥行きとしてそのまま反映される)。

オブジェクトひとつひとつの座標についてスクリーン上の座標からViewportToWorldPoint()して3D座標を出していけば…?と思ったのですが、
階層になった要素はゲームオブジェクト内の相対座標を持つことができ、object.transform.localPosition() というメソッドを使えば、相対座標を指定できるので、かけらたちを空のオブジェクトの中に入れ、親の要素だけViewportToWorldPoint()して、適宜拡大率を指定して並べれば良さそうです。

無事並べることができました。


かけらの削除機能


ようやくかけらを一覧表示させることができたので、次はボックスに表示したかけらの削除機能を追加することにしました。

すでに先ほどの画像で見えていますが、まず、かけらの右上に x の画像を貼ったゲームオブジェクトを作り、衝突判定用のコンポーネント(コライダー)を足します。

スクリーンをクリックするとその地点からRay(光線)が発射され、コライダーとRayの衝突を判定し、衝突したゲームオブジェクトの情報を取得、かけらが削除されるようにしました。

実際のコードです。

//クリックしたときに
RaycastHit hit;//衝突したオブジェクトの情報が入る構造体

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); //クリック位置から ray をとばす

if (Physics.Raycast(ray, out hit)) {
if (hit.transform.gameObject.tag == “DeleteIcon”) { //GameObject につけたタグ
   DeleteFramgent(hit.transform.gameObject); //削除機能を呼ぶ
 }
}


これで削除機能も実装できました。

まとめ


なんとか完成できましたが、踏める地雷を全部踏んでしまったような気がします。

APIを呼べばフロントがよしなにしてくれるわけではありません。
また、ただオブジェクトを設置しただけでは思い通りにはならないので、座標系についてちゃんと理解することが大切だと思いました。
アプリをつくっている人はすごい…。

今週のお茶菓子