Tutti Lab

元シリコンバレー在住のおっさん技術者、モバイルVRアプリ開発に挑戦中

Magic Leapが少しずつ明らかになってきています

WIREDにて「謎のARデバイス」Magic Leapの特集がされています。これまでデバイスに関する情報はほぼ皆無でしたが、今回キーデバイス(だと思われる)について言及がありました。このレンズ(?)を通して現実世界を見ることで、その現実世界に様々な3DCG拡張情報が表示される、これだけだとHoloLens等と一緒ですが、これらのように「スクリーン投射」でCGを重畳するのではなく、別の方法で実現しているそうです。いったいどんな方法なのでしょうか?
f:id:tuti107:20160426133559p:plain
いずれにせよ、ARが本格普及する時代には物理的な「スクリーン」は意味のないものになり、平面・立体のCGを現実空間上に貼り付けて利用することになります。PCにディスプレイは不要、ウィンドウを空間に貼り付けて利用する。アプリ・UIは激変します。今はまだ、Magic Leapがいつ出てくるのか(本当に出てくるのかも含め)わかりませんが、Meta2は今年の秋に出てくることですし、真剣にAR世代のアプリ・サービスを考え始めないと、と思っています。

Cardboard向けバーチャルキーボードを作ってみる

はじめに

Google Cardboardには、二種類の入力手段が用意されております。一つはGaze、画面中央に対象物が来るように首を動かすと、ゲージが現れます。ゲージがいっぱいになるまでそのままの状態を保つことで、対象物へのアクセスが可能となります。もう一つは、同じく画面中央に対象物が来るように首を動かすと照準が現れるので、その状態でボタン( Google Cardboardの右上部分、カメラのシャッターボタンのようなもの)を押下、対象物にアクセスするというものです。
いずれも首を動かして対象物の方を向く、という、VRらしい方法を前提にしております。
今回は「対象物の方向に向いてボタンを押す」入力手段を利用して、バーチャルキーボードを作ってみます。果たして「対象物の方向に向いてボタンを押す」方法で、問題なく文字入力ができるのか?その課題についても考えてみます。
f:id:tuti107:20160424043555p:plain

スクリプトの用意

Unity Assetには、有料のバーチャルキーボードのアセットが幾つかあるようですが、今回はそれらを利用せず、一からスクラッチしてみます。
利用するのは、前回ご説明したiTween、64 Flat Game Icons、あとはGoogle Cardboardです。まずはそれぞれインポートします。
次にスクリプトを作成します。今回は以下の3つのスクリプトを作成しました。

gist7e6babd903b2492c47bac7e68f74dd00

キーボード用のオブジェクトを用意

次にシーン内のオブジェクトを配置していきます。
最初に、デフォルトで配置されているMain CameraとDirectional lightを消し、Cardboard Mainを代わりに配置します。この時自動的に作成されるCardboardは不必要なので消してしまいます。ここまでは前回までと同様です。
次にキーボードに貼り付けるキーを作成します。Hierarchyにて右クリック→3D Object→Cubeを選択、名前をKeyとします。さらにそのKey上で右クリック→3D Object→3D Textを選択、名前をTextとします。これでKey配下にTextがある状態になります。Keyには、インスペクタより以下の通り設定をします。
f:id:tuti107:20160422003943p:plain
上記で作成したスクリプトKey.csをコンポーネントとして追加しText Meshに、上記でKey配下に作成したTextのText Meshをドラッグ&ドロップします(インスペクタ内のコンポーネントをドラッグ&ドロップしてもう一方のインスペクタに設定する方法は、こちらのインスペクタで関連付けを参照ください)。
Event Triggerには、Pointer Click時にKey#Push()を呼び出すように設定ください(Event Triggerについてはこちらのスケルトンをクリック可能にするを参照ください)。ここまで出来ましたら、KeyをProjectの任意のフォルダへドラッグ&ドロップし、KeyをPrefab化します(prefabについてはこちらのスケルトンをPrefab化するを参照ください)。なお、KeyのPosition、Rotation、Scaleについては、Board.cs内で計算・設定されるため適当な値で良いです。
次にKey群を配置するボードを作成します。Hierarchyにてクリック→3D Object→Cubeを選択、名前をBoardとします。さらにBoard上で右クリック→3D Object→Cubeを選択、名前をDisplayとします。さらに、そのBoard上で右クリック→3D Object→3D Textで、名前をTextとします。以上で、Board-Display-Textという構成のオブジェクトが作成されました。インスペクタでの設定は以下の通りです。
f:id:tuti107:20160422005047p:plain
上記で作成したスクリプトBoard.csをコンポーネントとして追加し、Key Prefabには上記でPrefab化したKeyを設定してください。Displayは上記で作成したDisplayをドラッグ&ドロップします。
f:id:tuti107:20160422005804p:plain
上記で作成したスクリプトDisplay.csをコンポーネントとして追加し、Textには、上記KeyのText同様、Dislay配下のTextのText Meshを設定します。

イベント処理

次にイベント処理用の設定を行います。まずHierarchyにて右クリック→Create Empty、名前をGazePointerとします。次にそのGamePointer上で右クリック→2D Object→Spriteを選択してください。そのSpriteのインスペクタにて、以下の通り、Transform/Spriteの設定を行います。Modifiers_Accuracyについては64x64のものを選択してください。
f:id:tuti107:20160422010339p:plain
次にHierarchyで右クリック→UI→Event Systemを選択、インスペクタよりGaze Input Moduleを追加し、以下の通り設定します。
f:id:tuti107:20160422010627p:plain
Cursorには、上記で作成したGamePointerをドラッグ&ドロップしてください。
最後に、CardboardMain/Head/MainCameraにPhysics Raycastを追加してください。
f:id:tuti107:20160422010842p:plain
これで完成です。

一応入力は出来るけど

f:id:tuti107:20160424043703p:plain
課題を列挙してみます。

  • 押したいキーの方を向き、そのキーを画面の中心(照準部)に合わせて、シャッターボタンを押す必要があるのですが、右下の方に配置されているキーを見ている時は、Displayに表示の「これまで入力した文字列」が見えません。
  • バーチャルキーボードの表示位置は、シーンの3D空間内となります。前回のライフを追加にてライフのハートをCardboard Main/Headに配置することで常に一定位置に表示する方法をご紹介しましたが、このような方法をとることができません。照準は常に画面中央なので、それぞれのキーに照準を当てることができなくなるためです(一つのキーしか押せません)。このため、アプリケーション毎にキーボードの表示位置を工夫する必要が出てきます。
  • 高速・長文・長時間の文字入力はかなり厳しいです。首も腕もかなり疲れます(想像していたよりは、かなり素早く入力できる印象でしたが)。

まずは今回、シンプルなバーチャルキーボードの実装をしてみましたが、まだこのままでは実用に耐えない印象です。ただし、工夫次第ではVRコンテンツで広く活用できるバーチャルキーボードへの進化も可能かな?と可能性は感じました。

Cardboardで360度立体視動画ビューワーを作る(2)

はじめに

前回は、EasyMovieTextureを使い、360度立体視動画ビューワーを開発しました。前回は、Assets/StreamingAssetsに360度立体視動画形式のmp4を直接配置して、これを視聴するという形でしたが、今回は二台のTheta Sより撮影した右目・左目用の360度画像をブロードキャストし、Cardboardにて視聴するところまで開発を進めます。
f:id:tuti107:20160420162743j:plain

Easy Movie Textureはライブストリーミングに対応しているのか?

まずは確認から。HLS形式のストリーミングを再生できるかどうか、テストしてみました。Media Player CtrlのStr File Nameに、適当なURL(http://..../**.m3u8)を指定し実行したところ、問題なく再生されました。Easy Movie Textureは、少なくともHLS形式のライブストリーミングに対応しているようです(他のプロトコルについては試しておりません。iOSへの配信を考慮するとHLS形式一択ですので)。
なお、「Http://..../**.mp4」形式の動画も問題なく再生されました。ご参考まで。

Theta Sの360度動画をPCへ取り込む

Theta Sには、USB/HDMIでPCに接続して、PC側でライブストリーミング表示する機能が搭載されています。USBなら1280x720 15fpsで取り込みが可能です。ただ、形式はDual Fisheye形式であり、このままでは前回開発したアプリで使用することができません。
f:id:tuti107:20160420145003p:plain
前回開発したアプリで360度立体視表示するためには、以下のような形式への変換が必要となります。
f:id:tuti107:20160420141059p:plain
以前はこの変換を自力でやる必要がありました(例えばこちらなどで取り上げられています)。しかし先月リリースされたRICOH THETA UVC Blenderを利用することで、Dual Fisheye形式ではなく変換後の形式でのライブストリーミングが可能となりました。
RICOH THETA UVC Blenderは、こちらよりダウンロード可能です(ライブストリーミング用アプリ、という名前です。なお基本アプリを未インストールの場合は、こちらを先にインストールしておく必要があります)。
インストール後、Theta Sの電源を入れずに、PCとTheta SをUSB接続し、THETA UVC Registerを起動してください。
f:id:tuti107:20160420142632p:plain
これで、そのTheta SがTHETA UVC BLENDERとして登録されます。試しに、Googleハングアウト等、USBカメラ画像を取り込むことができるアプリにて、カメラリストを表示してみると、THETA UVC BLENDER(***)が追加されていることを確認できます。これを選択すると、Dual Fisheye形式ではなく、本開発アプリで利用可能な形式にて表示されます。
今回は、右目・左目用の二台のTheta Sがありますので、それぞれ上記の手順で登録を行いました。

Open Broadcast Software

次に、撮影画像をブロードキャスト配信するための用意を行います。いろいろな方法・ソフトウェアがあるようですが、調べたところ、Open Broadcast Softwareを利用するのが良さそうです。Open Broadcast Softwareは、こちらからダウンロード可能です。
このアプリは、カメラキャプチャ映像のみならず、静止画・ウィンドウ・画面全体など様々なソースを組み合わせて一つの動画を構成し、これを録画・配信する機能を備えています。例えば、以下のように右目・左目用それぞれのTHETA UVC BLENDERを縦に並べて配置すると、あっという間に360度立体視動画の形式になります。
f:id:tuti107:20160420144221p:plain
ところが、実際この方法ではうまくいきません。右目用と左目用の映像が全くシンクロせず・フレームレートもバラバラのため、本映像を前回のアプリで視聴しても全く立体視動画には見えません。そこで、右目・左目をシンクロさせて縦に並べる方法を検討してみます。

OpenCVで二つのTHETA UVC BLENDERをキャプチャ・合成

できるだけ簡単な方法で、といろいろネットを探索したのですが、残念ながらコードを書くのが一番手っ取り早そうです。今回は、OpenCV3.1 + Javaで、二つのTHETA UVC BLENDERをキャプチャ・合成のコードを作成しました。
なお、OpenCV3.1の導入とJava(Eclipse)での利用手順については、設定が少し面倒なのですが、こちらにわかりやすく手順がまとめられておりますので、これを参考に進めてください。なお今回、別にJavaである必要性は特にないのですが、現在Macで開発している本環境を近日中にWindowsへ移植することを検討しており、移植性を考えてJavaにしてみました。
コードは以下の通りとなります。なお、24行目・25行目のnew VideoCapture()の引数値は、実行環境毎に異なります。それぞれ左目・右目用のTHETA UVC BLENDERの映像が取れるように調整をしてください。

これを実行すると、以下のように、二つのTHETA UVC BLENDER映像が縦に並べられた映像が表示されます。
f:id:tuti107:20160420151352p:plain
このプログラムを起動した状態で、再度Open Broadcast Softwareを起動し、今後は、ソースとしてウィンドウキャプチャを選択してください。
f:id:tuti107:20160420152052p:plain
空の名前でウィンドウを表示をチェックすると、ウィンドウから、先ほど起動したプログラムのウィンドウを選択可能となります。表示サイズがおかしい場合は、設定→映像→キャンバス解像度を1280x1440に設定の後、赤枠の丸部分をドラッグして、位置・サイズをキャンバスの大きさに合わせてください。
f:id:tuti107:20160420152846p:plain
f:id:tuti107:20160420152859p:plain
これで左右両眼の映像のタイミング・フレームレートがあった形での映像配信が可能となります(ただ当方のMacではすでに処理の限界に近づいており、ファンが猛烈に周り始めました)。

配信用のサーバを用意する

わざわざサーバを立てなくても、UStream等の既存のライブ配信サーバーを活用すれば、上記の設定だけで動画配信が可能です。ただし、インターネット上のサーバへアップロードするためレスポンスはそれ相当となってしまいます。今回はメイカーフェアー向けに可能な限り高速な動画配信を行いたいため、ローカルでサーバを立ち上げ、ローカルネットワーク内でのみ動画配信を行う形としました。
HLS形式のライブストリーミングを配信可能で、かつ軽量なサーバとしては、nginxというWEBサーバの人気が高いようです。なお、nginxはデフォルトでは、ライブ配信用のモジュール「nginx-rtmp-module」がdisableのため、これを有効化してコンパイル・インストールする必要があります。
nginx導入の手順、およびライブ配信のためのOpen Broadcast Softwareの設定については、こちらを参考にさせていただきました。なお、当方の環境(Mac)に合わせて、./configureで設定するパスを/usr/local/...に変更しました(/usr/local/etc/nginx/nginx.confのrtmp設定についても同様)。Open Broadcast Softwareの配信設定については、こちらに記載の通り、以下のように指定することで、http://PCのIPアドレス:8080/test.m3u8へアクセスすることで、前回開発したアプリからTheta Sで撮影の360度立体視映像を視聴することができました(冒頭、及び以下のCardboard映像は、本仕組みで視聴した我が家の天井です)。
f:id:tuti107:20160420160111p:plain
f:id:tuti107:20160420162743j:plain

まとめ

今回は、前回開発したCardboard用360度立体視動画ビューワーにて、Theta S(左右両眼用)で撮影・リアルタイム配信した映像を視聴するための環境を実現しました。確かにCardboardでTheta Sで撮影の映像を視聴することはできるのですが、処理負荷の高さからフレームレートは数FPSという状況、またライブ配信の遅延が20秒近くあるため、メイカーフェアーで「リアルタイム配信」を謳うのは厳しい感じの仕上がりです。
次回は、一旦Cardboardから離れて、Oculusにて、PCで取り込んだ右目・左目映像を直接表示する仕組みを作ってみたいと思います。

Cardboardで360度立体視動画ビューワーを作る(1)

はじめに

これまで計6回にわたり、CardboardでVRゲームアプリを作るという内容で書いてきました。今回からは(数回にわたり)、360度立体視動画のビューワーアプリを開発していきたいと思います。このアプリ、娘とハッカソンに参加で書いた、メイカーフェアー向けアプリとして仕上げていくつもりです。なおこのアプリは「オリンピックを360度立体視動画で配信する」というコンセプトであり、単に指定したファイルのビューワーとして機能するだけでなく、360度立体視撮影カメラと連携し、カメラで撮影した映像をリアルタイムで本アプリへ配信・鑑賞できるようにする機能が必要となります。
今回はまず手始めに、360度立体視動画ファイル(ストリーミングではない)をCardboardで視聴するためのアプリを作ります。次回以降にて、360度立体視撮影カメラと連携したシステムへ仕上げていく予定です。

Movie Texture

360度立体視視聴可能なアプリの実現は、実はそれほど難しくありません。ムービーファイルを読み込み、適当な3Dオブジェクト(360度映像の場合は球)の面上でムービーを再生するだけ、です。ただ今回は「立体視」なので、ムービーには左右両方の映像が含まれています。例えばこんな感じです。これを左右それぞれに分割し、左目用は左目用の球の面に、右目用は右目用の球の面に、貼り付けることになります。最後に、左目用の3Dオブジェクトは左目側だけ、右目用の3Dオブジェクトは右目側だけで表示するように、カメラの設定を変更すれば完成!となります。
ただここで問題となるのが、どうやってムービーファイルを読み込んで3Dオブジェクトの面上で再生するか、という点です。UnityにはMovieTextureというAPIが用意されておりますが、残念ながらモバイル(Android/iOS)には現時点で未対応です。
幸いAsset Storeでは、これをモバイルで実現するためのアセットが公開されております。色々と先人の調査結果を参照したところ、Easy Movie Textureというアセットが良い、との情報に行き着きました。
本アセットは現在$55、無料のアセットではありませんが、非常に簡単に利用でき、かつ再生制御のためのAPIも充実していて、お買い得なアセットかと思います。
本アセットの機能は、以下の動画を見れば一目瞭然かと思います。
youtu.be

球の3Dオブジェクトを入手

次にテクスチャを貼り付ける球ですが、Unityの3D Object Sphereではうまくいきません。今回は球の外面ではなく、カメラを球の中心に設置・内面にテクスチャを貼り付けてムービーを再生して、まるでプラネタリウムを観るような感じを実現する必要があるのですが、Sphereは球の内側から見ると透過してしまいます(何らか内側から見えるようにする設定があるのかもしれませんが、私は知りません。。)。また詳細はわからないのですが、いろいろな解説ページを見る限り、球の3D形状も何でも良いわけではないようです。
そこで、こちらのサイトにて公開されている「sphere100.fbx」を活用させていただくことにしました。

球をシーン上に配置

あとは、これら用意したアセット群をシーンに配置していきます。
まずは、前回ご紹介した手順で、Cardboard SDKを取り込みます。ProjectのAssets上で、右クリック→Import Package→Custom Packageを選択、CardboardSDKForUnity.unitypackageをインポートします。インポートしたらAssets/Cardboard/Prefab/CardboardMain.prefabをHierarchyにドラッグ&ドロップします。なお、CardboardというオブジェクトがHierarchyにあるかと思います。こちらは削除してください。同様に、デフォルトでHierarchy上に配置されているMain Camera、Directional lightもいらないので削除してください。
次に右目用・左目用それぞれの球を作成します。まずはこれらをグルーピングするための空オブジェクトを生成(Hierarchyより右クリック→Create Empty)、名前をSpheresとします。そこにぶら下げる形で、上記でダウンロードしたsphere100.fbxを二つ追加します。sphere100.fbxをAssets配下の適当なフォルダに入れておけば、それをHierarchyのSphere上でドラッグ&ドロップするだけです。二つドラッグ&ドロップののち、名前をそれぞれ、Sphere100_L, Sphere100_Rとしてください。以上で、Hierarchyは以下のような感じになります。
f:id:tuti107:20160416113810p:plain
それぞれ、Position/Scaleを以下の通り設定してください。xはマイナス値となっていますが、これは(なぜか)こうしないとムービーが左右反転してしまうためです(現在のところ原因不明)。
f:id:tuti107:20160416120203p:plain

Easy Movie Textureの設定

次に、上記でご紹介したEasy Movie Textureをインポートし、各種設定を行っていきます。
まずは、球の内側に貼り付けるテクスチャのマテリアルを用意します。利用するマテリアルはAssets/EasyMovieTexture/VideoMaterial.matなのですが、左右別個に必要となるため、VideoMaterial.matをコピーし、二つ複製、それぞれ名前をVideoMaterial_L.mat, VideoMaterial_R.matとします。なお複製ですが、Command+C→Command+V(WindowsならCtrl)、ではありません。Command+C→Command+Dで複製できます。
f:id:tuti107:20160416114051p:plain
これらマテリアルを作成したら、それぞれのTiling, Offsetの値を変更します。それぞれ、VideoMaterial_Lは、Tiling=(1, 0.5), Offset=(0, 0)、VideoMaterial_Rは、Tiling=(1, 0.5), Offset=(0, 0.5)とします。これは、上記で説明した通り、ムービーには左右両眼用のものが縦に並んで配置されており(左目用が上、右目用が下)、右目用の球・左目用の球にそれぞれ分離して貼り付ける必要があります。そこで、Tiling(1を全部とした時、縦・横それぞれどこまでテクスチャとして扱うか)を、(1,0.5)として、横方向は全部・縦方向はムービーの縦方向サイズの半分だけ使用、Offset(どの位置からテクスチャとして扱うか)を、左目用は(0,0)=左上、右目用は(0,0.5)=左側・縦方向は中央の位置、として、右目・左目それぞれの部分のみテクスチャとして利用する、としています。
f:id:tuti107:20160416115342p:plainf:id:tuti107:20160416115347p:plain
また、いずれもShaderをUnlit/Textureに変更してください。これで、このマテリアルが設定された面は照明の影響を受けなくなります。これらマテリアルの設定が終わりましたら、VideoMaterial_Lを、shere100_LのMaterialsへ、VideoMaterial_Rを、sphere100_RのMaterialsへそれぞれドラッグ&ドロップしてください。これで、これらそれぞれの球にそれぞれのマテリアルが設定されます。
f:id:tuti107:20160416120215p:plain
合わせて、sphere100_L, sphere100_RにそれぞれレイヤーEYE_L, EYE_Rを割り当てます。レイヤーは、インスペクター右上のLayerをクリックすることで、変更(追加)できます。まずLayer→Add Layerより、EYE_L, EYE_Rを追加します。
f:id:tuti107:20160416121335p:plain
その後再びLayerをクリックすると、EYE_L, EYE_Rを選択できるようになります。これら設定は、後述の「左目からは左目用の球を、右目からは右目用の球のみ見えるようにする」ために利用します。
最後に、Media Player Ctrlの設定を行います。Hirarchyにて右クリック→Create Emptyで空オブジェクトを作成、名前を「Player」としたのち、インスペクターよりAdd Componentを押下→Media Player Ctrlを追加してください(これがEasy Movie Textureの本体です)。このMedia Player Ctrl、どのオブジェクトに貼り付けてもいいのですが(例えばCardboardMain)、とりあえずこのような形としました。
f:id:tuti107:20160416125109p:plain

  • Str File Nameには、再生するムービーファイルの名前を設定します。ストリーミング形式(例えばhttp://www.test.com/test.mp4)も指定できるようですが、まだ試せていません。今回は、360度立体視動画のムービーファイルを用意し、これをAssets/StreamingAssets配下に格納、本ファイルのファイル名を設定しております(BSDance.mp4)。ムービーファイルはAssets/StreamingAssetsに置くようにしてください。なお、YouTubeのストリーミング動画をmp4に変換する方法についてはここでは触れません。
  • Target Materialには、再生したムービーを貼り付ける3Dオブジェクトを設定します。今回は、左右両眼用の球sphere100_L, sphere100_Rを設定します(Sizeを2とすることで二つ設定が可能です)。
  • B Full Screenはフルスクリーンビデオ再生の際にチェックします。今回は使用しません。
  • BSupport Rockchipは、特定のチップ搭載の端末時の問題解決用、とのことです。今回は使用しません(本チップ搭載の端末を持っていません)。
  • ScaleValueとobjResizeは、Game Objectのリサイズに利用します。今回は使用しません。
  • B Loopをチェックすると、ムービー再生完了時、自動的にループ再生します
  • B Auto playをチェックすると、Media Player Ctrlを設定したGameObjectがアクティブになった際に、自動的にムービー再生を開始します。

なお、iOS端末の場合、Unityのバージョンに合わせて、パッチを当てる必要があります。Unity 5以降なら、Assets/EasyMovieTexture/Unity5_Patch_IOS、Unity4.6.3以降なら、Assets/EasyMovieTexture/Unity463_Patch_IOS、をそれぞれダブルクリックし、必要なパッチをインポートしてください。

Cardboardの設定

最後に、Cardboardの設定を行います。右目・左目用それぞれのカメラのToggle Culling Maskを設定し、それぞれのカメラからそれぞれの目用の球のみ見えるようにします。
f:id:tuti107:20160416131945p:plainf:id:tuti107:20160416131948p:plain
ここで注意が必要なのが、Culling Maskではなく、Toggle Culling Maskだということです。その名の通りトグルした値の設定します。上記はMain Camera Leftの設定ですが、Toggle Culling Maskには右目用レイヤーのEYE_Rを設定します。すると、Main Camera Leftは設定されたEYE_R「以外」のレイヤーを表示するようになります(よって左目用の球は、EYE_Lレイヤーに設定されているので、見えます)。同様に、Main Camera RightのToggle Culling MaskにはEYE_Lを設定します。
以上で全ての設定が完了です。前回ご説明の通り、File→Build SettingsよりiOSまたはAndroidを選択後、Player Settingにて、Bundle IdentifierとDefault Orientationを設定し、Buildしてください。なお、iOSの場合は、XCodeにてBitcode=No, Security Frameworkの追加が必要な点、ご注意ください。
f:id:tuti107:20160416133222j:plain

まとめ

今回は、360度立体視動画ビューワーを作る、ということで、EasyMovieTextureを活用し、ファイル指定した360度立体視動画の再生が可能なアプリを作成するところまで進めました。1行もコードを書くことなくこれだけの機能が実現できるのは、まさにUnity, Asset Storeのおかげです。
次回は、本アプリでストリーミング動画再生にチャレンジする予定です。

Oculusの出荷が遅れているようです(2)

先ほどOculusからメールがきました。出荷は、5/2〜12となる、とのこと。予約開始と同時に注文をしたのですが、それでも一ヶ月以上の遅延となるようですね。
遅延の理由は部材の不足、とのことですのが、これ以上の遅延にならないことを祈るばかりです。

CardboardでVRアプリを作ってみる(6)

はじめに

前回までで、次々と出現する敵を倒すというゲームの中核部分は完成しました。
f:id:tuti107:20160403112007p:plain
今回は、ゲームオーバとコンティニュー処理を追加し、ゲームとして一通り完成させます。

CardboardでVRアプリを作ってみる(全体構成)

第1回(Cardboardのビルド)
第2回(シーンを構成)
第3回(敵を動かす)
第4回(敵を倒す)
第5回(敵を出現させる)
第6回(ゲームオーバーとコンティニュー処理)【今回】

ライフを追加

スケルトンに一度攻撃されるだけでゲームオーバ、だとあまりにシビアすぎるので、主人公にライフを設定し、ライフがなくなるとゲームオーバーになる、という形にします。
まずは、ライフを表示するためのアセットをダウンロード・インストールします。今回は、64 Flat Game Iconsを利用することにします。本アセットは、ゲーム等に応用できそうな様々なアイコン(32, 64, 128px)のセットです。
次に、このアセット等を利用して、ライフをスクリプト(Player.cs)に追加します(次節以降で説明するゲームオーバ・コンティニュー処理も含まれています)。

using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class Player : MonoBehaviour {

	public  Image damageImage; 
	private Color damageImageColor;
	private float damagedTime = 0f;

// --- 4/9 add start ---
	public Image gameOverImage;
	public TextMesh gameOverText;
	private Color gameOverColor;
	private Color gameOverTextColor;
	private float gameoverTime = 0f;
	public bool isGameover = false;

	public GameObject cont;
// --- 4/9 add end ---

	// Use this for initialization
	void Start () {
		damageImageColor = damageImage.color;
		ShowHP ();

// --- 4/9 add start ---
		gameOverColor = gameOverImage.color;
		gameOverTextColor = gameOverText.color;

		cont.SetActive (false);
// --- 4/9 add end ---
	}
	
	// Update is called once per frame
	void Update () {
	
		if (damagedTime > 0f) {
			float t = Time.time - damagedTime;
			if (t <= 1f) {
				damageImageColor.a = 1f - t;
			}
			else {
				damagedTime = 0f;
				damageImageColor.a = 0f;
			}
			damageImage.color = damageImageColor;
		}

// --- 4/9 add start ---
		if (gameoverTime > 0f) {
			float t = Time.time - gameoverTime;
			if (t <= 2f) {
				gameOverColor.a = t / 2f;
			} else if (t > 2f && t <= 3) {
				isGameover = true;
				cont.SetActive (true);
				gameOverColor.a = 3f - t;
				gameOverTextColor.a = (t - 2f); 
			} else {
				gameOverColor.a = 0f;
				gameOverTextColor.a = 1f;
			}
			gameOverImage.color = gameOverColor;
			gameOverText.color = gameOverTextColor;
		}
// --- 4/9 add end ---
	}

	public void Damage() {
		if (damagedTime == 0f) {
			damagedTime = Time.time;
// --- 4/9 add start ---
			HP-=0.5f;
			if (HP <= 0 && gameoverTime == 0f) {
				gameoverTime = Time.time;
			}
// --- 4/9 add end ---
			ShowHP ();
		}
	}

// --- 4/9 add start ---
	public GameObject lifePartsPrefab;
	public GameObject life;
	public float HP = 3;
	private void ShowHP() {

		if (life.transform.childCount == 0) {
			for (int i = 0; i < HP; i++) {
				GameObject lifeParts = Instantiate (lifePartsPrefab);
				lifeParts.transform.parent = life.transform;
				lifeParts.transform.localPosition = new Vector3 (i * 0.25f, 0, 0);
				lifeParts.transform.name = (i+1).ToString ();
			}
		}

		for (int i = 0; i < life.transform.childCount; i++) {
			GameObject obj = life.transform.GetChild (i).gameObject;
			float l = float.Parse (obj.name);
			SpriteRenderer sprite = obj.GetComponent<SpriteRenderer> ();
			if (l <= HP) {
				sprite.color = Color.red;
			} else {
				float col = HP - (l - 1);
				if (col < 0)
					col = 0;
				sprite.color = new Color (col, 0, 0);
			}
		}
	}
// --- 4/9 add end ---
}

上記の通り、スクリプトを修正して、HierarchyのCardboardMainをインスペクタで表示すると、以下のようになります。
f:id:tuti107:20160411013601p:plain
設定可能な項目が一気に増えました。まずは、下3つLife Parts Prefab, Life, HPから設定します。Life Parts Prefabには、先ほどインストールした64 Flat Game Iconsの中にある、128pxのElements_Lifeを設定します。ただし、このElements_Lifeは単なるpngファイルであり、直接Life Parts Prefabに設定することはできません。そこで、まずこのElements_Lifeが設定されたPrefabを作成することにします。
f:id:tuti107:20160411014248p:plain
まずHierarchy内で右クリック→2D Object→Spriteを選んでSpriteを作成してください。名前はわかりやすいもの(例えばLifeParts)に変更しておきます。このLifePartsのインスペクタのSpriteの右側の◉をクリックすると以下のようなウィンドウが現れ、64 Flat Game Iconsの内容が一覧表示されます。この中から128pxのElements_Life(ウィンドウ下にサイズが表示されます。ハートマークが3つ並んでいて、ここからはサイズの判断ができません。ウィンドウ下に表示されるサイズが「128x128」のものを選んでください)。
f:id:tuti107:20160411014948p:plain
合わせて、Scaleを(x,y,z)=(0.2, 0.2, 1)としてください。最後に、ここで作成したLifePartsをProjectの適当なフォルダへドラッグ&ドロップし、Prefab化します。Prefab化後は、Hierarchy内のLifePartsは不要となりますので、削除してください。最後に、Prefab化したLifePartsを、上記CardboardMainのインスペクタのLife Parts Prefabに設定すれば完了です。
次にLifeですが、まずCardboardMain→Headの下にLifeというオブジェクトを作ります(Headで右クリック→Create Emptyの後、名前をLifeに変更)。次にインスペクタよりPositionを(x, y, z)=(-0.8, 1.2, 1.5)に変更します。Head配下のこの位置とすることで、プレイヤーがどの方向を向いていても、常に画面の左上にライフを表示することが可能となります。ライフのように画面の特定位置に常に表示するものは、Head配下に配置するようにしてください。
このLife自体は空っぽのオブジェクトで、画面上には表示されませんが、本日追加したスクリプトのShowHP()メソッド内にて、Life配下に、Life Parts Prefabに設定されたSpriteが追加されるようになります。追加される数・ハートの色は、変数HPの値によって変化します。例えばHPが3の場合、赤色のハートが3つ配置されます。HPが変化した際に、このShowHP()を呼び出すことで、スケルトンからダメージを受けた際に、HPの値に応じてハートの状況が変化するようになります。
f:id:tuti107:20160411020758p:plain

ゲームオーバー処理

次にライフが0になった際のゲームオーバー処理を追加します。ゲームオーバーの際は、画面が次第に暗くなり→真っ暗になり→中央にGAME OVREの文字が表示される(画面は元の明るさに戻るが、スケルトン等は非表示となる)との形で表現することにします。
f:id:tuti107:20160411021848p:plain
まずは先ほどのLifeの要領で、Head配下に、3D Text (Headで右クリック→3D Object→3D Text)を生成、名前をGameoverTextとします。
f:id:tuti107:20160411022214p:plain
次にGameoverTextのインスペクタより、以下の通りPosition, Scale, Text, Font Size, Font, Colorを設定してください。これで上記のように、画面中央に赤色でGAME OVERの表示がされるようになります。
f:id:tuti107:20160411022402p:plain
なお、Colorについては下記の通りAを0としてください。Aは透明度を表すパラメータであり、0とすると完全透明で見えなくなります。ゲーム開始後からGAME OVERの表示がされている、というのはおかしいので、通常はA=0として透明にしておき、ゲームオーバになったらA=1(完全不透明)とするようにスクリプトで調整する、とします。
f:id:tuti107:20160411022948p:plain
次に、Hierarchyにて、Canvas配下にPanelを追加(Canvasにて右クリック→UI→Panel)し、名前をBlackとします。暗転の際に利用しますので、その名の通り、真っ黒にします。Source Imageは不要なので、削除(一覧左上のNoneを選択)してください。またColorについては、上記GameoverTextと同じ理由でA=0とします。スクリプトのUpdate()メソッドにてゲームオーバーの際、A値を変更し、暗転(及び暗転からの復帰)を表現しています。
f:id:tuti107:20160411023507p:plain
次に、GameoverTextインスペクタ内のText Meshと、Blackインスペクタ内のImageをそれぞれ、CardboardMainのインスペクタ内のGame Over Image、Game Over Textにドラッグ&ドロップします。インスペクタ内のコンポーネントを他のインスペクタへドラッグ&ドロップして設定する方法は、前回ご紹介した、インスペクタを2つ表示&片方をロックする、をご参照ください。
最後に、Skeleton.cs、Gate.csに、ゲームオーバ時処理を追加します。以下の変更を加えることで、ゲームオーバー画面ではスケルトンの出現・表示・移動がされないようにします。
まず、Gate.csは

	void Update () {
// --- 4/9 add start ---
		if (player.isGameover) {
			return;
		}
// --- 4/9 add end ---

次に、Skeleton.csは、

	void Update () {
// --- 4/9 add start ---
		if (player.isGameover) {
			gate.Remove (gameObject);
		}
// --- 4/9 add end ---

となります。

コンティニュー処理

最後に、ゲームオーバ画面にコンティニューボタンを配置し、このボタンが押されるとゲームを最初から再開する処理を実装します。
f:id:tuti107:20160411024847p:plain
まずは、ボタン押下アニメーション用にアセットiTweenをダウンロード・インストールします。Java ScriptでおなじみのTweenのUnity版です。指定したプロパティを指定した時間経過で次第に変化させるアニメーションを簡単に実現できます。今回は、このiTweenを利用して、コンティニューボタンを押すとボタンが引っ込み、また元の位置に戻る、というアニメーションを実現します。
コンティニューボタンの制御と、ゲーム再開処理のために、以下のスクリプト(Continue.cs)を作成します。

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEngine.SceneManagement;

public class Continue : MonoBehaviour {

	public Image blackImage;
	private Color blackImageColor;

	private float pushedTime = 0f;
	private Vector3 origPos;

	// Use this for initialization
	void Start () {
		origPos = transform.position;
		blackImageColor = blackImage.color;
	}
	
	// Update is called once per frame
	void Update () {
	
		if (pushedTime > 0f) {
			float t = Time.time - pushedTime;
			if (t < 0.5f) {
				blackImageColor.a = 0f;
			}
			else if (t >= 0.5f && t <= 1.5f) {
				blackImageColor.a = t - 0.5f;
			} else if (t > 1.5f) {
				blackImageColor.a = 1f;
				pushedTime = 0f;
				SceneManager.LoadScene("gameScene");
			}
			blackImage.color = blackImageColor;
		}
	}

	public void Push() {
		if (pushedTime == 0f) {
			pushedTime = Time.time;
			Vector3 newPos = new Vector3 (origPos.x, origPos.y-0.1f, origPos.z);
			iTween.MoveTo(gameObject, iTween.Hash("position", newPos, "oncomplete", "PushAnimCompleted", "time", 0.5f));
		}
	}

	public void PushAnimCompleted() {
		iTween.MoveTo(gameObject, iTween.Hash("position", origPos, "time", 0.5f));
	}
}

Push()が呼びされると、iTweenを利用して、このスクリプトが設定されたGameObjectのPositionを現在位置から(0, -0.1, 0)移動させた場所(少し地面にめり込む方向)へ0.5秒かけて移動させます。また、アニメーション完了後、PushAnimCompleted()を呼び出すようにしています。PushAnimCompleted()では、再びiTweenを利用して、0.5秒かけて元の位置に移動させています。これでボタンが引っ込み・また戻って来る様子を表現しています。
次にボタンの3Dオブジェクトを作成します。Hierarchyにて右クリック→3D Object→Capsuleを選択し、カプセル形状の3Dオブジェクトを作成、名前をContinueに変更します。次にこのContinue上で右クリック→3D Object→3D Textを選択し、Continue配下に3D Textオブジェクトを配置します。名前はContinueTextとします。
まず、Continueについてですが、インスペクタにて以下の通り、Position, Rotateを設定してください。また、Add Componentを押下して、上記で作成したContinueスクリプトの追加、Event Triggerの追加を行います。Continueスクリプト内のBlack Imageには、上記ゲームオーバー処理時に作成したCanvas→Black内のImageを設定してください。Event TriggerにはPointer Clickを追加し、このCapsuleオブジェクトがクリックされたら、Continue#Push()を呼び出すように設定します。本設定は、スケルトンをクリック可能にするにて説明した時と同様です。
f:id:tuti107:20160411031141p:plain
ContinueTextは、以下の通りインスペクタにて設定をします。これで、Capsule上にContinueの文字が表示されるようになります。
f:id:tuti107:20160411031325p:plain
スクリプトにて、Push()が呼び出されると上記のiTweenアニメーションを開始すると同時に、変数pushedTimeに現在時刻を設定しています。Update()メソッドではこのpushedTimeが設定されると、1秒かけて画面を暗転させ、その後SceneManager.LoadScene()を呼び出しています。このメソッドを呼び出すことで指定したシーンに切り替えることが可能です。まずここまで作成してきたシーン(Hierarchyに設定した一連のオブジェクト群)をGameSceneの名前で保存します。File→Save Scene Asを選択後、Save Asに「GameScene」を入力、またこのシーンを保存する場所を指定し、Saveボタンを押下してください。これでシーンが保存されます。これで、コンティニューボタンが押されるとシーンがリロードされ、最初からゲームが開始されます。今回は、ゲーム再開の目的でLoadScene()を利用しましたが、本機能を利用することで、ゲームをいくつかのパートに分け(タイトル、ゲーム本編1、ゲーム本編2、・・・)、進捗に応じてシーンを切り替える、ということが可能になります。

まとめ

今回までの6回にわたり、Cardboardを利用した簡単なFPSシューティングゲームを開発してきました。非常にシンプルな内容ですが、Cardboardでプレイするとなかなか迫力があります。こんな簡単にこれほどの迫力があるゲームが実現できるのは、まさにUnityとVR(Cardboard)のおかげかと思います。
また、敵キャラクタの追加・アイテムの追加など、少し工夫を加えることで、さらに面白く・迫力のあるゲームを実現することもできると思います。いろいろと挑戦をしていただけるとありがたいです!(是非、どのような工夫をしたか、ご連絡いただければと思います。)
次回からは、アプリ第二弾として、Cardboardで360度立体視動画のプレイヤーの開発を始めてみたいと思っています。

ソースコード

これまで開発してきたソースコードを以下におきました。ご自由に利用ください。

gistd6ff9931ddc8caafdc4be5fd11882fec

CardboardでVRアプリを作ってみる(5)

はじめに

前回は、敵を撃退するための機能を実装しました。かなりゲームらしくなってきたものの、今はシーンに配置したスケルトンを撃退するだけなので、あっという間にゲームが終了してしまいます。
f:id:tuti107:20160328082503p:plain
今回は、スケルトンの登場処理を追加し、スケルトンを撃退し続ければゲームを続けられる(時間とともに難易度を上げる)ようにします。

出現状態を追加

これまでの要領で、SkeletonAnimControllerに、出現状態(Appear)を追加します。本状態へ遷移させるためのトリガー「Appare」の追加も、以前と同様です。
f:id:tuti107:20160403080702p:plain
状態遷移(矢印)については、

  • Idle→Appear:Appearトリガーを遷移条件に追加する(Has Exit Timeチェックは外す)
  • Appear→Idle:Idleトリガーを遷移条件に追加する(Has Exit Timeチェックは外す)

とします。これも今まで同様です。
次に、Skeleton.csに、出現状態を追加します。以下のコードをSkeleton.csのUpdate()メソッド内に追加してください。

// --- 4/2 add start ---
		if (info.IsName ("Appear")) {
			float t = info.normalizedTime;

			if (t >= 1f) {
				ChangeState ("Idle");
			} else {
				transform.position = new Vector3 (transform.position.x, -1.4f + t * 1.6f, transform.position.z);
			}
		}
		else if (info.IsName ("Idle")) {
// --- 4/2 add end ---
		// if (info.IsName ("Idle")) { // remove 4/2

また、Start()メソッドに以下を追加します。

	void Start () {

		anim = GetComponent<Animator>();
// --- 4/2 add start ---
		ChangeState ("Appear");
// --- 4/2 add end ---
	}

これで、スケルトンの初期状態はAppearとなり、地面からぬっと出現するようになります。出現後Idle状態へ遷移するようになります。

スケルトンの出現場所を作成

次にスケルトンを一定時間毎に出現させる場所を作成します。Hierarchyで右クリック→Create Emptyで、GameObjectを生成し、名前を「Gates」としてください。さらに「Gates」上で右クリック→Create Emptyで、Gates配下にGameObjectを生成、名前をGateとしてください。次に、前回ご紹介したParticle「KY_effects/MagicEffectsFreePack/prefab/energyBlast」を「Gate」までドラッグドロップしてください。これで以下のような感じになるかと思います。
f:id:tuti107:20160403082422p:plain
次に、前回の要領で、このParticleの設定を行います。Positionは(0, -0.8, 0)とし、Play In Awakeのチェックを外します。このParticleはスケルトン出現時に使用します。単に地面から出てくるよりも、よりかっこいい登場の仕方となります。
f:id:tuti107:20160403082730p:plain
次に、上記で作成したGateに、以下のスクリプトを設定します。Gate.csを新規作成し、以下を入力してください。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Gate : MonoBehaviour {

	public int max = 3;
	public float duration = 30f;
	public float initial = 0f;
	public float durationDelta = 3f;
	public float durationMin = 10f;

	public Player player;
	public GameObject enemyPrefab;
	public ParticleSystem particeSystem;

	private float lastCreated;
	private List<GameObject> enemies = new List<GameObject>();

	// Use this for initialization
	void Start () {
		lastCreated = initial - duration + Time.time;
	}

	// Update is called once per frame
	void Update () {

		if ((Time.time - lastCreated > duration) && enemies.Count < max) {

			lastCreated = Time.time;
			duration -= durationDelta;
			if (duration < durationMin)
				duration = durationMin;

			GameObject obj = GameObject.Instantiate (enemyPrefab);
			obj.transform.parent = transform;
			obj.transform.position = transform.position;
			Skeleton skeleton = obj.GetComponent<Skeleton> ();
			skeleton.gate = this;
			skeleton.player = player;

			particeSystem.Play ();
		}
	}

	public void Remove(GameObject obj) {
		enemies.Remove (obj);
		GameObject.Destroy (obj);
	}
}

また、Skeleton.csに以下通り、gateの追加(冒頭部)、Uodate()メソッドの変更を行います。

// --- 4/2 add start ---
	public Gate gate;
// --- 4/2 add end ---
		} else if (info.IsName ("Dead")) {
			float t = info.normalizedTime;
			if (t >= 2f) {
// --- 4/2 add start ---
				if (gate != null) {
					gate.Remove (gameObject);
				}
// --- 4/2 add end ---
				GameObject.Destroy (gameObject);
			}
		}

次に、スクリプトをGateに設定します。Gateのインスペクタから、Add Component→Gateを選んでください。
Gate (Script)への設定は以下のとおりです。
f:id:tuti107:20160403105655p:plain
Playerは、前回の要領で、インスペクタを二つ表示し、CardboardMain内のPlayer (Script)をドラッグ&ドロップで設定します。Particle Systemには上記でGate配下に追加したenergyBlastをドラッグ&ドロップしてください。
Max, Duration, Initial, Duration Delta, Duration Minには、それぞれこの場所からスケルトンが登場する最大数、登場間隔(秒)、ゲーム開始後最初に登場するまでの時間(秒)、スケルトン登場毎にDurationを何秒づつ短くするか、最低Durationは何秒まで減少するか、をそれぞれ指定します。本設定値を変えたGateをシーン内に複数設置することで、いろいろな場所からいろいろなタイミングでスケルトンが登場するようになります。

スケルトンをPrefab化

上記のEnemy PrefabにはSkeleton@Skinをドラッグ&ドロップでも良いのですが、こうしてしまうと、Gateからの登場に関係なく、一つシーンにスケルトンを配置しておかなければならなくなります。そこで、Skeleton@Skinをシーンから、Projectの適当なフォルダへドラッグ&ドロップしPrefab化します。Prefab化されたSkeleton@Skinは、他のProject内のコンポーネント同様、ドラッグ&ドロップでシーン内やインスペクタ内に配置できるようになります。
f:id:tuti107:20160403110957p:plain
ここで設定されたSkeleton@SkinのPrefabは、Gate.cs内(GameObject.Instantiate(enemyPrefab))にて、Skeleton@Skinの複製を作成するために使用しています。このように、Prefabの複製を利用することで、スクリプトからシーン内に複数のキャラクタを自動生成・配置することが可能です。
Gateを一つ生成したら、これをCopy&Pasteして、複数に増やし、適当な場所に配置してください(上記のMax, Duration等の値を少しずつ変えると良いと思います)。
f:id:tuti107:20160403112853p:plain

Point lightの色を変える

最後に、現状明るすぎる画面を、スケルトンを倒すゲームっぽく、暗い感じに変更します。HierarchyよりPoint lightを選択し、インスペクタより照明の色を変更します。
f:id:tuti107:20160403111744p:plain
以上で、以下のような感じになります。
f:id:tuti107:20160403112007p:plain

まとめ

今回は、スケルトンを登場させる場所(Gate)を作成し、これをシーン内に配置することで、その場所から次々とスケルトンが登場するようにしました。次回は、ライフ・ゲームオーバー・コンティニュー処理を加え、本アプリの完成を目指します。