Tutti Lab

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

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

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

今朝方Oculusより「在庫が不足しているため、出荷が遅れている」旨のメールが。まずは4月12日までにオーダー状況をアップデートしてくれる、とのこと(今まで音沙汰ありませんでした)。
手元に届くのは4月下旬頃でしょうか?プリオーダ開始とほぼ同時に予約したので、すぐに来るのでは、と期待していましたが、残念です。

Oculus Rift 出荷開始!

娘を学校に送りがてら聞いていたラジオから「Oculus Riftが今日から出荷開始!」とのニュース。待っていました!先週くらいに「1−3週間の内に出荷」とのメールが来ていたので、そろそろか、とは思っていましたが、ようやくです。Oculusのブログにも、その旨、出ていますね。なお、ラジオでは、出荷台数は未公表とのことでした。どのくらいの数が売れたのでしょうね?
www.oculus.com
ちなみに、私の注文分は、まだ出荷されていないようです(未だクレジットカードの引き落としがされていませんでした)。。

娘とハッカソンに参加

キッズ向けハッカソン

先週の土曜日(3月26日)、娘と二人でキッズ向けハッカソンに参加しました。このイベント、シリコンバレーに拠点を置く複数の企業・団体が協賛するもので、お題として与えられた4つのテーマから好きなものを一つ選択し、ブレスト→プレゼン作成(可能ならプロトタイプも作成)し、皆の前で発表し審査員が審査、とキッズ向けながら本格的な内容。
娘は、私が普段からVRアプリを開発している様を横目で見ていたせいか、「2020年のオリンピックに向けて、現地で観戦できない人向けに、VRで臨場感たっぷりの観戦が可能なアプリを作る」というアイデアを着想。このネタでブレスト、プレゼン作成(娘の作業を横からサポートしただけですが)しました。

プロトタイプアプリ作成

娘から、プレゼンデモ用に何かアプリを作って欲しい、とのリクエスト。本イベント、開始からプレゼンまで二時間程度の非常に短いもののため、大したものは作れない。ということで、アセットストアからスタジアムの3Dモデルをダウンロードし、観客席前列にカメラを配置、ここからの景色をVRで観れる、という超突貫のアプリを作りました。
https://www.assetstore.unity3d.com/jp/#!/content/51123
f:id:tuti107:20160329143139p:plain

一位!そして。。

デモアプリのおかげ!かどうかはわかりませんが、立派なプレゼン、そして今熱いVRというテーマということもあり、なんと一位を頂けました!おめでとう!お疲れ様、さて帰ろうか、と思いきや、賞品は「メイカーフェアーにプロトタイプ出展のための軍資金$1000」。ということで、メイカーフェアーに向けて、突貫のプロトタイプアプリではなく、ちゃんとしたプロトタイプを開発することになりました。アイデアは娘、実装は私と、二人三脚で頑張っていこうと思います。