Tutti Lab

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

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