Tutti Lab

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

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

はじめに

前回は、Cardboardデモシーンに、ダンジョンの背景とスケルトンを配置し、ゲーム的な見た目を実現してみました。このスケルトン、単にブラブラ立っていただけですが、今回はスケルトンを移動させ、主人公(自分)に攻撃をする、という動きをつけてみます。

f:id:tuti107:20160322154528p:plain

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

第1回(Cardboardのビルド)

第2回(シーンを構成)

第3回(敵を動かす)【今回】

第4回(敵を倒す)

第5回(敵を出現させる)

第6回(ゲームオーバーとコンティニュー処理)

どのようにスケルトンを移動させるか?

一直線に主人公に向かって歩いてくる、走ってくる、ときどき休む、一言でスケルトンを移動させる、と言っても、どのように移動させるか、これはいかようにも定義できます。今回は、スケルトンを以下のルールに従って移動させることにします。

  1. まず移動する方向と、距離を決める。移動する方向は、主人公の方向に少しランダム値を加えた角度とする。距離は主人公までの距離の1/4に少しランダム値を加えた値とする
  2. 1で決めた方向を向く
  3. 1で決めた距離分移動する
  4. 主人公に十分近づいた場合は、攻撃をする。まだ距離がある場合は、1に戻る

このようなルールで移動させると、スケルトンは主人公の方向に向かながらも時どきキョロキョロと主人公を探すように立ち止まる、主人公を攻撃範囲内に捉えたら攻撃をする、という感じになります。

また、上記のルールに従ってスケルトンを動かすのに合わせて、適切なアニメーションに切り替えてやる必要があります。今回は、

  • 1, 2はIdle(Assets/FantasyMonster/Skeleton/ani/Skeleton@Idle)のアニメーション
  • 3はWalk(Assets/FantasyMonster/Skeleton/ani/Skeleton@Walk)のアニメーション
  • 4はAttack((Assets/FantasyMonster/Skeleton/ani/Skeleton@Attack))のアニメーション

を行うこととします。

状態とアニメーションを定義する

前回作成したSkeletonAnimControllerを拡充して、上記の各状態のアニメーションを定義します。SkeletonAnimControllerをダブルクリックしてAnimatorを開きます。そして、右クリック→Create State→Emptyを選択し、Stateを生成します。名前はWalkとしてください。同様にもう一つ名前がAttackのStateも生成します。

また、生成したStateに、前回のIdle State同様、アニメーションを設定します。

f:id:tuti107:20160324125130p:plain

次に前回作成したIdle Stateと、Walk Stateの間に状態遷移を生成します。Idleにマウスカーソルを合わせて右クリック→Make Transitionを選択し、その後Walkをクリックします。すると、IdleとWalkが矢印で結ばれます。同様に、WalkとIdle (先ほどと逆方向)、WalkとAttackに状態遷移を作成します。

f:id:tuti107:20160324122524p:plain

次に、上記で作成した各状態遷移に、状態遷移の条件を設定します。まずは、状態遷移のきっかけとなる「トリガー」を作成します。トリガーはその名の通り、状態遷移の引き金となるものです。例えば、「Idle StateからWalk Stateの状態遷移」に「Walkトリガーが設定されたら状態遷移する」との条件をつけておけば、Idle State中にWalkトリガーが設定されたらWalk Stateに遷移する、ようになります。

f:id:tuti107:20160324124230p:plain

f:id:tuti107:20160324124236p:plain

トリガーは、Animatorの左上にある「Parameters」タブを選択→右上側の+ボタンを押下→Triggerを選択→トリガーの名前を入力、することで生成できます。ここでは、上の図のように、Walk、Idle、Attackの3つのトリガーを作成することにします。

※Stateの名前とトリガーの名前を一致させる必要はありません。ここでは、後ほど説明するスクリプト処理の都合上、Stateと名前を一致させています。

次に、生成したトリガーを、各状態遷移に設定していきます。状態遷移(State間の矢印)を左クリックすると、インスペクタにこの状態遷移が表示されます。

f:id:tuti107:20160324125146p:plain

インスペクタ下部のConditionsより、状態遷移の条件を設定できます。Conditions右下の+ボタンを押下してコンボボックスを追加し、そこからトリガーを選択します。設定するトリガーはそれぞれ、

  • Idle→Walkの状態遷移:Walkトリガー
  • Walk→Idleの状態遷移:Idleトリガー
  • Walk→Attackの状態遷移:Attackトリガー

を設定します(そのまま、ですが)。また、必須ではありませんが、各トリガー設定の最、Settingsの時間表示部(上図の1:00 - 7:00と描かれている部分)の青帯部分を上記のように、IdleとWalkが重なっている部分と同じ大きさ(上図なら5:00がちょうど隠れるくらいの大きさ)に調整してください。これは、Idle状態からWalk状態に遷移する「遷移中」の状態をどのくらいの長さにするか、を設定するものです。ここを小さくするとIdleからWalkへすぐに切り替わることになりますが、このようにしてしまうと、Idleアニメーションから急にWalkアニメーションに切り替わり見た目に美しくありません。このため、ある程度余裕を持って状態遷移をするように設定をする必要があります。

スクリプトでスケルトンの動きを制御する

次に、スケルトンの動きのルールを作っていきます。ここでは(初めて!)プログラミングが必要になります。Assetsにスクリプトを保存する適当なフォルダを作成し、そのフォルダで右クリック→Create→C# Scriptを選択、名前はSkeletonとします。生成後Skeleton.csをダブルクリックして、エディタを起動します。エディタ起動後、以下をコピー&ペーストしてください。


    using UnityEngine;
using System.Collections;

public class Skeleton : MonoBehaviour {

	public GameObject player;

	private Vector3 nextDirection = Vector3.zero;
	private float nextDistance;
	private Vector3 startPoint;
	private float speed = 0.5f;

	private Animator anim;

	// Use this for initialization
	void Start () {

		anim = GetComponent();
	}

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

		if (IsStateChanging ())
			return;

		AnimatorStateInfo info = anim.GetCurrentAnimatorStateInfo (0);

		if (info.IsName("Idle")) {

			if (player != null) {
				if (nextDirection == Vector3.zero) {
					float dist = Vector3.Distance (player.transform.position, transform.position);
					float randAngle = dist < 3.0f ? 0f : (Random.value - 0.5f) * dist * 8;
					nextDirection = Quaternion.LookRotation (player.transform.position - transform.position).eulerAngles;
					nextDirection.y += randAngle;
					if (nextDirection.y < 0)
						nextDirection.y += 360;
					nextDirection.x = 0;
					nextDirection.z = 0;

					nextDistance = dist < 3.0f ? dist : dist / 4 + Random.value * (dist / 4);
					startPoint = transform.position;

				} else {
					transform.rotation = Quaternion.Slerp (transform.rotation, Quaternion.Euler (nextDirection), Time.deltaTime);
					if (Mathf.Abs (Mathf.DeltaAngle(transform.rotation.eulerAngles.y, nextDirection.y)) <= 1.0f) {
						ChangeState ("Walk");
					}
				}
			}
		} else if (info.IsName("Walk")) {
			
			float playerDist = Vector3.Distance (player.transform.position, transform.position);
			if (playerDist <= 2) {
				ChangeState ("Attack");
			} else {
				transform.position += transform.TransformDirection (Vector3.forward) * Time.deltaTime * speed;
				float dist = Vector3.Distance (startPoint, transform.position);
				if (dist >= nextDistance) {
					nextDirection = Vector3.zero;
					ChangeState ("Idle");
				}
			}
		} else if (info.IsName("Attack")) {
			// Next
		}
	}

	private string changingStateName = null;
	private void ChangeState(string name) {
		changingStateName = name;
		anim.SetTrigger (name);
	}

	private bool IsStateChanging() {
		AnimatorStateInfo next = anim.GetNextAnimatorStateInfo (0);
		bool ret = changingStateName != null && next.IsName (changingStateName);
		if (changingStateName != null && !ret) {
			changingStateName = null;
		}
		return ret;
	}
}

次に、作成したスクリプトをスケルトンに設定します。HierarchyにてSkeleton@Skinを選択してインスペクタを表示し、最下部Add Componentボタン押下→Skeletonを選択します。すると以下のように、Skeleton (Script) が追加されます。このPlayerプロパティにいは、CardboardMainを設定してください(◎を押してCardboardMainを選択するか、もしくはHierarchy内のCardboardMainをドラッグ&ドロップする)。

f:id:tuti107:20160324133027p:plain

再生ボタンを押下すると、スケルトンがこちらに向かって歩いてきて、自分にかなり迫った状態でこちらを切りつけてくる、という動きになります。Cardboardで見るとかなりの迫力です。

f:id:tuti107:20160324133810p:plain

スクリプトは一体何をやっているのか?

C#言語や、UnityでのスクリプトとしてのC#、MonoBehaviorとは?、などについては、たくさんの解説のページや書籍があるためそちらに任せるとして、なぜ上記のスクリプトにより、スケルトンがこちらに向かって歩いてくる・攻撃してくるようになったのか、その要点だけ説明をしていきます。毎フレーム呼び出されるUpdate()メソッドにて、

  • まずIsStateChanging()メソッドを呼び出し、現在状態遷移中(上で説明した、アニメーションの切り替えをスムースにすべく設定した、例えば、IdleからWalkに遷移している途中の状態)かどうかをチェック。状態遷移中の場合は、このフレームは何もしない
  • GetCurrentAnimatorStateInfo()メソッドを呼び出して、現在のアニメーションの状態を保持するオブジェクトを取得
  • 状態がIdleの場合は、移動する方向と距離を計算したのち、その方向へゆっくりと回転する。回転後、ChangeState("Walk")を呼び出して、Walk状態へ遷移
  • 状態がWalkの場合は、計算した距離分移動するまで前進(スケルトンが向いている方向は、transform.TransformDirection (Vector3.forward)で取得できる)。移動中、主人公の位置(player.transform.position)に近づいた場合は、ChangeState("Attack")を呼び出してAttack状態へ遷移。主人公に近づくことなく、計算した距離分の移動が完了した場合は、ChangeState("Idle")で、Idle状態に戻る

という処理を行っています。なお、なぜスケルトンは、主人公の位置がわかるのか、それは、上記でSkeleton (Script)のplayerへドラッグ&ドロップして設定したCardboardMainが、VR空間にて「自分の視点」となる場所の位置情報を保有しているためです。Skeleton (Script)のplayerに設定されたCardboardPlayerオブジェクトは、スクリプトのplayer変数を通じてアクセスできるようになるため、これによりスケルトンは主人公の位置を取得しています。

ChangeState()とIsStateChanging()

※若干複雑な内容です。訳がわからない場合は、読み飛ばしてください。

上記のChangeState(state)は、内部でAnimator#SetTrigger(state)を呼び出して、トリガーを設定するとともに、changingStateName変数に、stateを保持します。IsStateChanging()では、「次のState」を取得するメソッドGetNextAnimatorStateInfo()を呼び出して、次のステート、例えばIdleからWalkに遷移中ならWalk Stateを取得し、その名前とchangingStateName変数の値を比較します。すると、例えば、ChangeState("Walk")にてIdle StateからWalk Stateに遷移する場合は、Idle→Walkに遷移中はtrue、Walkへ遷移後はfalseとなります。このため、IsStateChanging()は、現在ChangeState()で指定したstateへ遷移中の場合はtrue、遷移完了後はfalseを返すことになります。

なぜ、こんな処理を行って状態遷移中であることを取得する必要があるのか、それは、GetCurrentAnimatorStateInfo()は、状態遷移中は「状態遷移前」のオブジェクトを返すのですが、これがスケルトンの移動やアニメーション制御に不都合を生じさせる、ためです。例えば、現在Idle状態で、計算した方向への回転を完了し、Walk状態へ移る場合、SetTrigger("Walk")を呼び出してWalk状態へ遷移することを期待するわけですが、実際には、SetTrigger("Walk")呼び出し後しばらく、GetCurrentAnimatorStateInfo()は、Idle Stateを返し続けます。なぜかというと、SetTrigger("Walk")呼び出しで、Idle→Walk状態遷移中、となるのですが、上記の通りスムースなアニメーションの切り替えのために一定期間を要します。この間GetCurrentAnimatorStateInfo()は、遷移前、すなわちIdleを返します。スクリプト側としては、SetTrigger()呼び出し後は、即次のステートなることを想定して、処理を記述したいものです(状態遷移中を意識すると処理が煩雑になる。できればシンプルな方が良い)。

そこで、IsStateChanging()で状態遷移中かどうかをチェックし、状態遷移中は移動処理をスキップするようにすることで、状態毎の移動処理をシンプル化しています。

まとめ

今回は、前回シーン内に設置したスケルトンに、動きのルール(スクリプト)と、各状態のアニメーション・状態遷移(Animator)を用意することで、スケルトンを動かしてみました。前回と比較し急にややこしい内容になったかもしれません。ただ、今回の内容が、全体を通じて一番難解な部分かと思います。この後は比較的シンプルな内容です。

今回はスケルトンからの一方的な攻撃を受けるだけ、でしたが、次回はスケルトンを攻撃し撃退する部分を実装します。